J'ai une grande table d'objets (15M + ligne) dans PostgreSQL 9.0.8, pour laquelle je veux rechercher un champ obsolète.
Je veux diviser la requête par millions, à des fins d'évolutivité et de concurrence, et je veux récupérer toutes les données avec le champ updated_at avec une date d'il y a quelques jours.
J'ai essayé de nombreux index et requêtes sur un million d'identifiants, et je n'arrive pas à obtenir des performances inférieures à 100 secondes avec le matériel Ronin de Heroku.
Je cherche des suggestions que je n'ai pas essayé de rendre aussi efficaces que possible.
ESSAYER # 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
ESSAYER # 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
ESSAYER # 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
ESSAYER # 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
ESSAYER # 5 (pour un seul mois de données obsolètes)
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
ESSAYER # 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
ESSAYEZ 7 (voir: 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
ESSAYER # 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
ESSAYER # 9 ( index partiel selon la suggestion d'Erwin, voir: 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
ESSAYEZ 10 ( [~ # ~] cluster [~ # ~], selon la suggestion d'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
==> Cette solution semble être la gagnante. Je vais devoir tester à fond pour vérifier les contre-impacts partout dans mon application.
Paramètres DB:
sélectionnez nom, min_val, max_val, boot_val dans 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)
Tout d'abord, est-ce possible? Vous écrivez:
Je veux récupérer toutes les données avec le champ updated_at avec une date de il y a quelques jours .
Mais votre condition WHERE
est:
(date (updated_at)) < (date (maintenant ()) - 7)
Cela ne devrait-il pas être >
?
Pour les performances optimales, vous pourriez ...
Vos index pourraient ressembler à:
CREATE INDEX objects_id_updated_at_idx (updated_at::date DESC NULLS LAST)
WHERE id BETWEEN 0 AND 999999
AND updated_at > '2012-10-01 0:0'::timestamp -- some minimum date
CREATE INDEX objects_id_updated_at_idx (updated_at::date DESC NULLS LAST)
WHERE id BETWEEN 1000000 AND 1999999
AND updated_at > '2012-10-01 0:0'::timestamp -- some minimum date
...
La deuxième condition exclut immédiatement les lignes non pertinentes de l'index, ce qui devrait le rendre plus petit et plus rapide - en fonction de votre distribution de données réelle. Conformément à mon commentaire préliminaire, je suppose que vous voulez plus récent lignes.
La condition automatiquement exclut les valeurs NULL dans updated_at
- que vous semblez autoriser dans le tableau et que vous souhaitez évidemment exclure dans la requête. L'utilité de l'indice se détériore avec le temps. La requête récupère toujours les dernières entrées. Recréez l'index avec une clause WHERE
mise à jour périodiquement. Cela nécessite un verrou exclusif sur la table, alors faites-le en dehors des heures d'ouverture. Il y a aussi CREATE INDEX CONCURRENTLY
pour minimiser la durée du verrouillage:
CREATE INDEX CONCURRENTLY objects_id_up_201211_idx; -- create new idx
DROP INDEX objects_id_up_201210_idx; -- then drop old
Réponse connexe sur SO:
Pour optimiser davantage, vous pouvez utiliser CLUSTER
comme nous l'avons mentionné dans les commentaires. Mais vous avez besoin d'un index complet pour cela. Ne fonctionne pas avec un index partiel. Vous créeriez temporairement:
CREATE INDEX objects_full_idx (id/1000000, updated_at::date DESC NULLS LAST);
Cette forme d'index complet correspond à l'ordre de tri des index partiels ci-dessus.
CLUSTER objects USING objects_full_idx;
ANALYZE objects;
Cela prendra un certain temps, car la table est réécrite physiquement. C'est aussi effectivement un VACUUM FULL
. Il a besoin d'un verrou d'écriture exclusif sur la table, alors faites-le en dehors des heures - à condition que vous puissiez vous le permettre. Encore une fois, il existe une alternative moins invasive: pg_repack
Vous pouvez ensuite supprimer à nouveau l'index. C'est un effet unique. J'essaierais au moins une fois pour voir combien vos requêtes en bénéficient. L'effet se détériore avec les opérations d'écriture ultérieures. Vous pouvez répéter cette procédure en dehors des heures de travail si vous voyez un effet considérable.
Si votre table reçoit beaucoup d'opérations d'écriture, vous devez peser les coûts et les avantages pour cette étape. Pour de nombreuses MISES À JOUR, envisagez de définir un FILLFACTOR
inférieur à 100. Faites-le avant vous CLUSTER
.
SELECT count(*)
FROM objects
WHERE id BETWEEN 0 AND 999999 -- match conditions of partial index!
AND updated_at > '2012-10-01 0:0'::timestamp
AND updated_at::date > (now()::date - 7)
Cette réponse connexe présente une technique plus avancée pour le partitionnement d'index:
Entre autres choses, il fournit un exemple de code pour la (re) création automatique d'index.
PostgreSQL 9.2 + a plusieurs nouvelles fonctionnalités pour vous. scans indexés uniquement à lui seul, cela en vaut la peine.
Assurez-vous que autovacuum
fonctionne correctement. L'énorme gain de CLUSTER
que vous avez signalé peut être dû en partie à l'implicite VACUUM FULL
que vous obtenez de CLUSTER
. Peut-être que cela est configuré automatiquement par Heroku, pas sûr.
Les paramètres de votre question semblent bons. Ce n'est donc probablement pas un problème ici et CLUSTER
était vraiment aussi efficace.
a finalement mûri en Postgres 12 . J'envisagerais de l'utiliser maintenant au lieu du partitionnement d'index manuel (ou du moins en plus). Partitionnement de plage avec updated_at
comme clé de partition. (Outre de multiples améliorations des performances générales, en particulier des données volumineuses et de l'index btree.)