SQL untuk menentukan hari akses berurutan minimum?


125

Tabel Riwayat Pengguna berikut berisi satu catatan untuk setiap hari pengguna tertentu telah mengakses situs web (dalam periode 24 jam UTC). Ini memiliki ribuan catatan, tetapi hanya satu catatan per hari per pengguna. Jika pengguna belum mengakses situs web untuk hari itu, tidak ada catatan yang akan dibuat.

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

Yang saya cari adalah kueri SQL pada tabel ini dengan kinerja yang baik , yang memberi tahu saya pengguna mana yang telah mengakses situs web selama (n) hari terus menerus tanpa melewatkan satu hari pun.

Dengan kata lain, berapa banyak pengguna yang memiliki (n) catatan dalam tabel ini dengan tanggal berurutan (hari sebelum, atau setelah hari) ? Jika ada hari yang hilang dari urutan tersebut, urutan tersebut rusak dan harus dimulai ulang lagi pada 1; kami mencari pengguna yang telah mencapai jumlah hari yang terus menerus di sini tanpa celah.

Kemiripan apa pun antara kueri ini dan lencana Stack Overflow tertentu benar-benar kebetulan, tentu saja .. :)


Saya mendapatkan badge enthusiast setelah 28 (<30) hari keanggotaan. Tasawuf.
Kirill V. Lyadvinsky

3
Apakah tanggal Anda disimpan sebagai UTC? Jika demikian, apa yang terjadi jika penduduk CA mengunjungi situs pada jam 8 pagi pada satu hari dan kemudian jam 8 malam pada hari berikutnya? Meskipun dia mengunjungi pada hari-hari berturut-turut di Zona Waktu Pasifik, itu tidak akan dicatat seperti itu di DB karena DB menyimpan waktu sebagai UTC.
Guy

Jeff / Jarrod - dapatkah Anda memeriksa meta.stackexchange.com/questions/865/… ?
Rob Farley

Jawaban:


69

Jawabannya jelas:

SELECT DISTINCT UserId
FROM UserHistory uh1
WHERE (
       SELECT COUNT(*) 
       FROM UserHistory uh2 
       WHERE uh2.CreationDate 
       BETWEEN uh1.CreationDate AND DATEADD(d, @days, uh1.CreationDate)
      ) = @days OR UserId = 52551

EDIT:

Oke, inilah jawaban serius saya:

DECLARE @days int
DECLARE @seconds bigint
SET @days = 30
SET @seconds = (@days * 24 * 60 * 60) - 1
SELECT DISTINCT UserId
FROM (
    SELECT uh1.UserId, Count(uh1.Id) as Conseq
    FROM UserHistory uh1
    INNER JOIN UserHistory uh2 ON uh2.CreationDate 
        BETWEEN uh1.CreationDate AND 
            DATEADD(s, @seconds, DATEADD(dd, DATEDIFF(dd, 0, uh1.CreationDate), 0))
        AND uh1.UserId = uh2.UserId
    GROUP BY uh1.Id, uh1.UserId
    ) as Tbl
WHERE Conseq >= @days

EDIT:

[Jeff Atwood] Ini adalah solusi cepat yang hebat dan pantas untuk diterima, tetapi solusi Rob Farley juga sangat baik dan bisa dibilang bahkan lebih cepat (!). Silakan periksa juga!


@Artem: Itulah yang awalnya saya pikirkan, tetapi ketika saya memikirkannya, jika Anda memiliki indeks pada (UserId, CreationDate), catatan akan muncul secara berurutan dalam indeks dan harus berkinerja baik.
Mehrdad Afshari

Beri suara positif untuk yang ini, saya mendapatkan hasil dalam ~ 15 detik pada 500 ribu baris.
Jim T

4
Pangkas CreateionDate menjadi beberapa hari dalam semua tes ini (hanya di sisi kanan atau Anda mematikan SARG) menggunakan DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) Ini bekerja dengan mengurangi tanggal yang disediakan dari nol - yang Microsoft SQL Server menginterpretasikan sebagai 1900-01-01 00:00:00 dan memberikan jumlah hari. Nilai ini kemudian ditambahkan kembali ke tanggal nol yang menghasilkan tanggal yang sama dengan waktu yang terpotong.
IDisposable

