2012-05-04 12 views
6

znajduje się tabelka z danymi wizyt:SQL problem - obliczyć max dni sekwencja

uid (INT) | created_at (DATETIME) 

chcę znaleźć, ile dni z rzędu użytkownik odwiedzał naszą aplikację. Tak na przykład:

SELECT DISTINCT DATE(created_at) AS d FROM visits WHERE uid = 123 

powróci:

 d  
------------ 
2012-04-28 
2012-04-29 
2012-04-30 
2012-05-03 
2012-05-04 

Istnieje 5 rekordy i dwa przedziały - 3 dni (28 - 30 APR) i 2 dni (3 - 4 maja).

Moje pytanie brzmi, jak znaleźć maksymalną liczbę dni, które użytkownik odwiedził aplikację z rzędu (3 dni w przykładzie). Próbowałem znaleźć odpowiednią funkcję w dokumentach SQL, ale bez powodzenia. Czy czegoś brakuje?


UPD: Dziękuję chłopaki za odpowiedzi! Właściwie pracuję z bazą danych vertica analytics (http://vertica.com/), jednak jest to bardzo rzadkie rozwiązanie i tylko kilka osób ma z tym doświadczenie. Mimo że obsługuje standard SQL-99.

Cóż, większość rozwiązań działa z niewielkimi modyfikacjami. W końcu stworzył własną wersję zapytania:

-- returns starts of the vitit series 
SELECT t1.d as s FROM testing t1 
LEFT JOIN testing t2 ON DATE(t2.d) = DATE(TIMESTAMPADD('day', -1, t1.d)) 
WHERE t2.d is null GROUP BY t1.d 

      s   
--------------------- 
2012-04-28 01:00:00 
2012-05-03 01:00:00 

-- returns end of the vitit series 
SELECT t1.d as f FROM testing t1 
LEFT JOIN testing t2 ON DATE(t2.d) = DATE(TIMESTAMPADD('day', 1, t1.d)) 
WHERE t2.d is null GROUP BY t1.d 

      f   
--------------------- 
2012-04-30 01:00:00 
2012-05-04 01:00:00 

Więc teraz tylko to, co musimy zrobić, to połączyć je w jakiś sposób, na przykład poprzez indeks wiersza.

SELECT s, f, DATEDIFF(day, s, f) + 1 as seq FROM (
    SELECT t1.d as s, ROW_NUMBER() OVER() as o1 FROM testing t1 
    LEFT JOIN testing t2 ON DATE(t2.d) = DATE(TIMESTAMPADD('day', -1, t1.d)) 
    WHERE t2.d is null GROUP BY t1.d 
) tbl1 LEFT JOIN (
    SELECT t1.d as f, ROW_NUMBER() OVER() as o2 FROM testing t1 
    LEFT JOIN testing t2 ON DATE(t2.d) = DATE(TIMESTAMPADD('day', 1, t1.d)) 
    WHERE t2.d is null GROUP BY t1.d 
) tbl2 ON o1 = o2 

Przykładowe wyjście:

  s   |   f   | seq 
---------------------+---------------------+----- 
2012-04-28 01:00:00 | 2012-04-30 01:00:00 | 3 
2012-05-03 01:00:00 | 2012-05-04 01:00:00 | 2 
+1

Tagged z dwoma różnymi implikacjami sql? Zarówno 'MySQL' jak i' PostGreSQL' mają różne zdolności ... – MatBailie

+1

+1 Dobre podejście. Na 'WHERE t2.d ma wartość NULL GROUP BY t1.d', możesz jednak usunąć' GROUP BY t1.d' jednak :-) Twoje "WHERE t2.d ma wartość null" już zwraca unikalne 't1.d' tak czy inaczej –

+0

Dziękuję za poradę, Michael! – deadrunk

Odpowiedz

7

Another podejście, najkrótsza, zrobić samosprzężenie:

with grouped_result as 
(
    select 
     sr.d, 
     sum((fr.d is null)::int) over(order by sr.d) as group_number 
    from tbl sr 
    left join tbl fr on sr.d = fr.d + interval '1 day' 
) 
select d, group_number, count(d) over m as consecutive_days 
from grouped_result 
window m as (partition by group_number) 

wyjściowa:

  d   | group_number | consecutive_days 
---------------------+--------------+------------------ 
2012-04-28 08:00:00 |   1 |    3 
2012-04-29 08:00:00 |   1 |    3 
2012-04-30 08:00:00 |   1 |    3 
2012-05-03 08:00:00 |   2 |    2 
2012-05-04 08:00:00 |   2 |    2 
(5 rows) 

