Оптимальная оптимизация запросов postgresql (отдельная агрегация строк с упорядочением)
У меня есть таблица событий, которая имеет очень похожую схему и распределение данных, как эта искусственная таблица, которую можно легко создать локально:
CREATE TABLE events AS
WITH args AS (
SELECT
300 AS scale_factor, -- feel free to reduce this to speed up local testing
1000 AS pa_count,
1 AS l_count_min,
29 AS l_count_rand,
10 AS c_count,
10 AS pr_count,
3 AS r_count,
'10 days'::interval AS time_range -- edit 2017-05-02: the real data set has years worth of data here, but the query time ranges stay small (a couple days)
)
SELECT
p.c_id,
'ABC'||lpad(p.pa_id::text, 13, '0') AS pa_id,
'abcdefgh-'||((random()*(SELECT pr_count-1 FROM args)+1))::int AS pr_id,
((random()*(SELECT r_count-1 FROM args)+1))::int AS r,
'2017-01-01Z00:00:00'::timestamp without time zone + random()*(SELECT time_range FROM args) AS t
FROM (
SELECT
pa_id,
((random()*(SELECT c_count-1 FROM args)+1))::int AS c_id,
(random()*(SELECT l_count_rand FROM args)+(SELECT l_count_min FROM args))::int AS l_count
FROM generate_series(1, (SELECT pa_count*scale_factor FROM args)) pa_id
) p
JOIN LATERAL (
SELECT generate_series(1, p.l_count)
) l(id) ON (true);
Выдержка из SELECT * FROM events
:
![введите описание изображения здесь]()
Мне нужен запрос, который выбирает все строки для данного c_id
в заданном диапазоне времени t
, а затем фильтрует их, чтобы включать только самые последние строки (через t
) для каждого уникального pr_id
и pa_id
, а затем подсчитывает количество комбинаций pr_id
и r
этих строк.
Это довольно много, так что вот 3 SQL-запроса, которые я придумал, которые дают желаемые результаты:
WITH query_a AS (
SELECT
pr_id,
r,
count(1) AS quantity
FROM (
SELECT DISTINCT ON (pr_id, pa_id)
pr_id,
pa_id,
r
FROM events
WHERE
c_id = 5 AND
t >= '2017-01-03Z00:00:00' AND
t < '2017-01-06Z00:00:00'
ORDER BY pr_id, pa_id, t DESC
) latest
GROUP BY
1,
2
ORDER BY 3, 2, 1 DESC
),
query_b AS (
SELECT
pr_id,
r,
count(1) AS quantity
FROM (
SELECT
pr_id,
pa_id,
first_not_null(r ORDER BY t DESC) AS r
FROM events
WHERE
c_id = 5 AND
t >= '2017-01-03Z00:00:00' AND
t < '2017-01-06Z00:00:00'
GROUP BY
1,
2
) latest
GROUP BY
1,
2
ORDER BY 3, 2, 1 DESC
),
query_c AS (
SELECT
pr_id,
r,
count(1) AS quantity
FROM (
SELECT
pr_id,
pa_id,
first_not_null(r) AS r
FROM events
WHERE
c_id = 5 AND
t >= '2017-01-03Z00:00:00' AND
t < '2017-01-06Z00:00:00'
GROUP BY
1,
2
) latest
GROUP BY
1,
2
ORDER BY 3, 2, 1 DESC
)
И вот пользовательская агрегированная функция, используемая query_b
и query_c
, а также то, что я считаю самым оптимальным индексом, настройками и условиями:
CREATE FUNCTION first_not_null_agg(before anyelement, value anyelement) RETURNS anyelement
LANGUAGE sql IMMUTABLE STRICT
AS $_$
SELECT $1;
$_$;
CREATE AGGREGATE first_not_null(anyelement) (
SFUNC = first_not_null_agg,
STYPE = anyelement
);
CREATE INDEX events_idx ON events USING btree (c_id, t DESC, pr_id, pa_id, r);
VACUUM ANALYZE events;
SET work_mem='128MB';
Моя дилемма заключается в том, что query_c
превосходит query_a
и query_b
в коэффициенте > 6x, но технически не гарантирует получение того же результата, что и другие запросы (обратите внимание на отсутствующий ORDER BY
в first_not_null
агрегат). Однако на практике он, похоже, выбирает план запроса, который, по моему мнению, является правильным и наиболее оптимальным.
Ниже приведены выходы EXPLAIN (ANALYZE, VERBOSE)
для всех трех запросов на моей локальной машине:
query_a
:
CTE Scan on query_a (cost=25810.77..26071.25 rows=13024 width=44) (actual time=3329.921..3329.934 rows=30 loops=1)
Output: query_a.pr_id, query_a.r, query_a.quantity
CTE query_a
-> Sort (cost=25778.21..25810.77 rows=13024 width=23) (actual time=3329.918..3329.921 rows=30 loops=1)
Output: events.pr_id, events.r, (count(1))
Sort Key: (count(1)), events.r, events.pr_id DESC
Sort Method: quicksort Memory: 27kB
-> HashAggregate (cost=24757.86..24888.10 rows=13024 width=23) (actual time=3329.849..3329.892 rows=30 loops=1)
Output: events.pr_id, events.r, count(1)
Group Key: events.pr_id, events.r
-> Unique (cost=21350.90..22478.71 rows=130237 width=40) (actual time=3168.656..3257.299 rows=116547 loops=1)
Output: events.pr_id, events.pa_id, events.r, events.t
-> Sort (cost=21350.90..21726.83 rows=150375 width=40) (actual time=3168.655..3209.095 rows=153795 loops=1)
Output: events.pr_id, events.pa_id, events.r, events.t
Sort Key: events.pr_id, events.pa_id, events.t DESC
Sort Method: quicksort Memory: 18160kB
-> Index Only Scan using events_idx on public.events (cost=0.56..8420.00 rows=150375 width=40) (actual time=0.038..101.584 rows=153795 loops=1)
Output: events.pr_id, events.pa_id, events.r, events.t
Index Cond: ((events.c_id = 5) AND (events.t >= '2017-01-03 00:00:00'::timestamp without time zone) AND (events.t < '2017-01-06 00:00:00'::timestamp without time zone))
Heap Fetches: 0
Planning time: 0.316 ms
Execution time: 3331.082 ms
query_b
:
CTE Scan on query_b (cost=67140.75..67409.53 rows=13439 width=44) (actual time=3761.077..3761.090 rows=30 loops=1)
Output: query_b.pr_id, query_b.r, query_b.quantity
CTE query_b
-> Sort (cost=67107.15..67140.75 rows=13439 width=23) (actual time=3761.074..3761.081 rows=30 loops=1)
Output: events.pr_id, (first_not_null(events.r ORDER BY events.t DESC)), (count(1))
Sort Key: (count(1)), (first_not_null(events.r ORDER BY events.t DESC)), events.pr_id DESC
Sort Method: quicksort Memory: 27kB
-> HashAggregate (cost=66051.24..66185.63 rows=13439 width=23) (actual time=3760.997..3761.049 rows=30 loops=1)
Output: events.pr_id, (first_not_null(events.r ORDER BY events.t DESC)), count(1)
Group Key: events.pr_id, first_not_null(events.r ORDER BY events.t DESC)
-> GroupAggregate (cost=22188.98..63699.49 rows=134386 width=32) (actual time=2961.471..3671.669 rows=116547 loops=1)
Output: events.pr_id, events.pa_id, first_not_null(events.r ORDER BY events.t DESC)
Group Key: events.pr_id, events.pa_id
-> Sort (cost=22188.98..22578.94 rows=155987 width=40) (actual time=2961.436..3012.440 rows=153795 loops=1)
Output: events.pr_id, events.pa_id, events.r, events.t
Sort Key: events.pr_id, events.pa_id
Sort Method: quicksort Memory: 18160kB
-> Index Only Scan using events_idx on public.events (cost=0.56..8734.27 rows=155987 width=40) (actual time=0.038..97.336 rows=153795 loops=1)
Output: events.pr_id, events.pa_id, events.r, events.t
Index Cond: ((events.c_id = 5) AND (events.t >= '2017-01-03 00:00:00'::timestamp without time zone) AND (events.t < '2017-01-06 00:00:00'::timestamp without time zone))
Heap Fetches: 0
Planning time: 0.385 ms
Execution time: 3761.852 ms
query_c
:
CTE Scan on query_c (cost=51400.06..51660.54 rows=13024 width=44) (actual time=524.382..524.395 rows=30 loops=1)
Output: query_c.pr_id, query_c.r, query_c.quantity
CTE query_c
-> Sort (cost=51367.50..51400.06 rows=13024 width=23) (actual time=524.380..524.384 rows=30 loops=1)
Output: events.pr_id, (first_not_null(events.r)), (count(1))
Sort Key: (count(1)), (first_not_null(events.r)), events.pr_id DESC
Sort Method: quicksort Memory: 27kB
-> HashAggregate (cost=50347.14..50477.38 rows=13024 width=23) (actual time=524.311..524.349 rows=30 loops=1)
Output: events.pr_id, (first_not_null(events.r)), count(1)
Group Key: events.pr_id, first_not_null(events.r)
-> HashAggregate (cost=46765.62..48067.99 rows=130237 width=32) (actual time=401.480..459.962 rows=116547 loops=1)
Output: events.pr_id, events.pa_id, first_not_null(events.r)
Group Key: events.pr_id, events.pa_id
-> Index Only Scan using events_idx on public.events (cost=0.56..8420.00 rows=150375 width=32) (actual time=0.027..109.459 rows=153795 loops=1)
Output: events.c_id, events.t, events.pr_id, events.pa_id, events.r
Index Cond: ((events.c_id = 5) AND (events.t >= '2017-01-03 00:00:00'::timestamp without time zone) AND (events.t < '2017-01-06 00:00:00'::timestamp without time zone))
Heap Fetches: 0
Planning time: 0.296 ms
Execution time: 525.566 ms
В целом, я считаю, что указанный выше индекс должен позволять query_a
и query_b
выполняться без узлов сортировки, которые замедляют их, но до сих пор я не смог убедить оптимизатора запросов postgres выполнять мои ставки.
Я также немного смущен тем, что столбец t
не включен в ключ сортировки для query_b
, учитывая, что quicksort нестабилен. Похоже, это может привести к неправильным результатам.
Я проверил, что все 3 запроса генерируют те же результаты, в которых выполняются следующие запросы, и проверяя, что они создают пустой набор результатов:
SELECT * FROM query_a
EXCEPT
SELECT * FROM query_b;
и
SELECT * FROM query_a
EXCEPT
SELECT * FROM query_c;
Я бы рассматривал query_a
как канонический запрос, когда сомневался.
Я очень благодарен за любой вклад в это. Я действительно нашел ужасно хакерское решение для достижения приемлемой производительности в моем приложении, но эта проблема продолжает охотиться на меня во сне (и на самом деле отпуск, который я сейчас нахожу)... 😬.
FWIW, я рассмотрел много похожих вопросов и ответов, которые руководствовались моим текущим мышлением, но я считаю, что есть что-то уникальное в отношении двух группировок столбцов (pr_id
, pa_id
) и приходится сортировать по 3-й столбец (t
), который не делает это дублирующимся вопросом.
Изменить: Внешние запросы в примере могут быть совершенно неактуальны в вопросе, поэтому не стесняйтесь игнорировать их, если это помогает.
Ответы
Ответ 1
Я бы рассмотрел query_a
как канонический запрос, когда сомневался.
Я нашел способ сделать query_a
на полсекунды быстрее.
Ваш внутренний запрос из query_a
SELECT DISTINCT ON (pr_id, pa_id)
требуется
ORDER BY pr_id, pa_id, t DESC
особенно с pr_id
и pa_id
, указанными первыми.
c_id = 5
является константой, но вы не можете использовать свой индекс event_idx (c_id, t DESC, pr_id, pa_id, r)
, потому что столбцы не организованы (pr_id, pa_id, t DESC)
, что требует ваше предложение ORDER BY
.
Если у вас был индекс не менее (pr_id, pa_id, t DESC)
, сортировка не должна произойти, потому что условие ORDER BY
выравнивается с этим индексом.
Итак, вот что я сделал.
CREATE INDEX events_idx2 ON events (c_id, pr_id, pa_id, t DESC, r);
Этот индекс может использоваться вашим внутренним запросом - по крайней мере теоретически.
К сожалению, планировщик запросов считает, что лучше сократить количество строк с помощью индекса events_idx
с помощью c_id
и x <= t < y
.
Postgres не имеет указателей, поэтому нам нужен другой способ убедить планировщика запросов взять новый индекс events_idx2
.
Один из способов принудительного использования events_idx2
- сделать другой индекс более дорогим.
Это можно сделать, удалив последний столбец r
из events_idx
и сделав его непригодным для query_a
(по крайней мере, непригодным для использования без загрузки страниц из кучи).
Контр-интуитивно перемещать столбец t
позже в макете индекса, потому что обычно первые столбцы будут выбраны для = и диапазонов, для которых c_id
и t
подходят хорошо.
Однако ваш ORDER BY (pr_id, pa_id, t DESC)
мандат, по крайней мере, этого подмножества как есть в вашем индексе. Конечно, мы по-прежнему сначала помещаем c_id
для сокращения строк как можно быстрее.
У вас все еще может быть индекс на (c_id, t DESC, pr_id, pa_id)
, если вам нужно, но он не может использоваться в query_a
.
Вот план запроса для query_a
с events_idx2
и events_idx
удален.
Найдите events_c_id_pr_id_pa_id_t_r_idx
, так как индексы имен PG автоматически, если вы не дадите им имя.
Мне это нравится, потому что я вижу порядок столбцов в имени индекса в каждом плане запроса.
Sort (cost=30076.71..30110.75 rows=13618 width=23) (actual time=426.898..426.914 rows=30 loops=1)
Sort Key: (count(1)), events.r, events.pr_id DESC
Sort Method: quicksort Memory: 27kB
-> HashAggregate (cost=29005.43..29141.61 rows=13618 width=23) (actual time=426.820..426.859 rows=30 loops=1)
Group Key: events.pr_id, events.r
-> Unique (cost=0.56..26622.33 rows=136177 width=40) (actual time=0.037..328.828 rows=117204 loops=1)
-> Index Only Scan using events_c_id_pr_id_pa_id_t_r_idx on events (cost=0.56..25830.50 rows=158366 width=40) (actual time=0.035..178.594 rows=154940 loops=1)
Index Cond: ((c_id = 5) AND (t >= '2017-01-03 00:00:00'::timestamp without time zone) AND (t < '2017-01-06 00:00:00'::timestamp without time zone))
Heap Fetches: 0
Planning time: 0.201 ms
Execution time: 427.017 ms
(11 Zeilen)
Планирование выполняется мгновенно, а производительность - второй, потому что индекс соответствует ORDER BY
внутреннего запроса.
При хорошей производительности на query_a
нет необходимости в дополнительной функции для ускорения альтернативных запросов query_b
и query_c
.
Примечания:
Как-то я не мог найти первичный ключ в ваших отношениях.
Вышеупомянутое предлагаемое решение работает без каких-либо первичных предположений.
Я все еще думаю, что у вас есть первичный ключ, но, возможно, забыл упомянуть об этом.
Естественный ключ pa_id
. Каждый pa_id
означает "вещь", которая имеет Об этом сообщается около 1... 30 событий.
Если pa_id
относится к нескольким c_id
's, то pa_id
не может быть ключом.
Если pr_id
и r
являются данными, возможно, (c_id, pa_id, t)
является уникальным ключом?
Также ваш индекс events_idx
не создается уникальным, но охватывает все столбцы отношения, поэтому вы можете иметь несколько равных строк - вы хотите разрешить это?
Если вам действительно нужны оба показателя events_idx
и предлагаемый events_idx2
, то вы будете иметь данные, которые будут храниться 3 раза (в два раза по индексам, один раз в куче).
Так как это действительно сложная оптимизация запросов, я прошу вас хотя бы подумать над добавлением щедрости для тех, кто отвечает на ваш вопрос, также потому, что он довольно долго сидел на SO без ответа.
РЕДАКТИРОВАТЬ A
Я вставил еще один набор данных, используя превосходную настройку выше, в основном удваивая количество строк.
На этот раз даты начинались с '2017-01-10'.
Все остальные параметры остались прежними.
Вот частичный индекс атрибута времени и его поведение запроса.
CREATE INDEX events_timerange ON events (c_id, pr_id, pa_id, t DESC, r) WHERE '2017-01-03' <= t AND t < '2017-01-06';
Sort (cost=12510.07..12546.55 rows=14591 width=23) (actual time=361.579..361.595 rows=30 loops=1)
Sort Key: (count(1)), events.r, events.pr_id DESC
Sort Method: quicksort Memory: 27kB
-> HashAggregate (cost=11354.99..11500.90 rows=14591 width=23) (actual time=361.503..361.543 rows=30 loops=1)
Group Key: events.pr_id, events.r
-> Unique (cost=0.55..8801.60 rows=145908 width=40) (actual time=0.026..265.084 rows=118571 loops=1)
-> Index Only Scan using events_timerange on events (cost=0.55..8014.70 rows=157380 width=40) (actual time=0.024..115.265 rows=155800 loops=1)
Index Cond: (c_id = 5)
Heap Fetches: 0
Planning time: 0.214 ms
Execution time: 361.692 ms
(11 Zeilen)
Без индекса events_timerange (регулярный полный индекс).
Sort (cost=65431.46..65467.93 rows=14591 width=23) (actual time=472.809..472.824 rows=30 loops=1)
Sort Key: (count(1)), events.r, events.pr_id DESC
Sort Method: quicksort Memory: 27kB
-> HashAggregate (cost=64276.38..64422.29 rows=14591 width=23) (actual time=472.732..472.776 rows=30 loops=1)
Group Key: events.pr_id, events.r
-> Unique (cost=0.56..61722.99 rows=145908 width=40) (actual time=0.024..374.392 rows=118571 loops=1)
-> Index Only Scan using events_c_id_pr_id_pa_id_t_r_idx on events (cost=0.56..60936.08 rows=157380 width=40) (actual time=0.021..222.987 rows=155800 loops=1)
Index Cond: ((c_id = 5) AND (t >= '2017-01-03 00:00:00'::timestamp without time zone) AND (t < '2017-01-06 00:00:00'::timestamp without time zone))
Heap Fetches: 0
Planning time: 0.171 ms
Execution time: 472.925 ms
(11 Zeilen)
С частичным индексом время выполнения примерно на 100 мс быстрее, а вся таблица в два раза больше.
(Примечание: во второй раз это было всего лишь на 50 мс. Преимущество должно увеличиваться, тем больше событий регистрируется, потому что запросы, требующие полного индекса, будут замедляться, поскольку вы подозреваете (и соглашаетесь)).
Кроме того, на моей машине полный индекс составляет 810 МБ для двух вставок (создайте таблицу + дополнительно с 2017-01-10).
Частичный индекс WHERE 2017-01-03 <= t < 2017-01-06 - всего 91 МБ.
Возможно, вы можете создавать частичные индексы ежемесячно или ежегодно?
В зависимости от того, какой временной диапазон запрашивается, возможно, нужно индексировать только последние данные или иначе только старые данные частично?
Я также попробовал частичное индексирование с помощью WHERE c_id = 5
, поэтому разбиение на разделы на c_id.
Sort (cost=51324.27..51361.47 rows=14880 width=23) (actual time=550.579..550.592 rows=30 loops=1)
Sort Key: (count(1)), events.r, events.pr_id DESC
Sort Method: quicksort Memory: 27kB
-> HashAggregate (cost=50144.21..50293.01 rows=14880 width=23) (actual time=550.481..550.528 rows=30 loops=1)
Group Key: events.pr_id, events.r
-> Unique (cost=0.42..47540.21 rows=148800 width=40) (actual time=0.050..443.393 rows=118571 loops=1)
-> Index Only Scan using events_cid on events (cost=0.42..46736.42 rows=160758 width=40) (actual time=0.047..269.676 rows=155800 loops=1)
Index Cond: ((t >= '2017-01-03 00:00:00'::timestamp without time zone) AND (t < '2017-01-06 00:00:00'::timestamp without time zone))
Heap Fetches: 0
Planning time: 0.366 ms
Execution time: 550.706 ms
(11 Zeilen)
Таким образом, частичная индексация также может быть жизнеспособным вариантом.
Если вы получаете все больше данных, вы также можете рассмотреть раздел, например, все строки в возрасте от двух лет и старше в отдельную таблицу или что-то еще.
Я не думаю, что здесь могут помочь индексы блоков Range BRIN (индексы).
Если машина более мясистая, чем моя, то вы можете просто вставить 10-кратное количество данных и сначала проверить поведение обычного полного индекса и как он ведет себя в возрастающей таблице.
Ответ 2
[EDITED]
Хорошо. Поскольку это зависит от вашего распределения данных, это еще один способ сделать это.
Сначала добавьте следующий индекс:
CREATE INDEX events_idx2 ON events (c_id, t DESC, pr_id, pa_id, r);
Извлеките MAX(t)
как можно быстрее, исходя из предположения, что подмножество будет меньше для объединения в родительскую таблицу. Однако он может быть медленнее, если набор данных не так мал.
SELECT
e.pr_id,
e.r,
count(1) AS quantity
FROM events e
JOIN (
SELECT
pr_id,
pa_id,
MAX(t) last_t
FROM events e
WHERE
c_id = 5
AND t >= '2017-01-03Z00:00:00'
AND t < '2017-01-06Z00:00:00'
GROUP BY
pr_id,
pa_id
) latest
ON (
c_id = 5
AND latest.pr_id = e.pr_id
AND latest.pa_id = e.pa_id
AND latest.last_t = e.t
)
GROUP BY
e.pr_id,
e.r
ORDER BY 3, 2, 1 DESC
Полный скрипт
SQL Fiddle
Настройка схемы PostgreSQL 9.3:
--PostgreSQL 9.6
--'\\' is a delimiter
-- CREATE TABLE events AS...
VACUUM ANALYZE events;
CREATE INDEX idx_events_idx ON events (c_id, t DESC, pr_id, pa_id, r);
Запрос 1:
-- query A
explain analyze SELECT
pr_id,
r,
count(1) AS quantity
FROM (
SELECT DISTINCT ON (pr_id, pa_id)
pr_id,
pa_id,
r
FROM events
WHERE
c_id = 5 AND
t >= '2017-01-03Z00:00:00' AND
t < '2017-01-06Z00:00:00'
ORDER BY pr_id, pa_id, t DESC
) latest
GROUP BY
1,
2
ORDER BY 3, 2, 1 DESC
Результаты:
QUERY PLAN
Sort (cost=2170.24..2170.74 rows=200 width=15) (actual time=358.239..358.245 rows=30 loops=1)
Sort Key: (count(1)), events.r, events.pr_id
Sort Method: quicksort Memory: 27kB
-> HashAggregate (cost=2160.60..2162.60 rows=200 width=15) (actual time=358.181..358.189 rows=30 loops=1)
-> Unique (cost=2012.69..2132.61 rows=1599 width=40) (actual time=327.345..353.750 rows=12098 loops=1)
-> Sort (cost=2012.69..2052.66 rows=15990 width=40) (actual time=327.344..348.686 rows=15966 loops=1)
Sort Key: events.pr_id, events.pa_id, events.t
Sort Method: external merge Disk: 792kB
-> Index Only Scan using idx_events_idx on events (cost=0.42..896.20 rows=15990 width=40) (actual time=0.059..5.475 rows=15966 loops=1)
Index Cond: ((c_id = 5) AND (t >= '2017-01-03 00:00:00'::timestamp without time zone) AND (t < '2017-01-06 00:00:00'::timestamp without time zone))
Heap Fetches: 0
Total runtime: 358.610 ms
Запрос 2:
-- query max/JOIN
explain analyze SELECT
e.pr_id,
e.r,
count(1) AS quantity
FROM events e
JOIN (
SELECT
pr_id,
pa_id,
MAX(t) last_t
FROM events e
WHERE
c_id = 5
AND t >= '2017-01-03Z00:00:00'
AND t < '2017-01-06Z00:00:00'
GROUP BY
pr_id,
pa_id
) latest
ON (
c_id = 5
AND latest.pr_id = e.pr_id
AND latest.pa_id = e.pa_id
AND latest.last_t = e.t
)
GROUP BY
e.pr_id,
e.r
ORDER BY 3, 2, 1 DESC
Результаты:
QUERY PLAN
Sort (cost=4153.31..4153.32 rows=1 width=15) (actual time=68.398..68.402 rows=30 loops=1)
Sort Key: (count(1)), e.r, e.pr_id
Sort Method: quicksort Memory: 27kB
-> HashAggregate (cost=4153.29..4153.30 rows=1 width=15) (actual time=68.363..68.371 rows=30 loops=1)
-> Merge Join (cost=1133.62..4153.29 rows=1 width=15) (actual time=35.083..64.154 rows=12098 loops=1)
Merge Cond: ((e.t = (max(e_1.t))) AND (e.pr_id = e_1.pr_id))
Join Filter: (e.pa_id = e_1.pa_id)
-> Index Only Scan Backward using idx_events_idx on events e (cost=0.42..2739.72 rows=53674 width=40) (actual time=0.010..8.073 rows=26661 loops=1)
Index Cond: (c_id = 5)
Heap Fetches: 0
-> Sort (cost=1133.19..1137.19 rows=1599 width=36) (actual time=29.778..32.885 rows=12098 loops=1)
Sort Key: (max(e_1.t)), e_1.pr_id
Sort Method: external sort Disk: 640kB
-> HashAggregate (cost=1016.12..1032.11 rows=1599 width=36) (actual time=12.731..16.738 rows=12098 loops=1)
-> Index Only Scan using idx_events_idx on events e_1 (cost=0.42..896.20 rows=15990 width=36) (actual time=0.029..5.084 rows=15966 loops=1)
Index Cond: ((c_id = 5) AND (t >= '2017-01-03 00:00:00'::timestamp without time zone) AND (t < '2017-01-06 00:00:00'::timestamp without time zone))
Heap Fetches: 0
Total runtime: 68.736 ms
Запрос 3:
DROP INDEX idx_events_idx
CREATE INDEX idx_events_flutter ON events (c_id, pr_id, pa_id, t DESC, r)
Запрос 5:
-- query A + index by flutter
explain analyze SELECT
pr_id,
r,
count(1) AS quantity
FROM (
SELECT DISTINCT ON (pr_id, pa_id)
pr_id,
pa_id,
r
FROM events
WHERE
c_id = 5 AND
t >= '2017-01-03Z00:00:00' AND
t < '2017-01-06Z00:00:00'
ORDER BY pr_id, pa_id, t DESC
) latest
GROUP BY
1,
2
ORDER BY 3, 2, 1 DESC
Результаты:
QUERY PLAN
Sort (cost=2744.82..2745.32 rows=200 width=15) (actual time=20.915..20.916 rows=30 loops=1)
Sort Key: (count(1)), events.r, events.pr_id
Sort Method: quicksort Memory: 27kB
-> HashAggregate (cost=2735.18..2737.18 rows=200 width=15) (actual time=20.883..20.892 rows=30 loops=1)
-> Unique (cost=0.42..2707.20 rows=1599 width=40) (actual time=0.037..16.488 rows=12098 loops=1)
-> Index Only Scan using idx_events_flutter on events (cost=0.42..2627.25 rows=15990 width=40) (actual time=0.036..10.893 rows=15966 loops=1)
Index Cond: ((c_id = 5) AND (t >= '2017-01-03 00:00:00'::timestamp without time zone) AND (t < '2017-01-06 00:00:00'::timestamp without time zone))
Heap Fetches: 0
Total runtime: 20.964 ms
Ответ 3
Только два разных метода (YMMV):
-- using a window finction to find the record with the most recent t::
EXPLAIN ANALYZE
SELECT pr_id, r, count(1) AS quantity
FROM (
SELECT DISTINCT ON (pr_id, pa_id)
pr_id, pa_id,
first_value(r) OVER www AS r
-- last_value(r) OVER www AS r
FROM events
WHERE c_id = 5
AND t >= '2017-01-03Z00:00:00'
AND t < '2017-01-06Z00:00:00'
WINDOW www AS (PARTITION BY pr_id, pa_id ORDER BY t DESC)
ORDER BY 1, 2, t DESC
) sss
GROUP BY 1, 2
ORDER BY 3, 2, 1 DESC
;
-- Avoiding the window function; find the MAX via NOT EXISTS() ::
EXPLAIN ANALYZE
SELECT pr_id, r, count(1) AS quantity
FROM (
SELECT DISTINCT ON (pr_id, pa_id)
pr_id, pa_id, r
FROM events e
WHERE c_id = 5
AND t >= '2017-01-03Z00:00:00'
AND t < '2017-01-06Z00:00:00'
AND NOT EXISTS ( SELECT * FROM events nx
WHERE nx.c_id = 5 AND nx.pr_id =e.pr_id AND nx.pa_id =e.pa_id
AND nx.t >= '2017-01-03Z00:00:00'
AND nx.t < '2017-01-06Z00:00:00'
AND nx.t > e.t
)
) sss
GROUP BY 1, 2
ORDER BY 3, 2, 1 DESC
;
Примечание: DISTINCT ON
можно исключить из второго запроса, результаты уже уникальны.
Ответ 4
Я бы попытался использовать стандартную функцию ROW_NUMBER()
с соответствующим индексом вместо Postgres-specific DISTINCT ON
, чтобы найти "последние" строки.
Индекс
CREATE INDEX ix_events ON events USING btree (c_id, pa_id, pr_id, t DESC, r);
Query
WITH
CTE_RN
AS
(
SELECT
pa_id
,pr_id
,r
,ROW_NUMBER() OVER (PARTITION BY c_id, pa_id, pr_id ORDER BY t DESC) AS rn
FROM events
WHERE
c_id = 5
AND t >= '2017-01-03Z00:00:00'
AND t < '2017-01-06Z00:00:00'
)
SELECT
pr_id
,r
,COUNT(*) AS quantity
FROM CTE_RN
WHERE rn = 1
GROUP BY
pr_id
,r
ORDER BY quantity, r, pr_id DESC
;
У меня нет Postgres, поэтому я использую http://rextester.com для тестирования. Я установил scale_factor
в 30 в генерации данных script, иначе для рекстератора слишком много времени. Я получаю следующий план запроса. Компонент времени должен быть проигнорирован, но вы можете видеть, что промежуточных сорта нет, только сортировка для окончательного ORDER BY
. См. http://rextester.com/GUFXY36037
Пожалуйста, попробуйте этот запрос на вашем оборудовании и ваших данных. Было бы интересно посмотреть, как он сравнивается с вашим запросом. Я заметил, что оптимизатор не выбирает этот индекс, если таблица имеет указанный вами индекс. Если вы видите то же самое на своем сервере, попробуйте сбросить или отключить другие индексы, чтобы получить план, который у меня есть.
1 Sort (cost=158.07..158.08 rows=1 width=44) (actual time=81.445..81.448 rows=30 loops=1)
2 Output: cte_rn.pr_id, cte_rn.r, (count(*))
3 Sort Key: (count(*)), cte_rn.r, cte_rn.pr_id DESC
4 Sort Method: quicksort Memory: 27kB
5 CTE cte_rn
6 -> WindowAgg (cost=0.42..157.78 rows=12 width=88) (actual time=0.204..56.215 rows=15130 loops=1)
7 Output: events.pa_id, events.pr_id, events.r, row_number() OVER (?), events.t, events.c_id
8 -> Index Only Scan using ix_events3 on public.events (cost=0.42..157.51 rows=12 width=80) (actual time=0.184..28.688 rows=15130 loops=1)
9 Output: events.c_id, events.pa_id, events.pr_id, events.t, events.r
10 Index Cond: ((events.c_id = 5) AND (events.t >= '2017-01-03 00:00:00'::timestamp without time zone) AND (events.t < '2017-01-06 00:00:00'::timestamp without time zone))
11 Heap Fetches: 15130
12 -> HashAggregate (cost=0.28..0.29 rows=1 width=44) (actual time=81.363..81.402 rows=30 loops=1)
13 Output: cte_rn.pr_id, cte_rn.r, count(*)
14 Group Key: cte_rn.pr_id, cte_rn.r
15 -> CTE Scan on cte_rn (cost=0.00..0.27 rows=1 width=36) (actual time=0.214..72.841 rows=11491 loops=1)
16 Output: cte_rn.pa_id, cte_rn.pr_id, cte_rn.r, cte_rn.rn
17 Filter: (cte_rn.rn = 1)
18 Rows Removed by Filter: 3639
19 Planning time: 0.452 ms
20 Execution time: 83.234 ms
Есть еще одна оптимизация, которую вы можете сделать, опираясь на внешние знания ваших данных.
Если вы можете гарантировать, что каждая пара pa_id, pr_id
имеет значения для каждого, скажем, дня, то вы можете безопасно уменьшить заданный пользователем диапазон t
до одного дня.
Это уменьшит количество строк, которые движок считывает и сортирует, если пользователь обычно задает диапазон t
дольше 1 дня.
Если вы не можете предоставить такую гарантию в своих данных для всех значений, но вы все еще знаете, что обычно все pa_id, pr_id
находятся близко друг к другу (через t
), и пользователь обычно предоставляет широкий диапазон для t
, вы можете запустить предварительный запрос, чтобы сузить диапазон t
для основного запроса.
Что-то вроде этого:
SELECT
MIN(MaxT) AS StartT
MAX(MaxT) AS EndT
FROM
(
SELECT
pa_id
,pr_id
,MAX(t) AS MaxT
FROM events
WHERE
c_id = 5
AND t >= '2017-01-03Z00:00:00'
AND t < '2017-01-06Z00:00:00'
GROUP BY
pa_id
,pr_id
) AS T
И затем используйте найденный StartT,EndT
в основном запросе, надеясь, что новый диапазон будет намного уже, чем оригинал, определенный пользователем.
Вышеприведенный запрос не должен сортировать строки, поэтому он должен быть быстрым. Основной запрос должен сортировать строки, но сортировки будет меньше, поэтому общее время выполнения может быть лучше.
Ответ 5
Итак, я немного поработал и попытался переместить вашу группу и отдельные данные в свои собственные таблицы, чтобы мы могли использовать несколько табличных индексов. Обратите внимание, что это решение работает только в том случае, если у вас есть контроль над тем, как данные вставляются в базу данных, т.е. Вы можете изменить приложение источника данных. Если нет, то увы, это спорный вопрос.
На практике вместо того, чтобы сразу вставлять в таблицу events
, вы должны сначала проверить, существуют ли реляционные date
и prpa
в соответствующих таблицах. Если нет, создайте их. Затем получите их id
и используйте это для оператора insert в таблице events
.
Прежде чем начать, я генерировал 10x увеличение производительности на query_c над query_a, и мой окончательный результат для перезаписанного query_a - это производительность в 4 раза. Если это не так хорошо, не стесняйтесь отключать.
Учитывая исходные запросы на выборку данных, которые вы указали в первом экземпляре, я вычислил следующие этапы:
query_a: 5228.518 ms
query_b: 5708.962 ms
query_c: 538.329 ms
Итак, примерно в 10 раз больше производительности, дайте или возьмите.
Я собираюсь изменить данные, сгенерированные в events
, и это изменение занимает довольно много времени. Вам не нужно будет делать это на практике, так как ваш INSERT
для таблиц будет уже рассмотрен.
Для моей оптимизации первым шагом является создание таблицы с датами, а затем переноса данных и привязки к ней в таблице events
, например:
CREATE TABLE dates (
id SERIAL,
year_part INTEGER NOT NULL,
month_part INTEGER NOT NULL,
day_part INTEGER NOT NULL
);
-- Total runtime: 8.281 ms
INSERT INTO dates(year_part, month_part, day_part) SELECT DISTINCT
EXTRACT(YEAR FROM t), EXTRACT(MONTH FROM t), EXTRACT(DAY FROM t)
FROM events;
-- Total runtime: 12802.900 ms
CREATE INDEX dates_ymd ON dates USING btree(year_part, month_part, day_part);
-- Total runtime: 13.750 ms
ALTER TABLE events ADD COLUMN date_id INTEGER;
-- Total runtime: 2.468ms
UPDATE events SET date_id = dates.id
FROM dates
WHERE EXTRACT(YEAR FROM t) = dates.year_part
AND EXTRACT(MONTH FROM t) = dates.month_part
AND EXTRACT(DAY FROM T) = dates.day_part
;
-- Total runtime: 388024.520 ms
Затем мы делаем то же самое, но с парой ключей (pr_id
, pa_id
), что не слишком сильно снижает мощность, но когда мы говорим о больших наборах, это может помочь в использовании памяти и замена и выключение:
CREATE TABLE prpa (
id SERIAL,
pr_id TEXT NOT NULL,
pa_id TEXT NOT NULL
);
-- Total runtime: 5.451 ms
CREATE INDEX events_prpa ON events USING btree(pr_id, pa_id);
-- Total runtime: 218,908.894 ms
INSERT INTO prpa(pr_id, pa_id) SELECT DISTINCT pr_id, pa_id FROM events;
-- Total runtime: 5566.760 ms
CREATE INDEX prpa_idx ON prpa USING btree(pr_id, pa_id);
-- Total runtime: 84185.057 ms
ALTER TABLE events ADD COLUMN prpa_id INTEGER;
-- Total runtime: 2.067 ms
UPDATE events SET prpa_id = prpa.id
FROM prpa
WHERE events.pr_id = prpa.pr_id
AND events.pa_id = prpa.pa_id;
-- Total runtime: 757915.192
DROP INDEX events_prpa;
-- Total runtime: 1041.556 ms
Наконец, позвольте избавиться от старых индексов и ныне несуществующих столбцов, а затем очистите новые таблицы:
DROP INDEX events_idx;
-- Total runtime: 1139.508 ms
ALTER TABLE events
DROP COLUMN pr_id,
DROP COLUMN pa_id
;
-- Total runtime: 5.376 ms
VACUUM ANALYSE prpa;
-- Total runtime: 1030.142
VACUUM ANALYSE dates;
-- Total runtime: 6652.151
Итак, теперь мы имеем следующие таблицы:
events (c_id, r, t, prpa_id, date_id)
dates (id, year_part, month_part, day_part)
prpa (id, pr_id, pa_id)
Теперь перейдем к индексу, нажав t DESC
до конца, где он принадлежит, что мы можем сделать теперь, потому что мы фильтруем результаты на dates
до ORDER
ing, что сокращает необходимость в t DESC
, чтобы быть столь заметным в индексе:
CREATE INDEX events_idx_new ON events USING btree (c_id, date_id, prpa_id, t DESC);
-- Total runtime: 27697.795
VACUUM ANALYSE events;
Теперь мы переписываем запрос (используя таблицу для хранения промежуточных результатов - я нахожу, что это хорошо работает с большими наборами данных) и awaaaaaay мы идем!
DROP TABLE IF EXISTS temp_results;
SELECT DISTINCT ON (prpa_id)
prpa_id,
r
INTO TEMPORARY temp_results
FROM events
INNER JOIN dates
ON dates.id = events.date_id
WHERE c_id = 5
AND dates.year_part BETWEEN 2017 AND 2017
AND dates.month_part BETWEEN 1 AND 1
AND dates.day_part BETWEEN 3 AND 5
ORDER BY prpa_id, t DESC;
SELECT
prpa.pr_id,
r,
count(1) AS quantity
FROM temp_results
INNER JOIN prpa ON prpa.id = temp_results.prpa_id
GROUP BY
1,
2
ORDER BY 3, 2, 1 DESC;
-- Total runtime: 1233.281 ms
Итак, это не 10-кратное увеличение производительности, а 4-х, что все равно.
Это решение представляет собой комбинацию нескольких методов, которые я нашел хорошо работающими с большими наборами данных и диапазонами дат. Даже если это недостаточно для ваших целей, здесь могут быть некоторые драгоценные камни, которые вы можете переделать на протяжении всей своей карьеры.
EDIT:
EXPLAIN ANALYZE в запросе SELECT INTO:
Unique (cost=171839.95..172360.53 rows=51332 width=16) (actual time=819.385..857.777 rows=117471 loops=1)
-> Sort (cost=171839.95..172100.24 rows=104117 width=16) (actual time=819.382..836.924 rows=155202 loops=1)
Sort Key: events.prpa_id, events.t
Sort Method: external sort Disk: 3944kB
-> Hash Join (cost=14340.24..163162.92 rows=104117 width=16) (actual time=126.929..673.293 rows=155202 loops=1)
Hash Cond: (events.date_id = dates.id)
-> Bitmap Heap Scan on events (cost=14338.97..160168.28 rows=520585 width=20) (actual time=126.572..575.852 rows=516503 loops=1)
Recheck Cond: (c_id = 5)
Heap Blocks: exact=29610
-> Bitmap Index Scan on events_idx2 (cost=0.00..14208.82 rows=520585 width=0) (actual time=118.769..118.769 rows=516503 loops=1)
Index Cond: (c_id = 5)
-> Hash (cost=1.25..1.25 rows=2 width=4) (actual time=0.326..0.326 rows=3 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 1kB
-> Seq Scan on dates (cost=0.00..1.25 rows=2 width=4) (actual time=0.320..0.323 rows=3 loops=1)
Filter: ((year_part >= 2017) AND (year_part <= 2017) AND (month_part >= 1) AND (month_part <= 1) AND (day_part >= 3) AND (day_part <= 5))
Rows Removed by Filter: 7
Planning time: 3.091 ms
Execution time: 913.543 ms
EXPLAIN ANALYZE в запросе SELECT:
(Примечание: мне пришлось изменить первый запрос, чтобы выбрать в фактическую таблицу, а не временную таблицу, чтобы получить план запроса для этого. AFAIK EXPLAIN ANALYZE работает только на одном запросе)
Sort (cost=89590.66..89595.66 rows=2000 width=15) (actual time=1248.535..1248.537 rows=30 loops=1)
Sort Key: (count(1)), temp_results.r, prpa.pr_id
Sort Method: quicksort Memory: 27kB
-> HashAggregate (cost=89461.00..89481.00 rows=2000 width=15) (actual time=1248.460..1248.468 rows=30 loops=1)
Group Key: prpa.pr_id, temp_results.r
-> Hash Join (cost=73821.20..88626.40 rows=111280 width=15) (actual time=798.861..1213.494 rows=117471 loops=1)
Hash Cond: (temp_results.prpa_id = prpa.id)
-> Seq Scan on temp_results (cost=0.00..1632.80 rows=111280 width=8) (actual time=0.024..17.401 rows=117471 loops=1)
-> Hash (cost=36958.31..36958.31 rows=2120631 width=15) (actual time=798.484..798.484 rows=2120631 loops=1)
Buckets: 16384 Batches: 32 Memory Usage: 3129kB
-> Seq Scan on prpa (cost=0.00..36958.31 rows=2120631 width=15) (actual time=0.126..350.664 rows=2120631 loops=1)
Planning time: 1.073 ms
Execution time: 1248.660 ms