1
yang bisa saya katakan adalah, tanpa perubahan IDisposable, perhitungannya salah . Saya sendiri memvalidasi datanya. Beberapa pengguna dengan jeda 1 hari AKAN mendapatkan lencana dengan tidak benar.
Jeff Atwood

3
Kueri ini berpotensi melewatkan kunjungan yang terjadi pada 23: 59: 59.5 - bagaimana jika mengubahnya menjadi:, yang ON uh2.CreationDate >= uh1.CreationDate AND uh2.CreationDate < DATEADD(dd, DATEDIFF(dd, 0, uh1.CreationDate) + @days, 0)berarti "Belum pada hari ke-31 nanti". Juga berarti Anda dapat melewati kalkulasi @seconds.
Rob Farley

147

Bagaimana dengan (dan pastikan pernyataan sebelumnya diakhiri dengan titik koma):

WITH numberedrows
     AS (SELECT ROW_NUMBER() OVER (PARTITION BY UserID 
                                       ORDER BY CreationDate)
                - DATEDIFF(day,'19000101',CreationDate) AS TheOffset,
                CreationDate,
                UserID
         FROM   tablename)
SELECT MIN(CreationDate),
       MAX(CreationDate),
       COUNT(*) AS NumConsecutiveDays,
       UserID
FROM   numberedrows
GROUP  BY UserID,
          TheOffset  

Idenya adalah bahwa jika kita memiliki daftar hari (sebagai angka), dan row_number, maka hari yang terlewat membuat offset antara kedua daftar ini sedikit lebih besar. Jadi kami mencari rentang yang memiliki offset yang konsisten.

Anda bisa menggunakan "ORDER BY NumConsecutiveDays DESC" di akhir ini, atau ucapkan "HAVING count (*)> 14" untuk ambang ...

Saya belum mengujinya - hanya menuliskannya di atas kepala saya. Semoga bekerja di SQL2005 dan seterusnya.

... dan akan sangat terbantu oleh indeks pada tablename (UserID, CreationDate)

Diedit: Ternyata Offset adalah kata yang dicadangkan, jadi saya menggunakan TheOffset sebagai gantinya.

Diedit: Saran untuk menggunakan JUMLAH (*) sangat valid - Saya seharusnya melakukannya sejak awal tetapi tidak benar-benar berpikir. Sebelumnya itu menggunakan tanggaliff (hari, min (CreationDate), max (CreationDate)) sebagai gantinya.

rampok


1
oh Anda juga harus menambahkan; sebelum dengan ->; dengan
Mladen Prajdic

2
Mladen - tidak, Anda harus mengakhiri pernyataan sebelumnya dengan titik koma. ;) Jeff - Ok, letakkan [Offset] sebagai gantinya. Saya kira Offset adalah kata yang dipesan. Seperti saya katakan, saya belum mengujinya.
Rob Farley

1
Hanya mengulang sendiri, karena ini adalah masalah yang sering terlihat. Pangkas CreateionDate menjadi beberapa hari dalam semua tes ini (hanya di sisi kanan atau Anda mematikan SARG) menggunakan DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) Ini bekerja dengan mengurangi tanggal yang disediakan dari nol - yang Microsoft SQL Server menginterpretasikan sebagai 1900-01-01 00:00:00 dan memberikan jumlah hari. Nilai ini kemudian ditambahkan kembali ke tanggal nol yang menghasilkan tanggal yang sama dengan waktu yang terpotong.
IDisposable

1
IDisposable - ya, saya sering melakukannya sendiri. Saya hanya tidak khawatir melakukannya di sini. Ini tidak akan lebih cepat daripada mentransmisikannya ke int, tetapi memiliki fleksibilitas untuk menghitung jam, bulan, apa pun.
Rob Farley

1
Saya baru saja menulis entri blog tentang memecahkan masalah ini dengan DENSE_RANK () juga. tinyurl.com/denserank
Rob Farley

18

Jika Anda dapat mengubah skema tabel, saya sarankan untuk menambahkan kolom LongestStreakke tabel yang akan Anda setel ke jumlah hari berurutan yang diakhiri dengan CreationDate. Sangat mudah untuk memperbarui tabel pada saat masuk (mirip dengan yang Anda lakukan, jika tidak ada baris hari ini, Anda akan memeriksa apakah ada baris untuk hari sebelumnya. Jika benar, Anda akan menambah LongestStreakdalam baris baru, jika tidak, Anda akan menyetelnya ke 1.)

Kueri akan terlihat jelas setelah menambahkan kolom ini:

if exists(select * from table
          where LongestStreak >= 30 and UserId = @UserId)
   -- award the Woot badge.

1
+1 Saya memiliki pemikiran serupa, tetapi dengan sedikit bidang (IsConsecutive) yang akan menjadi 1 jika ada rekor untuk hari sebelumnya, sebaliknya 0.
Fredrik Mörk

7
kami tidak akan mengubah skema untuk ini
Jeff Atwood

Dan IsConsecutive dapat menjadi kolom terhitung yang ditentukan di tabel UserHistory. Anda juga bisa membuatnya menjadi kolom terkomputasi yang terwujud (disimpan) yang dibuat saat baris disisipkan IFF (jika dan HANYA jika) Anda selalu memasukkan baris dalam urutan kronologis.
IDisposable

(karena TIDAK ADA YANG akan melakukan PILIHAN *, kita tahu menambahkan kolom yang dihitung ini tidak akan memengaruhi rencana kueri kecuali kolom tersebut direferensikan ... kan?!?)
IDisposable

3
ini jelas merupakan solusi yang valid tetapi bukan itu yang saya minta. Jadi saya memberikannya "jempol ke samping" ..
Jeff Atwood

6

Beberapa SQL ekspresif yang bagus di sepanjang baris:

select
        userId,
    dbo.MaxConsecutiveDates(CreationDate) as blah
from
    dbo.Logins
group by
    userId

Dengan asumsi Anda memiliki fungsi agregat yang ditentukan pengguna, sesuatu di sepanjang baris (waspadalah ini buggy):

using System;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
using System.Runtime.InteropServices;

namespace SqlServerProject1
{
    [StructLayout(LayoutKind.Sequential)]
    [Serializable]
    internal struct MaxConsecutiveState
    {
        public int CurrentSequentialDays;
        public int MaxSequentialDays;
        public SqlDateTime LastDate;
    }

    [Serializable]
    [SqlUserDefinedAggregate(
        Format.Native,
        IsInvariantToNulls = true, //optimizer property
        IsInvariantToDuplicates = false, //optimizer property
        IsInvariantToOrder = false) //optimizer property
    ]
    [StructLayout(LayoutKind.Sequential)]
    public class MaxConsecutiveDates
    {
        /// <summary>
        /// The variable that holds the intermediate result of the concatenation
        /// </summary>
        private MaxConsecutiveState _intermediateResult;

        /// <summary>
        /// Initialize the internal data structures
        /// </summary>
        public void Init()
        {
            _intermediateResult = new MaxConsecutiveState { LastDate = SqlDateTime.MinValue, CurrentSequentialDays = 0, MaxSequentialDays = 0 };
        }

        /// <summary>
        /// Accumulate the next value, not if the value is null
        /// </summary>
        /// <param name="value"></param>
        public void Accumulate(SqlDateTime value)
        {
            if (value.IsNull)
            {
                return;
            }
            int sequentialDays = _intermediateResult.CurrentSequentialDays;
            int maxSequentialDays = _intermediateResult.MaxSequentialDays;
            DateTime currentDate = value.Value.Date;
            if (currentDate.AddDays(-1).Equals(new DateTime(_intermediateResult.LastDate.TimeTicks)))
                sequentialDays++;
            else
            {
                maxSequentialDays = Math.Max(sequentialDays, maxSequentialDays);
                sequentialDays = 1;
            }
            _intermediateResult = new MaxConsecutiveState
                                      {
                                          CurrentSequentialDays = sequentialDays,
                                          LastDate = currentDate,
                                          MaxSequentialDays = maxSequentialDays
                                      };
        }

        /// <summary>
        /// Merge the partially computed aggregate with this aggregate.
        /// </summary>
        /// <param name="other"></param>
        public void Merge(MaxConsecutiveDates other)
        {
            // add stuff for two separate calculations
        }

