PostgreSQL-выборка строки, которая имеет максимальное значение для столбца
Я имею дело с таблицей Postgres (называемой "lives"), которая содержит записи со столбцами для time_stamp, usr_id, transaction_id и lives_remaining. Мне нужен запрос, который даст мне самые последние lives_remaining всего для каждого usr_id
- есть несколько пользователей (различные usr_id)
- time_stamp не является уникальным идентификатором: иногда события пользователя (по одной строке в таблице) происходят с одним и тем же отметка времени.
- trans_id уникален только для очень малых временных диапазонов: со временем он повторяется
- remaining_lives (для данного пользователя) может как увеличиваться, так и уменьшаться с течением времени
пример:
time_stamp|lives_remaining|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, мне нужен запрос, который дает результат такой:
time_stamp|lives_remaining|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 (первый), так и trans_id (второй), чтобы определить правильную строку. Затем мне также нужно передать эту информацию из подзапроса в основной запрос, который предоставит данные для других столбцов соответствующих строк. Это взломанный запрос, который я получил для работы:
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
Итак, это работает, но мне это не нравится. Для этого требуется запрос в запросе, самостоятельное соединение, и мне кажется, что это может быть намного проще, захватив строку, которую Макс обнаружил, чтобы иметь самую большую метку времени и trans_id. Таблица "живет" имеет десятки миллионов строк для разбора, поэтому я хотел бы, чтобы этот запрос был как можно быстрее и эффективнее. Я новичок в RDBM и Postgres в частности, поэтому я знаю, что мне нужно эффективно использовать правильные индексы. Я немного потерял о том, как оптимизировать.
я нашел a аналогичное обсуждение здесь. Могу ли я выполнить некоторый тип Postgres, эквивалентный аналитической функции Oracle?
любые советы по доступу к связанной информации столбца, используемой агрегатной функцией (например, MAX), созданию индексов и созданию лучших запросов будут высоко оценены!
P. S. Вы можете использовать следующие шаги, чтобы создать мой пример:
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);
8 ответов:
на таблице с 158k псевдослучайных строк (usr_id равномерно распределены между 0 и 10k,
trans_idравномерно распределены между 0 и 30),по стоимости запроса, ниже, я имею в виду оценку стоимости оптимизатора на основе стоимости Postgres (с Postgres' default
xxx_costvalues), который представляет собой взвешенную оценку функции требуемых ресурсов ввода-вывода и процессора; вы можете получить это, запустив PgAdminIII и запустив "запрос / объяснение (F7)" в запросе с " опциями запроса / объяснения" установите значение "анализировать"
- запрос Quassnoy имеет оценку стоимости 745k (!), и завершается в 1,3 секунды (учитывая, составной индекс на (
usr_id,trans_id,time_stamp))- запрос Билла имеет оценку стоимости 93k и завершается за 2,9 секунды (учитывая составной индекс на (
usr_id,trans_id))- запрос #1 ниже имеет оценку стоимости 16k, и завершает в 800ms (учитывая составной индекс на (
usr_id,trans_id,time_stamp))- запрос #2 ниже имеет оценку стоимости 14k и завершается в 800 мс (учитывая индекс составной функции на (
usr_id,EXTRACT(EPOCH FROM time_stamp),trans_id))
- это Postgres-specific
- запрос № 3 ниже (Postgres 8.4+) имеет оценку стоимости и время завершения, сопоставимые с (или лучше) запросом #2 (учитывая составной индекс на (
usr_id,time_stamp,trans_id)); Он имеет преимущество сканированияlivesтаблица только один раз и, если вы временно увеличить (если это необходимо) сортировки чтобы разместить сортировку в памяти,это будет самый быстрый из всех запросов.все времена выше включают извлечение полного набора результатов 10k строк.
ваша цель-минимальная оценка затрат и минимальное время выполнения запроса, с акцентом на сметную стоимость. Выполнение запроса может существенно зависеть от условий выполнения (например будут ли соответствующие строки уже полностью кэшированы в памяти или нет), тогда как оценка затрат-нет. С другой стороны, имейте в виду, что оценка затрат-это именно то, что оценка.
лучшее время выполнения запроса получается при работе на выделенной базе данных без нагрузки (например, играя с pgAdminIII на ПК разработки.) Время запроса будет варьироваться в зависимости от фактической загрузки машины / распространения доступа к данным. Когда один запрос появляется немного быстрее (много более высокая стоимость, как правило, будет разумнее выбрать тот, который имеет более высокое время выполнения, но более низкую стоимость.
если вы ожидаете, что во время выполнения запроса на вашей рабочей машине не будет конкуренции за память (например, кэш RDBMS и кэш файловой системы не будут разбиты параллельными запросами и/или деятельностью файловой системы), то время запроса, полученное в автономном режиме (например, 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 );
Я бы предложил чистую версию на основе
DISTINCT ON(см. docs):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;
вот еще один метод, который не использует коррелированные подзапросы или группы. Я не эксперт в настройке производительности 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.
мне нравится стиль ответ Майка Вудхауса на другой странице, которую вы упомянули. Это особенно лаконично, Когда максимизируемая вещь-это всего лишь один столбец, и в этом случае подзапрос может просто использовать
MAX(some_col)иGROUP BYдругие столбцы, но в вашем случае у вас есть 2-х частей количество должно быть максимизировано, вы все еще можете сделать это с помощью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)Ницца потому что это сокращает количество необходимых словоблудий.
Actaully есть хаки решение этой проблемы. Предположим, вы хотите выбрать самое большое дерево каждого леса в регионе.
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' s иWHEREусловиях.Кстати, обратите внимание, что
ORDER_BYнаarray_aggвводится в Postgresql 9.0
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 )создание индекса на
(usr_id, time_stamp, trans_id)значительно улучшит этот запрос.вы должны всегда, всегда есть какой-то
PRIMARY KEYв ваших таблицах.
Я думаю, что у вас здесь есть одна серьезная проблема: нет монотонно увеличивающегося "счетчика", чтобы гарантировать, что данная строка произошла позже, чем другая. Возьмем такой пример:
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вы не можете определить из этих данных, которая является самой последней записи. Это второй или последний? Нет функции сортировки или max (), которую вы можете применить к любым из этих данных, чтобы дать вам правильный ответ.
увеличение разрешения метки времени будет огромным помощь. Поскольку компонент database engine сериализует запросы, при достаточном разрешении можно гарантировать, что никакие две метки времени не будут одинаковыми.
кроме того, используйте trans_id, который не будет переворачиваться в течение очень, очень долгого времени. Наличие trans_id, который переворачивается, означает, что вы не можете сказать (для той же метки времени), является ли trans_id 6 более поздним, чем trans_id 1, Если вы не делаете какую-то сложную математику.
в Postgressql 9.5 появилась новая опция DISTINCT ON
SELECT DISTINCT ON (location) location, time, report FROM weather_reports ORDER BY location, time DESC;он устраняет повторяющиеся строки и оставляет только первую строку, как определено моим предложением ORDER BY.
посмотреть официальный документация
Comments