Minimum sıralı erişim günlerini belirlemek için SQL?


125

Aşağıdaki Kullanıcı Geçmişi tablosu, belirli bir kullanıcının bir web sitesine eriştiği her gün için bir kayıt içerir (24 saatlik UTC döneminde). Binlerce kaydı vardır, ancak kullanıcı başına günde yalnızca bir kaydı vardır. Kullanıcı o gün için web sitesine erişmemişse, kayıt oluşturulmayacaktır.

Kimlik Kullanıcı Kimliği Oluşturma Tarihi
------ ------ ------------
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

Aradığım şey, bu tablodaki iyi performansa sahip bir SQL sorgusu , bana hangi userid'lerin (n) kesintisiz gün boyunca web sitesine bir günü kaçırmadan eriştiğini söyleyen bir SQL sorgusu .

Başka bir deyişle, bu tabloda sıralı (gün-öncesi veya gün-sonrası) tarihleri ​​olan kaç kullanıcının (n) kaydı var ? Sekansta herhangi bir gün eksikse, sekans bozulur ve 1'de yeniden başlamalıdır; Burada aralıksız sürekli gün sayısına ulaşmış kullanıcılar arıyoruz.

Bu sorgu ile belirli bir Yığın Taşması rozeti arasındaki herhangi bir benzerlik elbette tamamen rastlantısaldır .. :)


28 (<30) günlük üyelikten sonra meraklı rozetini aldım. Mistisizm.
Kirill V. Lyadvinsky

3
Tarihiniz UTC olarak mı saklanıyor? Öyleyse, bir CA sakini siteyi bir gün sabah 8'de ve ardından ertesi gün akşam 8'de ziyaret ederse ne olur? Pasifik Saat Diliminde art arda günlerde ziyaret etmesine rağmen, DB zamanları UTC olarak sakladığından bu şekilde DB'ye kaydedilmez.
Guy

Jeff / Jarrod - meta.stackexchange.com/questions/865/… adresini kontrol edebilir misiniz lütfen?
Rob Farley

Yanıtlar:


69

Cevap belli ki:

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

DÜZENLE:

Tamam, işte benim ciddi cevabım:

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

DÜZENLE:

[Jeff Atwood] Bu harika bir hızlı çözüm ve kabul edilmeyi hak ediyor, ancak Rob Farley'in çözümü de mükemmel ve tartışmalı olarak daha da hızlı (!). Lütfen bir de kontrol edin!


@Artem: İlk başta düşündüğüm buydu ama düşündüğümde (UserId, CreationDate) üzerinde bir dizininiz varsa kayıtlar art arda dizinde görünecek ve iyi performans göstermesi gerekiyor.
Mehrdad Afshari

Bunun için oy verin, sonuçları 500 bin satırda ~ 15 saniye içinde geri alıyorum.
Jim T

4
DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) kullanarak tüm bu testlerde CreateionDate'i günlere kısaltın (yalnızca sağ tarafta veya SARG'yi öldürürseniz) Bu, sağlanan tarihi sıfırdan çıkararak çalışır - ki Microsoft SQL Server 1900-01-01 00:00:00 olarak yorumlar ve gün sayısını verir. Bu değer daha sonra sıfır tarihine yeniden eklenir ve aynı tarihin kesilmesiyle elde edilir.
IDisposable

1
Tek söyleyebileceğim, IDisposable'ın değişikliği olmadan hesaplamanın yanlış olduğu . Verileri şahsen kendim doğruladım. 1 gün boşluk bırakan bazı kullanıcılar rozeti yanlış bir şekilde alırlar.
Jeff Atwood

3
Bu sorgu, 23: 59: 59.5'te gerçekleşen bir ziyareti kaçırma potansiyeline sahiptir - bunu: ON uh2.CreationDate >= uh1.CreationDate AND uh2.CreationDate < DATEADD(dd, DATEDIFF(dd, 0, uh1.CreationDate) + @days, 0)"31. gün sonra henüz değil" anlamına gelecek şekilde değiştirmeye ne dersiniz ? Ayrıca @saniye hesaplamasını atlayabileceğiniz anlamına gelir.
Rob Farley

147

Peki ya (ve lütfen önceki ifadenin noktalı virgülle bittiğinden emin olun):

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  

Günlerin bir listesi (bir sayı olarak) ve bir satır_numarası varsa, kaçırılan günlerin bu iki liste arasındaki farkı biraz daha büyütmesidir. Bu yüzden tutarlı bir ofseti olan bir aralık arıyoruz.