        /// <summary>
        /// Called at the end of aggregation, to return the results of the aggregation.
        /// </summary>
        /// <returns></returns>
        public SqlInt32 Terminate()
        {
            int max = Math.Max((int) ((sbyte) _intermediateResult.CurrentSequentialDays), (sbyte) _intermediateResult.MaxSequentialDays);
            return new SqlInt32(max);
        }
    }
}

4

Sepertinya Anda bisa memanfaatkan fakta bahwa untuk terus menerus selama n hari akan membutuhkan n baris.

Jadi sesuatu seperti:

SELECT users.UserId, count(1) as cnt
FROM users
WHERE users.CreationDate > now() - INTERVAL 30 DAY
GROUP BY UserId
HAVING cnt = 30

ya, kita bisa mengelompokkannya dengan jumlah catatan, pasti .. tapi itu hanya menghilangkan beberapa kemungkinan, karena kita bisa memiliki 120 hari kunjungan dalam beberapa tahun dengan banyak celah harian
Jeff Atwood

1
Oke, tapi setelah Anda berhasil mendapatkan halaman ini, Anda hanya perlu menjalankannya sekali sehari. Saya pikir untuk kasus itu, sesuatu seperti di atas akan berhasil. Untuk mengejar ketinggalan, yang perlu Anda lakukan hanyalah mengubah klausa WHERE menjadi jendela geser menggunakan BETWEEN.
Bill

1
setiap pelaksanaan tugas tidak memiliki kewarganegaraan dan mandiri; itu tidak memiliki pengetahuan tentang lari sebelumnya selain tabel dalam pertanyaan
Jeff Atwood

3

Melakukan ini dengan satu kueri SQL tampaknya terlalu rumit bagi saya. Izinkan saya memecah jawaban ini menjadi dua bagian.

  1. Apa yang seharusnya Anda lakukan sampai sekarang dan harus mulai Anda lakukan sekarang:
    Jalankan tugas cron harian yang memeriksa setiap pengguna apakah dia telah masuk hari ini dan kemudian menambah penghitung jika dia memiliki atau menyetelnya ke 0 jika belum.
  2. Apa yang harus Anda lakukan sekarang:
    - Ekspor tabel ini ke server yang tidak menjalankan situs web Anda dan tidak akan diperlukan untuk sementara waktu. ;)
    - Sortir menurut pengguna, lalu tanggal.
    - lakukan secara berurutan, pertahankan ...

kita bisa menulis kode ke query-and-loop, itu .. dary menurut saya .. sepele. Saya ingin tahu tentang satu-satunya cara SQL saat ini.
Jeff Atwood

2

Jika ini sangat penting bagi Anda, sumber acara ini dan dorong tabel untuk memberi Anda info ini. Tidak perlu mematikan mesin dengan semua pertanyaan gila itu.


2

Anda dapat menggunakan CTE rekursif (SQL Server 2005+):

WITH recur_date AS (
        SELECT t.userid,
               t.creationDate,
               DATEADD(day, 1, t.created) 'nextDay',
               1 'level' 
          FROM TABLE t
         UNION ALL
        SELECT t.userid,
               t.creationDate,
               DATEADD(day, 1, t.created) 'nextDay',
               rd.level + 1 'level'
          FROM TABLE t
          JOIN recur_date rd on t.creationDate = rd.nextDay AND t.userid = rd.userid)
   SELECT t.*
    FROM recur_date t
   WHERE t.level = @numDays
ORDER BY t.userid

2

Joe Celko memiliki bab lengkap tentang ini dalam SQL untuk Smarties (menyebutnya Runs and Sequences). Saya tidak punya buku itu di rumah, jadi ketika saya mulai bekerja ... Saya akan menjawab ini. (dengan asumsi tabel riwayat disebut dbo.UserHistory dan jumlah hari adalah @Days)

Prospek lainnya adalah dari blog Tim SQL yang sedang berjalan

Ide lain yang saya miliki, tetapi tidak memiliki server SQL yang berguna untuk dikerjakan di sini adalah menggunakan CTE dengan ROW_NUMBER yang dipartisi seperti ini:

