繁体   English   中英

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

[英]PostgreSQL - column value changed - select query optimization

假设我们有一张桌子:

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小提琴

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

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不匹配val前一行(prev_val)的。 COALESCE处理第一行的特殊情况,它没有以前的值。

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

窗口功能

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

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;

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

如果您认为第一行被“更改”(与您的演示输出不同),您需要观察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演示了所有。

甚至可以不使用窗口功能。

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方法速度惊人,但需要隐式排序。

使用2 row_number()计算 :这也可以使用通常的“孤岛和间隙”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 as(select *,row_number()over(order by id)作为rn1,row_number()over(按val顺序划分id)作为rn2从p)选择min(id)作为id,val来自cte1 group by 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

一个简单的内连接就可以做到。 SQL小提琴

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

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM