Mengoptimalkan kueri Postgres dengan IN besar

30

Kueri ini mendapatkan daftar posting yang dibuat oleh orang yang Anda ikuti. Anda dapat mengikuti jumlah orang yang tidak terbatas, tetapi kebanyakan orang mengikuti <1000 lainnya.

Dengan gaya permintaan ini, optimasi yang jelas adalah dengan men-cache "Post"id, tetapi sayangnya saya tidak punya waktu untuk itu sekarang.

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;

Hasil:

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

Bagaimana ini dapat dioptimalkan untuk saat ini?

Saya memiliki indeks relevan berikut:

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

Mungkin ini memerlukan sebagian besar indeks komposit dengan createdAtdan di userIdmana private IS NULL?

Garrett
sumber

Jawaban:

28

Sebenarnya ada dua varian INkonstruk di Postgres. Satu bekerja dengan ekspresi subquery (mengembalikan set ), yang lain dengan daftar nilai , yang hanya singkatan untuk

expression = value1
OR
expression = value2
OR
...

Anda menggunakan formulir kedua, yang bagus untuk daftar pendek, tetapi jauh lebih lambat untuk daftar panjang. Berikan daftar nilai Anda sebagai ekspresi subquery sebagai gantinya. Saya baru-baru ini menyadari varian ini :

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

Saya suka melewatkan array, undest, dan bergabung dengannya. Performa yang sama, tetapi sintaks lebih pendek:

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

Setara selama tidak ada duplikat di set / array yang disediakan. Hapus formulir kedua dengan JOINmengembalikan baris duplikat, sedangkan yang pertama dengan INhanya mengembalikan satu contoh. Perbedaan yang halus ini menyebabkan rencana kueri yang berbeda juga.

Jelas, Anda memerlukan indeks "Posts"."userId".
Untuk daftar yang sangat panjang (ribuan), gunakan tabel temp yang diindeks seperti yang disarankan @Craig. Hal ini memungkinkan pemindaian indeks bitmap gabungan pada kedua tabel, yang biasanya lebih cepat segera setelah ada beberapa tupel per halaman data untuk diambil dari disk.

Terkait:

Selain itu: konvensi penamaan Anda tidak terlalu membantu, membuat kode Anda verbose dan sulit dibaca. Alih-alih menggunakan pengidentifikasi hukum, huruf kecil, tanda kutip

Erwin Brandstetter
sumber