Bunun sonunda "SİPARİŞ TARAFINDAN SAYISAL DESC" kullanabilir veya bir eşik için "OLAN sayı (*)> 14" diyebilirsiniz ...

Yine de bunu test etmedim - sadece kafamın üstünden yazıyorum. Umarım SQL2005 ve sonrasında çalışır.

... ve tablename (UserID, CreationDate) üzerindeki bir dizinden çok yardımcı olacaktır.

Düzenlendi: Offset'in ayrılmış bir kelime olduğu ortaya çıktı, bu yüzden onun yerine TheOffset kullandım.

Düzenlendi: COUNT (*) kullanma önerisi çok geçerli - bunu en başta yapmalıydım ama gerçekten düşünmüyordum. Daha önce bunun yerine tarihliiff (gün, min (OluşturmaTarihi), maks (OluşturmaTarihi)) kullanılıyordu.

soymak


1
oh ayrıca eklemelisiniz; önce ile ->; ile
Mladen Prajdic

2
Mladen - hayır, önceki ifadeyi noktalı virgülle bitirmelisiniz. ;) Jeff - Tamam, onun yerine [Ofset] yazın. Sanırım Ofset ayrılmış bir kelime. Dediğim gibi, test etmedim.
Rob Farley

1
Sadece kendimi tekrarlıyorum, çünkü bu sıkça görülen bir sorun. DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) kullanarak tüm bu testlerde CreateionDate'i günlere kısaltın (yalnızca sağ tarafta veya SARG'yi öldürürseniz) Bu, sağlanan tarihi sıfırdan çıkararak çalışır - ki Microsoft SQL Server 1900-01-01 00:00:00 olarak yorumlar ve gün sayısını verir. Bu değer daha sonra sıfır tarihine yeniden eklenir ve aynı tarihin kesilmesiyle elde edilir.
IDisposable

1
Tek kullanımlık - evet, bunu sık sık kendim yaparım. Burada yapması konusunda endişelenmedim. Bir int'e yayınlamaktan daha hızlı olmaz, ancak saatleri, ayları vb. Sayma esnekliğine sahiptir.
Rob Farley

1
Bunu DENSE_RANK () ile çözme hakkında bir blog yazısı da yazdım. tinyurl.com/denserank
Rob Farley

18

Eğer tablo şemasını değiştirebilir, ben bir sütun ekleyerek öneririm LongestStreaksize biten ardışık gün sayısı olarak ayarlanmış olur masaya CreationDate. Tabloyu giriş sırasında güncellemek kolaydır (halihazırda yaptığınız şeye benzer şekilde, içinde bulunduğunuz gün için satır yoksa, önceki gün için herhangi bir satır olup olmadığını kontrol edersiniz. Doğruysa LongestStreak, yeni satır, aksi takdirde 1'e ayarlarsınız.)

Sorgu, bu sütun eklendikten sonra anlaşılır olacaktır:

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

1
+1 Benzer bir düşüncem vardı, ancak bir önceki gün için bir rekor varsa 1 olacak bir bit alanı (IsConcess) ile, aksi takdirde 0
Fredrik Mörk

7
bunun için şemayı değiştirmeyeceğiz
Jeff Atwood

Ve IsConsecutive, UserHistory tablosunda tanımlanan hesaplanmış bir sütun olabilir. Ayrıca, satır IFF eklendiğinde (eğer ve YALNIZCA ise) satırları her zaman kronolojik sırayla eklerseniz oluşturulan bir materyalize (depolanan) hesaplanmış sütun yapabilirsiniz.
IDisposable

(HİÇ KİMSE bir SELECT * yapmayacağı için, bu hesaplanan sütunun eklenmesinin, sütuna başvurulmadıkça sorgu planlarını etkilemeyeceğini biliyoruz ... doğru adamlar?!?)
IDisposable

3
kesinlikle geçerli bir çözüm ama istediğim şey bu değil. Bu yüzden ona bir "baş parmak" veriyorum ..
Jeff Atwood

6

Şu satırlar boyunca güzel ifade edici bazı SQL:

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

Kullanıcı tanımlı bir toplama fonksiyonuna sahip olduğunuzu varsayarsak , şu satırlar boyunca bir şey (bunun hatalı olduğuna dikkat edin):

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

Görünüşe göre n gün boyunca sürekli olmanın n satır olmasını gerektireceği gerçeğinden yararlanabilirsiniz.

Yani şöyle bir şey:

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

evet, yapabiliriz kapısı kesin kayıtları sayısına göre bunu .. ama biz günlük boşlukları dolu birkaç yıl genelinde ziyaret 120 gün olabileceğinden sadece bazı olasılıkları ortadan kaldırır o
Jeff Atwood

1
Tamam, ama bu sayfanın ödülünü kazandığınızda, günde sadece bir kez çalıştırmanız gerekiyor. Bence bu durumda yukarıdaki gibi bir şey işe yarayabilir. Yetişmek için tek yapmanız gereken WHERE yan tümcesini BETWEEN kullanarak kayan bir pencereye çevirmektir.
Bill

1
görevin her çalıştırılması durumsuz ve bağımsızdır; sorudaki tablodan başka önceki çalışmalar hakkında hiçbir bilgisi yok
Jeff Atwood

3

Bunu tek bir SQL sorgusu ile yapmak bana aşırı derecede karmaşık görünüyor. Bu cevabı iki kısma ayırmama izin verin.

  1. Şimdiye kadar yapmış olmanız ve şimdi yapmaya başlamanız gereken şey:
    Bugün oturum açmış olsa bile her kullanıcıyı kontrol eden ve ardından varsa bir sayacı artıran veya yoksa 0'a ayarlayan günlük bir cron işi çalıştırın.
  2. Şimdi yapmanız gerekenler:
    - Bu tabloyu web sitenizi çalıştırmayan ve bir süre gerekmeyecek bir sunucuya aktarın. ;)
    - Kullanıcıya, ardından tarihe göre sıralayın.
    - sırayla gözden geçirin, bir sayaç tutun ...

