PostgreSQL-изменено значение столбца-выберите оптимизация запросов

скажем, у нас есть стол:

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 ответов


это как я бы сделал это с аналитиком:

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). The COALESCE обрабатывает особый случай первой строки, которая не имеет предыдущее значение.

аналитические функции могут показаться немного странными сначала, но поиск по аналитическим функциям находит много примеров того, как они работают. Например: http://www.cs.utexas.edu/~cannata/dbms/Analytic%20Functions%20in%20Oracle%208i%20and%209i.htm просто помните, что это этап после обработки. Вы не сможете выполнять фильтрацию и т. д. На значение аналитической функции, если вы подзапрос его.


функция окна

вместо COALESCE, вы можете указать значение по умолчанию из функции окна lag() напрямую. Незначительная деталь в этом случае, так как все столбцы 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. Basic 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
                )
        );

UPDATE: Самосоединение нерекурсивного 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 как ( выбирать *, row_number () over (order by id) as rn1, row_number () over (раздел по порядку val по id) как rn2 от П ) выбирать мин(ИД) ИД, вал из 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

простое внутреннее соединение может сделать это. 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