Postgresql: запрос для суммирования разницы дат между статусом

У меня есть таблица для мониторинга некоторого оборудования с указанием даты и времени и состояния. Я хотел бы рассчитать «бегущие дни» между статусами «RUN» и «STOP». Пробую следующий запрос:

select run.stamp - 
(
    -- select the first STOP status after the current RUN status
      select stamp 
      from well_monitoring 
      where stamp > run.stamp and status = 'STOP'
      order by stamp limit 1
  )
from well_monitoring run
where
  run.status = 'RUN'
  and ( -- we want only the first RUN
      select status 
      from well_monitoring 
      where stamp < run.stamp 
    order by stamp desc limit 1) <> 'RUN'
order by run.stamp

См. SQLFiddle для создания таблицы/данных и проверки запроса.

Когда я пытаюсь СУММИТЬ, чтобы получить общее количество рабочих дней:

select SUM( run.stamp - ... ) ...

У меня есть следующая ошибка:

ОШИБКА: столбец "run.stamp" должен присутствовать в предложении GROUP BY или использоваться в агрегатной функции. Позиция: 448.

Итак: - как я могу обновить свой запрос, чтобы получить сумму? - запрос имеет 2 подзапроса, есть ли лучший способ (cte?) сделать это?

(версия Postgres: 9.1.7)


person Philippe Gonday    schedule 05.06.2015    source источник
comment
Какую версию postgres вы используете? Но пошел бы на CTE ...   -  person JiriS    schedule 05.06.2015
comment
В скрипке все well_id равны 1. Это сделано намеренно?   -  person joop    schedule 05.06.2015
comment
Как насчет чего-то вроде sqlfiddle.com/#!15/e3ff0/21 ( с использованием КТР)   -  person JiriS    schedule 05.06.2015
comment
Я думаю, что это, по сути, проблема пробелов и островов. Добавил тег.   -  person joop    schedule 05.06.2015
comment
@joop На самом деле в таблице больше полей, и для моего вопроса я должен был также удалить well_id...   -  person Philippe Gonday    schedule 05.06.2015


Ответы (3)


Вы должны обернуть свой запрос следующим образом:

select sum(times) as sum_of_times 
from (

  select run.stamp - 
    (
      -- select the first STOP status after the current RUN status
        select stamp 
        from well_monitoring 
        where stamp > run.stamp and status = 'STOP'
        order by stamp limit 1
    ) times
  from well_monitoring run
  where
    run.status = 'RUN'
    and ( -- we want only the first RUN
        select status 
        from well_monitoring 
        where stamp < run.stamp 
      order by stamp desc limit 1) <> 'RUN'
  order by run.stamp

) alias

как в этом sqlfiddle.

В этом sqlfiddle вы можете увидеть, как использовать CTE, если вы хотите иметь все раз и сумма раз в одном наборе результатов.

person klin    schedule 05.06.2015
comment
Этот ответ ужасен, даже результат неверный. Извините, мне пришлось понизить голос. Я редко делаю. - person wildplasser; 10.06.2015
comment
Я чувствую себя польщенным, но ужасно — неконструктивная оценка. Почему вы считаете, что результат неверный? - person klin; 10.06.2015
comment
Может потому, что результат оказывается отрицательным? (однако он отвечает на первоначальный вопрос: как это исправить...) - person wildplasser; 10.06.2015
comment
И стандартный SQL, и Postgres допускают отрицательные интервалы. Как вы думаете, положительные интервалы — это хорошо, а отрицательные — ужасно? Вопрос не касался этой темы. - person klin; 10.06.2015
comment
Нет ничего плохого в отрицательных интервалах. Хотя, ИМХО, ОП хочет измерить накопленное количество времени, проведенного во всех интервалах. Положительный. (или: в прямом направлении) - person wildplasser; 10.06.2015
comment
Я не вижу никаких подсказок, подтверждающих ваше мнение. Основной вопрос OP: как я могу обновить свой запрос, чтобы получить сумму? - person klin; 10.06.2015

Это, вероятно, можно немного упростить, но оно работает (обратите внимание, что ему нужны оконные функции, поэтому его можно адаптировать к любой реализации SQL)

SELECT one.id , one.stamp, one.status
        , two.id, two.stamp, two.status
        , (two.stamp - one.stamp) AS diff
FROM well_monitoring one
JOIN well_monitoring two ON two.well_id = one.well_id
        AND two.stamp > one.stamp
        AND two.status = 'STOP'
        -- find the first STOP:
        -- there should be on other STOP
        -- between one: RUN
        --     and two: STOP
        AND NOT EXISTS (
                SELECT * FROM well_monitoring x
                WHERE x.well_id = one.well_id
                AND x.stamp > one.stamp
                AND x.stamp < two.stamp
                AND x.status = 'STOP'
                )
WHERE one.status = 'RUN'
        -- If there are consecutive RUNs
        -- (without an intervening STOP)
        -- one should be the first RUN
AND NOT EXISTS (
        SELECT * FROM well_monitoring x
        WHERE x.well_id = one.well_id
        AND x.status = 'RUN'
        AND x.stamp < one.stamp
        AND NOT EXISTS (
                SELECT * FROM well_monitoring xx
                WHERE xx.well_id = x.well_id
                AND xx.stamp > x.stamp
                AND xx.stamp < one.stamp
                AND xx.status <> 'RUN'
                )
        )
        ;

Добавление агрегации оставляется читателю в качестве упражнения.

person joop    schedule 05.06.2015

Это примерно то же самое, что и ответ @joop, но с использованием оконных функций для обнаружения края. Обратите внимание, что rank+not exists() необходим для связывания событий остановки с ближайшими к ним событиями запуска.

WITH edges AS (
        SELECT id AS this_id
        , status AS this_status
        , well_id AS well_id
        , LAG(status) over ww AS prev_status
        , dense_rank() over ww AS rnk
        FROM well_monitoring
        WINDOW ww AS (partition by well_id ORDER BY stamp)
        )
, starters AS (
        SELECT this_id, well_id, rnk
        FROM edges
        WHERE this_status = 'RUN'
        AND COALESCE(prev_status, 'OMG') <> 'RUN'
        )
, stoppers AS (
        SELECT this_id, well_id, rnk
        FROM edges
        WHERE prev_status = 'RUN'
        AND this_status <> 'RUN'
        )
SELECT m0.well_id
        , SUM(m1.stamp - m0.stamp)::interval AS duration
FROM starters s0
JOIN stoppers s1 ON s1.well_id = s0.well_id
        AND s1.rnk > s0.rnk
        AND NOT EXISTS (
                SELECT * FROM stoppers nx
                WHERE nx.well_id = s0.well_id
                AND nx.rnk > s0.rnk AND nx.rnk < s1.rnk
                )
JOIN well_monitoring m0 ON m0.id = s0.this_id
JOIN well_monitoring m1 ON m1.id = s1.this_id
GROUP BY m0.well_id
        ;

Результат:

 well_id |     duration      
---------+-------------------
       1 | 320 days 64:28:00
(1 row)

(Я подозреваю, что 64 часа - это ошибка...)

person wildplasser    schedule 06.06.2015