sorgu ve döngüye kod yazabiliriz, bu .. laf diyorum .. önemsiz. Şu anda tek yolu SQL'i merak ediyorum.
Jeff Atwood

2

Bu sizin için çok önemliyse, bu olayı kaynak gösterin ve size bu bilgiyi vermek için bir masa sürün. Tüm bu çılgın sorgularla makineyi öldürmeye gerek yok.


2

Özyinelemeli bir CTE (SQL Server 2005+) kullanabilirsiniz:

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'nun Smarties için SQL'de bununla ilgili eksiksiz bir bölümü vardır (Çalıştırmalar ve Sıralar olarak adlandırılır). O kitabım evde yok, o yüzden işe gittiğimde ... Aslında buna cevap vereceğim. (geçmiş tablosunun dbo.UserHistory olduğu ve gün sayısının @Days olduğu varsayılarak)

Başka bir ipucu da SQL Team'in çalıştırmalarla ilgili blogundan

Aklıma gelen diğer fikir, ancak burada çalışmak için kullanışlı bir SQL sunucum yok, bunun gibi ROW_NUMBER bölümlenmiş bir CTE kullanmak:

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

Yukarıdakiler muhtemelen olması gerekenden ÇOK DAHA ZOR , ancak "koşu" için tarihlerden başka bir tanımınız olduğunda beyin gıdıklaması olarak bırakılıyor.


2

Birkaç SQL Server 2012 seçeneği (aşağıdaki N = 100 varsayılarak).

;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 

Örnek verilerimle aşağıdakiler daha verimli çalıştı

;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

Her ikisi de soruda belirtilen, kullanıcı başına günde en fazla bir kayıt olduğu yönündeki kısıtlamaya dayanır.


1

Bunun gibi bir şey mi?

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

Siteye arka arkaya kimin eriştiğini belirlemek için basit bir matematik özelliği kullandım. Bu özellik, ilk erişim ile son kez arasındaki gün farkının erişim tablosu günlüğünüzdeki kayıt sayısına eşit olması gerektiğidir.

Oracle DB'de test ettiğim SQL betiği (diğer DB'lerde de çalışmalıdır):

-- 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;

Tablo hazırlık komut dosyası:

-- 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

İfadesi cast(convert(char(11), @startdate, 113) as datetime)geceyarısı başlayacak böylece tarih zaman parçası kaldırır.

Ayrıca creationdateve useridsütunlarının dizine eklendiğini varsayabilirim .

Bunun size tüm kullanıcıları ve birbirini takip eden toplam günlerini söylemeyeceğini anladım. Ancak, sizin seçtiğiniz bir tarihten sonra hangi kullanıcıların belirli sayıda gün ziyaret edeceğini size söyleyecektir.

Gözden geçirilmiş çözüm:

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

