Tengo una gran tabla de objetos (15M + fila) en PostgreSQL 9.0.8, para la que quiero consultar el campo desactualizado.
Quiero dividir la consulta por millones, para fines de escalabilidad y concurrencia, y quiero obtener todos los datos con el campo updated_at con una fecha de hace unos días.
He intentado muchos índices y consultas en un millón de ID, y parece que no puedo obtener el rendimiento en menos de 100 segundos con el hardware Ronin de Heroku.
Estoy buscando sugerencias que no he intentado hacer que esto sea lo más eficiente posible.
PRUEBA # 1
EXPLAIN ANALYZE SELECT count(*) FROM objects
WHERE (date(updated_at)) < (date(now())-7) AND id >= 5000001 AND id < 6000001;
INDEX USED: (date(updated_at),id)
268578.934 ms
PRUEBA # 2
EXPLAIN ANALYZE SELECT count(*) FROM objects
WHERE ((date(now()) - (date(updated_at)) > 7)) AND id >= 5000001 AND id < 6000001;
INDEX USED: primary key
335555.144 ms
PRUEBA # 3
EXPLAIN ANALYZE SELECT count(*) FROM objects
WHERE (date(updated_at)) < (date(now())-7) AND id/1000000 = 5;
INDEX USED: (date(updated_at),(id/1000000))
243427.042 ms
PRUEBA # 4
EXPLAIN ANALYZE SELECT count(*) FROM objects
WHERE (date(updated_at)) < (date(now())-7) AND id/1000000 = 5 AND updated_at IS NOT NULL;
INDEX USED: (date(updated_at),(id/1000000)) WHERE updated_at IS NOT NULL
706714.812 ms
PRUEBA # 5 (para un solo mes de datos desactualizados)
EXPLAIN ANALYZE SELECT count(*) FROM objects
WHERE (EXTRACT(MONTH from date(updated_at)) = 8) AND id/1000000 = 5;
INDEX USED: (EXTRACT(MONTH from date(updated_at)),(id/1000000))
107241.472 ms
PRUEBA # 6
EXPLAIN ANALYZE SELECT count(*) FROM objects
WHERE (date(updated_at)) < (date(now())-7) AND id/1000000 = 5;
INDEX USED: ( (id/1000000 ) ASC ,updated_at DESC NULLS LAST)
106842.395 ms
PRUEBA # 7 (ver: http://explain.depesz.com/s/DQP )
EXPLAIN ANALYZE SELECT count(*) FROM objects
WHERE id/1000000 = 5 and (date(updated_at)) < (date(now())-7);
INDEX USED: ( (id/1000000 ) ASC ,date(updated_at) DESC NULLS LAST);
100732.049 ms
Second try: 87280.728 ms
PRUEBA # 8
EXPLAIN ANALYZE SELECT count(*) FROM objects
WHERE (date(updated_at)) < (date(now())-7) AND id/1000000 = 5 AND updated_at IS NOT NULL;
INDEX USED: ( (id/1000000 ) ASC ,date(updated_at) ASC NULLS LAST);
129133.022 ms
PRUEBA # 9 ( índice parcial según la sugerencia de Erwin, ver: http://explain.depesz.com/s/p9A )
EXPLAIN ANALYZE SELECT count(*) FROM objects
WHERE id BETWEEN 5000000 AND 5999999 AND (date(updated_at)) < '2012-10-23'::date;
INDEX USED: (date(updated_at) DESC NULLS LAST)
WHERE id BETWEEN 5000000 AND 6000000 AND date(updated_at) < '2012-10-23'::date;
73861.047 ms
PRUEBA # 10 ( CLUSTER , según la sugerencia de Erwin).
CREATE INDEX ix_8 on objects ( (id/1000000 ) ASC ,date(updated_at) DESC NULLS LAST);
CLUSTER entities USING ix_8;
EXPLAIN ANALYZE SELECT count(*) FROM objects
WHERE id/1000000 = 5 and (date(updated_at)) < (date(now())-7) ;
4745.595 ms
EXPLAIN ANALYZE SELECT count(*) FROM objects
WHERE id/1000000 = 10 and (date(updated_at)) < (date(now())-7) ;
17573.639 ms
==> Esta solución parece ser la ganadora. Tendré que hacer pruebas exhaustivas para verificar los contraimpactos en todas partes de mi aplicación.
Configuraciones de DB:
seleccione nombre, min_val, max_val, boot_val de pg_settings;
name | min_val | max_val | boot_val
--------------------------------+-----------+--------------+-------------------
allow_system_table_mods | | | off
application_name | | |
archive_command | | |
archive_mode | | | off
archive_timeout | 0 | 2147483647 | 0
array_nulls | | | on
authentication_timeout | 1 | 600 | 60
autovacuum | | | on
autovacuum_analyze_scale_factor | 0 | 100 | 0.1
autovacuum_analyze_threshold | 0 | 2147483647 | 50
autovacuum_freeze_max_age | 100000000 | 2000000000 | 200000000
autovacuum_max_workers | 1 | 536870911 | 3
autovacuum_naptime | 1 | 2147483 | 60
autovacuum_vacuum_cost_delay | -1 | 100 | 20
autovacuum_vacuum_cost_limit | -1 | 10000 | -1
autovacuum_vacuum_scale_factor | 0 | 100 | 0.2
autovacuum_vacuum_threshold | 0 | 2147483647 | 50
backslash_quote | | | safe_encoding
bgwriter_delay | 10 | 10000 | 200
bgwriter_lru_maxpages | 0 | 1000 | 100
bgwriter_lru_multiplier | 0 | 10 | 2
block_size | 8192 | 8192 | 8192
bonjour | | | off
bonjour_name | | |
bytea_output | | | hex
check_function_bodies | | | on
checkpoint_completion_target | 0 | 1 | 0.5
checkpoint_segments | 1 | 2147483647 | 3
checkpoint_timeout | 30 | 3600 | 300
checkpoint_warning | 0 | 2147483647 | 30
client_encoding | | | SQL_ASCII
client_min_messages | | | notice
commit_delay | 0 | 100000 | 0
commit_siblings | 1 | 1000 | 5
constraint_exclusion | | | partition
cpu_index_tuple_cost | 0 | 1.79769e+308 | 0.005
cpu_operator_cost | 0 | 1.79769e+308 | 0.0025
cpu_tuple_cost | 0 | 1.79769e+308 | 0.01
cursor_tuple_fraction | 0 | 1 | 0.1
custom_variable_classes | | |
DateStyle | | | ISO, MDY
db_user_namespace | | | off
deadlock_timeout | 1 | 2147483 | 1000
debug_assertions | | | off
debug_pretty_print | | | on
debug_print_parse | | | off
debug_print_plan | | | off
debug_print_rewritten | | | off
default_statistics_target | 1 | 10000 | 100
default_tablespace | | |
default_text_search_config | | | pg_catalog.simple
default_transaction_isolation | | | read committed
default_transaction_read_only | | | off
default_with_oids | | | off
effective_cache_size | 1 | 2147483647 | 16384
effective_io_concurrency | 0 | 1000 | 1
enable_bitmapscan | | | on
enable_hashagg | | | on
enable_hashjoin | | | on
enable_indexscan | | | on
enable_material | | | on
enable_mergejoin | | | on
enable_nestloop | | | on
enable_seqscan | | | on
enable_sort | | | on
enable_tidscan | | | on
escape_string_warning | | | on
extra_float_digits | -15 | 3 | 0
from_collapse_limit | 1 | 2147483647 | 8
fsync | | | on
full_page_writes | | | on
geqo | | | on
geqo_effort | 1 | 10 | 5
geqo_generations | 0 | 2147483647 | 0
geqo_pool_size | 0 | 2147483647 | 0
geqo_seed | 0 | 1 | 0
geqo_selection_bias | 1.5 | 2 | 2
geqo_threshold | 2 | 2147483647 | 12
gin_fuzzy_search_limit | 0 | 2147483647 | 0
hot_standby | | | off
ignore_system_indexes | | | off
integer_datetimes | | | on
IntervalStyle | | | postgres
join_collapse_limit | 1 | 2147483647 | 8
krb_caseins_users | | | off
krb_srvname | | | postgres
lc_collate | | | C
lc_ctype | | | C
lc_messages | | |
lc_monetary | | | C
lc_numeric | | | C
lc_time | | | C
listen_addresses | | | localhost
lo_compat_privileges | | | off
local_preload_libraries | | |
log_autovacuum_min_duration | -1 | 2147483 | -1
log_checkpoints | | | off
log_connections | | | off
log_destination | | | stderr
log_disconnections | | | off
log_duration | | | off
log_error_verbosity | | | default
log_executor_stats | | | off
log_hostname | | | off
log_line_prefix | | |
log_lock_waits | | | off
log_min_duration_statement | -1 | 2147483 | -1
log_min_error_statement | | | error
log_min_messages | | | warning
log_parser_stats | | | off
log_planner_stats | | | off
log_rotation_age | 0 | 35791394 | 1440
log_rotation_size | 0 | 2097151 | 10240
log_statement | | | none
log_statement_stats | | | off
log_temp_files | -1 | 2147483647 | -1
log_timezone | | | UNKNOWN
log_truncate_on_rotation | | | off
logging_collector | | | off
maintenance_work_mem | 1024 | 2097151 | 16384
max_connections | 1 | 536870911 | 100
max_files_per_process | 25 | 2147483647 | 1000
max_function_args | 100 | 100 | 100
max_identifier_length | 63 | 63 | 63
max_index_keys | 32 | 32 | 32
max_locks_per_transaction | 10 | 2147483647 | 64
max_prepared_transactions | 0 | 536870911 | 0
max_stack_depth | 100 | 2097151 | 100
max_standby_archive_delay | -1 | 2147483 | 30000
max_standby_streaming_delay | -1 | 2147483 | 30000
max_wal_senders | 0 | 536870911 | 0
password_encryption | | | on
port | 1 | 65535 | 5432
post_auth_delay | 0 | 2147483647 | 0
pre_auth_delay | 0 | 60 | 0
random_page_cost | 0 | 1.79769e+308 | 4
search_path | | | "$user",public
segment_size | 131072 | 131072 | 131072
seq_page_cost | 0 | 1.79769e+308 | 1
server_encoding | | | SQL_ASCII
server_version | | | 9.0.8
server_version_num | 90008 | 90008 | 90008
session_replication_role | | | origin
shared_buffers | 16 | 1073741823 | 1024
silent_mode | | | off
sql_inheritance | | | on
ssl | | | off
ssl_renegotiation_limit | 0 | 2097151 | 524288
standard_conforming_strings | | | off
statement_timeout | 0 | 2147483647 | 0
superuser_reserved_connections | 0 | 536870911 | 3
synchronize_seqscans | | | on
synchronous_commit | | | on
syslog_facility | | | local0
syslog_ident | | | postgres
tcp_keepalives_count | 0 | 2147483647 | 0
tcp_keepalives_idle | 0 | 2147483647 | 0
tcp_keepalives_interval | 0 | 2147483647 | 0
temp_buffers | 100 | 1073741823 | 1024
temp_tablespaces | | |
TimeZone | | | UNKNOWN
timezone_abbreviations | | | UNKNOWN
trace_notify | | | off
trace_recovery_messages | | | log
trace_sort | | | off
track_activities | | | on
track_activity_query_size | 100 | 102400 | 1024
track_counts | | | on
track_functions | | | none
transaction_isolation | | |
transaction_read_only | | | off
transform_null_equals | | | off
unix_socket_group | | |
unix_socket_permissions | 0 | 511 | 511
update_process_title | | | on
vacuum_cost_delay | 0 | 100 | 0
vacuum_cost_limit | 1 | 10000 | 200
vacuum_cost_page_dirty | 0 | 10000 | 20
vacuum_cost_page_hit | 0 | 10000 | 1
vacuum_cost_page_miss | 0 | 10000 | 10
vacuum_defer_cleanup_age | 0 | 1000000 | 0
vacuum_freeze_min_age | 0 | 1000000000 | 50000000
vacuum_freeze_table_age | 0 | 2000000000 | 150000000
wal_block_size | 8192 | 8192 | 8192
wal_buffers | 4 | 2147483647 | 8
wal_keep_segments | 0 | 2147483647 | 0
wal_level | | | minimal
wal_segment_size | 2048 | 2048 | 2048
wal_sender_delay | 1 | 10000 | 200
wal_sync_method | | | fdatasync
wal_writer_delay | 1 | 10000 | 200
work_mem | 64 | 2097151 | 1024
xmlbinary | | | base64
xmloption | | | content
zero_damaged_pages | | | off
(195 rows)
Respuestas:
En primer lugar, ¿puede ser? Usted escribe:
Pero tu
WHERE
condición es:¿No debería ser eso
>
?Índices
Para un rendimiento óptimo , podrías ...
Sus índices podrían verse así:
...
La segunda condición excluye filas irrelevantes del índice de inmediato, lo que debería hacerlo más pequeño y más rápido, dependiendo de su distribución de datos real. De acuerdo con mi comentario preliminar, supongo que quieres más nuevos filas .
La condición también excluye automáticamente los valores NULL en
updated_at
- que parece permitir en la tabla y obviamente desea excluir en la consulta. La utilidad del índice se deteriora con el tiempo. La consulta siempre recupera las últimas entradas. Recree el índice con unaWHERE
cláusula actualizada periódicamente. Esto requiere un bloqueo exclusivo en la mesa, así que hágalo en horas libres. También hayCREATE INDEX CONCURRENTLY
que minimizar la duración del bloqueo:Respuesta relacionada sobre SO:
Para optimizar aún más, puede usar
CLUSTER
como mencionamos en los comentarios. Pero necesitas un índice completo para eso. No funciona con un índice parcial. Crearías temporalmente:Esta forma del índice completo coincide con el orden de clasificación de los índices parciales anteriores.
Esto llevará un tiempo, ya que la tabla se reescribe físicamente. También es efectivamente a
VACUUM FULL
. Necesita un bloqueo de escritura exclusivo sobre la mesa, así que hágalo fuera de horario, siempre que pueda permitírselo. Una vez más, hay una alternativa menos invasiva: pg_repackLuego puede soltar el índice nuevamente. Es un efecto de una sola vez. Al menos lo intentaría una vez para ver cuánto se benefician sus consultas. El efecto se deteriora con las operaciones de escritura posteriores. Puede repetir este procedimiento fuera de horario si observa un efecto considerable.
Si su tabla recibe muchas operaciones de escritura, debe sopesar los costos y los beneficios de este paso. Para muchas ACTUALIZACIONES, considere establecer un valor
FILLFACTOR
inferior a 100. Haga eso antes que ustedCLUSTER
.Consulta
Más
Esta respuesta relacionada presenta una técnica más avanzada para la partición de índices:
Entre otras cosas, proporciona un código de ejemplo para la (re) creación automática de índices.
PostgreSQL 9.2+ tiene varias características nuevas para usted. Escaneos de solo índice solo harán que valga la pena.
Asegúrese de que
autovacuum
esté funcionando correctamente. La gran gananciaCLUSTER
que ha informado puede deberse en parte a lo implícitoVACUUM FULL
que obtieneCLUSTER
. Tal vez esto es configurado por Heroku automáticamente, no estoy seguro.La configuración en su pregunta se ve bien. Entonces, probablemente no sea un problema aquí y
CLUSTER
realmente fue tan efectivo.Particionamiento declarativo
finalmente ha madurado en Postgres 12 . Consideraría usar eso ahora en lugar de la partición de índice manual (o al menos adicionalmente). Intervalo de partición con
updated_at
la clave de partición. (Además de las múltiples mejoras en el rendimiento general, el big data y el rendimiento del índice btree en particular).fuente