WITH Runs
AS
  (SELECT UserID
         , CreationDate
         , ROW_NUMBER() OVER(PARTITION BY UserId
                             ORDER BY CreationDate)
           - ROW_NUMBER() OVER(PARTITION BY UserId, NoBreak
                               ORDER BY CreationDate) AS RunNumber
  FROM
     (SELECT UH.UserID
           , UH.CreationDate
           , ISNULL((SELECT TOP 1 1 
              FROM dbo.UserHistory AS Prior 
              WHERE Prior.UserId = UH.UserId 
              AND Prior.CreationDate
                  BETWEEN DATEADD(dd, DATEDIFF(dd, 0, UH.CreationDate), -1)
                  AND DATEADD(dd, DATEDIFF(dd, 0, UH.CreationDate), 0)), 0) AS NoBreak
      FROM dbo.UserHistory AS UH) AS Consecutive
)
SELECT UserID, MIN(CreationDate) AS RunStart, MAX(CreationDate) AS RunEnd
FROM Runs
GROUP BY UserID, RunNumber
HAVING DATEDIFF(dd, MIN(CreationDate), MAX(CreationDate)) >= @Days

Hal di atas mungkin JAUH LEBIH KERAS daripada yang seharusnya, tetapi dibiarkan sebagai penggelitik otak ketika Anda memiliki definisi lain tentang "lari" daripada sekadar kencan.


2

Beberapa opsi SQL Server 2012 (dengan asumsi N = 100 di bawah).

;WITH T(UserID, NRowsPrevious)
     AS (SELECT UserID,
                DATEDIFF(DAY, 
                        LAG(CreationDate, 100) 
                            OVER 
                                (PARTITION BY UserID 
                                     ORDER BY CreationDate), 
                         CreationDate)
         FROM   UserHistory)
SELECT DISTINCT UserID
FROM   T
WHERE  NRowsPrevious = 100 

Meskipun dengan data sampel saya, berikut ini bekerja lebih efisien

;WITH U
         AS (SELECT DISTINCT UserId
             FROM   UserHistory) /*Ideally replace with Users table*/
    SELECT UserId
    FROM   U
           CROSS APPLY (SELECT TOP 1 *
                        FROM   (SELECT 
                                       DATEDIFF(DAY, 
                                                LAG(CreationDate, 100) 
                                                  OVER 
                                                   (ORDER BY CreationDate), 
                                                 CreationDate)
                                FROM   UserHistory UH
                                WHERE  U.UserId = UH.UserID) T(NRowsPrevious)
                        WHERE  NRowsPrevious = 100) O

Keduanya mengandalkan batasan yang dinyatakan dalam pertanyaan bahwa ada paling banyak satu record per hari per pengguna.


1

Sesuatu seperti ini?

select distinct userid
from table t1, table t2
where t1.UserId = t2.UserId 
  AND trunc(t1.CreationDate) = trunc(t2.CreationDate) + n
  AND (
    select count(*)
    from table t3
    where t1.UserId  = t3.UserId
      and CreationDate between trunc(t1.CreationDate) and trunc(t1.CreationDate)+n
   ) = n

1

Saya menggunakan properti matematika sederhana untuk mengidentifikasi siapa yang secara berurutan mengakses situs. Properti ini adalah Anda harus memiliki perbedaan hari antara akses pertama kali dan terakhir kali sama dengan jumlah catatan di log tabel akses Anda.

Berikut adalah skrip SQL yang saya uji di Oracle DB (seharusnya berfungsi di DB lain juga):

-- show basic understand of the math properties 
  select    ceil(max (creation_date) - min (creation_date))
              max_min_days_diff,
           count ( * ) real_day_count
    from   user_access_log
group by   user_id;


-- select all users that have consecutively accessed the site 
  select   user_id
    from   user_access_log
group by   user_id
  having       ceil(max (creation_date) - min (creation_date))
           / count ( * ) = 1;



-- get the count of all users that have consecutively accessed the site 
  select   count(user_id) user_count
    from   user_access_log
group by   user_id
  having   ceil(max (creation_date) - min (creation_date))
           / count ( * ) = 1;

Skrip persiapan tabel:

-- create table 
create table user_access_log (id           number, user_id      number, creation_date date);


-- insert seed data 
insert into user_access_log (id, user_id, creation_date)
  values   (1, 12, sysdate);

insert into user_access_log (id, user_id, creation_date)
  values   (2, 12, sysdate + 1);

insert into user_access_log (id, user_id, creation_date)
  values   (3, 12, sysdate + 2);

insert into user_access_log (id, user_id, creation_date)
  values   (4, 16, sysdate);

insert into user_access_log (id, user_id, creation_date)
  values   (5, 16, sysdate + 1);

insert into user_access_log (id, user_id, creation_date)
  values   (6, 16, sysdate + 5);

1
declare @startdate as datetime, @days as int
set @startdate = cast('11 Jan 2009' as datetime) -- The startdate
set @days = 5 -- The number of consecutive days

SELECT userid
      ,count(1) as [Number of Consecutive Days]
FROM UserHistory
WHERE creationdate >= @startdate
AND creationdate < dateadd(dd, @days, cast(convert(char(11), @startdate, 113)  as datetime))
GROUP BY userid
HAVING count(1) >= @days

Pernyataan tersebut cast(convert(char(11), @startdate, 113) as datetime)menghapus bagian waktu dari tanggal jadi kami mulai tengah malam.

Saya juga akan berasumsi bahwa creationdatedanuserid kolom diindeks.

Saya baru menyadari bahwa ini tidak akan memberi tahu Anda semua pengguna dan total hari mereka yang berurutan. Tetapi akan memberi tahu Anda pengguna mana yang akan mengunjungi sejumlah hari yang ditentukan sejak tanggal yang Anda pilih.

Solusi yang direvisi:

declare @days as int
set @days = 30
select t1.userid
from UserHistory t1
where (select count(1) 
       from UserHistory t3 
       where t3.userid = t1.userid
       and t3.creationdate >= DATEADD(dd, DATEDIFF(dd, 0, t1.creationdate), 0) 
       and t3.creationdate < DATEADD(dd, DATEDIFF(dd, 0, t1.creationdate) + @days, 0) 
       group by t3.userid
) >= @days
group by t1.userid

Saya telah memeriksa ini dan ini akan menanyakan semua pengguna dan semua tanggal. Ini didasarkan pada solusi pertama Spencer (lelucon?) , Tapi solusi saya berhasil.

Pembaruan: meningkatkan penanganan tanggal dalam solusi kedua.


dekat, tetapi kita membutuhkan sesuatu yang berfungsi untuk periode (n) hari apa pun, bukan pada tanggal mulai tetap
Jeff Atwood

0

Ini harus melakukan apa yang Anda inginkan tetapi saya tidak memiliki cukup data untuk menguji efisiensi. Hal-hal CONVERT / FLOOR yang berbelit-belit adalah untuk menghapus bagian waktu dari bidang datetime. Jika Anda menggunakan SQL Server 2008 maka Anda dapat menggunakan CAST (x.CreationDate AS DATE).

MENYATAKAN @Range sebagai INT
SET @Rentang = 10

PILIH DISTINCT UserId, CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate)))
  DARI tblUserLogin a
DI MANA ADA
   (PILIH 1 
      DARI tblUserLogin b 
     DI MANA a.userId = b.userId 
       DAN (PILIH JUMLAH (BERBEDA (CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, CreationDate))))) 
              DARI tblUserLogin c 
             DI MANA c.userid = b.userid 
               DAN CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, c.CreationDate))) ANTARA CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate))) dan CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate)) ) + @ Range-1) = @Range)

Skrip pembuatan

BUAT TABEL [dbo]. [TblUserLogin] (
    [Id] [int] IDENTITAS (1,1) BUKAN NIHIL,
    [UserId] [int] NULL,
    [CreationDate] [datetime] NULL
) AKTIF [UTAMA]

cukup brutal. 26 detik di 406.624 baris.
Jeff Atwood

Seberapa sering Anda memeriksa untuk memberikan lencana? Jika hanya sekali sehari maka hit selama 26 detik dalam periode yang lambat sepertinya tidak terlalu buruk. Meskipun demikian, kinerja akan melambat seiring pertumbuhan tabel. Setelah membaca ulang pertanyaan, menghapus waktu mungkin tidak relevan karena hanya ada satu catatan per hari.
Dave Barker

0

Spencer hampir melakukannya, tetapi ini seharusnya menjadi kode yang berfungsi:

SELECT DISTINCT UserId
FROM History h1
WHERE (
    SELECT COUNT(*) 
    FROM History
    WHERE UserId = h1.UserId AND CreationDate BETWEEN h1.CreationDate AND DATEADD(d, @n-1, h1.CreationDate)
) >= @n

