J'ai une requête avec 2 anti-jointures (seremails = 1m + lignes et abonnements = <lignes de 100k), 2 conditions et une sorte. J'ai créé un index pour les 2 conditions + tri, ce qui a accéléré la requête de 50%. Les deux anti-jonctions ont des indices. Cependant, la requête est trop lente (4 secondes sur la production).
Voici la requête:
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
Voici l'explique:
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
Et voici l'indice qui a amélioré la vitesse de 50%:
CREATE INDEX "users_email_subscribers_idx" ON "public"."Users" USING btree("createdAt" DESC) WHERE "isEmailVerified" = TRUE AND "emailUnsubscribeDate" IS NULL;
Edit: Je devrais aussi mentionner que le sers_email_subscrits_idx affiche une analyse d'index et non Index Seule Scan probable parce que l'indice est mis à jour régulièrement.
Vous avez d'énormes différences entre l'estimation du nombre de lignes par le planificateur et le nombre réel de lignes. Cela signifie que la raboteuse a choisi un plan basé sur de fausses informations.
Par exemple, Nested Loop Anti Join (cost=0.86..3470216.17 rows=2707688 width=49) (actual time=0.809..6062.152 rows=28607 loops=1)
signifie qu'il estimé qu'il obtiendrait 2 707 688 quand il a eu 28 607.
Soit vos statistiques ne sont pas exactes (et si vous n'avez jamais tachée autovacuum
Paramètres pour ces énormes table, je parierais dessus), soit vous avez une colonne qui dépend d'un autre qui ne fait pas partie de la clé (- troisième violation de forme normale ).
Pour rafraîchir votre statique plus fréquemment, vous pouvez régler les paramètres autovacuum
pour ces grandes tables. Je suggère fortement que vous lisez ce message de blog pour comprendre le réglage automatique.
Si votre modèle viole la troisième forme normale, vous pouvez soit corriger votre modèle (coûteux mais mieux pour une vision à long terme) soit laissez la raboteuse collecter des statistiques sur vos colonnes corrélées avec create statistics
(voir la documentation ICI ).
Votre meilleur pari est probablement de résoudre ce problème au niveau de l'application. Cela ressemble à une requête que vous exécutez dans le cadre d'un exercice de nettoyage des données. Si tel est le cas, pourquoi vous souciez-vous s'il faut 6 secondes pour courir et pourquoi le limitez-vous à 100 rangées plutôt que de les lire tous en un coup? Peut-être que vous pourriez utiliser une vue matérialisée ou un autre mécanisme de mise en cache. Si vous rejetez cette option, lisez activement pour certaines options "deuxième meilleures".
Je devrais également mentionner que les utilisateurs_email_subscrits_idx affichent une analyse de l'index et non une analyse de l'index probable, car l'index est mis à jour régulièrement.
Ce n'est pas pourquoi. Vous avez besoin de colonnes de la table des utilisateurs qui ne sont pas contenues dans l'index, telles que le prénom et l'ID. Si vous avez créé un index avec toutes ces colonnes à la fin de la liste de colonnes, vous obtiendrez une analyse d'index uniquement. Cela pourrait rendre la requête 20% plus rapide, mais ne va pas en faire 99% plus rapide.
Heap Fetches: 1153034
Vous devez aspirer des useremails plus agressivement. Encore une fois, ce ne sera pas une amélioration de 99%, mais cela devrait aider certains. Autovacuum n'a pas bon travail pour garder les tables suffisamment aspirés pour optimiser les analyses d'index uniquement. Vous pouvez faire des aspirateurs manuels. Ou vous pouvez essayer de forcer l'autovacuum pour faire un meilleur travail en abaissant le réglage de la table de "Autovacuum_vacuum_scale_factor" à zéro, puis réglez le tableau "Autovacuum_vacuum_threshold" pour contrôler l'aspirateur. Si la table est mise à jour au hasard tout au long de la table, je définirais "Autovacuum_Vacuum_Hreshold" sur environ 1/20 du nombre de blocs dans la table.
Comment fonctionne la requête si vous êtes expérimentalement set enable_nestedloop to off
? Cela vous donnera probablement des jointures anti-hasch et si votre version est suffisamment nouvelle, vous pourriez obtenir des versions parallèles d'entre elles.