【问题标题】:PostgreSQL - column value changed - select query optimizationPostgreSQL - 列值更改 - 选择查询优化
【发布时间】:2014-07-28 17:39:15
【问题描述】:

假设我们有一张桌子:

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);
标识值 1 1 2 0 3 0 4 1 5 1 6 1 7 0

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

标识值 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

【问题讨论】:

  • 您是否会认为 第一行 中的值从以前的“未知”或“无”“更改”?

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


【解决方案1】:

这就是我使用分析的方式:

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 请记住,这是一个后处理步骤。除非您对它进行子查询,否则您将无法对分析函数的值执行过滤等操作。

【讨论】:

  • 为了方便对窗口函数不太熟悉的未来读者,您能否解释一下为什么这样做/它在做什么?
  • @Clockwork-Muse 当然,已经添加了一些解释。
  • 没有COALESCE 也能正常工作,还是我遗漏了什么? sqlfiddle.com/#!15/30044/8
  • @Nailgun: COALESCE 仅在您的列可以为 NULL 时才有用。在这种情况下,COALESCE 仅在第一行开始 - 它不会改变任何内容。 val &lt;&gt; valval &lt;&gt; NULL 都不会计算为 TRUE - 这是 WHERE 子句中唯一重要的结果。所以,你可以在这里删除COALESCE。我在回答中写了更多内容。
  • @Glenn:Postgres 从不使用术语“分析函数”来表示窗口函数——对于这类函数的作用来说,这是一个相当奇怪的术语,至少在我看来。您可能来自 Oracle 背景。
【解决方案2】:

窗口函数

您可以直接从窗口函数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 演示全部。

【讨论】:

  • 你错过了我的极简主义方法。 (或者我应该尝试递归 CTE 解决方案吗?-)
  • 也许我会尝试 rCTE 解决方案。我确实认为最简单(用数学术语......)的解决方案应该是首选。
  • @wildplasser:我之前专注于窗口函数。您的 SQL 艺术作品无法进一步改进 AFAICT (+1)。不过,可能会融化一些毫无戒心的用户的大脑。至于 rCTE ... 给你。 :)
  • FROM p WHERE id = 1 -->> FROM p p1 WHERE NOT EXISTS ( SELECT 1 FROM p px WHERE px.id &lt; p1.id)
  • @wildplasser:当然。应用它使它发光。
【解决方案3】:

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

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

【讨论】:

  • 如果我使用 MySql,这将是公认的答案:)
  • 我做到了。在我的真实案例中,它稍微复杂一些,并且在 ~30000 行上使用 PostGis 点和地理区域而不是布尔值,可接受的解决方案表现更好。我不是 PostgreSql 专家,但似乎公认的解决方案成本更低:sqlfiddle.com/#!15/962ac/5sqlfiddle.com/#!15/30044/6 无论如何,谢谢你的版本,我并不比我的好。
【解决方案4】:

使用 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 作为 ( 选择 *, row_number() over(order by id) as rn1, row_number() over(partition by val order by id) as rn2 从 p ) 选择 min(id) 作为 id, 值 从 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

【讨论】:

    【解决方案5】:

    一个简单的内部连接就可以做到。 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
    

    【讨论】:

    • 虽然解决方案是正确的,但现实生活中的 ID 不能一一对应。
    • @Nailgun 您的示例数据应反映您的环境条件。
    • 就我而言,我总是假设真实数据中缺少 ID 号,无论提供的样本数据如何。哎呀,@Nailgun,我还假设这些值甚至不代表实际的插入订单,更不用说与业务相关的订单了! ID 之所以真正有价值,是因为它们的链接以及它们在源表中应该是唯一的这一事实。任何其他用途都是试图在不存在的地方赋予意义。
    猜你喜欢
    • 2021-12-17
    • 2021-07-10
    • 2011-04-11
    • 1970-01-01
    • 2013-09-22
    • 2014-06-13
    • 2022-01-12
    • 1970-01-01
    相关资源
    最近更新 更多