0

Di luar kepalaku, MySQLish:

SELECT start.UserId
FROM UserHistory AS start
  LEFT OUTER JOIN UserHistory AS pre_start ON pre_start.UserId=start.UserId
    AND DATE(pre_start.CreationDate)=DATE_SUB(DATE(start.CreationDate), INTERVAL 1 DAY)
  LEFT OUTER JOIN UserHistory AS subsequent ON subsequent.UserId=start.UserId
    AND DATE(subsequent.CreationDate)<=DATE_ADD(DATE(start.CreationDate), INTERVAL 30 DAY)
WHERE pre_start.Id IS NULL
GROUP BY start.Id
HAVING COUNT(subsequent.Id)=30

Belum teruji, dan hampir pasti membutuhkan beberapa konversi untuk MSSQL, tapi saya pikir itu memberi beberapa ide.


0

Bagaimana kalau seseorang menggunakan tabel Tally? Ini mengikuti pendekatan yang lebih algoritmik, dan rencana eksekusi sangat mudah. Isi tallyTable dengan angka dari 1 hingga 'MaxDaysBehind' yang ingin Anda pindai tabelnya (mis. 90 akan mencari 3 bulan di belakang, dll).

declare @ContinousDays int
set @ContinousDays = 30  -- select those that have 30 consecutive days

create table #tallyTable (Tally int)
insert into #tallyTable values (1)
...
insert into #tallyTable values (90) -- insert numbers for as many days behind as you want to scan

select [UserId],count(*),t.Tally from HistoryTable 
join #tallyTable as t on t.Tally>0
where [CreationDate]> getdate()-@ContinousDays-t.Tally and 
      [CreationDate]<getdate()-t.Tally 
group by [UserId],t.Tally 
having count(*)>=@ContinousDays

delete #tallyTable

0

Sesuaikan sedikit pertanyaan Bill. Anda mungkin harus memotong tanggal sebelum mengelompokkan untuk menghitung hanya satu login per hari ...

SELECT UserId from History 
WHERE CreationDate > ( now() - n )
GROUP BY UserId, 
DATEADD(dd, DATEDIFF(dd, 0, CreationDate), 0) AS TruncatedCreationDate  
HAVING COUNT(TruncatedCreationDate) >= n

DIEDIT untuk menggunakan DATEADD (hh, DATEDIFF (dd, 0, CreationDate), 0) alih-alih mengonversi (char (10), CreationDate, 101).

@IDisposable Saya mencari untuk menggunakan datepart sebelumnya tetapi saya terlalu malas untuk mencari sintaks jadi saya pikir id menggunakan konversi sebagai gantinya. Saya tidak tahu itu berdampak signifikan Terima kasih! sekarang saya tahu.


Memotong SQL DATETIME menjadi hanya-saat ini paling baik dilakukan dengan DATEADD (dd, DATEDIFF (dd, 0, UH.CreationDate), 0)
IDisposable

(di atas bekerja dengan mengambil selisih dalam satu hari penuh antara 0 (misal 1900-01-01 00: 00: 00.000) dan kemudian menambahkan selisih itu di seluruh hari kembali ke 0 (misal 1900-01-01 00:00:00) Hal ini menyebabkan bagian waktu DATETIME dibuang)
IDisposable

0

dengan asumsi skema yang berjalan seperti:

create table dba.visits
(
    id  integer not null,
    user_id integer not null,
    creation_date date not null
);

ini akan mengekstrak rentang yang berdekatan dari urutan tanggal dengan celah.

select l.creation_date  as start_d, -- Get first date in contiguous range
    (
        select min(a.creation_date ) as creation_date 
        from "DBA"."visits" a 
            left outer join "DBA"."visits" b on 
                   a.creation_date = dateadd(day, -1, b.creation_date ) and 
                   a.user_id  = b.user_id 
            where b.creation_date  is null and
                  a.creation_date  >= l.creation_date  and
                  a.user_id  = l.user_id 
    ) as end_d -- Get last date in contiguous range
from  "DBA"."visits" l
    left outer join "DBA"."visits" r on 
        r.creation_date  = dateadd(day, -1, l.creation_date ) and 
        r.user_id  = l.user_id 
    where r.creation_date  is null
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.