2014-03-03 2 views
7

Я нашел много stackoverflow QnAs около последовательных дней.
Все ответы слишком короткие для меня, чтобы понять, что происходит.последовательных дней в sql

Для конкретности, я составить модель (или таблицу)
(я использую PostgreSQL, если это имеет значение.)

CREATE TABLE work (
    id integer NOT NULL, 
    user_id integer NOT NULL, 
    arrived_at timestamp with time zone NOT NULL 
); 


insert into work(user_id, arrived_at) values(1, '01/03/2011'); 
insert into work(user_id, arrived_at) values(1, '01/04/2011'); 
  1. (в простейшей форме) Для данный пользователь, я хочу найти последний ряд дат.

  2. (Моя конечная цель) Для данного пользователя я хочу найти его последовательные рабочие дни.
    Если он пришел на работу вчера, он все еще (на сегодняшний день) имеет шанс работать в течение нескольких дней подряд. Поэтому я показываю ему несколько дней подряд.
    Но если он пропустил вчера, его последовательные дни будут либо 0, либо 1 в зависимости от того, пришел он сегодня или нет.

Скажите сегодня, 8-й день.

3 * 5 6 7 * = 3 days (5 to 7) 
3 * 5 6 7 8 = 4 days (5 to 8) 
3 4 5 * 7 * = 1 day (7 to 7) 
3 * * * * * = 0 day 
3 * * * * 8 = 1 day (8 to 8) 
+1

Интересный вопрос ... пожалуйста, вы можете добавить схему таблицы? –

+2

