Я имею дело с таблицей 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);
источник
MAX
BY
2 столбца!Ответы:
В таблице с 158 тыс. Псевдослучайных строк (usr_id равномерно распределен между 0 и 10 тыс.,
trans_id
Равномерно распределен между 0 и 30),Под стоимостью запроса ниже я имею в виду оценку стоимости оптимизатора Postgres (со значениями Postgres по умолчанию
xxx_cost
), которая представляет собой взвешенную функциональную оценку требуемых ресурсов ввода-вывода и ЦП; вы можете получить это, запустив PgAdminIII и запустив «Query / Explain (F7)» по запросу с «Query / Explain options», установленным на «Analyze»usr_id
,trans_id
,time_stamp
))usr_id
,trans_id
)).usr_id
,trans_id
,time_stamp
))usr_id
,EXTRACT(EPOCH FROM time_stamp)
,trans_id
))usr_id
,time_stamp
,trans_id
)); у него есть преимущество сканированияlives
таблицы только один раз, и, если вы временно увеличите (при необходимости) work_mem для размещения сортировки в памяти, это будет самый быстрый из всех запросов.Все указанные выше моменты включают получение полного набора результатов из 10 тыс. Строк.
Ваша цель - минимальная оценка стоимости и минимальное время выполнения запроса с упором на оценочную стоимость. Выполнение запроса может существенно зависеть от условий выполнения (например, от того, полностью ли кэшированы соответствующие строки в памяти или нет), в то время как оценка стоимости - нет. С другой стороны, имейте в виду, что смета - это именно оценка.
Наилучшее время выполнения запроса достигается при работе с выделенной базой данных без нагрузки (например, игра с pgAdminIII на ПК для разработки). Время запроса будет варьироваться в производственной среде в зависимости от фактической нагрузки на машину / распределения доступа к данным. Когда один запрос появляется немного быстрее (<20%), чем другой, но имеет гораздо более высокую стоимость, обычно будет разумнее выбрать тот, у которого больше время выполнения, но ниже стоимость.
Если вы ожидаете, что не будет конкуренции за память на вашем производственном компьютере во время выполнения запроса (например, кеш СУБД и кеш файловой системы не будут перегружены параллельными запросами и / или активностью файловой системы), тогда полученное вами время запроса в автономном режиме (например, pgAdminIII на ПК разработки) будет репрезентативным. Если в производственной системе существует конкуренция, время запроса будет уменьшаться пропорционально расчетному соотношению затрат, поскольку запрос с более низкой стоимостью не так сильно зависит от кеша, тогда как запрос с более высокой стоимостью будет повторно обращаться к одним и тем же данным снова и снова (запуск дополнительный ввод-вывод при отсутствии стабильного кеша), например:
Не забудьте запустить
ANALYZE lives
один раз после создания необходимых индексов.Запрос №1
Запрос №2
2013/01/29 обновление
Наконец, начиная с версии 8.4, Postgres поддерживает оконную функцию, что означает, что вы можете написать что-то настолько простое и эффективное, как:
Запрос №3
источник
Я бы предложил чистую версию на основе
DISTINCT ON
(см. Документы ):источник
Вот еще один метод, в котором не используются коррелированные подзапросы или GROUP BY. Я не эксперт в настройке производительности PostgreSQL, поэтому предлагаю вам попробовать как это, так и решения, предоставленные другими людьми, чтобы увидеть, какое из них лучше работает для вас.
Я предполагаю, что
trans_id
это уникально, по крайней мере, для любого заданного значенияtime_stamp
.источник
Мне нравится стиль ответа Майка Вудхауса на другой странице, которую вы упомянули. Это особенно лаконично, когда объект, который максимизируется, представляет собой только один столбец, и в этом случае подзапрос может просто использовать
MAX(some_col)
иGROUP BY
другие столбцы, но в вашем случае у вас есть количество из двух частей, которое нужно максимизировать, вы все равно можете сделать это, используяORDER BY
плюсLIMIT 1
вместо этого (как это сделал Quassnoi):Мне нравится использовать синтаксис конструктора строк,
WHERE (a, b, c) IN (subquery)
потому что он сокращает объем необходимой многословности.источник
На самом деле есть хакерское решение этой проблемы. Допустим, вы хотите выбрать самое большое дерево каждого леса в регионе.
Когда вы группируете деревья по лесам, вы получаете несортированный список деревьев, и вам нужно найти самое большое. Первое, что вам нужно сделать, это отсортировать строки по их размеру и выбрать первую из списка. Это может показаться неэффективным, но если у вас есть миллионы строк, это будет намного быстрее, чем решения, включающие
JOIN
иWHERE
условия.Кстати, обратите внимание, что
ORDER_BY
forarray_agg
представлен в Postgresql 9.0источник
SELECT usr_id, (array_agg(time_stamp ORDER BY time_stamp DESC))[1] AS timestamp, (array_agg(lives_remaining ORDER BY time_stamp DESC))[1] AS lives_remaining, (array_agg(trans_id ORDER BY time_stamp DESC))[1] AS trans_id FROM lives GROUP BY usr_id
В Postgressql 9.5 появилась новая опция DISTINCT ON.
Он удаляет повторяющиеся строки и оставляет только первую строку, как определено в предложении ORDER BY.
см. официальную документацию
источник
Создание индекса
(usr_id, time_stamp, trans_id)
значительно улучшит этот запрос.У вас всегда должно быть что-то
PRIMARY KEY
в ваших таблицах.источник
Я думаю, у вас здесь одна серьезная проблема: нет монотонно увеличивающегося «счетчика», чтобы гарантировать, что данная строка возникла позже, чем другая. Вот пример:
Вы не можете определить по этим данным, какая запись является самой последней. Это второй или последний? Нет функции sort или max (), которую вы можете применить к любым из этих данных, чтобы дать вам правильный ответ.
Увеличение разрешения отметки времени было бы огромным подспорьем. Поскольку ядро базы данных сериализует запросы, при достаточном разрешении вы можете гарантировать, что никакие две метки времени не будут одинаковыми.
В качестве альтернативы используйте trans_id, который не будет переноситься очень и очень долго. Наличие trans_id, которое переключается, означает, что вы не можете сказать (для той же временной метки), является ли trans_id 6 более поздним, чем trans_id 1, если вы не выполните сложную математику.
источник
Другое решение, которое может оказаться полезным.
источник