Комментарии 14
Если Вы хотите показать пути решения задачи и его оптимизации, то сначала надо как минимум зафиксировать исходные данные и конечный результат. Причём для конечного результата следует зафиксировать и набор полей, и типы данных. Произвольно "договариваться" на середине пути о смене формата вывода - неправильно.
В случае, когда 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.
Ну так речь именно про дипломы. Второй вариант я очень редко где-то встречал, так как он все равно не позволяет зафиксировать количество лидеров.
Первый вариант - это что-то типа олимпиады по информатике: "кто решил максимум задач - диплом первой степени, на одну меньше - второй".
А второй вариант - это спортивные соревнования "за место" обычно.
А второй вариант - это спортивные соревнования "за место" обычно.
Обычно, так не делается. Например, в легкой атлетике:
Если два (или более) участника показали одинаковый результат, то рассматривается второй результат, независимо от попытки, если и он одинаков, то — третий результат и так до выявления преимущества одного из участников. Если все показатели у них одинаковы, то им дается дополнительная попытка для выявления победителя.
SQL HowTo: TOP-N на субинтервалах