Как стать автором
Обновить

Комментарии 14

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

Если мы "рулим" обеими сторонами взаимодействия BL <--> DB, то "договориться" можем в любой момент.

Можно ли не сворачивать в json? Конечно, и "отсыпать" TOP-3 за одну дату несколькими записями. Но тогда "агрегировать" в рамках дня придется уже на стороне бизнес-логики, а оно там надо?..

В случае, когда code всего 27, а записей - миллионы, эффективней идти полностью по индексу. Пример сделал на 10 миллионах, так как на миллионе он не столь явно различается:

DROP TABLE IF EXISTS fact4agg;
CREATE TABLE fact4agg AS
  SELECT
    now()::date - (random() * 365)::integer dt      -- дата "факта"
  , chr(ascii('a') + (random() * 26)::integer) code -- код агрегации
  FROM
    generate_series(1, 1e7);
CREATE INDEX fact4agg_dt_code_Idx ON fact4agg(dt) INCLUDE(code);
CREATE INDEX fact4agg_code_dt_Idx ON fact4agg(code, dt);

Сначала Ваш запрос:

DROP TABLE IF EXISTS tmp_tmp;
EXPLAIN ANALYZE
CREATE TEMP TABLE tmp_tmp AS
WITH params(dtb, dte) AS (
  VALUES(now()::date - 30, now()::date)
)
SELECT
  dt
, code
, count(*) qty
FROM
  params
, fact4agg
WHERE
  dt BETWEEN dtb AND dte
GROUP BY
  1, 2;

HashAggregate  (cost=25677.39..25776.21 rows=9882 width=14) (actual time=219.549..219.739 rows=837 loops=1)
  Group Key: fact4agg.dt, fact4agg.code
  Batches: 1  Memory Usage: 529kB
  ->  Index Only Scan using fact4agg_dt_code_idx on fact4agg  (cost=0.45..19367.89 rows=841267 width=6) (actual time=0.025..89.172 rows=836228 loops=1)
        Index Cond: ((dt >= ((now())::date - 30)) AND (dt <= (now())::date))
        Heap Fetches: 0
Planning Time: 0.128 ms
Execution Time: 221.081 ms

А теперь через code:

DROP TABLE IF EXISTS tmp_tmp;
EXPLAIN ANALYZE
CREATE TEMP TABLE tmp_tmp AS
WITH Codes AS (
  SELECT chr(ascii('a') + V.n) AS code
  FROM generate_series(0, 26) V(n) ),
params(dtb, dte) AS (
  VALUES(now()::date - 30, now()::date)
)
SELECT Q.dt, C.code, Q.qty
FROM Codes C
CROSS JOIN params P
CROSS JOIN LATERAL (
  SELECT F.dt, COUNT(1) AS qty
  FROM fact4agg F
  WHERE F.code=C.code AND F.dt BETWEEN P.dtb AND P.dte
  GROUP BY F.dt ) Q;

Nested Loop  (cost=0.46..24324.77 rows=9882 width=44) (actual time=0.114..98.558 rows=837 loops=1)
  ->  Function Scan on generate_series v  (cost=0.00..0.27 rows=27 width=4) (actual time=0.005..0.010 rows=27 loops=1)
  ->  GroupAggregate  (cost=0.45..891.76 rows=366 width=12) (actual time=0.129..3.644 rows=31 loops=27)
        Group Key: f.dt
        ->  Index Only Scan using fact4agg_code_dt_idx on fact4agg f  (cost=0.45..732.31 rows=31158 width=4) (actual time=0.008..1.876 rows=30971 loops=27)
              Index Cond: ((code = chr((97 + v.n))) AND (dt >= ((now())::date - 30)) AND (dt <= (now())::date))
              Heap Fetches: 0
Planning Time: 0.193 ms
Execution Time: 99.835 ms

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

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

На практике такой подход хорошо работает даже для 100 тыс. полувагонов и полутора миллиардах событий для них. То есть, при отношении 1/10000 этот подход еще остается эффективным.

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

в общем случае закладываться на такое не выйдет

В общем случае лучше ни на что не закладываться. "Золотого молотка" не бывает )

Обратил внимание, что у Вас не честные TOP 3. Если у каких-то двух code одинаковый qty в какой-то день и оба на третьем месте, то Вы случайным образом одного из них выкидываете. В реальной жизни люди так могут и обидеться.