Bunu kontrol ettim ve tüm kullanıcıları ve tüm tarihleri ​​sorgulayacak. Spencer'ın 1. (şaka mı?) Çözümüne dayanıyor , ancak benimki işe yarıyor.

Güncelleme: ikinci çözümde tarih işlemeyi iyileştirdi.


yakın, ancak sabit bir başlangıç ​​tarihinde değil, herhangi bir (n) günlük dönem için çalışan bir şeye ihtiyacımız var
Jeff Atwood

0

Bu, istediğinizi yapmalı, ancak verimliliği test etmek için yeterli veriye sahip değilim. Kıvrımlı CONVERT / FLOOR maddesi, zaman bölümünü tarih saat alanından çıkarmaktır. SQL Server 2008 kullanıyorsanız, CAST (x.CreationDate AS DATE) kullanabilirsiniz.

@Range INT olarak BİLDİR
SET @Range = 10

DISTINCT UserId, CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate))) SEÇİN
  TblUserLogin a'dan
NEREDE VAR
   (SEÇ 1 
      TblUserLogin b'den 
     NEREDE a.userId = b.userId 
       VE (SAYI SEÇ (DISTINCT (DÖNÜŞTÜR (TARİH ZAMANI, ZEMİN (DÖNÜŞTÜR (FLOAT, OluşturmaTarihi))))) 
              TblUserLogin c'den 
             NEREDE c.userid = b.userid 
               VE DÖNÜŞTÜR (TARİH ZAMANI, ZEMİN (DÖNÜŞTÜR (YÜZEY, c.CreationDate))) ARASI DÖNÜŞTÜRME (TARİH ZAMANI, ZEMİN (DÖNÜŞTÜR (FLOAT, a. ) + @ Aralık-1) = @ Aralık)

Oluşturma komut dosyası

TABLO OLUŞTUR [dbo]. [TblUserLogin] (
    [Kimlik] [int] KİMLİK (1,1) BOŞ DEĞİL,
    [Kullanıcı Kimliği] [int] NULL,
    [CreationDate] [datetime] NULL
) AÇIK [BİRİNCİL]

oldukça acımasız. 406.624 satırda 26 saniye.
Jeff Atwood

Rozeti ödüllendirmek için ne sıklıkla bekliyorsunuz? Günde yalnızca bir kezse, yavaş bir dönemde 26 saniyelik bir vuruş o kadar da kötü görünmüyor. Yine de, tablo büyüdükçe performans yavaşlayacaktır. Soruyu yeniden okuduktan sonra, günde yalnızca bir kayıt olduğundan, zamanın çıkarılmasıyla ilgili olmayabilir.
Dave Barker

0

Spencer neredeyse başardı, ancak bu çalışma kodu olmalı:

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

Aklımın ucunda, 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

Test edilmemiş ve neredeyse kesinlikle MSSQL için biraz dönüşüm gerekiyor, ancak bence bu bazı fikirler veriyor.


0

Tally tablolarını kullanmaya ne dersiniz? Daha algoritmik bir yaklaşım izler ve yürütme planı bir esinti. TallyTable'ı, tabloyu taramak istediğiniz 1'den 'MaxDaysBehind'e kadar olan sayılarla doldurun (ör. 90, 3 ay geride kalacak, vb.).

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

Bill'in sorgusu biraz ince ayarlanıyor. Günde yalnızca bir girişi saymak için gruplamadan önce tarihi kısaltmanız gerekebilir ...

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

Convert (char (10), CreationDate, 101) yerine DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) kullanmak üzere DÜZENLENDİ.

@IDisposable Daha önce datepart'ı kullanmak istiyordum, ancak sözdizimini aramak için çok tembeldim, bu yüzden id'nin bunun yerine convert kullanmayı düşündüm. Bunun önemli bir etkisi olduğunu bilmiyorum Teşekkürler! şimdi biliyorum.


Yalnızca bugüne kadar bir SQL DATETIME kısaltması en iyi DATEADD (gg, DATEDIFF (gg, 0, UH.CreationDate), 0) ile yapılır
IDisposable

(yukarıdakiler, 0 arasındaki tam günlerdeki farkı alarak çalışır (örn. 1900-01-01 00: 00: 00.000) ve ardından bu farkı tam günlerde 0'a geri ekleyerek (örn. 1900-01-01 00:00:00) . Bu, DATETIME'ın zaman kısmının atılmasına neden olur)
IDisposable

0

şuna benzer bir şema varsayarsak:

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

bu, boşluklar içeren bir tarih dizisinden bitişik aralıkları çıkaracaktır.

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
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.