Суть
Запрос захвата в InboxDataProviderEf передаёт дискриминаторы параметром-массивом (= ANY({0})). На PostgreSQL 17+ это отменяет обрыв упорядоченного обхода индекса по литеральному LIMIT: планировщик не знает значений массива на этапе планирования, поэтому ORDER BY ScheduledStartIndexing LIMIT 25 перестаёт останавливать обход на первых 25 строках.
На целевой для проекта PostgreSQL 16.14 проблемы нет — там IN (a, b) нормализуется в тот же = ANY, планы тождественны. Поэтому на сегодня это не дефект, а отложенное ограничение, которое сработает при апгрейде БД.
Замеры
Схема и индексы из InboxEfExtensions, 196k чужих + 4k своих созревших сообщений.
PostgreSQL 18.4, = ANY(ARRAY[...]) — как в коде сейчас:
Sort (rows=25) Sort Method: quicksort Memory: 283kB
-> Bitmap Heap Scan (actual rows=3975) <- материализованы ВСЕ свои созревшие
PostgreSQL 18.4, IN (лит, лит) — на том же индексе:
Index Scan (30 buffers, 0.23 ms, без сортировки)
Сводка по версиям:
|
PG 16.14 (целевая) |
PG 17+ / 18.4 |
= ANY(ARRAY[...]) — как в коде |
Index Scan, 200k rows removed |
Bitmap Heap + Sort 3975 ради 25 |
IN (лит, лит) |
тот же план, тот же Filter |
Index Scan, 30 буферов |
| Разница |
нет |
есть, заметная |
Причина расхождения: планировщик 17+ умеет обрывать упорядоченный обход индекса по литеральному LIMIT, а = ANY с параметром-массивом эту способность отменяет. На 16 обрывать нечего, поэтому разницы не видно.
Почему это стоит зафиксировать
Комментарий в коде (InboxDataProviderEf.cs, ~строки 381-384) заявляет: «LIMIT намеренно остаётся литералом: планировщику нужно знать его, чтобы оборвать упорядоченный обход индекса на первых MessagesToProcess строках».
На PG 16 это утверждение неактивно. На PG 17+ оно активно — и = ANY его аннулирует. То есть код содержит намерение, которое сам же и ломает, ровно на тех версиях, где это намерение имеет смысл.
Деградация при апгрейде: 10k своих созревших сообщений (бэклог после инцидента) → каждый захват сортирует 10k строк ради 25. Стоимость растёт линейно с бэклогом, то есть дороже всего именно тогда, когда пропускная способность нужнее. Связать это с апгрейдом БД будет тяжело: код не менялся.
Возможное решение
Раскрывать дискриминаторы в IN (@d0, @d1, ...) — отдельный параметр на каждый, без склейки литералов. Сохраняет и экранирование (ради которого выбран = ANY), и sargability на 17+.
Решать имеет смысл к моменту апгрейда на PG 17+, не раньше: на 16.14 изменение не даст ничего, а горячий путь усложнит.
Контекст
Найдено при ревью #230 (Dex.Cap.Inbox, SUP-1085), тред r3593773403. Находка была снята как DISPUTED — корректно для целевой PG 16.14, но снята слишком широко: для 17+ она верна. Issue заводится, чтобы знание не потерялось к апгрейду.
Связанные: #231 (лимит Content), #232 (retention как окно дедупликации).
Суть
Запрос захвата в
InboxDataProviderEfпередаёт дискриминаторы параметром-массивом (= ANY({0})). На PostgreSQL 17+ это отменяет обрыв упорядоченного обхода индекса по литеральномуLIMIT: планировщик не знает значений массива на этапе планирования, поэтомуORDER BY ScheduledStartIndexing LIMIT 25перестаёт останавливать обход на первых 25 строках.На целевой для проекта PostgreSQL 16.14 проблемы нет — там
IN (a, b)нормализуется в тот же= ANY, планы тождественны. Поэтому на сегодня это не дефект, а отложенное ограничение, которое сработает при апгрейде БД.Замеры
Схема и индексы из
InboxEfExtensions, 196k чужих + 4k своих созревших сообщений.PostgreSQL 18.4,
= ANY(ARRAY[...])— как в коде сейчас:PostgreSQL 18.4,
IN (лит, лит)— на том же индексе:Сводка по версиям:
= ANY(ARRAY[...])— как в кодеIN (лит, лит)Причина расхождения: планировщик 17+ умеет обрывать упорядоченный обход индекса по литеральному
LIMIT, а= ANYс параметром-массивом эту способность отменяет. На 16 обрывать нечего, поэтому разницы не видно.Почему это стоит зафиксировать
Комментарий в коде (
InboxDataProviderEf.cs, ~строки 381-384) заявляет: «LIMIT намеренно остаётся литералом: планировщику нужно знать его, чтобы оборвать упорядоченный обход индекса на первыхMessagesToProcessстроках».На PG 16 это утверждение неактивно. На PG 17+ оно активно — и
= ANYего аннулирует. То есть код содержит намерение, которое сам же и ломает, ровно на тех версиях, где это намерение имеет смысл.Деградация при апгрейде: 10k своих созревших сообщений (бэклог после инцидента) → каждый захват сортирует 10k строк ради 25. Стоимость растёт линейно с бэклогом, то есть дороже всего именно тогда, когда пропускная способность нужнее. Связать это с апгрейдом БД будет тяжело: код не менялся.
Возможное решение
Раскрывать дискриминаторы в
IN (@d0, @d1, ...)— отдельный параметр на каждый, без склейки литералов. Сохраняет и экранирование (ради которого выбран= ANY), и sargability на 17+.Решать имеет смысл к моменту апгрейда на PG 17+, не раньше: на 16.14 изменение не даст ничего, а горячий путь усложнит.
Контекст
Найдено при ревью #230 (
Dex.Cap.Inbox, SUP-1085), тред r3593773403. Находка была снята как DISPUTED — корректно для целевой PG 16.14, но снята слишком широко: для 17+ она верна. Issue заводится, чтобы знание не потерялось к апгрейду.Связанные: #231 (лимит
Content), #232 (retention как окно дедупликации).