获取包含每列最后一个非 NULL 值集的行

Aan*_*ang 6 postgresql

假设有一张表

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”是指当表按列版本排序时,每列中的最后一个非空值。

版本列始终仅包含非空值。

该表不会很大,因为只有几千行。所以,不太担心性能。

Vér*_*ace 6

您可以执行以下操作(下面的所有代码都可以在此处的小提琴上找到):

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

  1. Quassnoi 选项 1 ~ 0.060 ms
  2. 杰拉德·皮尔 ~ 0.176 毫秒
  3. Quassnoi 选项 2 ~ 0.183 ms
  4. JD ~ 2.906 毫秒

有 20k 行(其中 A、B、C 列中有 19k 行均为空):

  1. Quassnoi 选项 1 ~ 8 ms
  2. Quassnoi 选项 2 ~ 15 ms
  3. 杰拉德·皮尔 ~ 37 毫秒
  4. JD ~ 49 毫秒

在 等上添加 3 个部分索引后,具有与上面相同的 20k 行(version) WHERE (col_a IS NOT NULL)

  1. Quassnoi 选项 1 ~ 0.8 ms
  2. Quassnoi 选项 2 ~ 14.0 ms
  3. 杰拉德·皮尔 ~ 36.9 毫秒
  4. JD ~ 48.5 毫秒

在 等上添加 3 个部分索引后(version) INCLUDE (col_a) WHERE (col_a IS NOT NULL),具有与上面相同的 20k 行。Quassnoi-1 查询在这里杀死了它,大大优于所有其他查询:

  1. Quassnoi 选项 1 ~ 0.12 ms
  2. Quassnoi 选项 2 ~ 11.9 ms
  3. 杰拉德·皮尔 ~ 36.9 毫秒
  4. JD ~ 48.9 毫秒

最后一次测试有 20k 行,但有分散的非空值。这里 Gerars 和 Quassnoi-2 都相当快,不如 Quassnoi-1,但差别很小:

  1. Quassnoi 选项 1 ~ 0.050 ms
  2. Quassnoi 选项 2 ~ 0.112 ms
  3. 杰拉德·皮尔 ~ 0.168 毫秒
  4. JD ~ 49.1 毫秒

关于在无法控制或不了解的服务器上执行测试的常见警告适用!

随着比赛的激烈,我会不断修改!


J.D*_*.D. 6

我相信根据您的更新,您希望按列对行进行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 ,否则会将其放在排序末尾(这是语句的一部分)。这会导致最后一个值不是要返回的值(当使用该函数时,并且因为该子句是)。versionNULLIS NULL THEN 0CASEversionNULLFIRST_VALUE()ORDER BYDESC

注意:此解决方案假设您的版本从 1 开始(具体来说,永远不会有版本 = 0)。如果可能的话,请将 CASE 子句中的 0 值替换为永远不会使用的负数,例如 -1 或 -9999。


Qua*_*noi 5

JD提出的解决方案是可行的,但它必须对整个表进行三次排序,并且执行相当昂贵的操作DISTINCT

正如评论中提到的,有效地解决这个任务需要一个类似于FIRST_VALUE过滤器的窗口函数,不幸的是,PostgreSQL 不支持。

理论上,这可以通过在记录类型(如 )上使用聚合函数来模拟MAX(ROW(CASE WHEN col_a IS NOT NULL THEN VERSION END, col_a)),但由于某种原因,PostgreSQL 也不支持记录类型上的MAXand MIN,尽管它确实支持记录类型上的比较和排序。

如果列上有唯一索引version,则可以使用这些查询之一。

选项1

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索引是唯一的,因此这次扫描很可能会使用该索引。如果所有三列中的最后一个值都足够接近底部,那么效率会更高。

选项2

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 次,检索每个版本的列值。

它只扫描一次,但索引会查找三次(以检索值)。如果非空值距离表底部相对较远,则扫描将需要更长的时间,因此仅执行一次扫描的成本超过了执行查找的额外成本。