Як обробити поганий план запитів, викликаний точною рівністю за типом діапазону?


28

Я здійснюю оновлення, де мені потрібна точна рівність tstzrangeзмінної. ~ 1М рядків змінено, і запит займає ~ 13 хвилин. Результат EXPLAIN ANALYZEможна побачити тут , а фактичні результати сильно відрізняються від результатів, оцінених планувальником запитів. Проблема полягає в тому, що сканування індексу t_rangeочікує повернення одного рядка.

Це, мабуть, пов'язане з тим, що статистичні дані про типи діапазонів зберігаються інакше, ніж дані інших типів. Дивлячись на pg_statsвигляд стовпця, n_distinctє -1, а інші поля (наприклад most_common_vals, most_common_freqs) порожні.

Однак має бути t_rangeдесь збережена статистика . Надзвичайно подібне оновлення, коли я використовую "в межах" на t_range замість точної рівності, займає близько 4 хвилин, і використовує істотно інший план запитів (див. Тут ). Другий план запитів має сенс для мене, оскільки буде використаний кожен рядок у темп-таблиці та значна частина таблиці історії. Що ще важливіше, планувальник запитів передбачає приблизно правильну кількість рядків для фільтра t_range.

Розподіл t_rangeдещо незвично. Я використовую цю таблицю для зберігання історичного стану іншої таблиці, а зміни в іншій таблиці відбуваються відразу на великих звалищах, тому не так багато різних значень t_range. Ось підрахунки, що відповідають кожному унікальному значенню t_range:

                              t_range                              |  count  
-------------------------------------------------------------------+---------
 ["2014-06-12 20:58:21.447478+00","2014-06-27 07:00:00+00")        |  994676
 ["2014-06-12 20:58:21.447478+00","2014-08-01 01:22:14.621887+00") |   36791
 ["2014-06-27 07:00:00+00","2014-08-01 07:00:01+00")               | 1000403
 ["2014-06-27 07:00:00+00",infinity)                               |   36791
 ["2014-08-01 07:00:01+00",infinity)                               |  999753

Підрахунки для окремих t_rangeвище повні, тому кардинальність становить ~ 3М (з яких на ~ 1М впливатиме будь-який запит оновлення).

Чому запит 1 працює набагато поганіше, ніж запит 2? У моєму випадку запит 2 - це хороша заміна, але якщо точно потрібна рівність діапазону, як я можу отримати Postgres використовувати більш розумний план запитів?

Визначення таблиці з індексами (випадання нерелевантних стовпців):

       Column        |   Type    |                                  Modifiers                                   
---------------------+-----------+------------------------------------------------------------------------------
 history_id          | integer   | not null default nextval('gtfs_stop_times_history_history_id_seq'::regclass)
 t_range             | tstzrange | not null
 trip_id             | text      | not null
 stop_sequence       | integer   | not null
 shape_dist_traveled | real      | 
Indexes:
    "gtfs_stop_times_history_pkey" PRIMARY KEY, btree (history_id)
    "gtfs_stop_times_history_t_range" gist (t_range)
    "gtfs_stop_times_history_trip_id" btree (trip_id)

Запит 1:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND sth.t_range = '["2014-08-01 07:00:01+00",infinity)'::tstzrange;

Запит 2:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND '2014-08-01 07:00:01+00'::timestamptz <@ sth.t_range;

Q1 оновлює 999753 рядків, а Q2 оновлення 999753 + 36791 = 1036544 (тобто таблиця темп така, що кожне рядок, що відповідає умові часового діапазону, оновлюється).

Я спробував цей запит у відповідь на коментар @ ypercube :

Запит 3:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND sth.t_range <@ '["2014-08-01 07:00:01+00",infinity)'::tstzrange
AND '["2014-08-01 07:00:01+00",infinity)'::tstzrange <@ sth.t_range;

План запиту та результати (див. Тут ) були проміжними між двома попередніми випадками (~ 6 хвилин).

2016/02/05 EDIT

Через 1,5 року більше не маючи доступу до даних, я створив тестову таблицю з тією ж структурою (без індексів) та подібною кардинальністю. У відповіді jjanes запропоновано, що причиною може бути впорядкування тимчасової таблиці, яка використовується для оновлення. Мені не вдалося перевірити гіпотезу безпосередньо, оскільки я не маю доступу track_io_timing(використовуючи Amazon RDS).

  1. Загальні результати були набагато швидшими (в декілька разів). Я здогадуюсь, що це пов’язано з вилученням індексів, що відповідає відповіді Ервіна .

  2. У цьому тестовому випадку запити 1 і 2 в основному займали однакову кількість часу, оскільки вони обидва використовували об'єднання злиття. Тобто, я не зміг викликати те, що спричиняло Postgres вибирати хеш-з'єднання, тому я не маю ясності, чому Postgres вибирав в першу чергу неякісний хеш-приєднання.


1
Що робити, якщо ви перетворили умову рівності (a = b)на дві умови "містить" (a @> b AND b @> a):? Чи змінюється план?
ypercubeᵀᴹ

@ypercube: план істотно змінюється, хоча все ще не зовсім оптимально - дивіться мою редакцію №2.
abeboparebop

1
Іншою ідеєю було б додати звичайний індекс btree, (lower(t_range),upper(t_range))оскільки ви перевіряєте рівність.
ypercubeᵀᴹ

Відповіді:


9

Найбільша різниця в часі у ваших планах виконання - це верхній вузол, саме ОНОВЛЕННЯ. Це говорить про те, що більшість вашого часу під час оновлення збирається введення вводу. Ви можете перевірити це, включивши track_io_timingі запустивши запити за допомогоюEXPLAIN (ANALYZE, BUFFERS)