Корректней выводить с учётом того, что какое-то место могут поделить. То есть делать выборку через DENSE_RANK():

WITH Codes AS (
  SELECT chr(ascii('a') + V.n) AS code
  FROM generate_series(0, 26) V(n) ),
params(dtb, dte) AS (
  VALUES(now()::date - 30, now()::date) ),
Aggregated AS (
  SELECT Q.dt, C.code, Q.qty,
    DENSE_RANK() OVER (PARTITION BY Q.dt ORDER BY Q.qty DESC) AS dr
  FROM Codes C
  CROSS JOIN params P
  CROSS JOIN LATERAL (
    SELECT F.dt, COUNT(1) AS qty
    FROM fact4agg F
    WHERE F.code=C.code AND F.dt BETWEEN P.dtb AND P.dte
    GROUP BY F.dt ) Q )
SELECT
  A.dt,
  json_agg(
    json_build_object('rank', A.dr, 'code', A.code, 'qty', A.qty)
    ORDER BY A.qty DESC ) json
FROM Aggregated A
WHERE A.dr<=3
GROUP BY A.dt;

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

Эту проблему PostgreSQL решил самостоятельно:

GroupAggregate  (cost=24931.06..25353.55 rows=200 width=36) (actual time=98.812..99.057 rows=31 loops=1)
  Group Key: f.dt
  ->  WindowAgg  (cost=24931.06..25178.11 rows=9882 width=52) (actual time=98.774..98.913 rows=96 loops=1)
        Run Condition: (dense_rank() OVER (?) <= 3)
        ->  Sort  (cost=24931.06..24955.77 rows=9882 width=16) (actual time=98.761..98.799 rows=837 loops=1)
              Sort Key: f.dt, (count(1)) DESC
              Sort Method: quicksort  Memory: 77kB
              ->  Nested Loop  (cost=0.46..24275.36 rows=9882 width=16) (actual time=0.122..98.513 rows=837 loops=1)
                    ->  Function Scan on generate_series v  (cost=0.00..0.27 rows=27 width=4) (actual time=0.006..0.011 rows=27 loops=1)
                    ->  GroupAggregate  (cost=0.45..891.76 rows=366 width=12) (actual time=0.133..3.645 rows=31 loops=27)
                          Group Key: f.dt
                          ->  Index Only Scan using fact4agg_code_dt_idx on fact4agg f  (cost=0.45..732.31 rows=31158 width=4) (actual time=0.011..1.881 rows=30971 loops=27)
                                Index Cond: ((code = chr((97 + v.n))) AND (dt >= ((now())::date - 30)) AND (dt <= (now())::date))
                                Heap Fetches: 0
Planning Time: 0.284 ms
Execution Time: 99.110 ms

Это я опять на 10 миллионах развлекался.

Таки да, поэтому в реальности для обеспечения стабильности порядка сортировки приходится сортировать по ФИО, коду страны, дополнительным очкам... но если очень хочется "не обидеть", можно использовать FETCH WITH TIES.

можно использовать FETCH WITH TIES

Можно. Но лучше все же не выбирать в БД записи, которые потом точно не собираетесь фетчить. DENSE_RANK() у меня даже стабильно несколько миллисекунд выигрывал, относительно Вашего решения через массив.

Они фетчатся уже в узле Index Only Scan, если мы считаем агрегаты динамически, а не храним. А если храним - то вычитаем лишь на 1 запись больше указанного лимита.

Посмотрите внимательней на мой запрос. Даже первое место могут поделить не только двое, но и трое, и четверо. Аналогично и со вторым местом. А WITH TIES добавит дубли только одного последнего в выборке места. Что получится, если у Вас первое место поделили четверо? А DENSE_RANK() это отрабатывает честно.

В этом смысле есть разные прикладные задачи.

"10 дипломов первой степени, 20 - второй и 30 - третьей" - это как раз про dense_rank.

"Иванов и Петров поделили 1/2 место, а Сидоров с Васечкиным - 3/4" - про WITH TIES.

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

Первый вариант - это что-то типа олимпиады по информатике: "кто решил максимум задач - диплом первой степени, на одну меньше - второй".

А второй вариант - это спортивные соревнования "за место" обычно.

А второй вариант - это спортивные соревнования "за место" обычно.

Обычно, так не делается. Например, в легкой атлетике:

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий