2つのアンチ結合(serEmails = 1M + rowsおよびSubscriptions = <100k rows)、2つの条件、およびソートを使用したクエリがあります。 2つの条件と並べ替えのインデックスを作成しました。これにより、クエリが50%高速化しました。両方の逆結合にはインデックスがあります。ただし、クエリが遅すぎる(本番環境では4秒)。
これがクエリです:
SELECT
"Users"."firstName",
"Users"."lastName",
"Users"."email",
"Users"."id"
FROM
"Users"
WHERE
NOT EXISTS (
SELECT
1
FROM
"UserEmails"
WHERE
"UserEmails"."userId" = "Users". ID
)
AND NOT EXISTS (
SELECT
1
FROM
"Subscriptions"
WHERE
"Subscriptions"."userId" = "Users". ID
)
AND "isEmailVerified" = TRUE
AND "emailUnsubscribeDate" IS NULL
ORDER BY
"Users"."createdAt" DESC
LIMIT 100
ここに説明があります:
Limit (cost=1.28..177.77 rows=100 width=49) (actual time=6171.121..6171.850 rows=100 loops=1)
-> Nested Loop Anti Join (cost=1.28..4665810.76 rows=2643614 width=49) (actual time=6171.119..6171.807 rows=100 loops=1)
-> Nested Loop Anti Join (cost=0.86..3470216.17 rows=2707688 width=49) (actual time=0.809..6062.152 rows=28607 loops=1)
-> Index Scan using users_email_subscribers_idx on "Users" (cost=0.43..1844686.50 rows=3312999 width=49) (actual time=0.055..2342.793 rows=1186607 loops=1)
-> Index Only Scan using "UserEmails_userId_emailId_key" on "UserEmails" (cost=0.43..0.49 rows=1 width=4) (actual time=0.002..0.002 rows=1 loops=1186607)
Index Cond: ("userId" = "Users".id)
Heap Fetches: 1153034
-> Index Only Scan using "Subscriptions_userId_type_key" on "Subscriptions" (cost=0.42..0.44 rows=1 width=4) (actual time=0.003..0.003 rows=1 loops=28607)
Index Cond: ("userId" = "Users".id)
Heap Fetches: 28507
Planning time: 2.346 ms
Execution time: 6171.963 ms
そして、これは速度を50%改善したインデックスです:
CREATE INDEX "users_email_subscribers_idx" ON "public"."Users" USING btree("createdAt" DESC) WHERE "isEmailVerified" = TRUE AND "emailUnsubscribeDate" IS NULL;
編集:sers_email_subscribers_idxはIndex Scanを表示しており、を表示していないことにも言及する必要がありますインデックスのみがスキャンされますインデックスが定期的に更新されている可能性があります。
プランナーによる行数の見積もりと実際の行数には大きな違いがあります。これは、プレーナーが誤った情報に基づいてプランを選択したことを意味します。
たとえば、Nested Loop Anti Join (cost=0.86..3470216.17 rows=2707688 width=49) (actual time=0.809..6062.152 rows=28607 loops=1)
は、実際に28 607を取得したときに2 707 688を取得すると推定したことを意味します。
統計が正確でない(そして、これらの巨大なテーブルのautovacuum
設定を一度も調整していない場合、私はそれに賭けるでしょう)、キーの一部ではない別の列に依存する1つの列がある(- 第3正規形違反 )。
Staticticsをより頻繁に更新するには、これらの大きなテーブルのautovacuum
設定を調整できます。自動バキュームのチューニングを理解するには、 そのブログ投稿 を読むことを強くお勧めします。
モデルが第3正規形に違反している場合は、モデルを修正する(コストがかかるが、長期的なビジョンにはより良い)か、Planerにcreate statistics
を使用して相関列の統計を収集させます(ドキュメントを参照 こちら )。
あなたの最善の策は、おそらくアプリケーションレベルでこれを解決することです。これは、データクリーニングの一環として実行するクエリのように見えます。もしそうなら、なぜ実行に6秒かかるか気にし、なぜすべてを一度に読み取るのではなく100行に制限するのですか?おそらく、マテリアライズドビューまたはその他のキャッシュメカニズムを使用できます。そのオプションを拒否する場合は、「次善の」オプションをいくつか読んでください。
また、users_email_subscribers_idxは、インデックスが定期的に更新されているため、インデックスのみのスキャンではなく、インデックススキャンを示していることにも言及する必要があります。
それが理由ではありません。インデックスに含まれていない、Usersテーブルの列(firstNameやidなど)が必要です。列リストの最後にそれらすべての列を含むインデックスを作成した場合、インデックスのみのスキャンが行われます。これにより、クエリが20%高速になる可能性がありますが、99%高速になるわけではありません。
Heap Fetches: 1153034
UserEmailをより積極的にバキュームする必要があります。繰り返しになりますが、99%の改善にはなりませんが、ある程度は役立つはずです。 Autovacuumは、インデックスのみのスキャンを最適化するためにテーブルを十分にバキューム処理した状態に保つのに適していません。手動掃除機を使用できます。または、テーブルごとの "autovacuum_vacuum_scale_factor"の設定をゼロに下げ、テーブルごとに "autovacuum_vacuum_threshold"を設定してバキューム処理を制御することで、autovacuumがより適切に機能するように強制できます。テーブルがテーブル全体でランダムに更新される場合、「autovacuum_vacuum_threshold」をテーブル内のブロック数の約1/20に設定します。
実験的にset enable_nestedloop to off
を実行すると、クエリはどのように機能しますか?これはおそらくハッシュアンチジョインを与えるでしょう、そしてあなたのバージョンが十分に新しい場合、それらの並列バージョンを得るかもしれません。