PostgreSQL - 列值已更改 - 选择查询优化

时间:2014-06-07 15:54:32

标签: sql postgresql window-functions gaps-and-islands

假设我们有一张桌子:

CREATE TABLE p
(
   id serial NOT NULL, 
   val boolean NOT NULL, 
   PRIMARY KEY (id)
);

填充了一些行:

insert into p (val)
values (true),(false),(false),(true),(true),(true),(false);
ID  VAL
1   1
2   0
3   0
4   1
5   1
6   1
7   0

我想确定何时更改了值。所以我的查询结果应该是:

ID  VAL
2   0
4   1
7   0

我有一个包含连接和子查询的解决方案:

select min(id) id, val from
(
  select p1.id, p1.val, max(p2.id) last_prev
  from p p1
  join p p2
    on p2.id < p1.id and p2.val != p1.val
  group by p1.id, p1.val
) tmp
group by val, last_prev
order by id;

但效率非常低,对于有很多行的表来说效果会非常慢 我相信使用PostgreSQL窗口函数可以提供更有效的解决方案吗?

SQL Fiddle

5 个答案:

答案 0 :(得分:4)

这就是我用分析方法做的事情:

SELECT id, val
  FROM ( SELECT id, val
           ,LAG(val) OVER (ORDER BY id) AS prev_val
       FROM p ) x
  WHERE val <> COALESCE(prev_val, val)
  ORDER BY id

更新(一些解释):

分析功能作为后处理步骤运行。查询结果分为分组(partition by),分析函数应用于分组的上下文中。

在这种情况下,查询是p的选择。正在应用的分析函数是LAG。由于没有partition by子句,因此只有一个分组:整个结果集。此分组按id排序。 LAG使用指定的顺序返回分组中上一行的值。结果是每行都有一个额外的列(别名prev_val),它是前一行的val。那是子查询。

然后我们查找val与前一行(prev_val)的val不匹配的行。 COALESCE处理第一行的特殊情况,该特殊情况没有先前的值。

分析函数起初可能看起来有点奇怪,但是对分析函数的搜索会发现很多例子都在讨论它们的工作原理。例如:http://www.cs.utexas.edu/~cannata/dbms/Analytic%20Functions%20in%20Oracle%208i%20and%209i.htm请记住,这是一个后处理步骤。除非您对其进行子查询,否则您将无法对分析函数的值执行过滤等。

答案 1 :(得分:4)

窗口功能

您可以直接从窗口函数lag()提供默认值,而不是调用COALESCE。这种情况下的一个小细节,因为所有列都已定义NOT NULL。但这可能是必要的,以区分&#34;没有前一行&#34;来自&#34;前一行中的NULL&#34;。

SELECT id, val
FROM  (
   SELECT id, val, lag(val, 1, val) OVER (ORDER BY id) <> val AS changed
   FROM   p
   ) sub
WHERE  changed
ORDER  BY id;

立即计算比较结果,因为前一个值本身并不重要,只有可能的变化。更短,可能会更快一点。

如果,您认为 第一行 是&#34;已更改&#34; (与您的演示输出建议不同),您需要观察NULL值 - 即使您的列已定义NOT NULL。如果没有上一行,则基本lag()会返回NULL

SELECT id, val
FROM  (
   SELECT id, val, lag(val) OVER (ORDER BY id) IS DISTINCT FROM val AS changed
   FROM   p
   ) sub
WHERE  changed
ORDER  BY id;

再次使用lag()的其他参数:

SELECT id, val
FROM  (
   SELECT id, val, lag(val, 1, NOT val) OVER (ORDER BY id) <> val AS changed
   FROM   p
   ) sub
WHERE  changed
ORDER  BY id;

递归CTE

作为概念的证明。 :) 绩效不会跟上发布的替代方案。

WITH RECURSIVE cte AS (
   SELECT id, val
   FROM   p
   WHERE  NOT EXISTS (
      SELECT 1
      FROM   p p0
      WHERE  p0.id < p.id
      )

   UNION ALL
   SELECT p.id, p.val
   FROM   cte
   JOIN   p ON p.id   > cte.id
           AND p.val <> cte.val
   WHERE NOT EXISTS (
     SELECT 1
     FROM   p p0
     WHERE  p0.id   > cte.id
     AND    p0.val <> cte.val
     AND    p0.id   < p.id
     )
  )
SELECT * FROM cte;

随着@wildplasser的改进。

SQL Fiddle展示所有。

答案 2 :(得分:2)

甚至可以在没有窗口功能的情况下完成。

SELECT * FROM p p0
WHERE EXISTS (
        SELECT * FROM p ex
        WHERE ex.id < p0.id
        AND ex.val <> p0.val
        AND NOT EXISTS (
                SELECT * FROM p nx
                WHERE nx.id < p0.id
                AND nx.id > ex.id
                )
        );

更新:自加入非递归CTE(也可以是子查询而不是CTE)

WITH drag AS (
        SELECT id
        , rank() OVER (ORDER BY id) AS rnk
        , val
        FROM p
        )
SELECT d1.*
FROM drag d1
JOIN drag d0 ON d0.rnk = d1.rnk -1
WHERE d1.val <> d0.val
        ;

这种非递归的CTE方法速度惊人,但需要隐式排序。

答案 3 :(得分:1)

使用2 row_number()次计算:这也可能与通常的&#34;岛和间隙&#34; SQL技术(如果由于某种原因你不能使用lag()窗口函数可能很有用:

with cte1 as (
    select
        *,
        row_number() over(order by id) as rn1,
        row_number() over(partition by val order by id) as rn2
    from p
)
select *, rn1 - rn2 as g
from cte1
order by id

因此,此查询将为您提供所有岛屿

ID VAL RN1 RN2  G
1   1   1   1   0
2   0   2   1   1
3   0   3   2   1
4   1   4   2   2
5   1   5   3   2
6   1   6   4   2
7   0   7   3   4

你看,G字段如何用于将这些岛屿组合在一起:

以cte1为(     选择         *         row_number()over(按id排序)为rn1,         row_number()over(由id by val order by partition)为rn2     来自p ) 选择     min(id)为id,     VAL 来自cte1 val,rn1 - rn2分组 按1排序

所以你得到

ID VAL
1   1
2   0
4   1
7   0

现在唯一的事情就是你必须删除第一条记录,这可以通过获取min(...) over()窗口函数来完成:

with cte1 as (
   ...
), cte2 as (
    select
        min(id) as id,
        val,
        min(min(id)) over() as mid
    from cte1
    group by val, rn1 - rn2
)
select id, val
from cte2
where id <> mid

结果:

ID VAL
2   0
4   1
7   0

答案 4 :(得分:0)

简单的内部联接可以做到这一点。 SQL Fiddle

select p2.id, p2.val
from
    p p1
    inner join
    p p2 on p2.id = p1.id + 1
where p2.val != p1.val