跳过特定条件和正确的 Lead() 用法的 SQL 差距

Posted

技术标签:

【中文标题】跳过特定条件和正确的 Lead() 用法的 SQL 差距【英文标题】:Jump SQL gap over specific condition & proper lead() usage 【发布时间】:2012-09-18 16:06:18 【问题描述】:

(PostgreSQL 8.4) 继续我的previous example,我希望进一步了解使用窗口函数处理间隙和孤岛。考虑下表和数据:

CREATE TABLE T1
(
  id SERIAL PRIMARY KEY,
  val INT,   -- some device
  status INT -- 0=OFF, 1=ON
);

INSERT INTO T1 (val, status) VALUES (10, 0);
INSERT INTO T1 (val, status) VALUES (11, 0);
INSERT INTO T1 (val, status) VALUES (11, 1);
INSERT INTO T1 (val, status) VALUES (10, 1);
INSERT INTO T1 (val, status) VALUES (11, 0);
INSERT INTO T1 (val, status) VALUES (10, 0);

如前所述,设备打开和关闭,这一次我希望提取一个特定的序列:

显示所有不重复的新ON 状态记录(同一设备连续两次) 显示来自当前ON 设备的适当OFF 状态

我能得到的最接近的是:

SELECT * FROM  (
  SELECT *
       ,lag(val, 1, 0) OVER (PARTITION BY status ORDER BY id) last_val
       ,lag(status, 1, -1) OVER (PARTITION BY val ORDER BY id) last_status
  FROM   t1
  ) x
WHERE  (last_val <> val OR last_status <> status)
AND    (status = 1 OR last_status <> -1)
ORDER BY id

这会过滤掉更多样本未包含的虚假数据,但本质上是关于取出后续重复项(无论状态如何)和不匹配的顶部 OFF 记录。返回记录3456,但我不想要第五个,它是一个OFF,它是在一个新的ON 之后出现的。所以我需要跳过这个差距,为当前活动的设备寻找下一个合适的OFF

    10 关闭——在这种情况下是假的,但会弄乱 lag() 11 关闭——在这种情况下是假的,但会弄乱 lag() 11 打开 -- 好的,新序列,包含在 SELECT 中 10 次开启 -- 好的,新序列,包含在 SELECT 中 11 关闭 -- 消息迟到,需要忽略间隙 10 关闭 -- 好的,正确关闭到第 4 行,需要包含在 SELECT 中

一旦正确过滤,我想在它上面使用lead() 来获取下一行的id(想象一个时间戳),并过滤掉所有不是ON 状态的记录。我想这将需要三个嵌入式 SELECT 语句。这将使我清楚地了解设备处于活动状态的时间,直到另一个ON 或正确转向OFF 的条件。

【问题讨论】:

【参考方案1】:

窗口函数查询

SELECT *
FROM  (
   SELECT *
         ,lag(val, 1, 0)    OVER (PARTITION BY status ORDER BY id) AS last_val
         ,lag(status, 1, 0) OVER w2 AS last_status
         ,lag(next_id)      OVER w2 AS next_id_of_last_status
   FROM  (
      SELECT *, lead(id) OVER (PARTITION BY status ORDER BY id) AS next_id
      FROM   t1
      ) AS t
   WINDOW w2 AS (PARTITION BY val ORDER BY id)
  ) x
WHERE (last_val <> val OR last_status <> status)
AND   (status = 1 
       OR last_status = 1
          AND ((next_id_of_last_status > id) OR next_id_of_last_status IS NULL)
      )
ORDER  BY id

除了what we already had,我们还需要有效的OFF开关。

如果设备在 (last_status = 1) 之前切换了 ON 并且之后的下一个 ON 操作出现在相关的 OFF 切换 (next_id_of_last_status &gt; id) 之后,则 OFF 切换有效。

我们必须提供最后一次ON 操作的特殊情况,因此我们另外检查NULL (OR next_id_of_last_status IS NULL)。

next_id_of_last_status 来自与我们获取last_status 相同的窗口。因此我为显式窗口声明引入了额外的语法,因此我不必重复自己:

WINDOW w2 AS (PARTITION BY val ORDER BY id)

并且我们需要获取之前子查询中最后一个状态的下一个 id(子查询 t)。

如果您已经理解了所有,那么在此查询之上添加lead() 以到达最终目的地应该没有问题。 :)

PL/pgSQL 函数

一旦变得如此复杂,就该切换到程序处理了。

这个相对简单的 plpgsql 函数会削弱复杂窗口函数查询的性能,原因很简单,它只需扫描整个表一次。

CREATE OR REPLACE FUNCTION valid_t1 (OUT t t1)  -- row variable of table type
  RETURNS SETOF t1 LANGUAGE plpgsql AS
$func$
DECLARE
   _last_on int := -1;  -- init with impossible value
BEGIN

FOR t IN
   SELECT * FROM t1 ORDER BY id
LOOP
   IF t.status = 1 THEN
      IF _last_on <> t.val THEN
         RETURN NEXT;
         _last_on := t.val;
      END IF;
   ELSE
      IF _last_on = t.val THEN
         RETURN NEXT;
         _last_on := -1;
      END IF;
   END IF;
END LOOP;

END
$func$;

呼叫:

SELECT * FROM valid_t1();

【讨论】:

这……太棒了。我应该把支票寄到哪里?说真的,还有一个额外的问题。为什么查询的 EXPLAIN ANALYZE 执行时间比实际执行时间长(3 倍)?作为参考,该表有大约 800 万条记录。 @denpanosekai:EXPLAIN ANALYZE 必须完成查询所做的所有事情以及一些额外的工作,因此它可能会更慢。不过,我认为这自 8.4 版以来有所改善。另外:当它最终完成时,结果如何?我希望该函数比查询快 3 到 4 倍 .. ? 其实都是13秒执行,但是函数返回381K记录,而Query返回327K记录。该死的,一定有一些额外的边缘条件我没有考虑到……我现在基本上是在分类垃圾。我认为这与同一时间戳上的重复 ON/OFF 记录有关(根据我的示例,实际上并未使用 ID 字段)。但是您的帮助非常宝贵,非常感谢您。 更重要的是,当测试干净的数据集(相同数量的记录,没有重复的时间戳)时,查询要快得多(能够在 20 毫秒内从 800 万条记录中提取 500 条记录,而 35 毫秒函数)据我所知,它们都没有使用索引,所以我不能说差异来自哪里。 @denpanosekai:这出乎意料。在 PostgreSQL 9.1.5 上的一个小数据集的快速测试中,该函数是迄今为止的赢家。它总是取决于许多因素。无论哪种方式都很酷,我同样爱我的两个孩子。 ;)

以上是关于跳过特定条件和正确的 Lead() 用法的 SQL 差距的主要内容,如果未能解决你的问题,请参考以下文章

MYSQL lag() 和lead()函数使用介绍

如果 2 个条件为真,如何正确跳过步骤

如何通过/跳过管道取决于特定条件

如何为不同的观察组使用 LAG 和 LEAD 窗口函数

ORACLE 偏移分析函数 lag()与lead() 用法

ORACLE 偏移分析函数 lag()与lead() 用法