【问题标题】:PostgreSQL - fetch the row which has the Max value for a columnPostgreSQL - 获取列的最大值的行
【发布时间】:2010-10-09 20:43:11
【问题描述】:

我正在处理一个 Postgres 表(称为“lives”),其中包含带有 time_stamp、usr_id、transaction_id 和 living_remaining 列的记录。我需要一个查询,该查询将为我提供每个 usr_id 的最新 lives_remaining 总数

  1. 有多个用户(不同的 usr_id)
  2. time_stamp 不是唯一标识符:有时用户事件(表中逐行)会以相同的 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

好的,所以这行得通,但我不喜欢它。它需要一个查询中的一个查询,一个自连接,在我看来,通过抓取 MAX 发现具有最大时间戳和 trans_id 的行可能会更简单。表“lives”有数千万行要解析,所以我希望这个查询尽可能快速和高效。我尤其是 RDBM 和 Postgres 的新手,所以我知道我需要有效地使用正确的索引。我对如何优化有点迷茫。

我发现了一个类似的讨论here。我可以执行某种与 Oracle 分析功能等效的 Postgres 类型吗?

任何关于访问聚合函数(如 MAX)使用的相关列信息、创建索引和创建更好的查询的建议将不胜感激!

附:您可以使用以下内容创建我的示例案例:

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

【问题讨论】:

  • Josh,您可能不喜欢查询自联接等这一事实,但就 RDBMS 而言,这没关系。
  • 自联接实际上最终会转换为一个简单的索引映射,其中内部 SELECT(带有 MAX 的那个)扫描索引丢弃不相关的条目,而外部 SELECT 只是抓取表中与缩小索引相对应的其余列。
  • 弗拉德,感谢您的提示和解释。它让我大开眼界,了解如何开始了解数据库的内部工作原理以及如何优化查询。 Quassnoi,感谢您对主键的出色查询和提示;比尔也是。很有帮助。
  • 感谢您向我展示如何获得MAX BY 2 列!

标签: sql postgresql query-optimization cbo cost-based-optimizer


【解决方案1】:

我会提出一个基于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;

【讨论】:

  • 这是一个简短而合理的答案。也有很好的参考价值!这应该是公认的答案。
  • 这似乎适用于我略有不同的应用程序,而没有其他方法。绝对应该提高知名度。
【解决方案2】:

在有 158k 伪随机行的表上(usr_id 均匀分布在 0 到 10k 之间,trans_id 均匀分布在 0 到 30 之间),

下面的查询成本指的是 Postgres 的基于成本的优化器的成本估算(使用 Postgres 的默认 xxx_cost 值),它是对所需 I/O 和 CPU 资源的加权函数估算;您可以通过启动 PgAdminIII 并在“查询/解释选项”设置为“分析”的查询上运行“查询/解释 (F7)”来获得此信息

  • Quassnoy 的查询估计成本为 745k (!),并在 1.3 秒内完成(给定 (usr_id, trans_id, time_stamp) 上的复合索引)
  • Bill 的查询估计成本为 93k,并在 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, @987654338 @));它的优点是只扫描lives 表一次,如果您临时增加(如果需要)work_mem 以适应内存中的排序,它将是迄今为止所有查询中最快的。

以上所有时间都包括检索完整的 10k 行结果集。

您的目标是最小化成本估算最小化查询执行时间,重点是估算成本。查询执行可能很大程度上取决于运行时条件(例如,相关行是否已经完全缓存在内存中),而成本估计则不然。另一方面,请记住,成本估算正是一个估算。

最佳查询执行时间是在无负载的专用数据库上运行时获得的(例如,在开发 PC 上使用 pgAdminIII。)查询时间会根据实际机器负载/数据访问分布在生产中有所不同。当一个查询看起来比另一个查询稍快 (很多时,通常选择执行时间较长但成本较低的查询更为明智。

如果您希望在运行查询时生产机器上不会存在内存竞争(例如,RDBMS 缓存和文件系统缓存不会被并发查询和/或文件系统活动破坏),那么查询您在独立(例如开发 PC 上的 pgAdminIII)模式下获得的时间将具有代表性。如果生产系统存在争用,查询时间将与估计的成本比率成比例下降,因为成本较低的查询不太依赖缓存成本较高的查询将重新访问一遍又一遍地重复相同的数据(在没有稳定缓存的情况下触发额外的 I/O),例如:

              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 支持Window Function,这意味着您可以编写如下简单高效的内容:

查询 #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
 );

