PostgreSQL - отримати рядок, що має значення Max для стовпця


96

Я маю справу з таблицею Postgres (яка називається "життя"), яка містить записи зі стовпцями для time_stamp, usr_id, transaction_id та lives_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 (перший), і 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

Гаразд, так це працює, але мені це не подобається. Це вимагає запиту в запиті, самостійного приєднання, і мені здається, що це може бути набагато простіше, захопивши рядок, який 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);

Джош, вам може не сподобатися той факт, що запит самостійно приєднується тощо, але це нормально, що стосується СУБД.
vladr

1
Те, до чого насправді перейде самоз’єднання, - це просте відображення індексу, де внутрішній SELECT (той, що має MAX) сканує індекс, викидаючи нерелевантні записи, а зовнішній SELECT просто захоплює решту стовпців таблиці що відповідає звуженому індексу.
vladr

Владе, дякую за поради та пояснення. Це відкрило мені очі про те, як почати розуміти внутрішню роботу бази даних та як оптимізувати запити. Quassnoi, дякую за чудовий запит та підказку щодо первинного ключа; Білл теж. Дуже корисний.
Джошуа Беррі

дякую, що показали мені, як отримати MAX BY2 стовпці!

Відповіді:


90

На таблиці із 158k псевдовипадковими рядками (usr_id рівномірно розподілений між 0 і 10k, trans_idрівномірно розподілений між 0 і 30),

Під вартістю запиту, нижче, я маю на увазі оцінку витрат оптимізатора на основі витрат Postgres (зі xxx_costзначеннями за замовчуванням Postgres ), яка є зваженою оцінкою функції необхідних ресурсів вводу-виводу та процесора; Ви можете отримати це, запустивши PgAdminIII та запустивши "Запит / Пояснити (F7)" у запиті, встановивши для параметра "Запит / Пояснити" значення "Аналіз"

  • Запит 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%), ніж інший, але має набагато більше вищу вартість, як правило, розумніше буде вибрати той, що має більший час виконання, але меншу вартість.

Якщо ви очікуєте, що на момент запуску запиту на вашій виробничій машині не буде конкуренції за пам’ять (наприклад, кеш 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один раз після створення необхідних індексів.


Запит No1

-- 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

Запит No2

-- 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 підтримує функцію вікна, тобто ви можете написати щось таке просте та ефективне, як:

Запит No3

-- 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
 );

Під складним індексом на (usr_id, trans_id, times_tamp), ви маєте на увазі щось на зразок "СТВОРИТИ ІНДЕКС live_blah_idx ON Live (usr_id, trans_id, time_stamp)"? Або я повинен створити три окремі індекси для кожного стовпця? Я повинен дотримуватися типового значення "USING btree", так?
Джошуа Беррі

1
Так першому вибору: я маю на увазі СТВОРИТИ ІНДЕКС live_blah_idx ON життя (usr_id, trans_id, time_stamp). :) На здоров’я.
vladr

Дякуємо, що навіть зробили порівняння витрат vladr! Дуже повна відповідь!
Адам

@vladr Я щойно натрапив на вашу відповідь. Я трохи збентежений, як ви говорите, запит 1 має вартість 16 тисяч, а запит 2 - 14 тисяч. Але далі в таблиці ви кажете, що запит 1 має вартість 5 тис., А запит 2 - 50 тис. Тож який запит є кращим для використання? :) спасибі
Houman

1
@Kave, таблиця призначена для гіпотетичної пари запитів, щоб проілюструвати приклад, а не два запити OP. Перейменування для зменшення плутанини.
vladr

77

Я б запропонував чисту версію на основі 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;

6
Це набагато коротка і слушна відповідь. Також має хорошу довідку! Це має бути прийнятою відповіддю.
Прахар Агравал

Здавалося, це спрацювало для мене в моєму дещо іншому додатку, де ніщо інше не могло б. Безумовно, його слід піднімати для більшої видимості.
Джим Фактор

8

Ось ще один метод, який, як правило, не використовує корельованих підзапитів або 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.


4

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


3

Насправді для цієї проблеми існує хакерське рішення. Скажімо, ви хочете вибрати найбільше дерево кожного лісу в регіоні.

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_BYfor array_aggвведено в Postgresql 9.0


У вас помилка. Вам потрібно написати ЗАМОВИТИ ДЛЯ tree_size.size DESC. Також для авторського завдання код буде виглядати так: 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
alexkovelsky

2

У Postgressql 9.5 є нова опція під назвою DISTINCT ON

SELECT DISTINCT ON (location) location, time, report
    FROM weather_reports
    ORDER BY location, time DESC;

Це виключає повторювані рядки та залишає лише перший рядок, як визначено моїм реченням ORDER BY.

дивіться офіційну документацію


1
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у своїх таблицях.


0

Я думаю, у вас тут є одна основна проблема: немає монотонно зростаючого "лічильника", який би гарантував, що даний рядок відбувся пізніше, ніж інший. Візьмемо цей приклад:

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 (), яку ви можете застосувати до будь-якої з цих даних, щоб дати правильну відповідь.

Збільшення роздільної здатності мітки часу було б величезною допомогою. Оскільки механізм бази даних серіалізує запити, з достатньою роздільною здатністю ви можете гарантувати, що жодні позначки часу не будуть однаковими.

Крім того, використовуйте trans_id, який не буде перевертатися дуже, дуже довго. Наявність перекручуваного trans_id означає, що ви не можете сказати (для тієї ж позначки часу), чи є trans_id 6 новішим, ніж trans_id 1, якщо ви не зробите складної математики.


Так, в ідеалі стовпець послідовності (автоінкремент) буде в порядку.
vladr

Припущення зверху полягало в тому, що при невеликих кроках часу trans_id не перевертається. Я погоджуюсь, що таблиці потрібен унікальний первинний індекс - як не повторюваний trans_id. (PS Я щасливий, що зараз у мене достатньо очок карми / репутації, щоб коментувати!)
Джошуа Беррі

Влад стверджує, що trans_id має досить короткий цикл, який часто перевертається. Навіть якщо ви враховуєте лише середні два рядки з моєї таблиці (trans_id = 6 і 1), ви все одно не можете сказати, який з останніх. Отже, використання max (trans_id) для даної позначки часу не буде працювати.
Barry Brown

Так, я покладаюсь на гарантію автора програми, що кортеж (time_stamp, trans_id) унікальний для даного користувача. Якщо справа не в цьому, тоді "SELECT l1.usr_id, l1.lives_left, ... FROM ... WHERE ..." має стати "SELECT l1.usr_id, MAX / MIN (l1.lives_left), ... FROM. .. ДЕ ... ГРУПА ЗА l1.usr_id, ...
vladr

0

Ще одне рішення, яке може вам виявитися корисним.

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.