Как справиться с неправильным планом запроса, вызванным точным равенством по типу диапазона?

28

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

Похоже, это связано с тем, что статистика по типам диапазонов хранится не так, как статистика других типов. Глядя на pg_statsпредставление для столбца, n_distinctэто -1, а другие поля (например most_common_vals, most_common_freqs) пусты.

Тем не менее, где-то должна храниться статистика t_range. Чрезвычайно похожее обновление, где я использую 'inside' для 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выше полны, поэтому количество элементов составляет ~ 3M (из которых ~ 1M будет зависеть от любого запроса на обновление).

Почему запрос 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 РЕДАКТИРОВАТЬ

Больше не имея доступа к данным через 1,5 года, я создал тестовую таблицу с той же структурой (без индексов) и схожим количеством элементов. В ответе jjanes предполагалось, что причиной может быть упорядочение временной таблицы, используемой для обновления. Я не смог проверить гипотезу напрямую, потому что у меня нет доступа к ней track_io_timing(с помощью Amazon RDS).

  1. Общие результаты были намного быстрее (в несколько раз). Я предполагаю, что это из-за удаления индексов, в соответствии с ответом Эрвина .

  2. В этом тестовом примере запросы 1 и 2 в основном занимали одинаковое количество времени, поскольку они оба использовали объединение слиянием. То есть я не смог вызвать то, что заставило Postgres выбрать хеш-соединение, поэтому у меня нет ясности относительно того, почему Postgres выбрал плохо работающее хеш-соединение.

abeboparebop
источник
1
Что делать , если вы преобразовали условие равенства (a = b)два «содержит» условия: (a @> b AND b @> a)? Меняется ли план?
ypercubeᵀᴹ
@ypercube: план существенно меняется, хотя он все еще не совсем оптимален - см. мою редакцию №2.
abeboparebop
1
Другая идея заключается в добавлении обычного индекса btree, (lower(t_range),upper(t_range))поскольку вы проверяете равенство.
ypercubeᵀᴹ

Ответы:

9

Самая большая разница во времени в ваших планах выполнения находится на верхнем узле, самом ОБНОВЛЕНИИ. Это говорит о том, что большая часть вашего времени уходит на IO во время обновления. Вы можете убедиться в этом, включив track_io_timingи выполнив запросы сEXPLAIN (ANALYZE, BUFFERS)

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

Обновляемая таблица, кажется, имеет свой физический порядок, соотнесенный со столбцом trip_id, и обновление строк в этом порядке приводит к эффективным шаблонам ввода-вывода с чтением вперед / последовательным чтением. В то время как физический порядок временной таблицы, кажется, приводит к большому количеству случайных чтений.

Если вы можете добавить order by trip_idв оператор, который создал временную таблицу, это может решить проблему для вас.

PostgreSQL не учитывает эффекты упорядочения ввода-вывода при планировании операции UPDATE. (В отличие от операций SELECT, где он учитывает их). Если бы PostgreSQL был умнее, он либо понял бы, что один план создает более эффективный порядок, либо вставил бы явный узел сортировки между обновлением и его дочерним узлом, чтобы обновление получало строки в порядке ctid.

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

jjanes
источник
К сожалению, я не могу изменить 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 Wiki - хорошая отправная точка.

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

С таким количеством обновленных строк, и если вы можете себе это позволить (без одновременного доступа или других зависимостей), может быть даже быстрее написать совершенно новую таблицу вместо обновления на месте. Инструкции в этом связанном ответе:

Эрвин Брандштеттер
источник