假设有一张表
version col_A col_B col_C
1 A1 B1 (null)
2 A2 B3 (null)
3 A3 B2 (null)
4 A5 (null) C1
5 A1 (null) (null)
Run Code Online (Sandbox Code Playgroud)
我需要的是获取 Postgres 中每列的最后一个非空值,即对于上表,我期望结果为(A1,B2,C1)。
这里的“last”是指当表按列版本排序时,每列中的最后一个非空值。
版本列始终仅包含非空值。
该表不会很大,因为只有几千行。所以,不太担心性能。
您可以执行以下操作(下面的所有代码都可以在此处的小提琴上找到):
CREATE TABLE tab
(
version INT PRIMARY KEY,
col_A TEXT,
col_B TEXT,
col_C TEXT
);
Run Code Online (Sandbox Code Playgroud)
数据:
INSERT INTO tab (version, col_A, col_B, col_C)
VALUES
(1, 'A1', 'B1', null),
(2, 'A2', 'B3', null),
(3, 'A3', 'B2', null),
(4, 'A4', null, 'C1'),
(5, 'A5', null, null);
Run Code Online (Sandbox Code Playgroud)
然后运行此查询:
select
(select col_a from tab where col_a is not null order by version desc limit 1) as a,
(select col_b from tab where col_b is not null order by version desc limit 1) as b,
(select col_c from tab where col_c is not null order by version desc limit 1) as c ;
Run Code Online (Sandbox Code Playgroud)
结果:
a b c
A5 B2 C1
Run Code Online (Sandbox Code Playgroud)
您可能想考虑放入类似COALESCE
函数的内容,以防一列(或多列?)仅包含NULL
值。最后,以后在问此类问题时,能否请您提供一下您的表格和数据?帮助我们帮助您 - PS 欢迎来到 dba.se!
截至 2021 年 11 月 26 日 07:40:00 UTC,我已经完成了性能分析,可在此处获取。
有 1k 行(并且 上有唯一索引(version)
):
有 20k 行(其中 A、B、C 列中有 19k 行均为空):
在 等上添加 3 个部分索引后,具有与上面相同的 20k 行(version) WHERE (col_a IS NOT NULL)
:
在 等上添加 3 个部分索引后(version) INCLUDE (col_a) WHERE (col_a IS NOT NULL)
,具有与上面相同的 20k 行。Quassnoi-1 查询在这里杀死了它,大大优于所有其他查询:
最后一次测试有 20k 行,但有分散的非空值。这里 Gerars 和 Quassnoi-2 都相当快,不如 Quassnoi-1,但差别很小:
关于在无法控制或不了解的服务器上执行测试的常见警告适用!
随着比赛的激烈,我会不断修改!
我相信根据您的更新,您希望按列对行进行version
排序以获得最后一个(非空)值。您可以通过使用窗口函数以几种不同的方式实现您的最终目标,但我发现FIRST_VALUE()
带有CASE
语句的窗口函数最简单,使用以下查询(用于演示的 dbfiddle):
SELECT DISTINCT
FIRST_VALUE(col_A) OVER (ORDER BY CASE WHEN col_A IS NULL THEN 0 ELSE version END DESC) AS LastA,
FIRST_VALUE(col_B) OVER (ORDER BY CASE WHEN col_B IS NULL THEN 0 ELSE version END DESC) AS LastB,
FIRST_VALUE(col_C) OVER (ORDER BY CASE WHEN col_C IS NULL THEN 0 ELSE version END DESC) AS LastC
FROM tab
Run Code Online (Sandbox Code Playgroud)
使用您更新的示例:
version col_A col_B col_C
1 A1 B1 (null)
2 A2 B3 (null)
3 A3 B2 (null)
4 A5 (null) C1
5 A1 (null) (null)
Run Code Online (Sandbox Code Playgroud)
这会产生结果:
LastA LastB LastC
A1 B2 C1
Run Code Online (Sandbox Code Playgroud)
此解决方案的关键是语句,该语句表示按列CASE
对值进行排序,除非该值是which ,否则会将其放在排序末尾(这是语句的一部分)。这会导致最后一个值不是要返回的值(当使用该函数时,并且因为该子句是)。version
NULL
IS NULL THEN 0
CASE
version
NULL
FIRST_VALUE()
ORDER BY
DESC
注意:此解决方案假设您的版本从 1 开始(具体来说,永远不会有版本 = 0)。如果可能的话,请将 CASE 子句中的 0 值替换为永远不会使用的负数,例如 -1 或 -9999。
JD提出的解决方案是可行的,但它必须对整个表进行三次排序,并且执行相当昂贵的操作DISTINCT
。
正如评论中提到的,有效地解决这个任务需要一个类似于FIRST_VALUE
过滤器的窗口函数,不幸的是,PostgreSQL 不支持。
理论上,这可以通过在记录类型(如 )上使用聚合函数来模拟MAX(ROW(CASE WHEN col_a IS NOT NULL THEN VERSION END, col_a))
,但由于某种原因,PostgreSQL 也不支持记录类型上的MAX
and MIN
,尽管它确实支持记录类型上的比较和排序。
如果列上有唯一索引version
,则可以使用这些查询之一。
SELECT (
SELECT col_a
FROM aang
WHERE col_a IS NOT NULL
ORDER BY
version DESC
LIMIT 1
),
(
SELECT col_b
FROM aang
WHERE col_b IS NOT NULL
ORDER BY
version DESC
LIMIT 1
),
(
SELECT col_c
FROM aang
WHERE col_c IS NOT NULL
ORDER BY
version DESC
LIMIT 1
)
Run Code Online (Sandbox Code Playgroud)
该查询仅从下往上扫描表三次,在每列的第一个值处停止。由于您的version
索引是唯一的,因此这次扫描很可能会使用该索引。如果所有三列中的最后一个值都足够接近底部,那么效率会更高。
SELECT a.col_a, b.col_b, c.col_c
FROM (
SELECT last_a, last_b, last_c
FROM (
SELECT *,
MAX(version) FILTER (WHERE col_a IS NOT NULL) OVER w AS last_a,
MAX(version) FILTER (WHERE col_b IS NOT NULL) OVER w AS last_b,
MAX(version) FILTER (WHERE col_c IS NOT NULL) OVER w AS last_c
FROM aang
WINDOW w AS (ORDER BY version DESC)
) q
WHERE (last_a, last_b, last_c) IS NOT NULL
ORDER BY
version DESC
LIMIT 1
) q
JOIN aang a
ON a.version = q.last_a
JOIN aang b
ON b.version = q.last_b
JOIN aang c
ON c.version = q.last_c
Run Code Online (Sandbox Code Playgroud)
该查询从下往上扫描表,与上一个查询相同,但只扫描一次。
然后,它使用窗口函数来记住值列(a、b 和 c)中没有空值的记录MAX
的字段最大值。version
当所有三个最大值都不为空时,它会停止扫描,并返回包含 field 的三个值的单个记录version
,这三个值对应于保存相应列的最后一个非空值的版本。
最后,它会连接回表 3 次,检索每个版本的列值。
它只扫描一次,但索引会查找三次(以检索值)。如果非空值距离表底部相对较远,则扫描将需要更长的时间,因此仅执行一次扫描的成本超过了执行查找的额外成本。