żywo Test: http://www.sqlfiddle.com/#!1/93789/1

sr = drugi rząd, fr = pierwszy wiersz (lub być może poprzedni rząd? ). Zasadniczo robimy śledzenie wstecz, jest to symulowane opóźnienie w bazie danych, które nie obsługuje LAG (Postgres obsługuje LAG, ale rozwiązaniem jest very long, ponieważ okno nie obsługuje zagnieżdżonych okien). Więc w tym zapytaniu, my wykorzystuje podejście hybrydowe, symulować LGD poprzez dołączyć, a następnie użyć SUM okienkowania przeciwko niej, to produkuje numer grupy

UPDATE

Zapomniałem postawić ostateczną zapytanie, zapytanie powyżej ilustrują podbudow z numeracją grupowej trzeba morph, że w ten sposób:

with grouped_result as 
(
    select 
     sr.d, 
     sum((fr.d is null)::int) over(order by sr.d) as group_number 
    from tbl sr 
    left join tbl fr on sr.d = fr.d + interval '1 day' 
) 
select min(d) as starting_date, max(d) as end_date, count(d) as consecutive_days 
from grouped_result 
group by group_number 
-- order by consecutive_days desc limit 1 


STARTING_DATE    END_DATE      CONSECUTIVE_DAYS 
April, 28 2012 08:00:00-0700 April, 30 2012 08:00:00-0700 3 
May, 03 2012 08:00:00-0700 May, 04 2012 08:00:00-0700 2 

UPDATE

wiem dlaczego mój other solution, który używa funkcji okna stał się długi, długo trwała moja próba zilustrowania logiki numerowania grup i liczenia w grupie. Gdybym przerwał ściganie, tak jak w moim MySql approach, funkcja okienkowania mogłaby być krótsza.Mimo, że tu jest mój stary podejście funkcja okienkowy, aczkolwiek lepiej teraz:

with headers as 
(
    select 
     d,lag(d) over m is null or d - lag(d) over m <> interval '1 day' as header 
    from tbl 
    window m as (order by d) 
)  
,sequence_group as 
(
    select d, sum(header::int) over (order by d) as group_number 
    from headers 
) 
select min(d) as starting_date,max(d) as ending_date,count(d) as consecutive_days 
from sequence_group 
group by group_number 
-- order by consecutive_days desc limit 1 

żywo Test: http://www.sqlfiddle.com/#!1/93789/21

+0

Dziękuję, Michael! Uratowałeś mnie! :) – deadrunk

+0

+1 za bardzo eleganckie rozwiązanie i nową buźkę ヅ – vyegorov

+0

+1: Bardzo ładnie. Nie jestem pewien, czy miałbym chociaż na temat bieżącej całkowitej odpowiedzi (aby wygenerować identyfikator grupy). Och, jak miło byłoby mieć klienta z tak nowoczesną implementacją sql. Byłby zainteresowany porównaniem tego z mechanizmem grupowania opartym na ROW_NUMBER() (który unika łączenia). – MatBailie

2

W MySQL można to zrobić:

SET @nextDate = CURRENT_DATE; 
SET @RowNum = 1; 

SELECT MAX(RowNumber) AS ConecutiveVisits 
FROM ( SELECT @RowNum := IF(@NextDate = Created_At, @RowNum + 1, 1) AS RowNumber, 
        Created_At, 
        @NextDate := DATE_ADD(Created_At, INTERVAL 1 DAY) AS NextDate 
      FROM Visits 
      ORDER BY Created_At 
     ) Visits 

Przykład tutaj:

http://sqlfiddle.com/#!2/6e035/8

Jednak nie jestem 100% cert w tym jest najlepszy sposób na zrobienie tego.

w PostgreSQL:

;WITH RECURSIVE VisitsCTE AS 
( SELECT Created_At, 1 AS ConsecutiveDays 
    FROM Visits 
    UNION ALL 
    SELECT v.Created_At, ConsecutiveDays + 1 
    FROM Visits v 
      INNER JOIN VisitsCTE cte 
       ON 1 + cte.Created_At = v.Created_At 
) 
SELECT MAX(ConsecutiveDays) AS ConsecutiveDays 
FROM VisitsCTE 

Przykład tutaj:

http://sqlfiddle.com/#!1/16c90/9

+0

Zamiast DATE_ADD() możesz użyć tylko prostego + –

2

wiem Postgresql ma coś podobnego do typowych wyrażeń tabeli jako dostępny w MSSQL. Nie jestem zaznajomiony z Postgresql, ale poniższy kod działa dla MSSQL i robi to, co chcesz.

