Русский
Русский
English
Статистика
Реклама

Explain.tensor.ru

SQL HowTo 1000 и один способ агрегации

19.06.2020 12:20:11 | Автор: admin
Наш СБИС, как и другие системы управления бизнесом, не обходится без формирования отчетов каждый руководитель любит сводные цифры, особенно всякие суммы по разделам и красивые "Итого".

А чтобы эти итоги собрать, необходимо по исходным данным вычислить значение некоторой агрегатной функции: количество, сумма, среднее, минимум, максимум, и, как правило, не одной.


Сегодня мы рассмотрим некоторые способы, с помощью которых можно вычислить агрегаты в PostgreSQL или ускорить выполнение SQL-запроса.

Совместные агрегаты


Давайте сразу возьмем задачу чуть посложнее, чем тривиальное вычисление единственного агрегата. Попробуем вычислить по одной и той же выборке несколько агрегатов одновременно, и пусть это будут количество и сумма элементов в некотором входном наборе:

->  $1 = '{2,3,5,7,11,13,17,19}'<-  count | sum      8 |  77

Это самый-самый простой случай просто сразу одновременно в запросе пишем count и sum:

SELECT  count(*), sum(prime)FROM  unnest('{2,3,5,7,11,13,17,19}'::integer[]) prime;

И хоть агрегатных функций мы использовали две, в плане у нас все хорошо узел Aggregate выполнялся всего лишь один раз:


Несовместимые агрегаты


Проблемы начинаются, если мы хотим вычислить сразу несколько агрегатов, но вот у каждого из них разные условия [не]включения одной и той же записи исходного набора. Например, попробуем вычислить для нашего набора количество чисел больше и меньше 10 отдельными полями:

->  $1 = '{2,3,5,7,11,13,17,19}'<-  countlt | countgt        4 |       4

Вложенные запросы


Понятно, что это можно сделать самым примитивным способом вложенными запросами к CTE, каждый из которых сначала выфильтровывает себе из общей выборки подходящий под условия кусочек, а потом считает на нем отдельный агрегат:

WITH src AS (  SELECT unnest('{2,3,5,7,11,13,17,19}'::integer[]) prime) SELECT  (SELECT count(*) FROM src WHERE prime < 10) countlt, (SELECT count(*) FROM src WHERE prime > 10) countgt;



Какие из этого плана можно сделать выводы? Много бегаем и много фильтруем дважды [CTE Scan + Rows Removed by Filter: 4].

А если выборка будет из 10K записей, а агрегатов захочется 3-4-5?.. Совсем нехорошо.

FILTER-агрегаты


Этот вариант, наверное, самый простой и понятный:

SELECT  count(*) FILTER(WHERE prime < 10) countlt, count(*) FILTER(WHERE prime > 10) countgtFROM  unnest('{2,3,5,7,11,13,17,19}'::integer[]) prime;



Он идеален во всех отношениях, быстро и эффективно решает все наши задачи, но имеет и ложку дегтя доступен только с версии 9.4 (мало ли у кого что на бою стоит).

Агрегаты от условия


Допустим, 9.4 еще не подвезли, а запрос все-таки хочется написать в один проход. В этом случае можно воспользоваться знанием, что count(*) FILTER(WHERE cond) эквивалентно sum(CASE cond):

SELECT  sum(CASE WHEN prime < 10 THEN 1 ELSE 0 END) countlt, sum(CASE WHEN prime > 10 THEN 1 ELSE 0 END) countgtFROM  unnest('{2,3,5,7,11,13,17,19}'::integer[]) prime;

Или можно чуть короче, если вспомнить о возможности скастовать boolean в integer вместо CASE с результатами 1/0:

SELECT  sum((prime < 10)::integer) countlt, sum((prime > 10)::integer) countgtFROM  unnest('{2,3,5,7,11,13,17,19}'::integer[]) prime;

Вообще, подход вычисления агрегата от некоторого условия достаточно универсален, но имеет и свои подводные камни.

Агрегация в массив


Допустим, мы хотим теперь получить не просто количество чисел, подходящих под то или иное условие, но массивы из них состоящие:

->  $1 = '{2,3,5,7,11,13,17,19}'<-   primeslt |   primesgt  {2,3,5,7} | {11,13,17,19}

И тут нам тоже поможет агрегат ведь он возвращает одно значение, но тип его может быть не обязательно скаляром, но записью таблицы или даже массивом с помощью array_agg.

Вариант с использованием FILTER очевиден:

WITH src AS (  SELECT unnest('{2,3,5,7,11,13,17,19}'::integer[]) prime)SELECT  array_agg(prime) FILTER(WHERE prime < 10) primeslt, array_agg(prime) FILTER(WHERE prime > 10) primesgtFROM  src;



А вот если попытаться превратить его в агрегат от условия придется разбираться с попаданием в набор NULL'ов, что уже совсем невесело:

WITH src AS (  SELECT unnest('{2,3,5,7,11,13,17,19}'::integer[]) prime) , tmp AS (  SELECT    array_agg(CASE WHEN prime < 10 THEN prime END) primeslt -- {2,3,5,7,NULL,NULL,NULL,NULL}  , array_agg(CASE WHEN prime > 10 THEN prime END) primesgt -- {NULL,NULL,NULL,NULL,11,13,17,19}  FROM    src)SELECT  ARRAY(SELECT * FROM unnest(primeslt) prime WHERE prime IS NOT NULL) primeslt, ARRAY(SELECT * FROM unnest(primesgt) prime WHERE prime IS NOT NULL) primesgtFROM  tmp;

Но если вам хоть немного повезло, и стоит хотя бы версия 9.3, то можно воспользоваться функцией array_remove для достижения того же эффекта:

WITH src AS (  SELECT unnest('{2,3,5,7,11,13,17,19}'::integer[]) prime) SELECT  array_remove(array_agg(CASE WHEN prime < 10 THEN prime END), NULL) primeslt, array_remove(array_agg(CASE WHEN prime > 10 THEN prime END), NULL) primesgtFROM  src;

Несколько агрегатов: Function Scan vs CTE


Мы тут внезапно вынесли наш исходный набор в CTE а почему? Потому что так банально быстрее. Давайте проверим на простом примере:

SELECT  array_agg(i) FILTER(WHERE i % 2 = 0) even, array_agg(i) FILTER(WHERE i % 2 = 1) oddFROM  generate_series(1, 1000000) i;



WITH src AS (  SELECT generate_series(1, 1000000) i)SELECT  array_agg(i) FILTER(WHERE i % 2 = 0) even, array_agg(i) FILTER(WHERE i % 2 = 1) oddFROM  src;



Почти на 40% быстрее! Пример, конечно, вырожденный, но эффект имеет место быть.

DISTINCT + OVER


Еще один способ агрегации за единственный проход заключается в использовании уникализации выборки с помощью DISTINCT и применения окна к агрегатной функции:

WITH src AS (  SELECT unnest('{2,3,5,7,11,13,17,19}'::integer[]) prime)SELECT DISTINCT  array_agg(prime) FILTER(WHERE prime < 10) OVER() primeslt, array_agg(prime) FILTER(WHERE prime > 10) OVER() primesgtFROM  src;

Единственная проблема такая группировка небесплатна:



Сложный агрегат


Но предположим, что мы хотим что-то этакое сложное, для чего нет подходящего агрегата:

->  $1 = '{2,3,5,7,11,13,17,19}'<-                 exp                  |   val  2 * 3 * 5 * 7 * 11 * 13 * 17 * 19 = | 9699690

В этом примере мы хотим вычислить произведение всех участвующих чисел, но такой агрегатной функции нету. Понятно, что ее можно создать, но как-то это неспортивно создавать по функции, если потребность возникает редко.

