Вот если бы я представил реализацию без описания "почему использованы такие ограничения" - тогда это неправомерное решение.
А сформулированная явно гипотеза, заведомо корректная относительно имеющихся данных, - имеет право на жизнь и анализ более тесно интегрированным в предметную область специалистом.
Мы же регулярно предполагаем разные вещи, которые могут быть истинными или ложными в зависимости от масштаба ситуации. Взять хотя бы такую простую вещь как расстояния - если вы измеряете дачный домик, то можно предположить, что вам хватит рулетки, а если маршрут самолета, то обязаны закладываться даже на форму планеты.
Я бы расстался с разработчиком, который, как Вы, стал настаивать на том, что он вправе в задаче использовать какие-то ограничения, которых нет в постановке задачи.
Скорее, стоит задуматься о судьбе разработчика (если мы говорим не просто о кодерах, а о грамотных специалистах уровня сеньора), который не уточнит эти граничные условия на основе имеющихся у него данных.
Вот мы смоделировали, уточнили применимость - можно рассматривать альтернативные варианты.
никто не давал гарантий, что основных средств будет всегда 1000, а не миллион ... дискретизация границ периодов используется до минут, а не дней
Так вот потому и отлаживать надо на данных, структурно приближенных к реальным. На относительно мелкой модели btree может выиграть, а на крупной - может и нет.
Основные средства - это вагоны, которых в конкретный момент под управлением может быть сотни тысяч. Суммарно же было под управлением за последние 20 лет - около миллиона.
Я не очень хорошо знаю примерный срок жизни вагона, но 100K/1M - это ни разу не "каждый третий". А когда еще лет через 5 окажется, что "в моменте" останется 75K/2M модель "от вагонов" может оказаться уже совсем неподходящей.
В том-то и дело, что опираться при оптимизациях подобного рода имеет смысл именно на "прикладные" особенности распределения данных в конкретных условиях.
Как в статье - почему мы хотим отобрать аж целую треть объектов? Это достаточно много, и есть риск вообще попадать на Seq Scan, пока по каждому из них не накопится достаточно записей.
Мы делали аналогичную задачу по отпускам сотрудников (понятно, что они ровно так же не могут пересекаться) - так вот наиболее эффективным вариантом оказался тоже не btree_gist, а перебор "от длин отпуска" - просто потому, что их очень небольшое количество "в жизни". Отпуск обычно берут на 1, 2 дня, на неделю, на 2 недели или декретный (реальных вариантов побольше, но не сильно).
На сгенерировавшихся у меня данных модель запроса из статьи - 4.8ms:
Nested Loop (cost=0.43..1471.40 rows=333 width=26) (actual time=0.065..4.819 rows=333 loops=1)
-> Function Scan on generate_series g (cost=0.00..3.33 rows=333 width=4) (actual time=0.028..0.066 rows=333 loops=1)
-> Subquery Scan on a (cost=0.43..4.40 rows=1 width=26) (actual time=0.014..0.014 rows=1 loops=333)
Filter: (upper(a.date_period) > '2020-01-01'::date)
-> Limit (cost=0.43..4.38 rows=1 width=30) (actual time=0.013..0.013 rows=1 loops=333)
-> Index Scan Backward using tmp_asset_location_asset_date_period on tmp_asset_location t (cost=0.43..1669.91 rows=422 width=30) (actual time=0.013..0.013 rows=1 loops=333)
Index Cond: ((asset_id = g.n) AND (lower(date_period) <= '2020-01-01'::date))
Planning Time: 0.223 ms
Execution Time: 4.880 ms
Теперь внимательно смотрим на ограничения генерации, что интервал у нас заведомо не превышает 15 дней. Исходя из этого запрос можно эффективнее переписать через поиск "от дат" вместо "от объекта" - 2.7ms:
EXPLAIN ANALYZE
SELECT DISTINCT ON(asset_id) -- могло попасть несколько интервалов объекта в эти 15 дней
*
FROM
tmp_asset_location
WHERE
lower(date_period) BETWEEN '2020-01-01'::date - 15 AND '2020-01-01'::date AND
asset_id IN (SELECT generate_series(3,1000,3))
ORDER BY
asset_id, lower(date_period) DESC
Теперь подложим под него же чуть более подходящий индекс - и уже 2.1ms:
DROP INDEX tmp_asset_location_asset_date_period;
CREATE UNIQUE INDEX tmp_asset_location_lower_asset ON tmp_asset_location(lower(date_period), asset_id);
А если теперь перебрать все подходящие даты "вручную" и поискать в каждой, то становится уже 0.6ms:
EXPLAIN ANALYZE
SELECT DISTINCT ON(asset_id)
*
FROM
generate_series('2020-01-01'::date - 15, '2020-01-01'::date, '1 day') dt
, tmp_asset_location
WHERE
lower(date_period) = dt::date AND -- точечно ищем подходящие объекты в каждой из дат
asset_id IN (SELECT generate_series(3,1000,3))
ORDER BY
asset_id, lower(date_period) DESC
Зависит от смысла, вкладываемого в термин "обход 2D лабиринта".
Если про удобство реализуемости таких алгоритмов на SQL, то можно посмотреть варианты прошлогодних решений: раз, два, три. Если же про вычислительную эффективность, то сравнивать сложно - один мелкий шаг в алгоритме может его замедлить на порядок.
Просто в качестве примера: день 4 (читайте завтра пост) я сначала попробовал решить "втупую" переведя вход в двумерный массив - и за 20 минут запрос не досчитался.
Мне кажется, вы так и не поняли основную идею моей статьи, хотя она присутствует в первом же абзаце - что агрегаты-по-соединению можно заменить на функции от массива без всяких соединений.
А оба приведенные здесь плана отбрасывают примерно в 6 раз больше записей соединения, чем выдают полезных.
Так LATERAL подразумевает неявный вызов JOIN согласно документации?
В случае возврата из LATERAL единственной записи, в плане видно, что узлов соединений (Nested Loop, ... Join) не появляется. Вот если их хотя бы две, как вот тут, то приходится тратиться и на соединение.
Если немного развить тему, то можно "агрегацию" оконными функциями внести внутрь скобок:
SELECT
cli.id
, first_value dt
FROM
cli
, LATERAL (
SELECT
first_value(dt) OVER w
, nth_value(dt, 2) OVER w
FROM
doc
WHERE
cli = cli.id
WINDOW w AS(ORDER BY cli, dt DESC ROWS UNBOUNDED PRECEDING)
ORDER BY
cli, dt DESC
LIMIT 2
) doc
WHERE
cli.name LIKE '!%' AND
nth_value IS NOT NULL;
Но результат все равно чуть хуже из-за необходимости фильтрации "первых" записей при соединении:
Безусловно, если делать JOIN по уже заведомо ограниченной парой записей выборке, то агрегатные функции и группировка становятся гораздо менее неэффективными. Но издержки на соединение и группировку никуда не исчезают.
Если закинете планы на explain, будут более наглядно видны затраты на каждом из узлов. Разницу в последних примерах я отношу к погрешности измерений - у меня до 10% разброс между замерами получался, но в статью брал минимально достигнутые.
При отсутствии параллельных узлов операции плана выполняются последовательно. Или речь о нескольких одновременно запущенных запросах? Но это не влияет же на распределение user/system.
Но, в условиях высокой нагрузки и параллельности - разница в производительности на порядки.
Параллельность тут вообще не при чем. Ни в одном из моих планов она даже не пыталась использоваться.
Вывод: В тесте "MAX" CPU интенсивнее используется приложением, тогда как в "ARRAY" выше нагрузка на ядро.
Тут все достаточно очевидно - при агрегации доминируют затраты на JOIN, производимые как раз в usertime. При его отсутствии, за счет уменьшения общего времени, увеличивается доля чтений данных в systime.
Программа пишется для решения определенных задач за конкретные деньги.
ФОТ программистов легко конвертируется в затраты на "железо" и обратно. Грубо, чем проще написать программу, тем менее эффективно (долго, дорого) она будет выполняться. Иногда ради эффективности можно многим пожертвовать.
В традиционных бизнес-системах документы по клиентам имеют тенденцию повторяться неоднократно (счета ежемесячно, договора ежегодно, отгрузки постоянным клиентам, ...).
Можно вот так, и даже будет использоваться Run Condition:
WITH pre AS (
SELECT
cli.id
, dt
, row_number() OVER (PARTITION BY cli.id ORDER BY doc.dt DESC) rn
FROM
cli
JOIN
doc
ON doc.cli = cli.id
WHERE
cli.name LIKE '!%'
)
SELECT
id
, max(dt) OVER (PARTITION BY id) dt
FROM
pre
WHERE
rn = 2;
Имеется в виду "не хватает подходящего". Там на картинке в узле Index Scan rows=1, Rows Removed by Filter: 48668 - то есть мы вычитали тучу лишних записей ради одной нужной.
Еще с 9.1 требует не всегда. В какой-то из более поздних версий определение доступных к возврату столбцов было заменено на что-то типа "однозначно функционально вычислимые".
[Безотносительно конкретной задачи]
Вот если бы я представил реализацию без описания "почему использованы такие ограничения" - тогда это неправомерное решение.
А сформулированная явно гипотеза, заведомо корректная относительно имеющихся данных, - имеет право на жизнь и анализ более тесно интегрированным в предметную область специалистом.
Мы же регулярно предполагаем разные вещи, которые могут быть истинными или ложными в зависимости от масштаба ситуации. Взять хотя бы такую простую вещь как расстояния - если вы измеряете дачный домик, то можно предположить, что вам хватит рулетки, а если маршрут самолета, то обязаны закладываться даже на форму планеты.
Скорее, стоит задуматься о судьбе разработчика (если мы говорим не просто о кодерах, а о грамотных специалистах уровня сеньора), который не уточнит эти граничные условия на основе имеющихся у него данных.
Вот мы смоделировали, уточнили применимость - можно рассматривать альтернативные варианты.
Так вот потому и отлаживать надо на данных, структурно приближенных к реальным. На относительно мелкой модели btree может выиграть, а на крупной - может и нет.
Я не очень хорошо знаю примерный срок жизни вагона, но 100K/1M - это ни разу не "каждый третий". А когда еще лет через 5 окажется, что "в моменте" останется 75K/2M модель "от вагонов" может оказаться уже совсем неподходящей.
В том-то и дело, что опираться при оптимизациях подобного рода имеет смысл именно на "прикладные" особенности распределения данных в конкретных условиях.
Как в статье - почему мы хотим отобрать аж целую треть объектов? Это достаточно много, и есть риск вообще попадать на Seq Scan, пока по каждому из них не накопится достаточно записей.
Мы делали аналогичную задачу по отпускам сотрудников (понятно, что они ровно так же не могут пересекаться) - так вот наиболее эффективным вариантом оказался тоже не btree_gist, а перебор "от длин отпуска" - просто потому, что их очень небольшое количество "в жизни". Отпуск обычно берут на 1, 2 дня, на неделю, на 2 недели или декретный (реальных вариантов побольше, но не сильно).
На сгенерировавшихся у меня данных модель запроса из статьи - 4.8ms:
Теперь внимательно смотрим на ограничения генерации, что интервал у нас заведомо не превышает 15 дней. Исходя из этого запрос можно эффективнее переписать через поиск "от дат" вместо "от объекта" - 2.7ms:
Теперь подложим под него же чуть более подходящий индекс - и уже 2.1ms:
А если теперь перебрать все подходящие даты "вручную" и поискать в каждой, то становится уже 0.6ms:
Зависит от смысла, вкладываемого в термин "обход 2D лабиринта".
Если про удобство реализуемости таких алгоритмов на SQL, то можно посмотреть варианты прошлогодних решений: раз, два, три. Если же про вычислительную эффективность, то сравнивать сложно - один мелкий шаг в алгоритме может его замедлить на порядок.
Просто в качестве примера: день 4 (читайте завтра пост) я сначала попробовал решить "втупую" переведя вход в двумерный массив - и за 20 минут запрос не досчитался.
Мне кажется, вы так и не поняли основную идею моей статьи, хотя она присутствует в первом же абзаце - что агрегаты-по-соединению можно заменить на функции от массива без всяких соединений.
А оба приведенные здесь плана отбрасывают примерно в 6 раз больше записей соединения, чем выдают полезных.
В случае возврата из
LATERALединственной записи, в плане видно, что узлов соединений (Nested Loop, ... Join) не появляется. Вот если их хотя бы две, как вот тут, то приходится тратиться и на соединение.Если немного развить тему, то можно "агрегацию" оконными функциями внести внутрь скобок:
Но результат все равно чуть хуже из-за необходимости фильтрации "первых" записей при соединении:
Безусловно, если делать JOIN по уже заведомо ограниченной парой записей выборке, то агрегатные функции и группировка становятся гораздо менее неэффективными. Но издержки на соединение и группировку никуда не исчезают.
Если закинете планы на explain, будут более наглядно видны затраты на каждом из узлов. Разницу в последних примерах я отношу к погрешности измерений - у меня до 10% разброс между замерами получался, но в статью брал минимально достигнутые.
При отсутствии параллельных узлов операции плана выполняются последовательно. Или речь о нескольких одновременно запущенных запросах? Но это не влияет же на распределение user/system.
Параллельность тут вообще не при чем. Ни в одном из моих планов она даже не пыталась использоваться.
Тут все достаточно очевидно - при агрегации доминируют затраты на JOIN, производимые как раз в usertime. При его отсутствии, за счет уменьшения общего времени, увеличивается доля чтений данных в systime.
LIMIT 2 - это чтобы нашлась вторая запись, если она есть - чтобы проверить условие задачи "документов по нему несколько".
Программа пишется для решения определенных задач за конкретные деньги.
ФОТ программистов легко конвертируется в затраты на "железо" и обратно. Грубо, чем проще написать программу, тем менее эффективно (долго, дорого) она будет выполняться. Иногда ради эффективности можно многим пожертвовать.
CTE не пишется на диск (
temp buffers), пока влезает в work_memв PG массивы нумеруются с 1, а не с 0
В традиционных бизнес-системах документы по клиентам имеют тенденцию повторяться неоднократно (счета ежемесячно, договора ежегодно, отгрузки постоянным клиентам, ...).
"Не считает" или "не читает"?
Можно вот так, и даже будет использоваться
Run Condition:Только медленнее в 1.5 раза от исходного:
Даже на PG16 это не работает, не говоря о более ранних версиях:
Ну и параллелиться
WindowAggпока не умеет.Кроме того, подсчет номера записи в выборке все-таки требует иметь эту выборку (полный JOIN) "под ногами" - то есть грабли ровно те же.
Имеется в виду "не хватает подходящего". Там на картинке в узле Index Scan rows=1, Rows Removed by Filter: 48668 - то есть мы вычитали тучу лишних записей ради одной нужной.
https://www.postgresql.org/docs/release/9.1.0/
Еще с 9.1 требует не всегда. В какой-то из более поздних версий определение доступных к возврату столбцов было заменено на что-то типа "однозначно функционально вычислимые".