create table #tempdates (
    mydate date 
) 

insert into #tempdates(mydate) values('2012-04-28') 
insert into #tempdates(mydate) values('2012-04-29') 
insert into #tempdates(mydate) values('2012-04-30') 
insert into #tempdates(mydate) values('2012-05-03') 
insert into #tempdates(mydate) values('2012-05-04'); 

with maxdays (s, e, c) 
as 
(
    select mydate, mydate, 1 
    from #tempdates 
    union all 
    select m.s, mydate, m.c + 1 
    from #tempdates t 
    inner join maxdays m on DATEADD(day, -1, t.mydate)=m.e 
) 
select MIN(o.s),o.e,max(o.c) 
from (
    select m1.s,max(m1.e) e,max(m1.c) c 
    from maxdays m1 
    group by m1.s 
) o 
group by o.e 

drop table #tempdates 

A oto skrzypce SQL: http://sqlfiddle.com/#!3/42b38/2

1

Poniższa Oracle powinien być przyjazny, i nie wymagają rekurencyjnego logiki.

;WITH 
    visit_dates (
    visit_id, 
    date_id, 
    group_id 
) 
AS 
(
    SELECT 
    ROW_NUMBER() OVER (ORDER BY TRUNC(created_at)), 
    TRUNC(SYSDATE) - TRUNC(created_at), 
    TRUNC(SYSDATE) - TRUNC(created_at) - ROW_NUMBER() OVER (ORDER BY TRUNC(created_at)) 
    FROM 
    visits 
    GROUP BY 
    TRUNC(created_at) 
) 
, 
    group_duration (
    group_id, 
    duration 
) 
AS 
(
    SELECT 
    group_id, 
    MAX(date_id) - MIN(date_id) + 1 AS duration 
    FROM 
    visit_dates 
    GROUP BY 
    group_id 
) 
SELECT 
    MAX(duration) AS max_duration 
FROM 
    group_duration 
1

Postgresql:

with headers as 
(
    select 
     d, 
     lag(d) over m is null or d - lag(d) over m <> interval '1 day' as header 

    from tbl 
    window m as (order by d) 
)  
,sequence_group as 
(
    select d, sum(header::int) over m as group_number 
    from headers 
    window m as (order by d) 
) 
,consecutive_list as 
(
    select d, group_number, count(d) over m as consecutive_count 
    from sequence_group 
    window m as (partition by group_number) 
) 
select * from consecutive_list 

podejście dziel i zwyciężaj: 3 kroki

1st krok, znajdź nagłówki:

with headers as 
(
    select 
     d, 
     lag(d) over m is null or d - lag(d) over m <> interval '1 day' as header 

    from tbl 
    window m as (order by d) 
) 
select * from headers 

wyjściowa:

  d   | header 
---------------------+-------- 
2012-04-28 08:00:00 | t 
2012-04-29 08:00:00 | f 
2012-04-30 08:00:00 | f 
2012-05-03 08:00:00 | t 
2012-05-04 08:00:00 | f 
(5 rows) 

2. krok, desygnowany grupowanie:

with headers as 
(
    select 
     d, 
     lag(d) over m is null or d - lag(d) over m <> interval '1 day' as header 

    from tbl 
    window m as (order by d) 
)  
,sequence_group as 
(
    select d, sum(header::int) over m as group_number 
    from headers 
    window m as (order by d) 
) 
select * from sequence_group 

wyjścia:

  d   | group_number 
---------------------+-------------- 
2012-04-28 08:00:00 |   1 
2012-04-29 08:00:00 |   1 
2012-04-30 08:00:00 |   1 
2012-05-03 08:00:00 |   2 
2012-05-04 08:00:00 |   2 
(5 rows) 

3. krok, liczyć max dni:

with headers as 
(
    select 
     d, 
     lag(d) over m is null or d - lag(d) over m <> interval '1 day' as header 

    from tbl 
    window m as (order by d) 
)  
,sequence_group as 
(
    select d, sum(header::int) over m as group_number 
    from headers 
    window m as (order by d) 
) 
,consecutive_list as 
(
select d, group_number, count(d) over m as consecutive_count 
from sequence_group 
window m as (partition by group_number) 
) 
select * from consecutive_list 

wyjściowy:

  d   | group_number | consecutive_count 
---------------------+--------------+----------------- 
2012-04-28 08:00:00 |   1 |    3 
2012-04-29 08:00:00 |   1 |    3 
2012-04-30 08:00:00 |   1 |    3 
2012-05-03 08:00:00 |   2 |    2 
2012-05-04 08:00:00 |   2 |    2 
(5 rows) 
1

