web-dev-qa-db-fra.com

Optimisation d'une requête Postgres avec un grand IN

Cette requête obtient une liste des publications créées par les personnes que vous suivez. Vous pouvez suivre un nombre illimité de personnes, mais la plupart des personnes suivent <1000 autres.

Avec ce style de requête, l'optimisation évidente serait de mettre en cache le "Post" ids, mais malheureusement je n'ai pas le temps pour le moment.

EXPLAIN ANALYZE SELECT
    "Post"."id",
    "Post"."actionId",
    "Post"."commentCount",
    ...
FROM
    "Posts" AS "Post"
INNER JOIN "Users" AS "user" ON "Post"."userId" = "user"."id"
LEFT OUTER JOIN "ActivityLogs" AS "activityLog" ON "Post"."activityLogId" = "activityLog"."id"
LEFT OUTER JOIN "WeightLogs" AS "weightLog" ON "Post"."weightLogId" = "weightLog"."id"
LEFT OUTER JOIN "Workouts" AS "workout" ON "Post"."workoutId" = "workout"."id"
LEFT OUTER JOIN "WorkoutLogs" AS "workoutLog" ON "Post"."workoutLogId" = "workoutLog"."id"
LEFT OUTER JOIN "Workouts" AS "workoutLog.workout" ON "workoutLog"."workoutId" = "workoutLog.workout"."id"
WHERE
"Post"."userId" IN (
    201486,
    1825186,
    998608,
    340844,
    271909,
    308218,
    341986,
    216893,
    1917226,
    ...  -- many more
)
AND "Post"."private" IS NULL
ORDER BY
    "Post"."createdAt" DESC
LIMIT 10;

Rendements:

Limit  (cost=3.01..4555.20 rows=10 width=2601) (actual time=7923.011..7973.138 rows=10 loops=1)
  ->  Nested Loop Left Join  (cost=3.01..9019264.02 rows=19813 width=2601) (actual time=7923.010..7973.133 rows=10 loops=1)
        ->  Nested Loop Left Join  (cost=2.58..8935617.96 rows=19813 width=2376) (actual time=7922.995..7973.063 rows=10 loops=1)
              ->  Nested Loop Left Join  (cost=2.15..8821537.89 rows=19813 width=2315) (actual time=7922.984..7961.868 rows=10 loops=1)
                    ->  Nested Loop Left Join  (cost=1.71..8700662.11 rows=19813 width=2090) (actual time=7922.981..7961.846 rows=10 loops=1)
                          ->  Nested Loop Left Join  (cost=1.29..8610743.68 rows=19813 width=2021) (actual time=7922.977..7961.816 rows=10 loops=1)
                                ->  Nested Loop  (cost=0.86..8498351.81 rows=19813 width=1964) (actual time=7922.972..7960.723 rows=10 loops=1)
                                      ->  Index Scan using posts_createdat_public_index on "Posts" "Post"  (cost=0.43..8366309.39 rows=20327 width=261) (actual time=7922.869..7960.509 rows=10 loops=1)
                                            Filter: ("userId" = ANY ('{201486,1825186,998608,340844,271909,308218,341986,216893,1917226, ... many more ...}'::integer[]))
                                            Rows Removed by Filter: 218360
                                      ->  Index Scan using "Users_pkey" on "Users" "user"  (cost=0.43..6.49 rows=1 width=1703) (actual time=0.005..0.006 rows=1 loops=10)
                                            Index Cond: (id = "Post"."userId")
                                ->  Index Scan using "ActivityLogs_pkey" on "ActivityLogs" "activityLog"  (cost=0.43..5.66 rows=1 width=57) (actual time=0.107..0.107 rows=0 loops=10)
                                      Index Cond: ("Post"."activityLogId" = id)
                          ->  Index Scan using "WeightLogs_pkey" on "WeightLogs" "weightLog"  (cost=0.42..4.53 rows=1 width=69) (actual time=0.001..0.001 rows=0 loops=10)
                                Index Cond: ("Post"."weightLogId" = id)
                    ->  Index Scan using "Workouts_pkey" on "Workouts" workout  (cost=0.43..6.09 rows=1 width=225) (actual time=0.001..0.001 rows=0 loops=10)
                          Index Cond: ("Post"."workoutId" = id)
              ->  Index Scan using "WorkoutLogs_pkey" on "WorkoutLogs" "workoutLog"  (cost=0.43..5.75 rows=1 width=61) (actual time=1.118..1.118 rows=0 loops=10)
                    Index Cond: ("Post"."workoutLogId" = id)
        ->  Index Scan using "Workouts_pkey" on "Workouts" "workoutLog.workout"  (cost=0.43..4.21 rows=1 width=225) (actual time=0.004..0.004 rows=0 loops=10)
              Index Cond: ("workoutLog"."workoutId" = id)
Total runtime: 7974.524 ms

Comment cela peut-il être optimisé pour le moment?

J'ai les index pertinents suivants:

-- Gets used
CREATE INDEX  "posts_createdat_public_index" ON "public"."Posts" USING btree("createdAt" DESC) WHERE "private" IS null;
-- Don't get used
CREATE INDEX  "posts_userid_fk_index" ON "public"."Posts" USING btree("userId");
CREATE INDEX  "posts_following_index" ON "public"."Posts" USING btree("userId", "createdAt" DESC) WHERE "private" IS null;

Cela nécessite peut-être un grand index composite partiel avec createdAt et userIdprivate IS NULL?

36
Garrett

Au lieu d'utiliser une énorme liste IN-, joignez-vous sur une expression VALUES, ou si la liste est suffisamment grande, utilisez une table temporaire, indexez-la, puis joignez-la.

Ce serait bien si PostgreSQL pouvait le faire en interne et automatiquement, mais à ce stade, le planificateur ne sait pas comment.

Sujets similaires:

36
Craig Ringer

Il existe en fait deux variantes différentes de la construction IN dans Postgres. On travaille avec une expression de sous-requête (renvoyant un set), l'autre avec une liste de valeurs , qui est juste un raccourci pour

expression = value1
OR
expression = value2
OR
...

Vous utilisez le deuxième formulaire, ce qui est bien pour une liste courte, mais beaucoup plus lent pour les listes longues. Fournissez plutôt votre liste de valeurs comme expression de sous-requête. J'ai récemment été mis au courant de cette variante :

WHERE "Post"."userId" IN (VALUES (201486), (1825186), (998608), ... )

J'aime passer un tableau, imbriquer et y adhérer. Performances similaires, mais la syntaxe est plus courte:

...
FROM   unnest('{201486,1825186,998608, ...}'::int[]) "userId"
JOIN   "Posts" "Post" USING ("userId")

Équivalent tant qu'il n'y a pas de doublons dans l'ensemble/tableau fourni. Sinon, le second formulaire avec un JOIN renvoie des lignes en double, tandis que le premier avec IN ne renvoie qu'une seule instance. Cette différence subtile entraîne également des plans de requête différents.

De toute évidence, vous avez besoin d'un index sur "Posts"."userId".
Pour très de longues listes (en milliers), choisissez une table temporaire indexée comme @Craig le suggère. Cela permet des analyses d'index bitmap combinées sur les deux tables, ce qui est généralement plus rapide dès qu'il y a plusieurs tuples par page de données à extraire du disque.

En relation:

En plus: votre convention de nommage n'est pas très utile, rend votre code verbeux et difficile à lire. Utilisez plutôt des identifiants légaux, minuscules et sans guillemets.

29
Erwin Brandstetter