PostgreSQL 9.0.8にオブジェクトの大きなテーブル(1500万行以上)があり、古いフィールドをクエリしたい。
スケーラビリティと同時実行性を確保するために、クエリを数百万で除算し、数日前の日付のupdated_atフィールドですべてのデータをフェッチしたいと思います。
100万のIDで多くのインデックスとクエリを試しましたが、HerokuのRoninハードウェアでは100秒未満でパフォーマンスを得ることができません。
私はこれを可能な限り効率的にすることを試みていない提案を探しています。
TRY#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
TRY#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
トライ#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
TRY#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
TRY#5(1か月の古いデータ)
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
TRY#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
TRY#7(参照: 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
TRY#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
TRY#9(部分索引、Erwinの提案に従って、以下を参照してください: 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
TRY#10([〜#〜] cluster [〜#〜]、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
==>この解決策が勝者のようです。私は、アプリケーションのあらゆる場所でカウンターインパクトを検証するために徹底的にテストする必要があります。
DB設定:
pg_settingsからname、min_val、max_val、boot_valを選択します。
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)
最初に、それはできますか?あなたが書く:
数日前の日付でupdated_atフィールドを含むすべてのデータをフェッチしたい。
しかし、WHERE
条件は次のとおりです。
(日付(updated_at)) < (日付(現在())-7)
>
にすべきではありませんか?
最適なパフォーマンスを得るには、...
インデックスは次のようになります。
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
...
2番目の条件は、無関係な行をインデックスからすぐに除外します。これにより、実際のデータ分布に応じて、より小さく、より速くなります。私の予備的なコメントに従って、私はあなたがnewer行が欲しいと仮定しています。
条件も自動的にupdated_at
のNULL値を除外します。これは、テーブルで許可しているようで、クエリで除外したいことは明らかです。インデックスの有用性は時間とともに低下します。クエリは常に最新のエントリを取得します。更新されたWHERE
句を使用してインデックスを定期的に再作成します。これにはテーブルの排他ロックが必要なので、営業時間外に行います。ロックの期間を最小限に抑えるためのCREATE INDEX CONCURRENTLY
もあります。
CREATE INDEX CONCURRENTLY objects_id_up_201211_idx; -- create new idx
DROP INDEX objects_id_up_201210_idx; -- then drop old
SOの関連回答:
さらに最適化するには、コメントで述べたように CLUSTER
を使用できます。ただし、そのためにはfullインデックスが必要です。部分インデックスでは機能しません。一時的に作成します:
CREATE INDEX objects_full_idx (id/1000000, updated_at::date DESC NULLS LAST);
この形式の完全索引は、上記の部分索引のソート順序と一致します。
CLUSTER objects USING objects_full_idx;
ANALYZE objects;
テーブルが物理的に書き換えられるため、これにはしばらく時間がかかります。事実上、VACUUM FULL
でもあります。テーブルに排他的な書き込みロックが必要なため、営業時間外に行う必要があります。繰り返しになりますが、より侵襲性の低い代替手段があります: pg_repack
その後、インデックスを再度削除できます。これは1回限りの効果です。クエリを少なくとも1回試して、クエリがどれほどの利益を得るかを確認します。効果は、後続の書き込み操作で悪化します。かなりの影響が見られる場合は、時間外にこの手順を繰り返すことができます。
テーブルが大量の書き込み操作を受け取る場合、このステップのコストと利益を比較検討する必要があります。多くのUPDATEでは、 FILLFACTOR
を100未満に設定することを検討してください。それを行うbeforeCLUSTER
。
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)
この関連する回答は、インデックスのパーティション分割のためのより高度な手法を示しています。
特に、自動インデックス(再)作成のサンプルコードを提供します。
PostgreSQL 9.2+にはいくつかの新機能があります。 インデックスのみのスキャンだけでも、それだけの価値があります。
autovacuum
が正しく実行されていることを確認します。あなたが報告したCLUSTER
による大きな利益は、一部にはCLUSTER
から得られる暗黙的なVACUUM FULL
が原因である可能性があります。たぶん、これはHerokuによって自動的に設定されます。
質問の設定は問題ありません。したがって、これはおそらくここでは問題ではなく、CLUSTER
は本当に効果的でした。
Postgres 12でようやく成熟した 。手動でのインデックスのパーティション分割の代わりに(または少なくとも追加的に)これを使用することを検討します。 updated_at
をパーティションキーとして使用した範囲パーティション化。 (一般的なパフォーマンスに対する複数の改善に加えて、特にビッグデータとbtreeインデックスのパフォーマンス。)