To dla MySQL, najkrótsza i wykorzystuje (tylko jedną zmienną) zmienną minimalna:

select 
    min(d) as starting_date, max(d) as ending_date, 
    count(d) as consecutive_days 
from 
(
    select 
    sr.d, 
    IF(fr.d is null,@group_number := @group_number + 1,@group_number) 
     as group_number 
    from tbl sr 
    left join tbl fr on sr.d = adddate(fr.d,interval 1 day) 
    cross join (select @group_number := 0) as grp 
) as x 
group by group_number 

wyjściowa:

STARTING_DATE     ENDING_DATE     CONSECUTIVE_DAYS 
April, 28 2012 08:00:00-0700 April, 30 2012 08:00:00-0700 3 
May, 03 2012 08:00:00-0700  May, 04 2012 08:00:00-0700 2 

żywo Test: http://www.sqlfiddle.com/#!2/65169/1

1

Dla PostgreSQL 8.4 lub nowszy, jest krótki i czysty sposób z funkcji okna i bez JOIN.
Spodziewam to być najszybszym rozwiązaniem pisał do tej pory:

WITH x AS (
    SELECT created_at AS d 
     , lag(created_at) OVER (ORDER BY created_at) = (created_at - 1) AS nu 
    FROM visits 
    WHERE uid = 1 
    ) 
    , y AS (
    SELECT d, count(NULLIF(nu, TRUE)) OVER (ORDER BY d) AS seq 
    FROM x 
    ) 
SELECT count(*) AS max_days, min(d) AS seq_from, max(d) AS seq_to 
FROM y 
GROUP BY seq 
ORDER BY 1 DESC 
LIMIT 1; 

Powroty:

max_days | seq_from | seq_to 
---------+------------+----------- 
3  | 2012-04-28 | 2012-04-30 

Zakładając, że created_at jest date i unique.

  1. W CTE x: każdego dnia odwiedza nas użytkownik, sprawdź, czy był tu wczoraj. Aby obliczyć "wczoraj", po prostu użyj created_at - 1 Pierwszy wiersz jest przypadkiem specjalnym i będzie tutaj generował NULL.

  2. W CTE y: obliczyć liczbę bieżącą "dni bez wczoraj do tej pory" (seq) na każdy dzień. Wartości NULL nie są liczone, więc count(NULLIF(nu, TRUE)) jest najszybszą i najkrótszą drogą, obejmującą również specjalny przypadek.

  3. Wreszcie, dni grupowe na seq i policz dni. Będąc przy tym dodałem pierwszy i ostatni dzień sekwencji. ORDER BY długość sekwencji i wybierz najdłuższą.

1

Było już kilka odpowiedzi na to pytanie. Jednak wszystkie instrukcje SQL wydają się zbyt skomplikowane. Można to osiągnąć za pomocą podstawowego SQL, sposobu na wyliczenie rzędów i pewnej arytmetyki dat.

Kluczową obserwacją jest to, że jeśli masz kilka dni i mają równoległą sekwencję liczb całkowitych, różnica jest stała, gdy dni są w sekwencji.

Poniższe zapytanie wykorzystuje tę obserwację odpowiedzieć oryginalne pytanie:

select uid, min(d) as startdate, count(*) as numdaysinseq 
from 
(
    select uid, d, adddate(d, interval -offset day) as groupstart 
    from 
    (
    select uid, d, row_number() over (partition by uid order by date) as offset 
    from 
    (
     SELECT DISTINCT uid, DATE(created_at) AS d 
     FROM visits 
    ) t 
    ) t 
) t 

Niestety, mysql nie posiada funkcję row_number(). Istnieje jednak obejście ze zmiennymi (a większość innych baz danych ma tę funkcję).

+0

