Optimieren einer Postgres-Abfrage mit einem großen IN

30

Diese Abfrage enthält eine Liste der Beiträge, die von Personen erstellt wurden, denen Sie folgen. Sie können einer unbegrenzten Anzahl von Personen folgen, aber die meisten Personen folgen <1000 anderen.

Bei diesem Abfragestil wäre die offensichtliche Optimierung, die "Post"IDs zwischenzuspeichern, aber dafür habe ich momentan leider keine Zeit.

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;

Erträge:

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

Wie kann dies vorerst optimiert werden?

Ich habe die folgenden relevanten Indizes:

-- 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;

Vielleicht erfordert dies einen großen Teil-Composite-Index mit createdAtund userIdwo private IS NULL?

Garrett
quelle

Antworten:

29

Anstatt eine große INListe zu verwenden, verknüpfen Sie einen VALUESAusdruck. Wenn die Liste jedoch groß genug ist, verwenden Sie eine temporäre Tabelle, indizieren Sie sie und verknüpfen Sie sie.

Es wäre schön, wenn PostgreSQL dies intern und automatisch tun könnte, aber zu diesem Zeitpunkt weiß der Planer nicht, wie.

Ähnliche Themen:

Craig Ringer
quelle
28

Es gibt tatsächlich zwei verschiedene Varianten des INKonstrukts in Postgres. Einer arbeitet mit einem Unterabfrageausdruck (der eine Menge zurückgibt ), der andere mit einer Liste von Werten , die nur eine Abkürzung für ist

expression = value1
OR
expression = value2
OR
...

Sie verwenden das zweite Formular, das für eine kurze Liste in Ordnung ist, für lange Listen jedoch viel langsamer. Geben Sie stattdessen Ihre Werteliste als Unterabfrageausdruck an. Ich wurde kürzlich auf diese Variante aufmerksam gemacht :

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

Ich mag es, ein Array zu übergeben, unnest und sich ihm anzuschließen. Ähnliche Leistung, aber die Syntax ist kürzer:

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

Gleichbedeutend, solange das angegebene Set / Array keine Duplikate enthält . Anderenfalls gibt das zweite Formular mit JOINdoppelten Zeilen zurück, während das erste mit INnur einer Instanz zurückgibt. Dies subtile Unterschied führt auch zu unterschiedlichen Abfrageplänen.

Offensichtlich benötigen Sie einen Index für "Posts"."userId".
Für sehr lange Listen (Tausende) empfehlen wir eine indizierte temporäre Tabelle wie @Craig. Dies ermöglicht kombinierte Bitmap-Index-Scans über beide Tabellen, was in der Regel schneller ist, sobald mehrere Tupel pro Datenseite von der Festplatte abgerufen werden müssen.

Verbunden:

Abgesehen davon: Ihre Namenskonvention ist nicht sehr hilfreich, macht Ihren Code ausführlich und schwer zu lesen. Verwenden Sie lieber legale Bezeichner ohne Anführungszeichen in Kleinbuchstaben.

Erwin Brandstetter
quelle