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. Таблица "живет" имеет десятки миллионов строк для разбора, поэтому я хотел бы, чтобы этот запрос был как можно быстрее и эффективнее. Я новичок в 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);
1418   8  

8 ответов:

на таблице с 158k псевдослучайных строк (usr_id равномерно распределены между 0 и 10k,trans_id равномерно распределены между 0 и 30),

по стоимости запроса, ниже, я имею в виду оценку стоимости оптимизатора на основе стоимости Postgres (с Postgres' default xxx_cost values), который представляет собой взвешенную оценку функции требуемых ресурсов ввода-вывода и процессора; вы можете получить это, запустив 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

    Ничего не найдено.