'-offset' trick nie działa na vertice :( – deadrunk

1

Ujrzawszy podejście zapytania OP na ich bazie Vertica starałem dokonywania dwóch sprzężeń uruchomić w tym samym czasie:

Te wersje PostgreSQL oraz SQL Server Query powinien zarówno praca w Vertica

wersji PostgreSQL:

select 
    min(gr.d) as start_date, 
    max(gr.d) as end_date, 
    date_part('day', max(gr.d) - min(gr.d))+1 as consecutive_days 
from 
(
    select 
    cr.d, (row_number() over() - 1)/2 as pair_number 
    from tbl cr 
    left join tbl pr on pr.d = cr.d - interval '1 day' 
    left join tbl nr on nr.d = cr.d + interval '1 day' 
    where pr.d is null <> nr.d is null 
) as gr 
group by pair_number 
order by start_date 

Jeśli chodzi o pr.d is null <> nr.d is null. Oznacza to, że albo poprzedni wiersz ma wartość NULL, albo następny wiersz ma wartość NULL, ale nigdy nie może mieć wartości NULL, więc w zasadzie usuwa ona niesekwencyjne daty, ponieważ kolejny nietypowy dat "& następny wiersz jest zerowy (i to w zasadzie podaje nam wszystkie daty, które są tylko nagłówkami i stopkami). Nazywa się to również XOR operation

Jeśli jesteśmy już tylko kolejnymi datami, możemy teraz powiązać je poprzez ROW_NUMBER:

(row_number() over() - 1)/2 as pair_number 

row_number() rozpoczyna się 1, musimy odjąć go 1 (możemy również zamiast tego dodaj 1), a następnie podzielimy go przez dwa; to sprawia, że ​​sparowane datę obok siebie

żywo testu: http://www.sqlfiddle.com/#!1/fc440/7


To jest wersja SQL Server:

select 
    min(gr.d) as start_date, 
    max(gr.d) as end_date, 
    datediff(day, min(gr.d),max(gr.d)) +1 as consecutive_days 
from 
(
    select 
    cr.d, (row_number() over(order by cr.d) - 1)/2 as pair_number 
    from tbl cr 
    left join tbl pr on pr.d = dateadd(day,-1,cr.d) 
    left join tbl nr on nr.d = dateadd(day,+1,cr.d) 
    where   
     case when pr.d is null then 1 else 0 end 
    <> case when nr.d is null then 1 else 0 end 
) as gr 
group by pair_number 
order by start_date 

sama logika jak wyżej, z wyjątkiem sztucznych różnic dotyczących funkcji daty . Serwer sql wymaga klauzuli ORDER BY na swoim OVER, natomiast PostgreSQL OVER może pozostać pusty.

SQL Server ma pierwszorzędną wartość logiczną, to dlaczego nie możemy porównać wartości logicznych bezpośrednio:

pr.d is null <> nr.d is null 

Musimy to zrobić w SQL Server:

case when pr.d is null then 1 else 0 end 
<> case when nr.d is null then 1 else 0 end 

żywo Test: http://www.sqlfiddle.com/#!3/65df2/17

2

Wszystkie są bardzo dobrymi odpowiedziami, ale myślę, że powinienem przyczynić się, pokazując inne podejście wykorzystujące zdolność analityczną specyficzną dla Vertica (w końcu jest częścią tego, co yo zapłaciłeś za). I obiecuję, że ostateczne zapytanie jest krótkie.

Najpierw należy użyć warunku conditional_true_event(). Z dokumentacji Vertica za:

Przypisuje numer okna wydarzenie do każdego wiersza, zaczynając od 0, a zwiększa liczbę o 1, gdy wynik logicznego argumentu wyrażenie prawdziwe.

Przykład kwerendy wygląda następująco:

select uid, created_at, 
     conditional_true_event(created_at - lag(created_at) > '1 day') 
     over (partition by uid order by created_at) as seq_id 
from visits; 

i wyjście:

uid created_at   seq_id 
--- ------------------- ------ 
123 2012-04-28 00:00:00 0  
123 2012-04-29 00:00:00 0  
123 2012-04-30 00:00:00 0  
123 2012-05-03 00:00:00 1  
123 2012-05-04 00:00:00 1  
123 2012-06-04 00:00:00 2  
123 2012-06-04 00:00:00 2  

Teraz ostateczna zapytania staje się łatwe:

select uid, seq_id, count(1) num_days, min(created_at) s, max(created_at) f 
from 
(
    select uid, created_at, 
     conditional_true_event(created_at - lag(created_at) > '1 day') 
     over (partition by uid order by created_at) as seq_id 
    from visits 
) as seq 
group by uid, seq_id; 

Wyjście końcowa:

uid seq_id num_days s     f      
--- ------ -------- ------------------- ------------------- 
123 0  3   2012-04-28 00:00:00 2012-04-30 00:00:00 
123 1  2   2012-05-03 00:00:00 2012-05-04 00:00:00 
123 2  2   2012-06-04 00:00:00 2012-06-04 00:00:00 

Ostatnia uwaga: num_days to faktycznie liczba wierszy zapytania wewnętrznego. Jeśli istnieją dwie wizyty w oryginalnej tabeli (tj. Duplikaty), możesz to obejść.