Різні плани представляють рядки, які потрібно оновлювати в різних порядках. Одне в trip_idпорядку, а інше в тому порядку, в якому вони фізично перебувають у темп-таблиці.

Оновлена ​​таблиця, здається, має фізичний порядок, пов'язаний зі стовпцем trip_id, і оновлення рядків у цьому порядку призводить до ефективних моделей вводу-виводу з читанням вперед / послідовним зчитуванням. Хоча фізичний порядок темп-таблиці, здається, призводить до безлічі випадкових зчитувань.

Якщо ви можете додати order by trip_idдо заяви, який створив таблицю темпів, це може вирішити проблему для вас.

PostgreSQL не враховує наслідків впорядкування вводу-виводу при плануванні операції UPDATE. (На відміну від операцій SELECT, де вони їх враховують). Якби PostgreSQL був розумнішим, він би або усвідомлював, що один план створює більш ефективний порядок, або він би втручав явний сортувальний вузол між оновленням і його дочірнім вузлом, щоб оновлення подавало рядки в ctid порядку.

Ви вірні, що PostgreSQL виконує погану роботу, визначаючи вибірковість рівності приєднання на діапазонах. Однак це стосується лише вашої основної проблеми. Більш ефективний запит у виділеній частині оновлення може випадково трапити рядки в належне оновлення в кращому порядку, але якщо так, то це в основному не пощастить.


На жаль, я не в змозі змінити track_io_timing, і (оскільки минуло півтора року!) Я більше не маю доступу до оригінальних даних. Однак я перевірив вашу теорію, створивши таблиці з однаковою схемою та подібним розміром (мільйони рядків) та запустивши два різні оновлення - одне, в якому таблицю оновлення темпів було відсортовано як оригінальну таблицю, а інше, в якій її сортували квазі випадковим чином. На жаль, два оновлення займають приблизно однакову кількість часу, що означає, що впорядкування таблиці оновлень не впливає на цей запит.
abeboparebop

7

Я не точно впевнений, чому вибірковість предиката рівності настільки докорінно завищена індексом GiST на tstzrangeстовпчику. Хоча це як і раніше залишається цікавим, здається, не має значення для вашого конкретного випадку.

Оскільки ваші UPDATEмодифікують третину (!) Усіх існуючих рядків 3M, індекс зовсім не допоможе . Навпаки, поступове оновлення індексу на додаток до таблиці призведе до значних витрат на вашу UPDATE.

Просто тримайте простий Запит 1 . Просте, радикальне рішення - скинути індекс перед UPDATE. Якщо вам це потрібно для інших цілей, створіть його знову після UPDATE. Це все-таки буде швидше, ніж підтримка індексу під час великих UPDATE.

Для UPDATEтретьої частини всіх рядків, ймовірно, заплатить скинути всі інші індекси - і заново створити їх після UPDATE. Єдиний мінус: вам потрібні додаткові привілеї та ексклюзивний замок на столі (лише на короткий момент, якщо ви використовуєте CREATE INDEX CONCURRENTLY).

Ідея @ ypercube використовувати btree замість індексу GiST в принципі виглядає добре. Але не для третини всіх рядків (де індекс не є корисним для початку), а не просто (lower(t_range),upper(t_range)), оскільки це tstzrangeне дискретний тип діапазону.

Більшість типів дискретних діапазонів мають канонічну форму, що робить поняття "рівності" більш простим: нижня і верхня межа значення в канонічній формі визначають його. Документація:

Тип дискретного діапазону повинен мати функцію канонізації, яка усвідомлює бажаний розмір кроку для типу елемента. Функція канонізації заряджається перетворенням еквівалентних значень типу діапазону для того, щоб мати однакові зображення, зокрема послідовно включені або виключні межі. Якщо функція канонізації не вказана, то діапазони з різним форматуванням завжди трактуються як неоднакові, навіть якщо вони можуть представляти однаковий набір значень у дійсності.

Вбудовані типи дальності int4range, int8rangeі daterangeбудь-яке використання канонічної формі , яка включає в себе нижню межу і НЕ включає верхню межу; тобто [). Тим не менш, визначені користувачем типи діапазонів можуть використовувати інші умови.

Це не так tstzrange, коли інклюзивність верхньої та нижньої меж потрібно вважати рівністю. Можливий індекс btree повинен бути на:

(lower(t_range), upper(t_range), lower_inc(t_range), upper_inc(t_range))

І запити повинні були використовувати однакові вирази в WHEREпункті.

Можна спокусити просто проіндексувати ціле значення, подане на text: (cast(t_range AS text))- але це вираження не є, IMMUTABLEоскільки текстове представлення timestamptzзначень залежить від поточного timezoneналаштування. Вам потрібно буде ввести додаткові кроки у функцію IMMUTABLEобгортки, яка створює канонічну форму, і створити функціональний індекс для цього ...

Додаткові заходи / альтернативні ідеї

Якщо shape_dist_traveledви вже можете мати те саме значення, що і tt.shape_dist_traveledдля декількох оновлених рядків (і ви не покладаєтесь на будь-які побічні ефекти UPDATEподібних тригерів ...), ви можете зробити запит швидше, виключивши порожні оновлення:

WHERE ...
AND   shape_dist_traveled IS DISTINCT FROM tt.shape_dist_traveled;

Звичайно, застосовуються всі загальні поради щодо оптимізації продуктивності. Вікі Postgres - хороша відправна точка.

VACUUM FULLбуде для вас отрутою, оскільки деякі мертві кортежі (або відведене місце FILLFACTOR) корисні для UPDATEпродуктивності.

З цією кількістю оновлених рядків, і якщо ви можете собі це дозволити (без паралельного доступу чи інших залежностей), можливо, буде ще швидше написати абсолютно нову таблицю замість того, щоб оновити на місці. Інструкції з цієї відповіді:

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