PostgreSQL-выборка строки, имеющей максимальное значение для столбца

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

  1. есть несколько пользователей (различные usr_id)
  2. time_stamp не является уникальным идентификатором: иногда события пользователя (по строке в таблице) будут происходить с тем же отметка времени.
  3. trans_id уникален только для очень небольших временных диапазонов: со временем он повторяется
  4. 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. Таблица "lives" имеет десятки миллионов строк для анализа, поэтому я хотел бы, чтобы этот запрос был как можно быстрее и эффективнее. Я новичок в RDBM и Postgres в частности, поэтому я знаю, что мне нужно эффективно использовать правильные индексы. Я немного запутался в том, как оптимизировать.

я нашел похожие обсуждения здесь. Могу ли я выполнить некоторый тип 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 по умолчанию xxx_cost values), который является взвешенной оценкой функции необходимых ресурсов ввода-вывода и процессора; вы можете получить это, запустив PgAdminIII и запустив "Query / Explain (F7)" в запросе с " Query/Explain options" значение "анализировать"

  • запрос 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 и завершается в 800ms (учитывая индекс составной функции на (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 на ПК разработки.) Время запроса будет варьироваться в зависимости от фактической загрузки машины/распространения доступа к данным. Когда один запрос появляется немного быстрее (много более высокая стоимость, как правило, будет разумнее выбрать тот, у которого более высокое время выполнения, но более низкая стоимость.

когда вы ожидаете, что на вашей производственной машине не будет конкуренции за память во время выполнения запроса (например, кэш СУБД и кэш файловой системы не будут разбиты параллельными запросами и/или активностью файловой системы), время запроса, полученное в автономном режиме (например, 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 вместо этого (как сделано Quassnoi):

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

вы не можете определить из этих данных, которая является самой последней записи. Второй или последний? Нет функции sort или 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.

посмотреть официальный документация