PostgreSQL - получить строку, которая имеет максимальное значение для столбца

Я имею дело с таблицей Postgres (называемой "жизнями"), которая содержит записи со столбцами для time_stamp, usr_id, transaction_id и life_remaining. Мне нужен запрос, который предоставит мне самое последнее количество жизней_ремайн для каждого usr_id

  1. Есть несколько пользователей (разные usr_id)
  2. time_stamp не является уникальным идентификатором: иногда пользовательские события (одно за строкой в ​​таблице) будут происходить с одной и той же time_stamp.
  3. trans_id уникален только для очень малых временных диапазонов: со временем он повторяется
  4. оставшееся_продолжение (для данного пользователя) может как увеличиваться, так и уменьшаться с течением времени

пример:

отметка_времени | жизнь_ремонта | 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)

Решение

В таблице с 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.

см. официальную документацию

Вы можете сделать это с помощью оконных функций

SELECT t.*
FROM
    (SELECT
        *,
        ROW_NUMBER() OVER(PARTITION BY usr_id ORDER BY time_stamp DESC) as r
    FROM lives) as t
WHERE t.r = 1

Мне нравится стиль ответа Майка Вудхауса на другой странице, которую вы упомянули. Это особенно кратко, когда объект, который максимизируется, представляет собой всего лишь один столбец, и в этом случае подзапрос может просто использовать 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;