Оптимізація запиту Postgres з великим IN


30

Цей запит отримує список публікацій, створених людьми, яких ви стежите. Ви можете стежити за необмеженою кількістю людей, але більшість людей наслідують <1000 інших.

З цим стилем запитів очевидною оптимізацією було б кешування "Post"ідентифікаторів, але, на жаль, зараз у мене немає часу на це.

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;

Врожайність:

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

Як це можна оптимізувати на даний момент?

У мене є такі відповідні показники:

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

Можливо, для цього потрібен великий частковий складений індекс з createdAtі userIdде private IS NULL?

Відповіді:


29

Замість використання величезного списку IN, приєднайтесь до VALUESвиразу, або якщо список досить великий, використовуйте таблицю темп, індексуйте його, а потім приєднайтесь до нього.

Було б добре, якби PostgreSQL міг зробити це внутрішньо і автоматично, але в цей момент планувальник не знає, як.

Подібні теми:


28

Насправді є два різних варіанти INконструкції в Postgres. Один працює з виразом підзапиту (повертає набір ), інший - зі списком значень , який є просто скороченням для

expression = value1
OR
expression = value2
OR
...

Ви використовуєте другу форму, яка чудово підходить для короткого списку, але набагато повільніше для довгих списків. Надайте свій список значень як вираз підзапиту. Нещодавно мені стало відомо про такий варіант :

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

Мені подобається передавати масив, нестримно і приєднуватися до нього. Подібна продуктивність, але синтаксис коротший:

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

Еквівалентно, доки в наданому наборі / масиві немає дублікатів . В іншому вигляді JOINповертаються дублікати рядків, в той час як перша з INлише повертає один екземпляр. Ця тонка різниця спричиняє і різні плани запитів.

Очевидно, що вам потрібен індекс на "Posts"."userId".
Для дуже довгих списків (тисяч) перейдіть з індексованою таблицею темп, як @Craig запропонував. Це дозволяє комбінувати сканування індексу растрових зображень над обома таблицями, що, як правило, швидше, як тільки з диска виходить кілька кортежів на кожній сторінці даних.

Пов'язані:

Убік: ваша угода про іменування не дуже корисна, робить ваш код багатослівним і важким для читання. Скоріше скористайтеся легальними, мало котированими ідентифікаторами.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.