Данные схемы и образца (как «CREATE TABLE» и «INSERT's») и ожидаемые результаты. –

+0

Пожалуйста, добавьте реальные данные образца DDL +. Никаких сокращенных обозначений, пожалуйста. – joop

ответ

2

Вот мое решение этой проблемы с помощью CTE

WITH RECURSIVE CTE(attendanceDate) 
AS 
(
    SELECT * FROM 
    (
     SELECT attendanceDate FROM attendance WHERE attendanceDate = current_date 
     OR attendanceDate = current_date - INTERVAL '1 day' 
     ORDER BY attendanceDate DESC 
     LIMIT 1 
    ) tab 
    UNION ALL 

    SELECT a.attendanceDate FROM attendance a 
    INNER JOIN CTE c 
    ON a.attendanceDate = c.attendanceDate - INTERVAL '1 day' 
) 
SELECT COUNT(*) FROM CTE; 

Проверьте код на SQL Fiddle

Вот как работает запрос:

  1. Он выбирает сегодняшнюю запись от attendance таблица.Если сегодняшняя запись не доступна, то он выбирает вчерашняя запись
  2. Затем он продолжает добавлять рекурсивно записи за день до наименьшей даты

Если вы хотите, чтобы выбрать последний раз подряд диапазон дат, независимо от того, когда было последнее посещение пользователя (сегодня , вчера или х дней до), то инициализация часть КТР должна быть заменена ниже фрагмент кода:

SELECT MAX(attendanceDate) FROM attendance 

[EDIT] Вот запрос на SQL Скрипке, который решает свой вопрос # 1: SQL Fiddle

+0

Можете ли вы дать мне оригинальную скрипку, которая, казалось, решила мой вопрос №1? (без сегодняшнего/вчерашнего рассмотрения), чтобы я мог сначала понять основы вашего запроса? – eugene

+0

http://www.sqlfiddle.com/#!15/7016f/1 –

+0

См. Изменение –

0
-- some data 
CREATE table dayworked (
     id SERIAL NOT NULL PRIMARY KEY 
     , user_id INTEGER NOT NULL 
     , arrived_at DATE NOT NULL 
     , UNIQUE (user_id, arrived_at) 
     ); 

INSERT INTO dayworked(user_id, arrived_at) VALUES 
(1, '2014-02-03') 
,(1, '2014-02-05') 
,(1, '2014-02-06') 
,(1, '2014-02-07') 
     -- 
,(2, '2014-02-03') 
,(2, '2014-02-05') 
,(2, '2014-02-06') 
,(2, '2014-02-07') 
,(2, '2014-02-08') 
     -- 
,(3, '2014-02-03') 
,(3, '2014-02-04') 
,(3, '2014-02-05') 
,(3, '2014-02-07') 
     -- 
,(5, '2014-02-08') 
     ; 

-- The query 
WITH RECURSIVE stretch AS (
     SELECT dw.user_id AS user_id 
       , dw.arrived_at AS first_day 
       , dw.arrived_at AS last_day 
       , 1::INTEGER AS nday 
     FROM dayworked dw 
     WHERE NOT EXISTS (-- Find start of chain: no previous day 
       SELECT * FROM dayworked nx 
       WHERE nx.user_id = dw.user_id 
       AND nx. arrived_at = dw.arrived_at -1 
       ) 
     UNION ALL 
     SELECT dw.user_id AS user_id 
       , st.first_day AS first_day 
       , dw.arrived_at AS last_day 
       , 1+st.nday AS nday 
     FROM dayworked dw -- connect to chain: previous day := day before this day 
     JOIN stretch st ON st.user_id = dw.user_id AND st.last_day = dw.arrived_at -1 
     ) 
SELECT * FROM stretch st 
WHERE (st.nday > 1 OR st.first_day = NOW()::date) -- either more than one consecutive dat or starting today 
AND NOT EXISTS (-- Only the most recent stretch 
     SELECT * FROM stretch nx 
     WHERE nx.user_id = st .user_id 
     AND nx.first_day > st.first_day 
     ) 
AND NOT EXISTS (-- omit partial chains 
     SELECT * FROM stretch nx 
     WHERE nx.user_id = st .user_id 
     AND nx.first_day = st.first_day 
     AND nx.last_day > st.last_day 
     ) 
     ; 

Результат:

CREATE TABLE 
INSERT 0 14 
user_id | first_day | last_day | nday 
---------+------------+------------+------ 
     1 | 2014-02-05 | 2014-02-07 | 3 
     2 | 2014-02-05 | 2014-02-08 | 4 
(2 rows) 
0

Вы можете создать агрегат с типами диапазона:

Create function sfunc (tstzrange, timestamptz) 
    returns tstzrange 
    language sql strict as $$ 
     select case when $2 - upper($1) <= '1 day'::interval 
       then tstzrange(lower($1), $2, '[]') 
       else tstzrange($2, $2, '[]') end 
    $$; 

Create aggregate consecutive (timestamptz) (
     sfunc = sfunc, 
     stype = tstzrange, 
     initcond = '[,]' 
); 

Используйте агрегат с правильным порядком ГЕТ последовательный выбор дня для последнего arrived_at:

Select user_id, consecutive(arrived_at order by arrived_at) 
    from work 
    group by user_id; 

    ┌─────────┬─────────────────────────────────────────────────────┐ 
    │ user_id │      consecutive      │ 
    ├─────────┼─────────────────────────────────────────────────────┤ 
    │  1 │ ["2011-01-03 00:00:00+02","2011-01-05 00:00:00+02"] │ 
    │  2 │ ["2011-01-06 00:00:00+02","2011-01-06 00:00:00+02"] │ 
    └─────────┴─────────────────────────────────────────────────────┘ 

Использование агрегата в функции окна :

Select *, 
     consecutive(arrived_at) 
       over (partition by user_id order by arrived_at) 
    from work; 

    ┌────┬─────────┬────────────────────────┬─────────────────────────────────────────────────────┐ 
    │ id │ user_id │  arrived_at  │      consecutive      │ 
    ├────┼─────────┼────────────────────────┼─────────────────────────────────────────────────────┤ 
    │ 1 │  1 │ 2011-01-03 00:00:00+02 │ ["2011-01-03 00:00:00+02","2011-01-03 00:00:00+02"] │ 
    │ 2 │  1 │ 2011-01-04 00:00:00+02 │ ["2011-01-03 00:00:00+02","2011-01-04 00:00:00+02"] │ 
    │ 3 │  1 │ 2011-01-05 00:00:00+02 │ ["2011-01-03 00:00:00+02","2011-01-05 00:00:00+02"] │ 
    │ 4 │  2 │ 2011-01-06 00:00:00+02 │ ["2011-01-06 00:00:00+02","2011-01-06 00:00:00+02"] │ 
    └────┴─────────┴────────────────────────┴─────────────────────────────────────────────────────┘ 

Запрос результаты, чтобы найти то, что вам нужно:

With work_detail as (select *, 
      consecutive(arrived_at) 
        over (partition by user_id order by arrived_at) 
     from work) 
    select arrived_at, upper(consecutive) - lower(consecutive) as days 
     from work_detail 
      where user_id = 1 and upper(consecutive) != lower(consecutive) 
      order by arrived_at desc 
       limit 1; 

    ┌────────────────────────┬────────┐ 
    │  arrived_at  │ days │ 
    ├────────────────────────┼────────┤ 
    │ 2011-01-05 00:00:00+02 │ 2 days │ 
    └────────────────────────┴────────┘ 
0

Вы даже можете сделать это без рекурсивных CTE:
с generate_series(), LEFT JOIN, row_count() и окончательный LIMIT 1:

1 для «сегодня» плюс дни подряд до «вчера»:

SELECT count(*) -- 1/0 for "today" 
    + COALESCE((-- + optional count of consecutive days up until "yesterday" 
     SELECT ct 
     FROM (
      SELECT d.ct, count(w.arrived_at) OVER (ORDER BY d.ct) AS day_ct 
      FROM generate_series(1, 8) AS d(ct) -- maximum = 8 
      LEFT JOIN work w ON w.arrived_at >= current_date - d.ct 
          AND w.arrived_at < current_date - (d.ct - 1) 
          AND w.user_id = 1 -- given user 
     ) sub 
     WHERE ct = day_ct 
     ORDER BY ct DESC 
     LIMIT 1 
     ), 0) AS total 
FROM work 
WHERE arrived_at >= current_date -- no future timestamps 
AND user_id = 1     -- given user 

Предполагая 0 или 1 запись в день. Быть быстрым.

Для лучшей производительности (для этого или решения КТР, так), вы бы иметь многоколоночном индекс, как:

CREATE INDEX foo_idx ON work (user_id,arrived_at); 
+0

будет ли это быстрее, чем решение CTE? – eugene

+0

@eugene: Наверное, да. Рассмотрим упрощенное обновление. Можете ли вы запустить «EXPLAIN ANALYZE» с любым вариантом в ваших данных? –

+0

У меня пока еще недостаточно большой набор данных. и это заставило меня довольно долгое время преобразовать ответ на мою фактическую схему. :( – eugene

Смежные вопросы