【讨论】:

  • 通过 (usr_id, trans_id, times_tamp) 上的复合索引,您的意思是像“CREATE INDEX living_blah_idx ON lives (usr_id, trans_id, time_stamp)”之类的东西吗?还是应该为每列创建三个单独的索引?我应该坚持默认的“使用 btree”,对吧?
  • 第一个选择是的:我的意思是 CREATE INDEX living_blah_idx ON lives (usr_id, trans_id, time_stamp)。 :) 干杯。
  • 感谢您进行成本比较 vladr!非常完整的答案!
  • @vladr 我刚刚看到你的答案。我有点困惑,正如您所说,查询 1 的成本为 16k,查询 2 的成本为 14k。但在表的更下方,您说查询 1 的成本为 5k,查询 2 的成本为 50k。那么哪个查询是首选的查询呢? :) 谢谢
  • @Kave,该表是为了说明一个假设的一对查询,而不是 OP 的两个查询。重命名以减少混淆。
【解决方案3】:

这是另一种方法,恰好不使用相关子查询或 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】:

    我喜欢你提到的另一页上Mike Woodhouse's answer 的风格。当被最大化的东西只是一个列时,它特别简洁,在这种情况下,子查询可以只使用MAX(some_col)GROUP BY 其他列,但是在你的情况下,你有一个两部分的数量要最大化,你仍然可以通过使用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) 很好,因为它减少了所需的冗长。

    【讨论】:

      【解决方案5】:

      Postgressql 9.5 中有一个名为 DISTINCT ON 的新选项

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

      它消除了重复的行,只留下 ORDER BY 子句定义的第一行。

      见官方documentation

      【讨论】:

      • 优秀,顺便说一句,通过避免 seq 扫描,指定具有索引的位置范围将大大加快速度。
      【解决方案6】:

      实际上,这个问题有一个 hacky 解决方案。假设您要选择一个区域中每个森林中最大的树。

      SELECT (array_agg(tree.id ORDER BY tree_size.size)))[1]
      FROM tree JOIN forest ON (tree.forest = forest.id)
      GROUP BY forest.id
      

      当您按森林对树木进行分组时,会有一个未排序的树木列表,您需要找到最大的树木。您应该做的第一件事是按行的大小对行进行排序,然后选择列表中的第一个。它可能看起来效率低下,但如果您有数百万行,它将比包含JOINWHERE 条件的解决方案快得多。

      顺便说一句,注意 ORDER_BY for array_agg 是在 Postgresql 9.0 中引入的

      【讨论】:

      • 你有一个错误。您需要编写 ORDER BY 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
      【解决方案7】:
      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

      【讨论】:

        【解决方案8】:

        你可以用窗口函数来做到这一点

        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
        

        【讨论】:

          【解决方案9】:

          我认为您在这里遇到了一个主要问题:没有单调递增的“计数器”来保证给定行发生的时间晚于另一行。举个例子:

          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 更新,除非您进行一些复杂的数学运算。

          【讨论】:

          • 是的,理想情况下,顺序(自动增量)列应该是有序的。
          • 上面的假设是对于小的时间增量,trans_id 不会翻转。我同意该表需要一个唯一的主索引——比如非重复的 trans_id。 (P.S. 我很高兴我现在有足够的业力/声望点来发表评论!)
          • Vlad 指出 trans_id 的周期相当短,而且频繁地翻转。即使您只考虑我表中的中间两行(trans_id = 6 和 1),您仍然无法分辨哪一行是最新的。因此,对给定的时间戳使用 max(trans_id) 是行不通的。
          • 是的,我依赖应用程序作者的保证,即 (time_stamp,trans_id) 元组对于给定用户是唯一的。如果不是这种情况,则 "SELECT l1.usr_id,l1.lives_left,... FROM ... WHERE ..." 必须变为 "SELECT l1.usr_id,MAX/MIN(l1.lives_left),... FROM 。 .. WHERE ... GROUP BY l1.usr_id,...
          猜你喜欢
          • 1970-01-01
          • 2019-10-16
          • 1970-01-01
          • 2014-12-21
          • 2010-09-12
          • 2014-09-08
          相关资源
          最近更新 更多