PostgreSQL – 获取列的最大值的行
我正在处理一个包含time_stamp,usr_id,transaction_id和lives_remaining列的logging的Postgres表(称为“lives”)。 我需要一个查询,将给我每个usr_id最近lives_remaining总
- 有多个用户(不同的usr_id的)
- time_stamp不是一个唯一的标识符:有时用户事件(在表中按行排列)将以相同的time_stamp出现。
- trans_id只有在非常小的时间范围内才是唯一的:随着时间的推移它会重复
- remaining_lives(对于给定的用户)可随时间增加和减less
例:
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 | 五 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的行可能会简单得多。 表“生活”有几千万行parsing,所以我希望这个查询尽可能快速和有效。 我是RDBM和Postgres的新手,所以我知道我需要有效地使用正确的索引。 我在如何优化方面有点失落。
我在这里find了类似的讨论。 我可以执行一些类似于Oracle分析函数的Postgres吗?
任何有关访问聚集函数(如MAX)所使用的相关列信息的build议,创build索引以及创build更好的查询都将非常感谢!
PS您可以使用以下来创build我的示例情况:
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);
在具有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的成本估计,并且在800ms中完成(给定(
usr_id
,trans_id
,time_stamp
)的复合索引) - 下面的查询#2具有14k的成本估计,并且在800ms(在(
usr_id
,EXTRACT(EPOCH FROM time_stamp)
,trans_id
)上给出复合函数索引)- 这是Postgres特定的
- 查询#3 (Postgres 8.4+)的成本估计和完成时间与查询#2相当(或优于)(给定(
usr_id
,time_stamp
,trans_id
)的复合索引); 它具有只扫描一次lives
表的优点,如果你临时增加(如果需要的话) work_mem以适应内存中的sorting,它将是所有查询中最快的。
以上所有时间都包括检索完整的10k行结果集。
您的目标是最小的成本估算和最less的查询执行时间,重点是估计成本。 查询执行可以显着依赖运行时条件(例如,相关行是否已经完全caching在内存中),而成本估计则不是。 另一方面,请记住,成本估算正是这个估算。
最好的查询执行时间是在没有负载的情况下在专用数据库上运行时获得的(例如,在开发PC上使用pgAdminIII进行播放)。根据实际的机器负载/数据访问传播,查询时间会有所不同。 当一个查询比另一个查询稍微快一些(<20%),但是成本高得多时,select执行时间较长但成本较低的查询通常更明智一些。
如果您希望在运行查询时生产计算机上的内存不会存在竞争(例如,RDBMS高速caching和文件系统高速caching不会被并发查询和/或文件系统活动所破坏),那么您获得的查询时间在独立模式(例如开发PC上的pgAdminIII)模式将是有代表性的。 如果生产系统上存在争用,则查询时间将与估计的成本比例成比例地下降,因为具有较低成本的查询不依赖于高速caching, 而具有较高成本的查询将一遍又一遍地重复访问相同的数据(触发在没有稳定的caching的情况下额外的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 |
创build必要的索引后,不要忘记运行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 );
我会build议基于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;
这是另一种方法,恰好不使用相关的子查询或GROUP BY。 我并不擅长PostgreSQL的性能调优,所以我build议你试试这个和其他人给出的解决scheme,看看哪个更适合你。
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
是唯一的至less超过任何给定的time_stamp
值。
我喜欢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)
很好,因为它减less了所需的字面量。
对于这个问题,有一个很好的解决办法。 假设你想select一个地区的每个森林的最大的树。
SELECT (array_agg(tree.id ORDER BY tree_size.size)))[1] FROM tree JOIN forest ON (tree.forest = forest.id) GROUP BY forest.id
当你通过森林分组树木时,会有一个未sorting的树木列表,你需要find最大的树木。 你应该做的第一件事是按行的大小sorting,并select列表中的第一个。 它可能看起来效率低下,但是如果你有数百万行,它将比包含JOIN
和WHERE
条件的解决scheme快得多。
顺便说一下,注意array_agg
ORDER_BY
是在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)
上创build一个索引将大大改善这个查询。
你应该总是在表格中总是有一些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
您无法从这个数据中确定最近的条目。 这是第二个还是最后一个? 没有sorting或max()函数可以应用于任何这些数据给你正确的答案。
增加时间戳的分辨率将是一个巨大的帮助。 由于数据库引擎序列化请求,并且具有足够的分辨率,所以可以保证没有两个时间戳是相同的。
或者,使用trans_id,这将不会翻转很长很长的时间。 有一个trans_id翻转意味着你不能告诉(相同的时间戳)trans_id 6是否比trans_id 1更新,除非你做一些复杂的math。