идеологически это как-то не правильно не спрашивать мнение базы
Увы, есть "как правильно", а есть "как эффективно". Ради эффективности мы даже были вынуждены выпилить все FK - просто потому, что это добавляет запросы на каждую вставку. Понятно, что в каких-нить финансовых системах, где критична логическая целостность данных, такого делать, пожалуй, не стоит. Но иногда оно того стоит.
зачем нужен uuid в централизованной системе типа postgres которая вам всегда может сказать какой следующий номер у пк или какой следующий диапазон номеров?
Ровно затем, чтобы этого не делать - не спрашивать у БД. То есть, с одной стороны, снять эту нагрузку с БД, с другой - иметь возможность идентифицировать связанные сущности на стороне клиента до начала обращения к БД.
Вот тут было подробно про COPY-потоки связанных записей без FK - присваиваем uuid'ы на клиенте, дальше пуляем в БД независимо.
Для этого я сделал небольшой хелпер-конвертер значений во float тип данных.
Гораздо интереснее обратный конвертер из units/nano в float - например, когда вы хотите вычислить целевую цену от входной - хотя бы для стоп-лосса как sourcePrice - 1%.
Например, вот тут вычисления опираются на floor, что в случае вычисленной цены типа 99.999999... даст {units : 99, nano : 999999999}, что слегка отличается от целевого ожидаемого {units : 100, nano : 0}.
Но самая большая проблема в этой реализации заключается в том, что такое представление целевой цены никак не соотносится с шагом цены инструмента (например, 0.02), и заявка с {units : 99, nano : 999999999} просто будет отклонена с ошибкой.
У меня получилось что-то типа вот такого:
const units2value = data => {
if (!data) {
return null;
}
const {units, nano} = data;
return !nano ? units : units + nano/1e9;
};
// minPriceIncrement берется из getInstrumentBy конкретного инструмента
const precision = -Math.floor(Math.log10(units2value(minPriceIncrement)));
const value2units = (value, p = precision) => {
if (value === undefined || value === null) {
return undefined;
}
if (Number.isInteger(value)) {
return {units : value, nano : 0};
}
let [
units
, nano
] = value
.toFixed(p) // ограничиваем значение целевой точностью
.split('.') // делим на целую и дробную части
.map((s, i) => Number(!i ? s : s.padEnd(9, '0'))); // nano расширяем до 9 цифр
const {
units : _units
, nano : _nano
} = minPriceIncrement;
if (p > 0 && !Number.isInteger(Math.log10(_nano))) { // округление до ближайшего шага цены
nano = Math.round(nano/_nano) * _nano;
if (nano == 1e9) { // защита от .999...
nano = 0;
units++;
}
}
return {units, nano : Math.sign(value) * nano};
};
Да, собственно, это и написано во втором же абзаце.
Причем эта реализация, несмотря на вроде-бы-неподходящий профиль языка, обычно ничуть не проигрывает в понятности и краткости примерам на других ЯП: JavaScript, Python, Go, Rust.
Здесь имеется ввиду время выполнения запроса как основной фактор.
Так-то конкретно в этой статье про время исполнения не говорится ровно ничего (поскольку оно и так копеечное) - лишь приведена модель реализации конкретного алгоритма на SQL.
Многоуровневая структура запроса на SQL (как и в приведенном примере)
Тут ее в общем-то и нету. )) Реально многоуровневую можно увидеть, например, тут.
Вот если бы я представил реализацию без описания "почему использованы такие ограничения" - тогда это неправомерное решение.
А сформулированная явно гипотеза, заведомо корректная относительно имеющихся данных, - имеет право на жизнь и анализ более тесно интегрированным в предметную область специалистом.
Мы же регулярно предполагаем разные вещи, которые могут быть истинными или ложными в зависимости от масштаба ситуации. Взять хотя бы такую простую вещь как расстояния - если вы измеряете дачный домик, то можно предположить, что вам хватит рулетки, а если маршрут самолета, то обязаны закладываться даже на форму планеты.
Я бы расстался с разработчиком, который, как Вы, стал настаивать на том, что он вправе в задаче использовать какие-то ограничения, которых нет в постановке задачи.
Скорее, стоит задуматься о судьбе разработчика (если мы говорим не просто о кодерах, а о грамотных специалистах уровня сеньора), который не уточнит эти граничные условия на основе имеющихся у него данных.
Вот мы смоделировали, уточнили применимость - можно рассматривать альтернативные варианты.
никто не давал гарантий, что основных средств будет всегда 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.
Программа пишется для решения определенных задач за конкретные деньги.
ФОТ программистов легко конвертируется в затраты на "железо" и обратно. Грубо, чем проще написать программу, тем менее эффективно (долго, дорого) она будет выполняться. Иногда ради эффективности можно многим пожертвовать.
Увы, есть "как правильно", а есть "как эффективно". Ради эффективности мы даже были вынуждены выпилить все FK - просто потому, что это добавляет запросы на каждую вставку.
Понятно, что в каких-нить финансовых системах, где критична логическая целостность данных, такого делать, пожалуй, не стоит. Но иногда оно того стоит.
Ровно затем, чтобы этого не делать - не спрашивать у БД. То есть, с одной стороны, снять эту нагрузку с БД, с другой - иметь возможность идентифицировать связанные сущности на стороне клиента до начала обращения к БД.
Вот тут было подробно про COPY-потоки связанных записей без FK - присваиваем uuid'ы на клиенте, дальше пуляем в БД независимо.
Гораздо интереснее обратный конвертер из units/nano в float - например, когда вы хотите вычислить целевую цену от входной - хотя бы для стоп-лосса как
sourcePrice - 1%.Например, вот тут вычисления опираются на
floor, что в случае вычисленной цены типа99.999999...даст{units : 99, nano : 999999999}, что слегка отличается от целевого ожидаемого{units : 100, nano : 0}.Но самая большая проблема в этой реализации заключается в том, что такое представление целевой цены никак не соотносится с шагом цены инструмента (например,
0.02), и заявка с{units : 99, nano : 999999999}просто будет отклонена с ошибкой.У меня получилось что-то типа вот такого:
Да, собственно, это и написано во втором же абзаце.
Причем эта реализация, несмотря на вроде-бы-неподходящий профиль языка, обычно ничуть не проигрывает в понятности и краткости примерам на других ЯП: JavaScript, Python, Go, Rust.
Так-то конкретно в этой статье про время исполнения не говорится ровно ничего (поскольку оно и так копеечное) - лишь приведена модель реализации конкретного алгоритма на SQL.
Тут ее в общем-то и нету. )) Реально многоуровневую можно увидеть, например, тут.
[Безотносительно конкретной задачи]
Вот если бы я представил реализацию без описания "почему использованы такие ограничения" - тогда это неправомерное решение.
А сформулированная явно гипотеза, заведомо корректная относительно имеющихся данных, - имеет право на жизнь и анализ более тесно интегрированным в предметную область специалистом.
Мы же регулярно предполагаем разные вещи, которые могут быть истинными или ложными в зависимости от масштаба ситуации. Взять хотя бы такую простую вещь как расстояния - если вы измеряете дачный домик, то можно предположить, что вам хватит рулетки, а если маршрут самолета, то обязаны закладываться даже на форму планеты.
Скорее, стоит задуматься о судьбе разработчика (если мы говорим не просто о кодерах, а о грамотных специалистах уровня сеньора), который не уточнит эти граничные условия на основе имеющихся у него данных.
Вот мы смоделировали, уточнили применимость - можно рассматривать альтернативные варианты.
Так вот потому и отлаживать надо на данных, структурно приближенных к реальным. На относительно мелкой модели 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