Poniższa tabela historii użytkownika zawiera jeden rekord na każdy dzień, w którym dany użytkownik odwiedził witrynę (w okresie 24 godzin UTC). Ma wiele tysięcy rekordów, ale tylko jeden rekord dziennie na użytkownika. Jeśli użytkownik nie uzyskał dostępu do witryny w tym dniu, żaden rekord nie zostanie wygenerowany.
Id UserId CreationDate ------ ------ ------------ 750997 12 2009-07-07 18: 42: 20.723 750998 15 2009-07-07 18: 42: 20.927 751000 19 2009-07-07 18: 42: 22.283
To, czego szukam, to zapytanie SQL w tej tabeli z dobrą wydajnością , które mówi mi, które identyfikatory użytkowników miały dostęp do witryny przez (n) ciągłych dni, nie tracąc ani jednego dnia.
Innymi słowy, ilu użytkowników ma (n) rekordów w tej tabeli z datami sekwencyjnymi (dzień przed lub dzień po) ? Jeśli w sekwencji brakuje któregokolwiek dnia, sekwencja jest przerywana i powinna zostać wznowiona od 1; szukamy użytkowników, którzy osiągnęli tutaj ciągłą liczbę dni bez przerw.
Każde podobieństwo między tym zapytaniem a konkretną odznaką przepełnienia stosu jest oczywiście czysto przypadkowe. :)
źródło
Odpowiedzi:
Odpowiedź brzmi oczywiście:
EDYTOWAĆ:
OK, oto moja poważna odpowiedź:
EDYTOWAĆ:
[Jeff Atwood] To świetne, szybkie rozwiązanie i zasługuje na akceptację, ale rozwiązanie Roba Farleya jest również doskonałe i prawdopodobnie nawet szybsze (!). Sprawdź też!
źródło
ON uh2.CreationDate >= uh1.CreationDate AND uh2.CreationDate < DATEADD(dd, DATEDIFF(dd, 0, uh1.CreationDate) + @days, 0)
na:, aby oznaczało „Jeszcze nie 31 dnia później”. Oznacza również, że możesz pominąć obliczenia @seconds.A co z (i upewnij się, że poprzednie stwierdzenie kończyło się średnikiem):
Chodzi o to, że jeśli mamy listę dni (jako liczbę) i wiersz_numer, to pominięte dni sprawiają, że przesunięcie między tymi dwiema listami jest nieco większe. Szukamy więc zakresu o stałym przesunięciu.
Na końcu tego tekstu możesz użyć „ORDER BY NumConsecutiveDays DESC” lub powiedzieć „HAVING count (*)> 14” jako progu ...
Nie testowałem tego jednak - po prostu zapisałem to z czubka głowy. Mam nadzieję, że działa w SQL2005 i nowszych.
... i bardzo by pomógł indeks w nazwie tabeli (UserID, CreationDate)
Edytowano: Okazuje się, że Offset jest słowem zastrzeżonym, więc zamiast tego użyłem TheOffset.
Edytowano: Sugestia użycia COUNT (*) jest bardzo ważna - powinienem był to zrobić w pierwszej kolejności, ale tak naprawdę nie myślałem. Wcześniej zamiast tego korzystał z metody datediff (day, min (CreationDate), max (CreationDate)).
Obrabować
źródło
Jeśli możesz zmienić schemat tabeli, sugeruję dodanie kolumny
LongestStreak
do tabeli, w której ustawisz liczbę kolejnych dni kończących się naCreationDate
. Aktualizacja tabeli podczas logowania jest łatwa (podobnie jak to, co już robisz, jeśli w bieżącym dniu nie ma żadnych wierszy, sprawdzisz, czy istnieje żaden wiersz z poprzedniego dnia. Jeśli prawda, zwiększysz wartośćLongestStreak
w nowy wiersz, w przeciwnym razie ustawisz go na 1.)Zapytanie będzie oczywiste po dodaniu tej kolumny:
źródło
Kilka ładnie wyrazistych SQL na wzór:
Zakładając, że masz zdefiniowaną przez użytkownika funkcję agregującą, coś w rodzaju (uwaga, to jest błędne):
źródło
Wydaje się, że możesz skorzystać z faktu, że ciągłość przez n dni wymagałaby n wierszy.
Więc coś takiego:
źródło
Zrobienie tego za pomocą pojedynczego zapytania SQL wydaje mi się zbyt skomplikowane. Pozwólcie, że podzielę tę odpowiedź na dwie części.
Uruchom codzienne zadanie crona, które sprawdza każdego użytkownika, czy był dzisiaj zalogowany, a następnie zwiększa licznik, jeśli ma, lub ustawia go na 0, jeśli nie.
- Wyeksportuj tę tabelę na serwer, na którym nie działa Twoja witryna i przez jakiś czas nie będzie potrzebna. ;)
- Sortuj według użytkownika, a następnie daty.
- przejdź przez to sekwencyjnie, zachowaj licznik ...
źródło
Jeśli jest to dla Ciebie tak ważne, znajdź to wydarzenie i przygotuj tabelę, aby przekazać Ci te informacje. Nie ma potrzeby zabijania maszyny tymi wszystkimi szalonymi zapytaniami.
źródło
Możesz użyć rekurencyjnego CTE (SQL Server 2005+):
źródło
Joe Celko ma pełny rozdział na ten temat w SQL for Smarties (nazywając go Runs and Sequences). Nie mam tej książki w domu, więc kiedy dotrę do pracy ... Odpowiem na to. (zakładając, że tabela historii nazywa się dbo.UserHistory, a liczba dni to @Days)
Kolejny trop pochodzi z bloga SQL Team o uruchomieniach
Innym pomysłem, który miałem, ale nie mam pod ręką serwera SQL do pracy, jest użycie CTE z partycjonowanym ROW_NUMBER w następujący sposób:
Powyższe jest prawdopodobnie o wiele trudniejsze niż powinno, ale pozostawione jako łaskotanie mózgu, gdy masz inną definicję „biegania” niż tylko randki.
źródło
Kilka opcji SQL Server 2012 (przy założeniu, że N = 100 poniżej).
Chociaż z moimi przykładowymi danymi, poniższe okazały się bardziej wydajne
Oba opierają się na ograniczeniu podanym w pytaniu, że na użytkownika przypada najwyżej jeden rekord dziennie.
źródło
Coś takiego?
źródło
Użyłem prostej właściwości matematycznej, aby określić, kto kolejno odwiedzał witrynę. Ta właściwość polega na tym, że różnica dnia między pierwszym a ostatnim dostępem powinna być równa liczbie rekordów w dzienniku tabeli dostępu.
Oto skrypt SQL, który przetestowałem w Oracle DB (powinien działać również w innych DB):
Skrypt przygotowania tabeli:
źródło
Twierdzenie
cast(convert(char(11), @startdate, 113) as datetime)
usuwa część godziny z daty, więc zaczynamy o północy.Zakładam również, że
creationdate
iuserid
kolumny są indeksowane.Właśnie zdałem sobie sprawę, że to nie powie Ci wszystkich użytkowników i ich łącznej liczby kolejnych dni. Ale powie Ci, którzy użytkownicy będą odwiedzać określoną liczbę dni od wybranej przez Ciebie daty.
Zmienione rozwiązanie:
Sprawdziłem to i zapyta o wszystkich użytkowników i wszystkie daty. Opiera się na pierwszym (żartowym) rozwiązaniu Spencera , ale moje działa.
Aktualizacja: poprawiono obsługę dat w drugim rozwiązaniu.
źródło
Powinno to zrobić, co chcesz, ale nie mam wystarczających danych, aby przetestować wydajność. Zagmatwana funkcja CONVERT / FLOOR polega na usunięciu części czasu z pola daty i godziny. Jeśli używasz SQL Server 2008, możesz użyć CAST (x.CreationDate AS DATE).
Skrypt tworzenia
źródło
Spencer prawie to zrobił, ale powinien to być działający kod:
źródło
Nie mogę się doczekać, MySQLish:
Nie przetestowane i prawie na pewno wymaga konwersji dla MSSQL, ale myślę, że to daje kilka pomysłów.
źródło
A co powiesz na używanie tabel Tally? Opiera się na bardziej algorytmicznym podejściu, a plan wykonania jest bardzo prosty. Wypełnij tabelę tallyTable liczbami od 1 do „MaxDaysBehind”, które chcesz przeskanować w tabeli (tj. 90 będzie szukać za 3 miesiące do tyłu itp.).
źródło
Trochę poprawiam zapytanie Billa. Być może trzeba będzie skrócić datę przed grupowaniem, aby liczyć tylko jedno logowanie dziennie ...
EDITED, aby użyć DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) zamiast konwersji (char (10), CreationDate, 101).
@IDisposable Chciałem użyć datepart wcześniej, ale byłem zbyt leniwy, aby sprawdzić składnię, więc pomyślałem, że zamiast tego użyłem identyfikatora Convert. Nie wiem, że miało to znaczący wpływ. Dzięki! teraz wiem.
źródło
zakładając schemat, który wygląda następująco:
spowoduje to wyodrębnienie ciągłych zakresów z sekwencji dat z lukami.
źródło