Соберем запрос, который решит нашу задачу:

WITH RECURSIVE src AS (  SELECT    *  FROM    unnest('{2,3,5,7,11,13,17,19}'::integer[])      WITH ORDINALITY T(prime, rn)), T(rn, exp, val) AS (  SELECT    0::bigint    -- база агрегации  , '{}'::integer[]  , 1UNION ALL  SELECT    src.rn    -- итеративное вычисление сразу всех агрегатов  , exp || src.prime  , val * src.prime   FROM    T  JOIN    src      ON src.rn = T.rn + 1 -- переход к следующей записи)SELECT  array_to_string(exp, ' * ') || ' = ' exp, valFROM  TORDER BY -- отбор финального результата  rn DESCLIMIT 1;



Кучеряво! Попробуем упростить и ускорить, опираясь на тот факт, что входной аргумент заведомо является массивом то есть может быть перебран поэлементно:

WITH RECURSIVE src AS (  SELECT '{2,3,5,7,11,13,17,19}'::integer[] arr), T(i, exp, val) AS (  SELECT    1::bigint    -- база агрегации  , '{}'::integer[]  , 1UNION ALL  SELECT    i + 1    -- итеративное вычисление сразу всех агрегатов  , exp || arr[i]  , val * arr[i]  FROM    T  , src  WHERE    i <= array_length(arr, 1))SELECT  array_to_string(exp, ' * ') || ' = ' exp, valFROM  TORDER BY -- отбор финального результата  i DESCLIMIT 1;



Намного лучше!

Math.bonus


Применим string_agg и немного математической магии:


WITH src AS (  SELECT unnest('{2,3,5,7,11,13,17,19}'::integer[]) prime)SELECT  string_agg(prime::text, ' * ') || ' = ' exp, exp(sum(ln(prime)))::integer val -- для любителей математикиFROM  src;

Подробнее..

Вооруженным глазом наглядно о проблемах PostgreSQL-запроса

29.07.2020 22:04:59 | Автор: admin
Продолжаем открывать для публичного доступа новый фукционал нашего сервиса анализа планов выполнения запросов в PostgreSQL explain.tensor.ru. Сегодня мы научимся определять больные места навскидку в больших и сложных планах, лишь мельком взглянув на них вооруженным глазом


В этом нам помогут различные варианты визуализации:



Сокращенный текстовый вид


Оригинальный текст достаточно несложного плана уже вызывает проблемы при анализе:



Поэтому мы предпочитаем сокращенный вид, когда влево-вправо вынесена ключевая информация о времени выполнения и использованных buffers каждого узла, и очень просто заметить максимумы:



Круговая диаграмма


Но иногда даже просто понять где болит сильнее всего непросто, особенно, если он содержит несколько десятков узлов и даже сокращенная форма плана занимает 2-3 экрана.



В этом случае на помощь придет обычная круговая диаграмма:



Сразу, навскидку, видна примерная доля потребления ресурсов каждым из узлов. При наведении на него, слева в текстовом представлении мы увидим иконку у выбранного узла.

Плитка


Увы, piechart плохо показывает отношения между разными узлами и самые горячие точки. Для этого гораздо лучше подойдет вариант отображения плиткой:



Диаграмма выполнения


Но оба эти варианта не показывают полную цепочку вложений служебных узлов CTE/InitPlain/SubPlan его можно увидеть только на диаграмме реального выполнения:



Нужно больше метрик!


Если вы снимаете план реального выполнения запроса как EXPLAIN (ANALYZE), то увидите там только затраченное время. Но очень часто этого недостаточно для правильных выводов!

Например, выполняя запрос на непрогретом кэше вы получите (но не увидите!) время получения данных с носителя, а вовсе не работы самого запроса.

Поэтому пара рекомендаций:

  • Используйте EXPLAIN (ANALYZE, BUFFERS), чтобы увидеть объем вычитываемых страниц данных. Эта величина практически не подвержена колебаниям от нагрузки самого сервера и может быть использована в качестве метрики при оптимизации.
  • Используйте track_io_timing, чтобы понимать, сколько именно времени заняла работа с носителем.

И вот если ваш план содержит не только время, но и buffers или i/o timings, то на каждой из вариантов диаграмм вы сможете переключиться в режим анализа этих метрик. Иногда можно сразу увидеть, например, что больше половины всех чтений пришлось на единственный проблемный узел:



Предыдущие статьи по теме:

Подробнее..

Правильно cчитаем параллельные планы PostgreSQL

10.08.2020 12:13:56 | Автор: admin
Исторически, модель работы сервера PostgreSQL выглядит как множество независимых процессов с частично разделяемой памятью. Каждый из них обслуживает только одно клиентское подключение и один запрос в любой момент времени и никакой многопоточности.

Поэтому внутри каждого отдельного процесса нет никаких традиционных странных проблем с параллельным выполнением кода, блокировками, race condition, А разработка самой СУБД приятна и проста.

Но эта же простота накладывает существенное ограничение. Раз внутри процесса всего один рабочий поток, то и использовать он может не более одного ядра CPU для выполнения запроса а, значит, скорость работы сервера впрямую зависит от частоты и архитектуры отдельного ядра.

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


Со схемами работы некоторых параллельных узлов можно ознакомиться в статье Parallelism in PostgreSQL by Ibrar Ahmed, откуда взято и это изображение.
Правда, читать планы в этом случае становится нетривиально.

Вкратце хронология внедрения параллельного исполнения операций плана выглядит так:


Поэтому, если вы пользуетесь одной из последних версий PostgreSQL, шансы увидеть в плане Parallel ... весьма велики. А с ним приходят и

Странности со временем


Возьмем план из PostgreSQL 9.6:


[посмотреть на explain.tensor.ru]

Только один Parallel Seq Scan выполнялся 153.621ms внутри поддерева, а Gather вместе со всеми подузлами всего 104.867ms.

Как так? Суммарно времени наверху стало меньше?..

Взглянем чуть подробнее на Gather-узел:

Gather (actual time=0.969..104.867 rows=333333 loops=1)  Workers Planned: 2  Workers Launched: 2  Buffers: shared hit=4425

Workers Launched: 2 говорит нам о том, что дополнительно к основному процессу ниже по дереву были задействованы еще 2 дополнительных итого 3. Поэтому все, что происходило внутри Gather-поддерева является суммарным творчеством всех 3 процессов сразу.

Теперь посмотрим, что там в Parallel Seq Scan:

Parallel Seq Scan on tst (actual time=0.024..51.207 rows=111111 loops=3)  Filter: ((i % 3) = 0)  Rows Removed by Filter: 222222  Buffers: shared hit=4425

Ага! loops=3 это сводная информация по всем 3 процессам. И, в среднем, каждый такой цикл занял по 51.207ms. То есть суммарно для отработки этого узла серверу понадобилось 51.207 x 3 = 153.621 миллисекунды процессорного времени. То есть если мы хотим понять чем был занят сервер именно это число и поможет нам понять.
Замечу, что для понимания реального времени выполнения надо суммарное время разделить на количество worker'ов то есть [actual time] x [loops] / [Workers Launched].

В нашем примере каждый worker выполнил лишь один цикл по узлу, поэтому 153.621 / 3 = 51.207. И да, теперь уже нет ничего странного, что единственный Gather в головном процессе выполнился за как бы меньшее время.

Итого: смотрите на explain.tensor.ru суммарное (по всем процессам) время узла, чтобы понять, какой именно нагрузкой был занят ваш сервер, и оптимизации какой части запроса стоит уделять время.

В этом смысле поведение того же explain.depesz.com, показывающего сразу усредненное реальное время, выглядит менее полезным для целей отладки:



