PostgreSQL - получить строку, которая имеет максимальное значение для столбца
Я имею дело с таблицей Postgres (называемой "жизнями"), которая содержит записи со столбцами для time_stamp, usr_id, transaction_id и life_remaining. Мне нужен запрос, который предоставит мне самое последнее количество жизней_ремайн для каждого usr_id
- Есть несколько пользователей (разные usr_id)
- time_stamp не является уникальным идентификатором: иногда пользовательские события (одно за строкой в таблице) будут происходить с одной и той же time_stamp.
- trans_id уникален только для очень малых временных диапазонов: со временем он повторяется
- оставшееся_продолжение (для данного пользователя) может как увеличиваться, так и уменьшаться с течением времени
пример:
отметка_времени | жизнь_ремонта | usr_id | trans_id ----------------------------------------- 07:00 | 1 | 1 | 1 09:00 | 4 | 2 | 2 10:00 | 2 | 3 | 3 10:00 | 1 | 2 | 4 11:00 | 4 | 1 | 5 11:00 | 3 | 1 | 6 13:00 | 3 | 3 | 1
Поскольку мне нужно будет получить доступ к другим столбцам строки с последними данными для каждого заданного usr_id, мне нужен запрос, который дает такой результат:
отметка_времени | жизнь_ремонта | usr_id | trans_id ----------------------------------------- 11:00 | 3 | 1 | 6 10:00 | 1 | 2 | 4 13:00 | 3 | 3 | 1
Как уже упоминалось, каждый usr_id может приносить или терять жизни, и иногда эти события с отметками времени происходят так близко друг к другу, что имеют одинаковую отметку времени! Следовательно, этот запрос не будет работать:
SELECT b.time_stamp,b.lives_remaining,b.usr_id,b.trans_id FROM
(SELECT usr_id, max(time_stamp) AS max_timestamp
FROM lives GROUP BY usr_id ORDER BY usr_id) a
JOIN lives b ON a.max_timestamp = b.time_stamp
Вместо этого мне нужно использовать time_stamp (first) и trans_id (second), чтобы определить правильную строку. Затем мне также нужно передать эту информацию из подзапроса в основной запрос, который предоставит данные для других столбцов соответствующих строк. Это взломанный запрос, с которым мне пришлось работать:
SELECT b.time_stamp,b.lives_remaining,b.usr_id,b.trans_id FROM
(SELECT usr_id, max(time_stamp || '*' || trans_id)
AS max_timestamp_transid
FROM lives GROUP BY usr_id ORDER BY usr_id) a
JOIN lives b ON a.max_timestamp_transid = b.time_stamp || '*' || b.trans_id
ORDER BY b.usr_id
Хорошо, это работает, но мне это не нравится. Для этого требуется запрос внутри запроса, самосоединение, и мне кажется, что это может быть намного проще, если взять строку, которая, как обнаружил MAX, имеет наибольшую временную метку и trans_id. Таблица "живет" содержит десятки миллионов строк для анализа, поэтому мне хотелось бы, чтобы этот запрос был как можно более быстрым и эффективным. Я новичок в RDBM и Postgres в частности, поэтому знаю, что мне нужно эффективно использовать правильные индексы. Я немного не понимаю, как оптимизировать.
Я нашел подобное обсуждение здесь . Могу ли я выполнить какой-либо тип Postgres, эквивалентный аналитической функции Oracle?
Мы будем очень благодарны за любые советы по доступу к связанной информации о столбцах, используемой агрегатной функцией (например, MAX), созданию индексов и созданию более качественных запросов!
PS Для создания моего примера вы можете использовать следующее:
create TABLE lives (time_stamp timestamp, lives_remaining integer,
usr_id integer, trans_id integer);
insert into lives values ('2000-01-01 07:00', 1, 1, 1);
insert into lives values ('2000-01-01 09:00', 4, 2, 2);
insert into lives values ('2000-01-01 10:00', 2, 3, 3);
insert into lives values ('2000-01-01 10:00', 1, 2, 4);
insert into lives values ('2000-01-01 11:00', 4, 1, 5);
insert into lives values ('2000-01-01 11:00', 3, 1, 6);
insert into lives values ('2000-01-01 13:00', 3, 3, 1);
Ответов (9)9
В таблице с 158 тыс. Псевдослучайных строк (usr_id равномерно распределен между 0 и 10 тыс., trans_id
Равномерно распределен между 0 и 30),
Под стоимостью запроса ниже я имею в виду оценку стоимости оптимизатора Postgres на основе затрат (со значениями Postgres по умолчанию xxx_cost
), которая представляет собой взвешенную функциональную оценку требуемых ресурсов ввода-вывода и ЦП; вы можете получить это, запустив PgAdminIII и запустив «Query / Explain (F7)» для запроса с «Query / Explain options», установленным на «Analyze»
- Запрос Quassnoy имеет оценку стоимости 745k (!), И завершает в 1,3 секунды ( с учетом соединения индекс (
usr_id
,trans_id
,time_stamp
)) - Запрос Билла оценивается в 93 тыс. И выполняется за 2,9 секунды (с учетом составного индекса на (
usr_id
,trans_id
)). - Запрос # 1 ниже имеет оценку стоимости 16k, и завершается в 800 мс ( с учетом составного индекса по (
usr_id
,trans_id
,time_stamp
)) - Запрос # 2 ниже имеет оценку стоимости 14k, и завершается в 800 мс ( с учетом составного индекса функции на (
usr_id
,EXTRACT(EPOCH FROM time_stamp)
,trans_id
))- это специфично для Postgres
- Запрос # 3 ниже (Postgres 8.4+) имеет оценку стоимости и времени завершения , сравнимую с (или лучше , чем) запрос # 2 (учитывая соединение индекс (
usr_id
,time_stamp
,trans_id
)); у него есть преимущество сканированияlives
таблицы только один раз, и, если вы временно увеличите (при необходимости) work_mem для размещения сортировки в памяти, это будет самый быстрый из всех запросов.
Все приведенные выше моменты включают получение полного набора результатов из 10 тыс. Строк.
Ваша цель - минимальная оценка стоимости и минимальное время выполнения запроса с упором на оценочную стоимость. Выполнение запроса может в значительной степени зависеть от условий выполнения (например, от того, полностью ли кэшированы соответствующие строки в памяти или нет), в то время как оценка стоимости - нет. С другой стороны, имейте в виду, что смета - это именно оценка.
Наилучшее время выполнения запроса достигается при работе с выделенной базой данных без нагрузки (например, игра с pgAdminIII на ПК для разработки). Время запроса будет варьироваться в производственной среде в зависимости от фактической нагрузки на машину / распределения доступа к данным. Когда один запрос выглядит немного быстрее (<20%), чем другой, но имеет гораздо более высокую стоимость, обычно будет разумнее выбрать тот, у которого больше время выполнения, но ниже стоимость.
Если вы ожидаете, что не будет конкуренции за память на вашем производственном компьютере во время выполнения запроса (например, кеш реляционной СУБД и кеш файловой системы не будут обрабатываться параллельными запросами и / или активностью файловой системы), тогда полученное вами время запроса в автономном режиме (например, pgAdminIII на ПК для разработки) будет репрезентативным. Если в производственной системе есть конкуренция, время запроса будет уменьшаться пропорционально расчетному соотношению затрат, поскольку запрос с более низкой стоимостью не так сильно зависит от кеша, тогда как запрос с более высокой стоимостью будет повторно обращаться к одним и тем же данным снова и снова (запуск дополнительный ввод-вывод при отсутствии стабильного кеша), например:
cost | time (dedicated machine) | time (under load) |
-------------------+--------------------------+-----------------------+
some query A: 5k | (all data cached) 900ms | (less i/o) 1000ms |
some query B: 50k | (all data cached) 900ms | (lots of i/o) 10000ms |
Не забудьте запустить ANALYZE lives
один раз после создания необходимых индексов.
Запрос №1
-- incrementally narrow down the result set via inner joins
-- the CBO may elect to perform one full index scan combined
-- with cascading index lookups, or as hash aggregates terminated
-- by one nested index lookup into lives - on my machine
-- the latter query plan was selected given my memory settings and
-- histogram
SELECT
l1.*
FROM
lives AS l1
INNER JOIN (
SELECT
usr_id,
MAX(time_stamp) AS time_stamp_max
FROM
lives
GROUP BY
usr_id
) AS l2
ON
l1.usr_id = l2.usr_id AND
l1.time_stamp = l2.time_stamp_max
INNER JOIN (
SELECT
usr_id,
time_stamp,
MAX(trans_id) AS trans_max
FROM
lives
GROUP BY
usr_id, time_stamp
) AS l3
ON
l1.usr_id = l3.usr_id AND
l1.time_stamp = l3.time_stamp AND
l1.trans_id = l3.trans_max
Запрос №2
-- cheat to obtain a max of the (time_stamp, trans_id) tuple in one pass
-- this results in a single table scan and one nested index lookup into lives,
-- by far the least I/O intensive operation even in case of great scarcity
-- of memory (least reliant on cache for the best performance)
SELECT
l1.*
FROM
lives AS l1
INNER JOIN (
SELECT
usr_id,
MAX(ARRAY[EXTRACT(EPOCH FROM time_stamp),trans_id])
AS compound_time_stamp
FROM
lives
GROUP BY
usr_id
) AS l2
ON
l1.usr_id = l2.usr_id AND
EXTRACT(EPOCH FROM l1.time_stamp) = l2.compound_time_stamp[1] AND
l1.trans_id = l2.compound_time_stamp[2]
2013/01/29 обновление
Наконец, начиная с версии 8.4, Postgres поддерживает оконную функцию, что означает, что вы можете написать что-то настолько простое и эффективное, как:
Запрос №3
-- use Window Functions
-- performs a SINGLE scan of the table
SELECT DISTINCT ON (usr_id)
last_value(time_stamp) OVER wnd,
last_value(lives_remaining) OVER wnd,
usr_id,
last_value(trans_id) OVER wnd
FROM lives
WINDOW wnd AS (
PARTITION BY usr_id ORDER BY time_stamp, trans_id
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
);
На самом деле для этой проблемы есть хакерское решение. Допустим, вы хотите выбрать самое большое дерево каждого леса в регионе.
SELECT (array_agg(tree.id ORDER BY tree_size.size)))[1]
FROM tree JOIN forest ON (tree.forest = forest.id)
GROUP BY forest.id
Когда вы группируете деревья по лесам, вы получаете несортированный список деревьев, и вам нужно найти самое большое из них. Первое, что вам следует сделать, это отсортировать строки по их размеру и выбрать первую строку в вашем списке. Это может показаться неэффективным, но если у вас есть миллионы строк, это будет намного быстрее, чем решения, включающие JOIN
и WHERE
условия.
Кстати, обратите внимание, что ORDER_BY
for array_agg
представлен в Postgresql 9.0
В Postgressql 9.5 появилась новая опция DISTINCT ON.
SELECT DISTINCT ON (location) location, time, report
FROM weather_reports
ORDER BY location, time DESC;
Он удаляет повторяющиеся строки и оставляет только первую строку, как определено в предложении ORDER BY.
см. официальную документацию
Мне нравится стиль ответа Майка Вудхауса на другой странице, которую вы упомянули. Это особенно кратко, когда объект, который максимизируется, представляет собой всего лишь один столбец, и в этом случае подзапрос может просто использовать MAX(some_col)
и GROUP BY
другие столбцы, но в вашем случае у вас есть количество из двух частей, которое нужно максимизировать, вы все равно можете сделать это, используя ORDER BY
плюс LIMIT 1
вместо этого (как это сделал Квассной):
SELECT *
FROM lives outer
WHERE (usr_id, time_stamp, trans_id) IN (
SELECT usr_id, time_stamp, trans_id
FROM lives sq
WHERE sq.usr_id = outer.usr_id
ORDER BY trans_id, time_stamp
LIMIT 1
)
Мне нравится использовать синтаксис конструктора строк, WHERE (a, b, c) IN (subquery)
потому что он сокращает количество необходимого словоблудия.
SELECT l.*
FROM (
SELECT DISTINCT usr_id
FROM lives
) lo, lives l
WHERE l.ctid = (
SELECT ctid
FROM lives li
WHERE li.usr_id = lo.usr_id
ORDER BY
time_stamp DESC, trans_id DESC
LIMIT 1
)
Creating an index on (usr_id, time_stamp, trans_id)
will greatly improve this query.
You should always, always have some kind of PRIMARY KEY
in your tables.
Вот еще один метод, в котором не используются коррелированные подзапросы или GROUP BY. Я не эксперт в настройке производительности PostgreSQL, поэтому предлагаю вам попробовать как это, так и решения, предоставленные другими людьми, чтобы увидеть, какое из них лучше работает для вас.
SELECT l1.*
FROM lives l1 LEFT OUTER JOIN lives l2
ON (l1.usr_id = l2.usr_id AND (l1.time_stamp < l2.time_stamp
OR (l1.time_stamp = l2.time_stamp AND l1.trans_id < l2.trans_id)))
WHERE l2.usr_id IS NULL
ORDER BY l1.usr_id;
Я предполагаю, что trans_id
это уникально, по крайней мере, для любого заданного значения time_stamp
.
Я думаю, у вас здесь одна серьезная проблема: нет монотонно увеличивающегося «счетчика», чтобы гарантировать, что данная строка возникла позже, чем другая. Возьмем этот пример:
timestamp lives_remaining user_id trans_id
10:00 4 3 5
10:00 5 3 6
10:00 3 3 1
10:00 2 3 2
Вы не можете определить по этим данным, какая запись является самой последней. Это второй или последний? Нет функции sort или max (), которую вы можете применить к любым из этих данных, чтобы дать вам правильный ответ.
Увеличение разрешения отметки времени было бы огромным подспорьем. Поскольку ядро базы данных сериализует запросы, при достаточном разрешении вы можете гарантировать, что никакие две метки времени не будут одинаковыми.
В качестве альтернативы используйте trans_id, который не будет переноситься очень и очень долго. Наличие trans_id, которое переключается, означает, что вы не можете сказать (для той же временной метки), является ли trans_id 6 более поздним, чем trans_id 1, если вы не выполните сложную математику.
Я бы предложил чистую версию на основе DISTINCT ON
(см. Документы ):
SELECT DISTINCT ON (usr_id)
time_stamp,
lives_remaining,
usr_id,
trans_id
FROM lives
ORDER BY usr_id, time_stamp DESC, trans_id DESC;