PostgreSQL - 获取具有列的Max值的行

时间:2009-02-25 16:37:24

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

我正在处理Postgres表(称为“生命”),该表包含time_stamp,usr_id,transaction_id和lives_remaining列的记录。我需要一个查询,它会为我提供每个usr_id的最新life_remaining总数

  1. 有多个用户(不同的usr_id's)
  2. time_stamp不是唯一标识符:有时用户事件(表中逐行)将以相同的time_stamp发生。
  3. trans_id仅在非常小的时间范围内是唯一的:随着时间的推移它会重复
  4. remaining_lives(对于给定用户)可以随时间增加或减少
  5. 示例:

    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(first)和trans_id(second)来识别正确的行。然后,我还需要将该信息从子查询传递给主查询,该查询将为相应行的其他列提供数据。这是我已经开始工作的被黑客攻击的查询:

    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的行可以更简单。表“living”有数千万行要解析,所以我希望这个查询尽可能快速有效。我是RDBM和Postgres的新手,所以我知道我需要有效地使用正确的索引。我对如何优化感到有点迷失。

    我发现了类似的讨论here。我可以执行某种类型的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);
    

9 个答案:

答案 0 :(得分:78)

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

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

  • Quassnoy的查询成本估算为745k(!),并在1.3秒内完成(给定复合索引(usr_idtrans_idtime_stamp))
  • Bill的查询成本估计为93k,并在2.9秒内完成(给定(usr_idtrans_id上的复合索引))
  • 下面的查询#1 的费用估算值为16k,并在800毫秒内完成(给定(usr_idtrans_idtime_stamp上的复合索引) )
  • 下面的查询#2 的费用估算为14k,并在800毫秒内完成(给定(usr_idEXTRACT(EPOCH FROM time_stamp)trans_id上的复合函数索引))
    • 这是Postgres特定的
  • 下面的查询#3 (Postgres 8.4+)的成本估算和完成时间与(或更好)查询#2相当(给定(usr_id上的复合索引,{ {1}},time_stamp));它的优点是只扫描trans_id表一次,如果你暂时增加(如果需要)work_mem以适应内存中的排序,它将是所有查询中最快的。

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

您的目标是最低成本估算最短查询执行时间,重点是估算成本。查询执行可以显着地依赖于运行时条件(例如,相关行是否已经完全缓存在存储器中),而成本估计则不是。另一方面,请记住,成本估算正是估计值。

在没有负载的情况下在专用数据库上运行时获得最佳查询执行时间(例如,在开发PC上使用pgAdminIII)。查询时间将根据实际机器负载/数据访问传播而有所不同。如果一个查询看起来比另一个查询稍快(<20%)但成本 更高,那么选择具有更高执行时间但成本更低的那个通常更明智。

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

lives

在创建必要的索引后,不要忘记运行 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 |


查询#1

ANALYZE lives

查询#2

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

2013/01/29更新

最后,从版本8.4开始,Postgres支持Window Function,这意味着您可以编写简单有效的内容:

查询#3

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

答案 1 :(得分:52)

我建议使用基于select * from mytable where mydate = current date - (case when dayofweek(current date) = 1 then 2 -- sonntag when dayofweek(current date) = 2 then 3 -- montag else 1 end) days 的简洁版本(请参阅docs):

DISTINCT ON

答案 2 :(得分:7)

这是另一种方法,碰巧没有使用相关的子查询或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的任何给定值上都是唯一的。

答案 3 :(得分:4)

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

答案 4 :(得分:3)

对于这个问题,实际上有一个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条件的解决方案快得多。

BTW,请注意Postgresql 9.0中引入ORDER_BY的{​​{1}}

答案 5 :(得分:2)

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

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

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

请参阅官方documentation

答案 6 :(得分: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

答案 7 :(得分: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更新,除非你做了一些复杂的数学运算。

答案 8 :(得分: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