Не согласны? Добро пожаловать в комментарии!

Gather Merge теряет все


Теперь выполним тот же запрос на версии PostgreSQL 10:


[посмотреть на explain.tensor.ru]

Обратим внимание, что в плане у нас вместо узла Gather теперь оказался Gather Merge, а последовательное чтение таблицы превратилось в упорядоченное, по индексу, хоть и параллельное. Вот что говорит по этому поводу мануал:
Когда над параллельной частью плана оказывается узел Gather Merge, а не Gather, это означает, что все процессы, выполняющие части параллельного плана, выдают кортежи в отсортированном порядке, и что ведущий процесс выполняет слияние с сохранением порядка. Узел же Gather, напротив, получает кортежи от подчинённых процессов в произвольном удобном ему порядке, нарушая порядок сортировки, который мог существовать.

Но не все ладно в датском королевстве:

Limit (actual time=110.740..113.138 rows=10000 loops=1)  Buffers: shared hit=888 read=801, temp read=18 written=218  I/O Timings: read=9.709  ->  Gather Merge (actual time=110.739..117.654 rows=10000 loops=1)        Workers Planned: 2        Workers Launched: 2        Buffers: shared hit=2943 read=1578, temp read=24 written=571        I/O Timings: read=17.156

При передаче атрибутов Buffers и I/O Timings вверх по дереву часть данных была безвременно утрачена. Мы можем оценить размер этой потери как раз примерно в 2/3, которые формируются вспомогательными процессами.

Увы, в самом плане эту информацию взять неоткуда отсюда и минусы на вышележащем узле. И если посмотреть дальнейшую эволюцию этого плана в PostgreSQL 12, то он принципиально не меняется, разве что добавляется немного статистики по каждому worker на Sort-узле:

Limit (actual time=77.063..80.480 rows=10000 loops=1)  Buffers: shared hit=1764, temp read=223 written=355  ->  Gather Merge (actual time=77.060..81.892 rows=10000 loops=1)        Workers Planned: 2        Workers Launched: 2        Buffers: shared hit=4519, temp read=575 written=856        ->  Sort (actual time=72.630..73.252 rows=4278 loops=3)              Sort Key: i              Sort Method: external merge  Disk: 1832kB              Worker 0:  Sort Method: external merge  Disk: 1512kB              Worker 1:  Sort Method: external merge  Disk: 1248kB              Buffers: shared hit=4519, temp read=575 written=856              ->  Parallel Seq Scan on tst (actual time=0.014..44.970 rows=111111 loops=3)                    Filter: ((i % 3) = 0)                    Rows Removed by Filter: 222222                    Buffers: shared hit=4425Planning Time: 0.142 msExecution Time: 83.884 ms

Итого: не доверяйте данным узла над Gather Merge.
Подробнее..

PostgreSQL Query Profiler как сопоставить план и запрос

03.09.2020 20:17:52 | Автор: admin
Многие, кто уже пользуется explain.tensor.ru нашим сервисом визуализации планов PostgreSQL, возможно, не в курсе одной из его суперсособностей превращать сложно читаемый кусок лога сервера


в красиво оформленный запрос с контекстными подсказками по соответствующим узлам плана:


В этой расшифровке второй части своего доклада на PGConf.Russia 2020 я расскажу, как нам удалось это сделать.
С транскриптом первой части, посвященной типовым проблемам производительности запросов и их решениям, можно ознакомиться в статье Рецепты для хворающих SQL-запросов.


Сначала займемся раскраской и раскрашивать будем уже не план, его мы уже разукрасили, он у нас уже красивый и понятный, а запрос.

Нам показалось, что вот так неформатированной простыней вытащенный из лога запрос выглядит очень уж некрасиво и потому неудобно.


Особенно, когда разработчики в коде клеят тело запроса (это, конечно, антипаттерн, но бывает) в одну строку. Жуть!

Давайте это нарисуем как-то более красиво.


А если мы сможем это нарисовать красиво, то есть разобрать и собрать обратно тело запроса, то потом сможем и к каждому объекту этого запроса прицепить подсказку что происходило в соответствующей точке плана.

Синтаксическое дерево запроса


Чтобы это сделать, запрос сначала нужно разобрать.


Поскольку, у нас ядро системы работает на NodeJS, то мы сделали к нему модулек, можете на GitHub его найти. На самом деле, это является расширенными биндингами к внутренностям парсера самого PostgreSQL. То есть просто бинарно скомпилирована грамматика и к ней сделаны биндинги со стороны NodeJS. Мы взяли за основу чужие модули тут тайны никакой большой нет.

Скармливаем тело запроса на вход нашей функции на выходе получаем разобранное синтаксическое дерево в виде JSON-объекта.


Теперь по этому дереву можно пробежаться в обратную сторону и собрать запрос с теми отступами, раскраской, форматированием, которое нам хочется. Нет, это не настраивается, но нам показалось, что именно так будет удобно.


Сопоставление узлов запроса и плана


Теперь посмотрим, как можно совместить план, который мы разобрали на первом шаге, и запрос, который разобрали на втором.

Давайте возьмем простой пример у нас есть запрос, который формирует CTE и два раза из нее читает. Он генерирует такой план.


CTE


Если на него внимательно посмотреть, что до 12-й версии (или начиная с нее с ключевым словом MATERIALIZED) формирование CTE является безусловным барьером для планировщика.


А, значит, если мы видим где-то в запросе генерацию CTE и где-то в плане узел CTE, то эти узлы однозначно между собой бьются, мы можем сразу же их совместить.

Задача со звездочкой: CTE бывают вложенные.

Бывают очень плохо вложенные, и даже одноименные. Например, вы можете внутри CTE A сделать CTE X, и на том же уровне внутри CTE B сделать опять CTE X:

WITH A AS (  WITH X AS (...)  SELECT ...), B AS (  WITH X AS (...)  SELECT ...)...

При сопоставлении вы должны это понимать. Понимать это глазами даже видя план, даже видя тело запроса очень тяжело. Если у вас генерация CTE сложная, вложенная, запросы большие тогда и вовсе неосознаваемо.

UNION


Если у нас в запросе есть ключевое слово UNION [ALL] (оператор соединения двух выборок), то ему в плане соответствует либо узел Append, либо какой-нибудь Recursive Union.


То, что сверху над UNION это первый потомок нашего узла, что снизу второй. Если через UNION у нас поклеено несколько блоков сразу, то Append-узел все равно будет только один, а вот детей у него будет не два, а много по порядку как они идут, соответственно:

  (...) -- #1UNION ALL  (...) -- #2UNION ALL  (...) -- #3

Append  -> ... #1  -> ... #2  -> ... #3

Задача со звездочкой: внутри генерации рекурсивной выборки (WITH RECURSIVE) тоже может быть больше одного UNION. Но всегда рекурсивным является только самый последний блок после последнего UNION. Все, что выше это один, но другой UNION:

