PostgreSQL-获取具有列最大值的行


96

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

  1. 有多个用户(与usr_id不同)
  2. time_stamp不是唯一的标识符:有时,用户事件(表中的每一行)将使用相同的time_stamp发生。
  3. trans_id仅在很小的时间范围内才是唯一的:随着时间的流逝,它会重复
  4. (对于给定的用户)剩余的生存时间可以随着时间增加和减少

例:

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的新手,所以我知道我需要有效地使用适当的索引。我对如何优化有些迷惑。

我在这里找到了类似的讨论。我可以执行某种与Oracle分析功能等效的Postgres吗?

任何有关访问由聚合函数(如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);

Josh,您可能不喜欢查询自联接等事实,但是就RDBMS而言这没关系。
vladr

1
自联接实际上将转换为一个简单的索引映射,其中内部SELECT(具有MAX的SELECT)扫描索引以丢弃不相关的条目,而外部SELECT仅从表中获取其余列对应于缩小的索引。
vladr

弗拉德,谢谢你的提示和解释。它使我开始了解如何开始了解数据库的内部工作以及如何优化查询。Quassnoi,感谢您的宝贵询问和有关主键的提示;比尔 很有帮助。
约书亚·贝里

感谢您向我展示如何获得MAX BY2列!

Answers:


90

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

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

  • Quassnoy的查询有745k成本估算(!),并完成了130秒(给出一个复合索引(usr_idtrans_idtime_stamp))
  • Bill的查询的费用估算为93k,并在2.9秒内完成(鉴于(usr_idtrans_id)上的复合索引)
  • 查询#1的下方具有16K成本估算,和在800ms的结束(在给定的化合物指数(usr_idtrans_idtime_stamp))
  • 查询#2的下方具有14K成本估算,和在800ms的结束(在给定的化合物功能指数(usr_idEXTRACT(EPOCH FROM time_stamp)trans_id))
    • 这是Postgres特有的
  • 下面的查询#3(Postgres的8.4+)具有成本估算和完成时间相当(或更好)的查询#2(在给定(一个复合索引usr_idtime_stamptrans_id)); 它具有lives仅扫描表一次的优点,并且,如果您临时增加(如果需要)work_mem以容纳内存中的排序,则它将是所有查询中最快的。

上面所有时间都包括检索全部1万行结果集。

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

在没有负载的专用数据库上运行时(例如在开发PC上使用pgAdminIII),可以获得最佳的查询执行时间。查询时间将根据实际的机器负载/数据访问范围而有所不同。当一个查询稍快出现(<20%)比其它但是具有更高的成本,这将通常是明智的选择具有较高的执行时间,但成本更低。

如果您希望运行查询时生产机器上的内存没有竞争(例如,并发查询和/或文件系统活动不会破坏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)上的复合索引,您是说类似“创建索引lives_blah_idx ON生活(usr_id,trans_id,time_stamp)”的意思吗?还是应该为每列创建三个单独的索引?我应该使用默认的“使用btree”,对吗?
约书亚·贝瑞

1
首选:是的。我的意思是创建索引lives_blah_idx ON生活(usr_id,trans_id,time_stamp)。:)干杯。
vladr

甚至感谢您进行成本比较vladr!很完整的答案!
亚当

@vladr我刚遇到您的答案。我有点困惑,因为您说查询1的成本为16k,而查询2的成本为14k。但是在表的下方,您说查询1的成本为5k,而查询2的成本为50k。那么哪个查询是首选使用呢?:)谢谢
Houman

1
@Kave,该表用于假设的一对查询,以说明示例,而不是OP的两个查询。重命名以减少混乱。
vladr 2012年

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
这是简短而合理的答案。也有很好的参考!这应该是公认的答案。
Prakhar Agrawal

对于我略有不同的应用程序,这似乎对我有用,其他什么也不会。绝对应该提出来提高可见度。
Jim Factor

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

我喜欢您提到的另一页上Mike Woodhouse的回答风格。当要最大化的对象仅是一列时,这尤其简洁。在这种情况下,子查询可以使用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)很好,因为它减少了所需的语言量。


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

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

顺便说一句,请注意ORDER_BYfor 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
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

您不能从此数据中确定哪个是最新条目。是第二个还是最后一个?没有可以用于任何此数据的sort或max()函数,可以为您提供正确的答案。

增加时间戳的分辨率将有很大的帮助。由于数据库引擎对请求进行了序列化,因此具有足够的分辨率,您可以保证没有两个时间戳相同。

或者,使用不会在很长一段时间内翻转的trans_id。拥有一个trans_id滚动意味着您不能(对于相同的时间戳)知道trans_id 6是否比trans_id 1更新,除非您进行一些复杂的数学运算。


是的,理想情况下,顺序(自动增量)列应该是按顺序排列的。
vladr

从上面的假设是,对于较小的时间增量,trans_id不会翻转。我同意该表需要一个唯一的主索引-就像一个非重复的trans_id。(PS我很高兴我现在有足够的业力/声誉点可以发表评论!)
Joshua Berry

弗拉德指出,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,...
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
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.