Отметить несмежные диапазоны дат

Фон (ввод)

Глобальная сеть исторической климатологии отметила неверные или ошибочные данные в своем сборе данных о погоде. После удаления этих элементов остаются ряды данных, в которых больше нет смежных датированных разделов. Данные выглядят следующим образом:

"2007-12-01";14 -- Start of December
"2007-12-29";8
"2007-12-30";11
"2007-12-31";7
"2008-01-01";8 -- Start of January
"2008-01-02";12
"2008-01-29";0
"2008-01-31";7
"2008-02-01";4 -- Start of February
... entire month is complete ...
"2008-02-29";12
"2008-03-01";14  -- Start of March
"2008-03-02";17
"2008-03-05";17

Проблема (выход)

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

D;"2007-12-01";14 -- Start of December
D;"2007-12-29";8
D;"2007-12-30";11
D;"2007-12-31";7
D;"2008-01-01";8 -- Start of January
D;"2008-01-02";12
D;"2008-01-29";0
D;"2008-01-31";7
"2008-02-01";4 -- Start of February
... entire month is complete ...
"2008-02-29";12
D;"2008-03-01";14  -- Start of March
D;"2008-03-02";17
D;"2008-03-05";17

Некоторые измерения были сделаны в 1843 году.

Вопрос

Для всех метеостанций, как бы вы отметили все дни в месяцах, в которых отсутствует один или несколько дней?

Исходный код

Код для выбора данных выглядит примерно так:

select
  m.id,
  m.taken,
  m.station_id,
  m.amount
from
  climate.measurement

Связанные идеи

Создайте таблицу, заполненную смежными датами, и сравните их с датами измеренных данных.

Обновлять

Проблема может быть воссоздана с помощью SQL в этом разделе.

Таблица

Таблица создается следующим образом:

CREATE TABLE climate.calendar
(
  id serial NOT NULL,
  n character varying(2) NOT NULL,
  d date NOT NULL,
  "valid" boolean NOT NULL DEFAULT true,
  CONSTRAINT calendar_pk PRIMARY KEY (id)
)
WITH (
  OIDS=FALSE
);

Генерировать данные

Следующий SQL вставляет данные в таблицу (id [int], name [varchar], date [date], valid [boolean]):

insert into climate.calendar (n, d) 
    select 'A', (date('1982-01-1') + (n || ' days')::interval)::date cal_date
    from generate_series(0, date('2011-04-9') - date('1982-01-1') ) n
insert into climate.calendar (n, d) 
    select 'B', (date('1982-01-1') + (n || ' days')::interval)::date cal_date
    from generate_series(0, date('2011-04-9') - date('1982-01-1') ) n
insert into climate.calendar (n, d) 
    select 'C', (date('1982-01-1') + (n || ' days')::interval)::date cal_date
    from generate_series(0, date('2011-04-9') - date('1982-01-1') ) n
insert into climate.calendar (n, d) 
    select 'D', (date('1982-01-1') + (n || ' days')::interval)::date cal_date
    from generate_series(0, date('2011-04-9') - date('1982-01-1') ) n
insert into climate.calendar (n, d) 
    select 'E', (date('1982-01-1') + (n || ' days')::interval)::date cal_date
    from generate_series(0, date('2011-04-9') - date('1982-01-1') ) n
insert into climate.calendar (n, d) 
    select 'F', (date('1982-01-1') + (n || ' days')::interval)::date cal_date
    from generate_series(0, date('2011-04-9') - date('1982-01-1') ) n

Значения с 'A' по 'F' представляют названия метеостанций, которые проводили измерения в определенный день.

Удалить случайные строки

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

delete from climate.calendar where id in (select id from climate.calendar order by random() limit 5000);

Попытка №1

Следующее не переключает флаг valid на false для всех дней месяца, когда в месяце отсутствует один или несколько дней:

UPDATE climate.calendar
SET valid = false
WHERE date_trunc('month', d) IN (
    SELECT DISTINCT date_trunc('month', d)
    FROM climate.calendar A
    WHERE NOT EXISTS (
        SELECT 1
        FROM climate.calendar B
        WHERE A.d - 1 = B.d
   )
);

Попытка №2

Следующий SQL возвращает пустой набор результатов:

with gen_calendar as (
    select (date('1982-01-1') + (n || ' days')::interval)::date cal_date
    from generate_series(0, date('2011-04-9') - date('1982-01-1') ) n
)
select gc.cal_date
from gen_calendar gc
left join climate.calendar c on c.d = gc.cal_date
where c.d is null;

Попытка №3

Следующий SQL генерирует все возможные комбинации названий станций и дат:

select
  distinct( cc.n ), t.d
from
  climate.calendar cc,
  (
    select (date('1982-01-1') + (n || ' days')::interval)::date d
    from generate_series(0, date('2011-04-9') - date('1982-01-1') ) n
  ) t
order by
  cc.n

Однако в реальных данных есть несколько сотен станций, а даты восходят к середине 1800-х годов, поэтому декартова датировка всех станций слишком велика. Такой подход мог бы сработать, если бы достаточно времени ... Должен быть более быстрый способ.

Попытка №4

PostgreSQL имеет оконные функции.

Как выбрать определенные изменения с помощью оконных функций в postgres

Спасибо!


person Dave Jarvis    schedule 05.05.2011    source источник
comment
Это способ сказать, что вы хотите определить месяцы, в которых есть пробелы?   -  person Mike Sherrill 'Cat Recall'    schedule 06.05.2011
comment
@Catcall: Да. Это сложный способ сказать это. Учитывая количество дней в месяце, високосные годы и так далее.   -  person Dave Jarvis    schedule 06.05.2011
comment
+1 за правки, которые сделали мне проблему более понятной.   -  person Mike Sherrill 'Cat Recall'    schedule 08.05.2011


Ответы (3)


generate_series ()

Функция generate_series() PostgreSQL может создать представление, содержащее последовательный список дат:

with calendar as (
    select ((select min(date) from test)::date + (n || ' days')::interval)::date cal_date
    from generate_series(0, (select max(date) - min(date) from test)) n
)
select cal_date
from calendar c
left join test t on t.date = c.cal_date
where t.date is null;

Выражение select max(date) - min(date) from test могло отличаться на единицу.

Подсчет дней в месяц

Один из способов определить недопустимые месяцы - создать два представления. Первый подсчитывает количество ежедневных показаний, которые каждая станция должна производить за каждый месяц. (Обратите внимание, что climate.calendar переводится в climate_calendar.) Вторая возвращает фактические ежедневные показания каждой станции, произведенные за месяц.

Максимальное количество дней в месяц на станцию

Это представление вернет фактическое количество дней в месяце для каждой станции. (Например, в феврале всегда будет 28 или 29 дней.)

create view count_max_station_calendar_days as 
with calendar as (
    select ((select min(d) from climate_calendar)::date + (n || ' days')::interval)::date cal_date
    from generate_series(0, (select max(d) - min(d) from climate_calendar)) n
)
select n, extract(year from cal_date) yr, extract(month from cal_date) mo, count(*) num_days
from stations cross join calendar
group by n, yr, mo
order by n, yr, mo

Фактические дни в месяц на станцию

Общее количество возвращенных дней будет меньше, чем подсчетов. (Например, в январе всегда будет 31 день или меньше.)

create view count_actual_station_calendar_days as
select n, extract(year from d) yr, extract(month from d) mo, count(*) num_days
from climate_calendar
group by n, yr, mo
order by n, yr, mo;

Отбросьте предложения ORDER BY в рабочей среде (они полезны при разработке).

Сравнить просмотры

Присоединитесь к двум представлениям, чтобы определить станции и месяцы, которые необходимо отметить, в новом представлении:

create view invalid_station_months as 
select m.n, m.yr, m.mo, m.num_days - a.num_days num_days_missing
from count_max_station_calendar_days m
inner join count_actual_station_calendar_days a
       on (m.n = a.n and m.yr = a.yr and m.mo = a.mo and m.num_days <> a.num_days)

n   yr    mo  num_days_missing
--
A   1982  1   1
E   2007  3   1

Столбец num_days_missing не нужен, но он полезен.

Это строки, которые необходимо обновить:

select cc.* 
from climate_calendar cc
inner join invalid_station_months im 
        on (cc.n = im.n and 
            extract(year from cc.d) = im.yr and
            extract(month from cc.d) = im.mo)
where valid = true

Обновление базы данных

Для их обновления удобна клавиша id.

update climate_calendar
set valid = false
where id in (
    select id
    from climate_calendar cc
    inner join invalid_station_months im 
        on (cc.n = im.n and 
            extract(year from cc.d) = im.yr and
            extract(month from cc.d) = im.mo)
    where valid = true
);
person Mike Sherrill 'Cat Recall'    schedule 06.05.2011
comment
@ Дэйв Джарвис: Я многое добавил на основе правок к вашему вопросу. Он включает три полезных представления и рабочий оператор UPDATE. - person Mike Sherrill 'Cat Recall'; 08.05.2011
comment
Подсчет количества дней полезен. Еще раз спасибо. - person Dave Jarvis; 08.05.2011
comment
Запрос успешно возвращен: затронуто 498215579 строк, время выполнения 173600096 мс. ;-) Необходимо 96 ГБ временного места. - person Dave Jarvis; 13.05.2011
comment
@ Дэйв Джарвис: Тебе ведь все равно нужен перерыв, верно? Вы сделали это за одну транзакцию? - person Mike Sherrill 'Cat Recall'; 13.05.2011

Вот один из способов сделать это, если у вас есть поле BOOLEAN с именем is_contiguous. При необходимости измените:

UPDATE measurement
SET is_contiguous = FALSE
WHERE NOT EXISTS (
  SELECT 1
    FROM measurement B
   WHERE measurement.taken - 1 = B.taken
);

Редактировать:

Я считаю, что неправильно понял ваше требование. Я думал, вы хотите отметить отдельные даты, которые не являются смежными. Но, очевидно, вы хотите пометить даты за целый месяц как несмежные, если в нем отсутствует какое-либо количество дней.

Изменить 2:

Вот измененная версия моего исходного (неправильного) запроса, который выбирает отдельные месяцы, в которых отсутствуют какие-либо дни:

UPDATE measurement
SET is_contiguous = FALSE
WHERE date_trunc('month', taken) IN (
    SELECT DISTINCT date_trunc('month', taken)
    FROM measurement A
    WHERE NOT EXISTS (
        SELECT 1
        FROM measurement B
        WHERE A.taken - 1 = B.taken
   )
);
person Sam Choukri    schedule 06.05.2011

Предполагая, что не может быть более одной строки в день, это должно вернуть все месяцы, для которых количество строк не равно количеству дней в месяце.

SELECT station_id, DATE_TRUNC('month', d)
FROM climate.calendar
GROUP BY station_id, DATE_TRUNC('month', d)
HAVING COUNT(*) <> 
  DATE_PART('month',
            DATE_TRUNC('month', d) + INTERVAL '1 month' - INTERVAL '1 day')
person Andriy M    schedule 08.05.2011
comment
Спасибо. К сожалению, это возвращается каждый месяц для каждого года, потому что не учитывается распределение дат по отдельной станции. - person Dave Jarvis; 08.05.2011