WITH RECURSIVE T AS(  (...) -- #1UNION ALL  (...) -- #2, тут кончается генерация стартового состояния рекурсииUNION ALL  (...) -- #3, только этот блок рекурсивный и может содержать обращение к T)...

Такие примеры тоже надо уметь расклеивать. Вот в этом примере мы видим, что UNION-сегментов в нашем запросе было 3 штуки. Соответственно, одному UNION соответствует Append-узел, а другому Recursive Union.


Чтение-запись данных


Все, разложили, теперь мы знаем, какой кусочек запроса какому кусочку плана соответствует. И в этих кусочках мы можем легко и непринужденно найти те объекты, которые читаются.

С точки зрения запроса мы не знаем таблица это или CTE, но обозначаются они одинаковым узлом RangeVar. А в плане читается это тоже достаточно ограниченный набор узлов:

  • Seq Scan on [tbl]
  • Bitmap Heap Scan on [tbl]
  • Index [Only] Scan [Backward] using [idx] on [tbl]
  • CTE Scan on [cte]
  • Insert/Update/Delete on [tbl]

Структуру плана и запроса мы знаем, соответствие блоков знаем, имена объектов знаем делаем однозначное сопоставление.


Опять-таки задача со звездочкой. Берем запрос, выполняем, у нас никаких алиасов нет мы просто два раза из одной CTE почитали.


Смотрим в план что за беда? Почему у нас алиас вылез? Мы его не заказывали. Откуда он такой номерной?

PostgreSQL сам его добавляет. Надо просто понимать, что именно такой алиас для нас для целей сопоставления с планом никакого смысла не несет, он просто здесь добавлен. Не будем на него обращать внимания.

Вторая задача со звездочкой: если у нас идет чтение из секционированной таблицы, то мы получим узел Append или Merge Append, который будет состоять из большого количества детей, и каждый из которых будет каким-то Scan'ом из таблицы-секции: Seq Scan, Bitmap Heap Scan или Index Scan. Но, в любом случае, эти дети будут не сложными запросами так эти узлы и можно отличать от Append при UNION.


Такие узлы мы тоже понимаем, собираем в одну кучку и говорим: "все, что ты читал из megatable это вот тут и вниз по дереву".

Простые узлы получения данных




Values Scan в плане соответствует VALUES в запросе.

Result это запрос без FROM вроде SELECT 1. Или когда у вас заведомо ложное выражение в WHERE-блоке (тогда возникает атрибут One-Time Filter):

EXPLAIN ANALYZESELECT * FROM pg_class WHERE FALSE; -- или 0 = 1

Result  (cost=0.00..0.00 rows=0 width=230) (actual time=0.000..0.000 rows=0 loops=1)  One-Time Filter: false

Function Scan мапятся на одноименные SRF.

А вот с вложенными запросами все сложнее к сожалению, они не всегда превращаются в InitPlan/SubPlan. Иногда они превращаются в ... Join или ... Anti Join, особенно когда вы пишете что-то вроде WHERE NOT EXISTS .... И вот там совмещать не всегда получается в тексте плана соответствующих узлам плана операторов нет.

Опять-таки задача со звездочкой: несколько VALUES в запросе. В этом случае и в плане вы получите несколько узлов Values Scan.


Отличить их один от другого помогут номерные суффиксы он добавляется именно в порядке нахождения соответствующих VALUES-блоков по ходу запроса сверху вниз.

Обработка данных


Вроде все в нашем запросе разобрали остался только Limit.


Но тут все просто такие узлы как Limit, Sort, Aggregate, WindowAgg, Unique мапятся один-в-один на соответствующие операторы в запросе, если они там есть. Тут никаких звездочек и сложностей нет.


JOIN


Сложности возникают, когда мы хотим совместить JOIN между собой. Это сделать не всегда, но можно.


С точки зрения парсера запроса, у нас есть узел JoinExpr, у которого ровно два потомка левый и правый. Это, соответственно, то что над вашим JOIN и то что под ним в запросе написано.

А с точки зрения плана это два потомка у какого-то * Loop/* Join-узла. Nested Loop, Hash Anti Join, вот что-то такое.

Воспользуемся простой логикой: если у нас есть таблички A и B, которые джойнятся между собой в плане, то в запросе они могли быть расположены либо A-JOIN-B, либо B-JOIN-A. Попробуем совместить так, попробуем совместить наоборот, и так пока такие пары не кончатся.

Возьмем наше синтаксическое дерево, возьмем наш план, посмотрим на них непохоже!


Перерисуем в виде графов о, уже стало что-то на что-то похоже!


Давайте обратим внимание, что у нас есть узлы, у которых одновременно есть дети B и C нам неважно в каком порядке. Совместим их и перевернем картинку узла.


Посмотрим еще раз. Теперь у нас есть узлы с детьми A и пары (B + C) совместим и их.


Отлично! Получается, что мы эти два JOIN из запроса с узлами плана удачно совместили.

Увы, эта задача решается не всегда.


Например, если в запросе A JOIN B JOIN C, а в плане в первую очередь соединились крайние узлы A и C. А в запросе нет такого оператора, нам нечего подсветить, не к чему привязать подсказку. То же самое с запятой, когда вы пишете A, B.

Но, в большинстве случаев, почти все узлы удается развязать и получить вот такой профайлинг слева по времени буквально, как в Google Chrome, когда вы код на JavaScript анализируете. Вы видите сколько времени каждая строка и каждый оператор выполнялись.


А чтобы вам всем этим было удобнее пользоваться, мы сделали хранение архива, где вы можете сохранить и потом найти свои планы вместе с ассоциированными запросами или с кем-то поделиться ссылкой.

Если же вам надо просто привести нечитаемый запрос в адекватный вид, воспользуйтесь нашим нормализатором.

Подробнее..

Анализируем слона по частям

29.10.2020 14:07:14 | Автор: admin
Если вы регулярно отлаживаете производительность запросов к PostgreSQL, а EXPLAIN (ANALYZE, BUFFERS) ... любимый инструмент познания особенностей работы этой СУБД, то новые полезные фишки нашего сервиса визуализации и анализа планов explain.tensor.ru наверняка пригодятся вам в этом нелегком деле.

Но сразу напомню, что без полноценного всестороннего мониторинга базы PostgreSQL использовать только анализ плана это выступать с позиции мудреца #5!


[источник КДПВ, Слепые и слон]

Самуил Маршак, 1940
Ученый спор

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

Другой по хоботу слона
Провел рукой своей
И заявил, что слон одна
Из безопасных змей.

Ощупал третий два клыка,
И утверждает он:
На два отточенных штыка
Похож индийский слон!

Слепец четвертый, почесав
Колено у слона,
Установил, что слон шершав,
Как старая сосна.

А пятый, подойдя к слону
Со стороны хвоста,
Определил, что слон в длину
Не больше чем глиста.

Возникли распри у слепцов
И длились целый год.
Потом слепцы в конце концов
Пустили руки в ход.

А так как пятый был силен,
Он всем зажал уста.
И состоит отныне слон
Из одного хвоста!

Итак, сегодня в программе:

  • меняем шевроны на погоны
  • сводим мега-планы
  • ведем персональный архив
  • изучаем генеалогию планов
  • всматриваемся в окна

Не цветом единым!


Исторически, при просмотре плана самые горячие узлы мы отмечали вертикальным шевроном слева от значения чем больше значение, тем насыщеннее цвет.



Но в такой модели плохо воспринимается соотношение величин например, отклонение на 30% по разнице в оттенках может заметить только натренированный глаз. Поэтому мы сделали гистограмму из горизонтальных погонов.



Полезная статистика для мега-планов


Многие не замечают у плана вкладку Статистика, вот она справа:


А кто и заметил вряд ли активно пользовался. Мы решили исправить это упущение, и сделать ее действительно полезной для анализа крупногабаритных планов (100+ узлов).

Группировка узлов


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

А для наглядности каждый тип узла несет цветовую метку:

  • красная чтение данных
    узлы Seq Scan, Index Scan, CTE Scan и разные другие ... Scan
  • желтая обработка данных
    узлы Sort, Unique, Aggregate, Group, Materialize, ...
  • зеленая соединения
    узлы Nested Loop, Merge Join, Hash Join, ...


Сортировка по любому показателю


Если вдруг вам требуется анализ не по суммарному времени, а типу узла, например просто кликните по заголовку столбца и все будет:



Контекстная подсказка узла


Чтобы детально понять вклад конкретного узла в группе, наведите на номер любого из них и увидите традиционную подсказку, что конкретно там происходило:



Персональный архив планов

Без регистрации и SMS!

Если вы активно пользуетесь нашим сервисом, то теперь найти свои ранее анализировавшиеся планы будет гораздо легче достаточно переключиться на вкладку мои в архиве. Сюда планы попадают независимо от публикации в общем архиве и видны только вам.


Генеалогия планов


Раньше найти в архиве какой-то конкретный из своих планов было достаточно сложно, теперь просто. Их можно именовать и группировать в генеалогическое древо оптимизаций!

Просто укажите имя при добавлении плана:



или на уже существующем плане вы можете задать его, отредактировать или добавить связанный план:



Тогда можно будет оперативно переключаться по всему дереву вариантов, чтобы оценить эффект от тех или иных оптимизаций:



А если ссылка на конкретный план вдруг потерялась, его можно легко опознать по имени в персональном архиве:


Всматриваемся в окна


Небольшое, но полезное усовершенствование Query Profiler, о котором я писал ранее мы научили его заглядывать в окна и правильно сопоставлять с узлами плана:

->  WindowAgg   ==>  WINDOW / OVER      ->  Sort  ==>    PARTITION BY / ORDER BY

как несколько независимых определений окна (WINDOW) в рамках одного запроса:



так и сортировки в оконных функциях без явного определения:



Удачной охоты на разные неэффективности!
Подробнее..

One Tool to Analyze Them All

07.12.2020 12:09:16 | Автор: admin
Мы рады сообщить о реализации на explain.tensor.ru базовой поддержки анализа и визуализации планов, специфичных для PostgreSQL-совместимых решений: Timescale, Citus, Greenplum и Redshift.


Так что если в будущем вам встретится план, похожий на результат обычного EXPLAIN, но с некоторыми странными узлами вы знаете, куда идти.

EXPLAIN <-> SQL


В развитие темы сопоставления узлов плана и запроса добавлена возможность быстрого просмотра и переключения между ними:



Поддерживается определение узлов [Parallel] Custom Scan (ChunkAppend):




Собственное время Task-узлов, выполняющихся на отдельных нодах кластера, определяется на основании фактического Execution Time:



Время родителя Custom Scan (Citus Adaptive) вычисляется корректно, даже в случае одновременного выполнения Task сразу на нескольких узлах кластера:


Подробнее об анализе EXPLAIN в Citus.


Поддерживаются специфичные узлы вроде Broadcast Motion, Redistribute Motion, Gather Motion, Partition Selector, Sequence:


Подробнее об анализе EXPLAIN в Greenplum.


Поддерживается работа со всеми XN-узлами, включая специфичные XN Network, XN Merge, XN Window и data redistribution у Join-узлов:



Подробнее об анализе EXPLAIN в Redshift.



Если вдруг вы нашли что-то, что мы пока не учли напишите в комментариях.
Подробнее..

Анализируем слона вместе с коллегами

15.06.2021 14:10:21 | Автор: admin

Если ваша жизнь DBA, сопровождающего PostgreSQL, наполнена вопросами "а почему так медленно?" и "как сделать, чтобы запрос не тормозил?", наш сервис анализа и визуализации планов запросовexplain.tensor.ru сделает ее немного легче за счет привлечения коллег и обновленных подсказок.

м/ф "Следствие ведут Колобки"м/ф "Следствие ведут Колобки"

"Ландон из зе кепитал оф Грейт Британ"

Да, наконец свершилось то, о чем нас так долго просили ради иностранных коллег - мы поддержали работу сервиса не только "на русском", но и на других языках. Пока - только на английском, зато полноценно, включая и перевод рекомендательных подсказок.

Обсуждайте проблемный план там, где вам удобноОбсуждайте проблемный план там, где вам удобно

А чтобы коллегу можно было оперативно подключить к анализу проблем, добавлена возможность оперативно "расшарить" ссылку на план через популярные мессенджеры, соцсети или почту.

Подсказки к плану

Про базовый набор подсказок и способов ими воспользоваться я уже рассказывал в статье "Рецепты для хворающих SQL-запросов" - теперь мы сделали их еще больше и удобнее!

Рекомендательные подсказки узлов планаРекомендательные подсказки узлов плана
  • цветовая и текстовая кодировка разных видов

    Теперь можно легко отличить с первого взгляда проблему избыточной фильтрации из-за отсутствия подходящего индекса от потери времени на сортировке, не уместившейся в памяти.

  • несколько подсказок у одного узла

    Неудачно использованный индекс с большим количеством отбрасываемых записей может легко соседствовать с неактуальной статистикой на таблице и быть ее следствием.

  • на самом видном месте

    Больше не нужно листать огромный план, чтобы увидеть, где там возникла проблема - все подсказки уже сдублированы в заголовке, и достаточно всего одного клика, чтобы перейти к нужному узлу.

Все подсказки - вместе, клик - и вы на местеВсе подсказки - вместе, клик - и вы на месте

Точно так же клик на заголовке столбца сразу перенесет фокус на узел с максимальным значением, так что выяснить "кто тут отфильтровал миллион записей" можно за полсекунды.

Масштабируемая диаграмма

Если прежде увидеть "горячие точки" на потоковой диаграмме выполнения большого плана было сложно за счет возникавшей прокрутки, то теперь сразу запускается просмотр в режиме масштабирования. Но вернуться к режиму прокрутки можно в один клик.

Карта будущего сражения за производительностьКарта будущего сражения за производительность

Пользуйтесь! Возникнут идеи или замечания - прошу в комментарии.

Подробнее..

PostgreSQL Antipatterns работаем с отрезками в кровавом энтерпрайзе

10.11.2020 10:22:20 | Автор: admin
В различных бизнес-приложениях регулярно возникает необходимость решить какую-либо задачу с отрезками/интервалами. Самое сложное в них понять, что это именно одна из таких задач.


Как правило, они отчаянно маскируются, и даже у нас в СБИС их найти можно в абсолютно разных сферах управления предприятием: контроле рабочего времени, оценке загрузки линий АТС или даже в бухгалтерском учете.
Отличие enterprise [решения] от всего остального он всегда идёт от запросов бизнеса и решает какую-то бизнес-задачу. [src]
Вот и давайте посмотрим, какие именно прикладные задачи и как можно решить с помощью PostgreSQL и сократить время анализа данных с нескольких секунд на бизнес-логике до десятков миллисекунд, умея эффективно применять следующие алгоритмы непосредственно внутри SQL-запроса:

  • поиск отрезков, пересекающих точку/интервал
  • слияние отрезков по максимальному перекрытию
  • подсчет количества отрезков в каждой точке

Отрезки в точке


Начнем наши исследования с самой простой задачи обнаружения отрезков, пересекающих конкретную точку. В бизнес-задачах, как правило, такие отрезки имеют какой-то временной характер то есть границами являются или date, или timestamp.



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

CREATE TABLE ranges(  owner    integer, dtb -- начало отрезка    date, dte -- окончание отрезка    date);INSERT INTO ranges(  owner, dtb, dte)SELECT  (random() * 1e3)::integer, dtb, dtb + (random() * 1e2)::integerFROM  (    SELECT      now()::date - (random() * 1e3)::integer dtb    FROM      generate_series(1, 1e5)  ) T;

Попробуем выяснить, кто из сотрудников числился на больничном в новогоднюю ночь.

BETWEEN + btree


Как правило, первый вариант запроса будет выглядеть как-то так:

SELECT  *FROM  rangesWHERE  '2020-01-01'::date BETWEEN dtb AND dte;

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

CREATE INDEX ON ranges(dtb, dte);

И это не работает примерно никак, потому что мы все равно перебрали всю таблицу (100K записей, из которых 95K отбросили):


Чтобы понять почему, достаточно представить визуально, как именно должен работать такой индекс, как мы делали это в статье DBA: находим бесполезные индексы: значение dtb ограничено только справа, dte только слева, в результате имеем два диапазонных ключа, которые совместно по btree работают плохо.

*range + GiST


Но, конечно же, эффективное решение есть это использование возможностей диапазонных типов и GiST-индексов:

CREATE INDEX ON ranges  USING gist( -- используем "пространственный" GiST-индекс    daterange(dtb, dte, '[]') -- формируем диапазон дат  );

Наш запрос теперь нужно модифицировать к такому виду:

SELECT  *FROM  rangesWHERE  daterange(dtb, dte, '[]') @> '2020-01-01'::date; -- значение входит в диапазон



С помощью подходящего индекса мы сократили время работы запроса в 3 раза.

Отрезки по группе


Но зачем нам все 5K записей больничных? Ведь, как правило, нас интересуют только коллеги по отделу то есть какая-то конкретная группа отрезков, которую надо отфильтровать из общего набора.



Поэтому усложним задачу и добавим в нашу модель принадлежность одному из отделов:

ALTER TABLE ranges ADD COLUMN segment integer;UPDATE ranges SET segment = (random() * 1e2)::integer;

SELECT  *FROM  rangesWHERE  segment = 1 AND  daterange(dtb, dte, '[]') @> '2020-01-01'::date;



Индекс, конечно, по-прежнему используется, но теперь получается, что 99% данных мы вычитываем зря 4945 строк отбрасывается при проверке сегмента.

Решение очевидно добавим нужное нам поле в индекс, да?..

CREATE INDEX ON ranges USING gist(segment, daterange(dtb, dte, '[]'));-- ОШИБКА:  для типа данных integer не определён класс операторов по умолчанию для метода доступа "gist"-- HINT:  Вы должны указать класс операторов для индекса или определить класс операторов по умолчанию для этого типа данных.

Увы, нет GiST-индекс просто так не поддерживает операции над скалярными типами. Зато если подключить расширение btree_gist научится:

CREATE EXTENSION btree_gist;CREATE INDEX ON ranges USING gist(segment, daterange(dtb, dte, '[]'));



Наш запрос избавился от всех неиндексных фильтраций и стал в 20 раз быстрее! До кучи, еще и читать стал в 10 раз меньше данных (buffers).

Отрезки на интервале


Но пересечение отрезков с точкой требуется гораздо реже, чем пересечение отрезков с интервалом.
Типичная бизнес-задача: "кто ходил в отпуск в этом месяце?", "какие задачи в моем календаре на сегодня?"



Собственно, все нужные для этого индексы у нас уже есть, осталось правильно записать условие:

SELECT  *FROM  rangesWHERE  segment = 1 AND  daterange(dtb, dte, '[]') && daterange('2020-01-01', '2020-02-01', '[)'); -- пересечение отрезков, без включения правой границы



Объединение отрезков


Допустим, мы нашли когда и кто ходил в отпуск а теперь хотим узнать, когда же именно команда играла в сокращенном составе. Для этого нам придется найти объединение отрезков по максимальному перекрытию.



Если бы мы знали заранее все интервалы, участвующие в объединении, то могли бы просто написать через оператор "+":

SELECT int4range('[0,2]') + int4range('[1,3]');-- [0,4)

К сожалению, для наборов интервалов штатной функции агрегации вроде range_agg пока не реализовано. Ее, конечно, можно создать самостоятельно, но как-то это и выглядит страшно, и базу засоряет лишними сущностями поэтому сделаем это обычным SQL-запросом.



Вариант, работающий даже в версии 8.4, приведен в PostgreSQL Wiki:

WITH rng(s, e) AS (  VALUES    ( 1,  3)  , ( 2,  4)  , ( 5,  6)  , ( 5,  8)  , ( 6,  9)  , ( 7, 10)  , ( 8, 10)  , (10, 11)  , (10, 15)  , (11, 12)  , (12, 13))SELECT  min(s) s, max(e) eFROM  (    SELECT      s    , e    , max(new_start) OVER(ORDER BY s, e) left_edge    FROM      (        SELECT          s        , e        , CASE            WHEN s < max(le) OVER(ORDER BY s, e) THEN              NULL            ELSE              s          END new_start        FROM          (            SELECT              s            , e            , lag(e) OVER(ORDER BY s, e) le            FROM              rng          ) s1      ) s2  ) s3GROUP BY  left_edge;

Но план у него выглядит не слишком приятно сортировка, три WindowAgg, группировка:



И обратите внимание, что в точке 10 он не сливает касающиеся интервалы, хотя обычно это требуется как в нашей задаче про отпуска.

Давайте сконструируем запрос, который нас устроит:

  1. Отсортируем все отрезки по паре (начало, конец).
  2. Для каждого начала вычислим, стоит ли оно правее максимального из предшествующих ему концов. Если левее вообще ничего нет или самый правый конец из предыдущих все равно левее это точка начала искомого интервала.
  3. Отнесем все отрезки в группы по количеству предшествующих им найденных начал.
  4. В каждой группе берем наименьшее из начал и наибольший из концов это и есть искомый непрерывный интервал.



WITH rng(s, e) AS (  VALUES    ( 1,  3)  , ( 2,  4)  , ( 5,  6)  , ( 5,  8)  , ( 6,  9)  , ( 7, 10)  , ( 8, 10)  , (10, 11)  , (10, 15)  , (11, 12)  , (12, 13))SELECT -- min/max по группе  min(s) s, max(e) eFROM  (    SELECT      *    , sum(ns::integer) OVER(ORDER BY s, e) grp -- определение групп    FROM      (        SELECT          *        , coalesce(s > max(e) OVER(ORDER BY s, e ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING), TRUE) ns -- начало правее самого правого из предыдущих концов == разрыв        FROM          rng      ) t  ) tGROUP BY  grp;

Ну, и одно выполнение WindowAgg нам удалось отыграть:


Длина объединенных отрезков


Если же перед нами не стоит цели вычислять сами целевые отрезки, а нужна только их суммарная длина, то можно сократить количество WindowAgg до единственного узла:

  1. Для каждого отрезка вычисляем максимум концов всех предыдущих.
  2. Берем разность между концом отрезка и большим из начала отрезка и найденного конца.
  3. Осталось только просуммировать полученные разности.



WITH rng(s, e) AS (  VALUES    ( 1,  3)  , ( 2,  4)  , ( 5,  6)  , ( 5,  8)  , ( 6,  9)  , ( 7, 10)  , ( 8, 10)  , (10, 11)  , (10, 15)  , (11, 12)  , (12, 13))SELECT  sum(delta)FROM  (    SELECT      *    , greatest(        e - greatest(          max(e) OVER(ORDER BY s, e ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING)        , s        )      , 0      ) delta    FROM      rng  ) T;


Спасибо imschur за этот вариант оптимизации.

Подсчет отрезков на каждом интервале


Итак, мы смогли узнать, сколько всего времени у нас работали не все сотрудники. Но ведь их отсутствовало разное количество в разные промежутки времени а сколько именно когда?
Типичная бизнес-задача: анализ и распределение нагрузки например, между операторами call-центра: "Сколько звонков мы обслуживаем одновременно? Сколько для этого нужно операторов в ночной смене?"
  1. Присвоим каждому началу отрезка вес +1, а каждому концу -1.
  2. Просуммируем накопительно значения в каждой точке это и есть количество отрезков на интервале, начинающемся с этой точки и вплоть до следующей по порядку.



WITH rng(s, e) AS (  VALUES    ( 1,  3)  , ( 2,  4)  , ( 5,  6)  , ( 5,  8)  , ( 6,  9)  , ( 7, 10)  , ( 8, 10)  , (10, 11)  , (10, 15)  , (11, 12)  , (12, 13))SELECT DISTINCT ON(p) -- уникализация до единственного значения в точке  p, sum(v) OVER(ORDER BY p) qty -- накопительная суммаFROM  (    SELECT      s p    , +1 v    FROM      rng  UNION ALL    SELECT      e p    , -1 v    FROM      rng  ) TORDER BY  1;

На этом сегодня все. Если знаете еще какие-то полезные нетривиальные алгоритмы работы с отрезками для решения бизнес-задач напишите в комментариях.
Подробнее..

Агрегаты в БД прокси-таблицы

08.02.2021 20:16:10 | Автор: admin

Мы заканчиваем мини-серию статей о работе с агрегатами в PostgreSQL:

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

Поговорим об этом на примере коллектора нашего сервиса анализа PostgreSQL-логов, о котором я уже рассказывал в предыдущих статьях:

C точки зрения дальнейшей работы с таблицей агрегатов, всегда выгодно все-такииметь единственный экземпляр записи в ней, а не пытаться перезаписывать его многократно, поэтому в нашем распоряжении оказываются вариантыпромежуточной агрегации в памятипроцесса иотдельного "накатывания" изменений.

Если первая оптимизация достаточно очевидна (посылать в базу не "10 раз +1", а "1 раз +10"), то о второй стоит рассказать подробнее.

"Все, что нажито непосильным трудом!.."

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

Давайте посмотрим, из чего конкретно состоит время вставки нового экземпляра записи агрегата.

Я уже рассказывал, что для вставки в агрегаты мы используем тот же самыйCOPY, что и в таблицы "фактов" + триггер, который преобразует вставку вINSERT ON CONFLICT ... DO UPDATE. Агрегатов у нас существенно меньше, чем летящих в них фактов - то есть, практически каждая вставка приводит кUPDATE! А что это у нас "технически"?..

  • накладываемRowExclusiveLock

  • находим по индексу и вычитываемтекущий образ записи

  • прописываем служебное поле xmaxв нем

  • вставляем новый образ записис измененными данными вWAL-файли heap таблицы

  • вписываем изменения вовсе относящиеся индексы- если повезло, будет HOT update с чуть меньшей нагрузкой

Как-то очень много всяких "читаем" и "пишем" получается. А в активный агрегат через секунду прилетит следующий UPDATE, и следующий...

Создаем прокси-таблицу

А вот было бы хорошо, если бы можно было вставлять сразу в такую табличку - чтобыни индексов, ни WAL-файлов, ни UPDATE'ов - только "чистые"INSERT... А ведь так можно сделать!

Давайте породим промежуточную таблицу по формату целевой:

CREATE UNLOGGED TABLE px$agg(  LIKE agg);

Зачем нужен UNLOGGED и что он дает, можно подробно прочитать в статье "DBA: грамотно организовываем синхронизации и импорты".

Теперь все что мы хотим записать - будем писать прямо сюда без всяких триггеров и индексов.

Обрабатываем прокси-таблицу

Но в целевую-то таблицу данные у нас теперь никак не попадают - значит, надо кому-то это поручить. Причем, чтобы нам не пришлось решать никаких конфликтов параллельности, поручим этокому-то одному, вообще постороннемуотносительно вставки данных.

В нашей архитектуре для этого как раз удачно оказался диспетчер-балансировщик, описанный в статье "Динамическая балансировка нагрузки в pull-схеме".

Мы можемкаждые 10 секунд вычитывать все содержимоев прокси-таблице (да, это будет Seq Scan, но это самый быстрый способ доступа ко "всем" данным таблицы), динамически агрегировать в памяти процесса PostgreSQL без передачи на клиента, исразу вставлять в целевую таблицуза один запрос.

Если не знаете, как совместить вычисление разных агрегатов в одном запросе, стоит прочитать "SQL HowTo: 1000 и один способ агрегации".

После того, как мы все вычитали, сагрегировали и вставили - просто и быстро зачистим прокси с помощьюTRUNCATE:

BEGIN;  INSERT INTO agg  SELECT    pk1    ...  , pkN  , <aggfunc>(val1) -- sum/min/max/...  , <aggfunc>(val2)    ...  FROM    px$agg -- тот самый Seq Scan  GROUP BY -- агрегация в памяти в разрезе PK таблицы агрегатов = (pk1, ..., pkN)    pk1    ...  , pkN;  TRUNCATE px$agg;COMMIT;

Замечу, что это надоделать в одной транзакции, поскольку мало ли какие ошибки могут произойти при вставке, аTRUNCATEвне этой транзакции все зачистит, и данные - потеряем.

Защита от параллельной вставки

Но вот беда, пока мы читали-агрегировали, в таблицу кто-то мог что-то еще записать. А мы этозачистим, не обработав.

Можно, конечно, повесить перед началомLOCKна всю таблицу, чтобы уж никто точно ничего не смог ничего с ней сделать, но это не наш вариант - ведь ждать будет слишком много вставок.

Зато... мы можем практически мгновенноподменить ее на пустую такую же!

BEGIN;  SET LOCAL lock_timeout = '100ms'; -- ждем блокировку не дольше 100мс  LOCK TABLE px$agg IN ACCESS EXCLUSIVE MODE; -- собственно, блокируем от всех  ALTER TABLE px$agg RENAME TO px$agg_swap; -- обменяли имена двух табличек  ALTER TABLE px$agg_ RENAME TO px$agg;  ALTER TABLE px$agg_swap RENAME TO px$agg_;COMMIT;

Как только мы дождемся блокировку, так сразу и переименование пройдет. Если не дождемся - то попробуем в следующий раз, не страшно.

В принципе, можно обойтись и двумя RENAME, но прокси-таблиц тогда должно быть больше:

  1. px -> px0, px1 -> px

  2. px -> px1, px0 -> px

Примерно так же живет внутренняя "механика" брокера очередей PgQ и базирующейся на ней репликации Londiste.

Пруфы

Теперь самое интересное - сколько это все дало:

Мы получили снижениепримерно на 10% по дисковой нагрузке, ускорили вставку в таблицу агрегатов и избавились от ожидания блокировок на них.

Подробнее..

Чего энтерпрайзу в PostgreSQL не хватает

26.04.2021 10:24:01 | Автор: admin

В конце прошлого года Иван Панченко предложил мне рассказать на внутреннем семинаре Postgres Pro, чего, по нашему опыту использования PostgreSQL в "кровавом энтерпрайзе" "Тензора", не хватает в этой СУБД.

С докладом пока так и не сложилось, зато появилась эта статья, в которой я постарался собрать наиболее показательные вещи и "хотелки", которые вызывают "напряги" при активном использовании PostgreSQL в реальном бизнесе.

Обслуживание сервера

Легковесный менеджер соединений

он же Built-in connection pooler

Сейчас каждое соединение с PostgreSQL инициирует создание на сервере полноценного дочернего процесса, который занимается его обслуживанием и выполнением запросов.

Процесс старта ресурсоемкий и небыстрый сам по себе, а постоянно существующий процесс еще и резервирует на себя некоторую долю RAM, которая со временем имеет свойство расти за счет "накачки" метаинформацией.

Просто представьте, что у вас в базе развернуто миллион таблиц, к которым вы достаточно случайно обращаетесь. Только на системных таблицах pg_class, pg_depend, pg_statistics это даст объем порядка 1GB, который рано или поздно окажется в памяти процесса.

Поэтому в реальных условиях соединения непосредственно с БД стараются беречь, без нужды новые не создавать, а неактивные закрывать поскорее. Для этого "в разрыв" БЛ-БД ставят отдельный сервис connection pooler, который принимает от БЛ все "хотелки" по открытию/закрытию соединений и выполнению запросов, а в БД транслирует их в ограниченное количество длительно живущих коннектов.

Пример работы connection poolПример работы connection pool

Классические представители:

Но, по-любому, это какой-то дополнительный элемент в и без него непростой архитектуре, который требует дополнительного присмотра и обслуживания, чего хотелось бы избежать.

Поэтому еще в начале 2018 года Константин Книжник начал эксперименты с реализацией менеджера соединений "из коробки" на основе интеграции потоков (thread model) в ядро PostgreSQL. В настоящий момент его патч включен на ревью в июльский Commitfest, так что ждем и надеемся на появление хотя бы в v15.

64-bit XID

Если вы пишете в базу много данных, как мы, то достаточно скоро у вас запустится он - autovacuum (to prevent wraparound), чья единственная задача - пробежать по данным и "поправить" их так, чтобы уберечь счетчик транзакций от переполнения.

Одно из наглядных объяснений 32-bit transaction ID wraparoundОдно из наглядных объяснений 32-bit transaction ID wraparound

Но вот какой-то не особо необходимый с прикладной точки зрения процесс на сильно нагруженной (а иначе бы он у вас еще нескоро случился) системе, который будет активно читать-писать диск - это совсем не то, что хотелось бы видеть на базе "в рабочий полдень".

А чтобы никаких подобных процессов не запускалось, достаточно расширить идентификатор транзакции xid с 32 до 64 бит - соответствующий патч Александр Коротков запустил еще в 2017 году. В ядро он тогда так и не был включен, зато попал в Postgres ProEnterprise, откуда рано или поздно доберется и до "ванильного" ядра.

Система хранения данных

Микротаблицы

Каждая таблица и индекс в PostgreSQL с точки зрения хранения представляет из себя не меньше 3 файлов:

То есть если вам необходимо иметь небольшую статичную "словарную" табличку на пару десятков записей, то вы автоматически получаете 3 файла по 8KB, хотя можно было бы обойтись и единственной страницей heap.

Поэтому такая табличка, созданная в каждой из клиентских схем, где изолированы пользовательские данные, сразу дает резкое увеличение количества файлов на носителе (миллионы их!), что негативно влияет как на время бэкапа, так и на производительность всей дисковой подсистемы.

zheap

Если предыдущий патч устраняет необходимость в достаточно редкой операции, то использование хранилища zheap призвано помочь сэкономить ресурсы сервера на таблицах, в которых почти не бывает ROLLBACK - обычно это различные "логоподобные" вещи или агрегатные "срезы", где важно только текущее состояние, но UPDATE случаются постоянно.

Информация о транзакциях хранится отдельно от контейнера записиИнформация о транзакциях хранится отдельно от контейнера записи

Почитать по теме:

Append-only Storage

В бизнесе СУБД часто используются для хранения многократно повторяющихся или монотонно увеличивающихся значений - например, логи, дата и время создания какого-то документа, его числовой PK, ...

Знание этого факта позволяет существенно сэкономить записываемый объем. Например, переведя базу нашего сервиса мониторинга с v12 на v13, мы сразу получили примерно 10% выигрыша в объеме индексов за счет дедупликации в btree-индексах на реальных данных.

В эту же категорию можно отнести уже принятый патч "BRIN multi-minmax and bloom indexes".

В идеале, хотелось бы иметь возможность назначать часть полей индексов или целых таблиц как "no-MVCC" (то есть не поддерживается никакое версионирование, а потому - и изменение данных, плюс мгновенная видимость со стороны других транзакций), чтобы иметь возможность сэкономить на полях поддержки MVCC.

Timescale базируется на ядре PostgreSQL, но "заточена" на timeseries-dataTimescale базируется на ядре PostgreSQL, но "заточена" на timeseries-data

Отложенная индексация

Одной из причин Uber-скандала, всколыхнувшего PostgreSQL-сообщество несколько лет назад была Index Write Amplification, когда записываемый в таблицу кортеж сразу же одновременно записывается и во все индексы, подходящие по условию для него. Получается, чем больше индексов есть на таблице, тем дольше будет производиться вставка в нее.

Но ведь большинство этих индексов заведомо не будут использованы для поиска этой записи сразу же после завершения запроса! Точнее, разработчик точно может отметить эти индексы и их желаемую "актуальность", примерно как это настраивается для реплики.

CREATE INDEX ... WITH (max_lag = '1min');

В этом случае сервер сам сможет "размазать" необходимые операции во времени для балансировки нагрузки. Наиболее актуальна эта тема при конкуренции с поисковыми системами вроде Sphinx/ElasticSearch, где основная задача "найти вообще", а не "найти прямо сразу сейчас".

Почитать по теме:

Columnar Storage

В идеале - в ядре или в contrib иметь возможность подключения колоночного хранилища для различных аналитических нужд.

Хранение данных "по столбцам"Хранение данных "по столбцам"

Да, есть решения от Citus, но "в энтерпрайзе" не всегда есть возможность установки дополнительных внешних модулей.

In-memory Storage

Появление очень быстрого нетранзакционного хранилища без сброса на диск сильно помогло бы использовать разноуровневые кэши прямо в PostgreSQL, а не выносить их куда-то в Redis, например - получился бы некий аналог Oracle TimesTen In-Memory Database и Tarantool.

Масштабирование

TEMPORARY TABLE и реплики

Если для генерации отчетов в своей системе вы предпочитаете не писать мегасложные запросы, а более простые итерации по временным таблицам, то неприятностей будет ровно две:

И если с первой проблемой еще как-то можно смириться и исправлять регулярным VACUUM pg_class, то второй недостаток достаточно сильно мешает.

Multimaster

Давно хочется иметь "из коробки".

SQL

SQL-defined Index

Было бы фантастично уметь описывать новые виды индексов прямо на SQL/PLPGSQL, без необходимости C-кодинга - фактически, тут нет ограничений, кроме производительности из-за необходимости сделать все "здесь и сейчас". Но если вспомнить про описанную выше возможность отложенной индексации, то задача уже не кажется такой уж нереальной.

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

Почитать по теме:

Мониторинг

В инфраструктуре PostgreSQL сильно не хватает удобных средств быстрого визуального контроля и анализа происходящего с производительностью базы. Тут мы в меру собственных сил стараемся создавать подобные инструменты и формировать некоторую базу возможных оптимизаций.

Производительность запросов

Сам анализ, куда ушло время в запросе по тексту плана является нетривиальным, но если его прогнать через визуализацию на explain.tensor.ru, становится попроще:

Визуальный анализ плана запросаВизуальный анализ плана запросаДиаграмма выполнения планаДиаграмма выполнения плана

Почитать по теме:

Снапшоты статистики таблиц

Чтобы определить, в каком именно таблице/индексе "сильно болит", у Oracle есть AWR а наиболее близким аналогом для PostgreSQL является pg_profile.

Но идея хранить дополнительные данных ровно в той же базе, на которой и так идут проблемы (иначе зачем вам нужен столь подробный мониторинг?), нам показалась не слишком эффективной. Поэтому мы вынесли съем, хранение и визуализацию этой информации на отдельный сервис, который собирает у нас внутри компании всю статистику о работе PostgreSQL-серверов.

Тепловая карта операций по таблицамТепловая карта операций по таблицам

Почитать по теме:


Это - что напрягает нас в этой, безусловно, отличной СУБД. В комментариях оставляйте рассказы о своих "болях" и "хотелках" при использовании PostgreSQL.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru