SQL เพื่อกำหนดวันเข้าใช้งานตามลำดับขั้นต่ำ?


125

ตารางประวัติผู้ใช้ต่อไปนี้มีหนึ่งระเบียนสำหรับทุกวันที่ผู้ใช้รายหนึ่งเข้าถึงเว็บไซต์ (ในช่วงเวลา UTC 24 ชั่วโมง) มีบันทึกมากมายหลายพันรายการ แต่มีเพียงหนึ่งรายการต่อวันต่อผู้ใช้ หากผู้ใช้ไม่ได้เข้าถึงเว็บไซต์ในวันนั้นจะไม่มีการสร้างบันทึก

Id UserId CreationDate
------ ------ ------------
750997 12 2552-07-07 18: 42: 20.723
750998 15 2552-07-07 18: 42: 20.927
751000 19 2009-07-07 18: 42: 22.283

สิ่งที่ฉันกำลังมองหาคือแบบสอบถาม SQL ในตารางนี้ที่มีประสิทธิภาพที่ดีซึ่งจะบอกฉันว่าผู้ใช้รายใดได้เข้าถึงเว็บไซต์เป็นเวลา (n) วันต่อเนื่องโดยไม่ขาดวัน

ในคำอื่น ๆจำนวนผู้ใช้ที่มี (n) ระเบียนในตารางนี้มีลำดับ (วันก่อนหรือวันหลัง) วันที่ ? หากวันใดขาดหายไปจากลำดับลำดับจะเสียและควรเริ่มใหม่อีกครั้งที่ 1; เรากำลังมองหาผู้ใช้ที่มีจำนวนวันต่อเนื่องที่นี่โดยไม่มีช่องว่าง

ความคล้ายคลึงใด ๆ ระหว่างข้อความค้นหานี้กับป้าย Stack Overflowนั้นเป็นเรื่องบังเอิญอย่างแท้จริง.. :)


ฉันได้รับป้ายผู้สนใจหลังจากเป็นสมาชิก 28 (<30) วัน เวทมนตร์
Kirill V. Lyadvinsky

3
วันที่ของคุณถูกจัดเก็บเป็น UTC หรือไม่? ถ้าเป็นเช่นนั้นจะเกิดอะไรขึ้นหากผู้อยู่อาศัยในแคลิฟอร์เนียเข้าเยี่ยมชมเว็บไซต์เวลา 8.00 น. ของวันหนึ่งแล้ว 20.00 น. ในวันรุ่งขึ้น? แม้ว่าเขา / เธอจะเข้าชมติดต่อกันหลายวันในเขตเวลาแปซิฟิก แต่จะไม่ถูกบันทึกในฐานข้อมูลเช่นนี้เนื่องจากฐานข้อมูลจัดเก็บเวลาเป็น UTC
Guy

คำตอบ:


69

คำตอบชัดเจน:

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

แก้ไข:

โอเคนี่คือคำตอบที่จริงจังของฉัน:

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

แก้ไข:

[Jeff Atwood] นี่เป็นวิธีแก้ปัญหาที่รวดเร็วและสมควรได้รับการยอมรับ แต่วิธีแก้ปัญหาของ Rob Farley นั้นยอดเยี่ยมและเร็วกว่า (!) โปรดตรวจสอบด้วย!


@ Artem: นั่นคือสิ่งที่ฉันคิดในตอนแรก แต่เมื่อฉันคิดเกี่ยวกับเรื่องนี้หากคุณมีดัชนีบน (UserId, CreationDate) ระเบียนจะปรากฏขึ้นในดัชนีอย่างต่อเนื่องและควรทำงานได้ดี
Mehrdad Afshari

โหวตให้คะแนนอันนี้ฉันได้รับผลลัพธ์กลับมาใน ~ 15 วินาทีใน 500k แถว
Jim T

4
ตัด CreateionDate ลงเป็นวันในการทดสอบเหล่านี้ทั้งหมด (ทางด้านขวาเท่านั้นหรือคุณฆ่า SARG) โดยใช้ DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) ซึ่งทำงานโดยการลบวันที่ที่ให้มาจากศูนย์ซึ่ง Microsoft SQL Server แปลว่า 1900-01-01 00:00:00 และระบุจำนวนวัน จากนั้นค่านี้จะถูกเพิ่มเข้าไปในวันที่เป็นศูนย์โดยให้วันที่เดียวกันกับเวลาที่ถูกตัดทอน
IDisposable

1
ทั้งหมดที่ผมสามารถบอกคุณได้คือไม่มีการเปลี่ยนแปลง IDisposable ของการคำนวณไม่ถูกต้อง ฉันตรวจสอบข้อมูลเป็นการส่วนตัวด้วยตัวเอง ผู้ใช้บางคนที่มีช่องว่าง 1 วันWOULDรับบัตรไม่ถูกต้อง
Jeff Atwood

3
ข้อความค้นหานี้มีแนวโน้มที่จะพลาดการเยี่ยมชมที่เกิดขึ้นในเวลา 23: 59: 59.5 - วิธีการเปลี่ยนเป็น: ON uh2.CreationDate >= uh1.CreationDate AND uh2.CreationDate < DATEADD(dd, DATEDIFF(dd, 0, uh1.CreationDate) + @days, 0)หมายถึง "ยังไม่ถึงวันที่ 31 ในภายหลัง" นอกจากนี้คุณสามารถข้ามการคำนวณ @ วินาที
Rob Farley

147

เป็นอย่างไรบ้าง (และโปรดตรวจสอบให้แน่ใจว่าคำสั่งก่อนหน้านี้ลงท้ายด้วยเครื่องหมายอัฒภาค):

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  

ความคิดที่ว่าถ้าเรามีรายชื่อวัน (เป็นตัวเลข) และ row_number วันที่พลาดไปจะทำให้ค่าชดเชยระหว่างสองรายการนี้ใหญ่ขึ้นเล็กน้อย เราจึงมองหาช่วงที่มีค่าชดเชยสม่ำเสมอ

คุณสามารถใช้ "ORDER BY NumConsecutiveDays DESC" ที่ส่วนท้ายของสิ่งนี้หรือพูดว่า "HAVING count (*)> 14" สำหรับเกณฑ์ ...

ฉันยังไม่ได้ทดสอบสิ่งนี้ - เพียงแค่เขียนไว้ด้านบนของหัว หวังว่าจะใช้งานได้ใน SQL2005 และบน

... และจะได้รับความช่วยเหลืออย่างมากจากดัชนีบน tablename (UserID, CreationDate)

แก้ไข: ปรากฎว่า Offset เป็นคำสงวนดังนั้นฉันจึงใช้ TheOffset แทน

แก้ไข: คำแนะนำในการใช้ COUNT (*) นั้นถูกต้องมาก - ฉันควรจะทำอย่างนั้นตั้งแต่แรก แต่ไม่ได้คิดจริงๆ ก่อนหน้านี้ใช้วันที่ (วันนาที (วันที่สร้าง) สูงสุด (วันที่สร้าง)) แทน

ปล้น


1
คุณควรเพิ่มด้วย before with ->; with
Mladen Prajdic

2
Mladen - ไม่คุณควรจบประโยคก่อนหน้าด้วยเครื่องหมายอัฒภาค ;) Jeff - โอเคใส่ [Offset] แทน ฉันเดาว่า Offset เป็นคำสงวน อย่างที่บอกฉันไม่ได้ทดสอบ
Rob Farley

1
แค่พูดซ้ำ ๆ กับตัวเองเพราะนี่เป็นปัญหาที่พบเห็นได้บ่อย ตัด CreateionDate ลงเป็นวันในการทดสอบเหล่านี้ทั้งหมด (ทางด้านขวาเท่านั้นหรือคุณฆ่า SARG) โดยใช้ DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) ซึ่งทำงานโดยการลบวันที่ที่ให้มาจากศูนย์ซึ่ง Microsoft SQL Server แปลว่า 1900-01-01 00:00:00 และระบุจำนวนวัน จากนั้นค่านี้จะถูกเพิ่มเข้าไปในวันที่เป็นศูนย์โดยให้วันที่เดียวกันกับเวลาที่ถูกตัดทอน
IDisposable

1
IDisposable - ใช่ฉันทำแบบนั้นบ่อยๆ ฉันไม่ได้กังวลเกี่ยวกับการทำที่นี่ มันจะไม่เร็วไปกว่าการแคสต์เป็น int แต่มีความยืดหยุ่นในการนับชั่วโมงเดือนอะไรก็ตาม
Rob Farley

1
ฉันเพิ่งเขียนบล็อกโพสต์เกี่ยวกับการแก้ปัญหานี้ด้วย DENSE_RANK () ด้วย tinyurl.com/denserank
Rob Farley

18

หากคุณสามารถเปลี่ยนสคีมาของตารางได้ฉันขอแนะนำให้เพิ่มคอลัมน์LongestStreakลงในตารางซึ่งคุณจะกำหนดเป็นจำนวนวันตามลำดับที่ลงท้ายCreationDateด้วย ง่ายต่อการอัปเดตตารางในเวลาเข้าสู่ระบบ (คล้ายกับสิ่งที่คุณทำอยู่แล้วหากไม่มีแถวของวันปัจจุบันคุณจะตรวจสอบว่ามีแถวของวันก่อนหน้าหรือไม่ถ้าเป็นจริงคุณจะเพิ่มLongestStreakใน แถวใหม่มิฉะนั้นคุณจะตั้งค่าเป็น 1)

ข้อความค้นหาจะชัดเจนหลังจากเพิ่มคอลัมน์นี้:

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

1
+1 ฉันมีความคิดคล้าย ๆ กัน แต่มีฟิลด์บิต (IsConsecutive) ที่จะเป็น 1 หากมีการบันทึกของวันก่อนหน้ามิฉะนั้นจะเป็น 0
Fredrik Mörk

7
เราจะไม่เปลี่ยนสคีมาสำหรับสิ่งนี้
Jeff Atwood

และ IsConsecutive สามารถเป็นคอลัมน์จากการคำนวณที่กำหนดไว้ในตาราง UserHistory คุณยังสามารถทำให้เป็นคอลัมน์คำนวณที่เป็นรูปธรรม (ที่เก็บไว้) ที่สร้างขึ้นเมื่อแทรกแถว IFF (ถ้าและเฉพาะในกรณี) คุณจะแทรกแถวตามลำดับเวลาเสมอ
IDisposable

(เนื่องจาก NOBODY จะทำการ SELECT * เราทราบดีว่าการเพิ่มคอลัมน์ที่คำนวณนี้จะไม่ส่งผลกระทบต่อแผนการสืบค้นเว้นแต่ว่าคอลัมน์จะถูกอ้างอิง ... ใช่มั้ย!?)
IDisposable

3
เป็นวิธีแก้ปัญหาที่ถูกต้องแน่นอน แต่ไม่ใช่สิ่งที่ฉันขอ ก็เลยยกนิ้วให้ ..
Jeff Atwood

6

SQL ที่แสดงออกอย่างสวยงามตามแนวของ:

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

สมมติว่าคุณมีฟังก์ชันการรวมที่กำหนดโดยผู้ใช้ตามแนวของ (ระวังนี่คือบั๊กกี้):

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

ดูเหมือนว่าคุณสามารถใช้ประโยชน์จากข้อเท็จจริงที่ว่าการต่อเนื่องเกิน n วันจะต้องมี n แถว

สิ่งที่ชอบ:

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

ใช่เราสามารถกำหนดประตูได้ตามจำนวนบันทึกอย่างแน่นอน .. แต่นั่นช่วยกำจัดความเป็นไปได้บางอย่างเท่านั้นเนื่องจากเรามีเวลาเยี่ยมชม 120 วันในช่วงหลายปีที่มีช่องว่างมากมายในแต่ละวัน
Jeff Atwood

1
โอเค แต่เมื่อคุณได้รับรางวัลจากหน้านี้แล้วคุณจะต้องเรียกใช้เพียงครั้งเดียวต่อวัน ฉันคิดว่าสำหรับกรณีนั้นสิ่งที่กล่าวมาข้างต้นจะเป็นเคล็ด ในการติดตามสิ่งที่คุณต้องทำคือเปลี่ยน WHERE clause เป็นหน้าต่างบานเลื่อนโดยใช้ BETWEEN
บิล

1
การดำเนินงานแต่ละครั้งไม่มีสถานะและเป็นแบบสแตนด์อโลน ไม่มีความรู้เกี่ยวกับการวิ่งก่อนหน้านี้นอกเหนือจากตารางในคำถาม
Jeff Atwood

3

การทำเช่นนี้กับแบบสอบถาม SQL เดียวดูเหมือนจะซับซ้อนเกินไปสำหรับฉัน ขอแบ่งคำตอบนี้ออกเป็นสองส่วน

  1. สิ่งที่คุณควรทำจนถึงตอนนี้และควรเริ่มทำตอนนี้:
    เรียกใช้งาน cron รายวันที่ตรวจสอบผู้ใช้ทุกคนที่เข้าสู่ระบบในวันนี้จากนั้นเพิ่มตัวนับหากมีหรือตั้งค่าเป็น 0 หากเขายังไม่ได้
  2. สิ่งที่คุณควรทำตอนนี้:
    - ส่งออกตารางนี้ไปยังเซิร์ฟเวอร์ที่ไม่ได้ใช้งานเว็บไซต์ของคุณและไม่จำเป็นต้องใช้ไปสักพัก ;)
    - จัดเรียงตามผู้ใช้แล้ววันที่
    - ผ่านมันไปตามลำดับเก็บเคาน์เตอร์ ...

เราสามารถเขียนโค้ดเพื่อค้นหาและวนซ้ำได้นั่นคือ .. dary ฉันพูดว่า .. เล็กน้อย ฉันสงสัยเกี่ยวกับ SQL วิธีเดียวในขณะนี้
Jeff Atwood

2

หากสิ่งนี้สำคัญสำหรับคุณมากให้หาแหล่งที่มาของเหตุการณ์นี้และขับเคลื่อนตารางเพื่อให้ข้อมูลนี้แก่คุณ ไม่จำเป็นต้องฆ่าเครื่องจักรด้วยคำค้นหาที่บ้าคลั่งทั้งหมด


2

คุณสามารถใช้ CTE แบบเรียกซ้ำ (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 มีบทที่สมบูรณ์เกี่ยวกับเรื่องนี้ใน SQL for Smarties (เรียกว่า Runs and Sequences) ฉันไม่มีหนังสือเล่มนั้นที่บ้านดังนั้นเมื่อฉันไปทำงาน ... ฉันจะตอบตามความเป็นจริง (สมมติว่าตารางประวัติเรียกว่า dbo.UserHistory และจำนวนวันคือ @Days)

ลูกค้าเป้าหมายอื่นมาจากบล็อกของ SQL Team เกี่ยวกับการทำงาน

ความคิดอื่น ๆ ที่ฉันมี แต่ไม่มีเซิร์ฟเวอร์ SQL ที่สะดวกในการทำงานที่นี่คือการใช้ CTE กับ ROW_NUMBER ที่แบ่งพาร์ติชันดังนี้:

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

ดังกล่าวข้างต้นมีแนวโน้มWAY แข็งกว่ามันจะต้องมี แต่ที่เหลือเป็นคันสมองสำหรับเมื่อคุณมีบางความหมายอื่น ๆ ของ "รัน" มากกว่าเพียงแค่วัน


2

ตัวเลือก SQL Server 2012สองสามตัว (สมมติว่า N = 100 ด้านล่าง)

;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 

แม้ว่าข้อมูลตัวอย่างของฉันสิ่งต่อไปนี้จะมีประสิทธิภาพมากกว่า

;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

ทั้งสองอาศัยข้อ จำกัด ที่ระบุไว้ในคำถามที่ว่ามีการบันทึกสูงสุดหนึ่งรายการต่อวันต่อผู้ใช้


1

อะไรทำนองนี้?

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

ฉันใช้คุณสมบัติทางคณิตศาสตร์อย่างง่ายเพื่อระบุว่าใครเข้าถึงไซต์อย่างต่อเนื่อง คุณสมบัตินี้คือคุณควรมีความแตกต่างของวันระหว่างการเข้าถึงครั้งแรกและครั้งสุดท้ายเท่ากับจำนวนบันทึกในบันทึกตารางการเข้าถึงของคุณ

นี่คือสคริปต์ SQL ที่ฉันทดสอบใน Oracle DB (ควรทำงานในฐานข้อมูลอื่นด้วย):

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

สคริปต์การเตรียมตาราง:

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

คำสั่ง cast(convert(char(11), @startdate, 113) as datetime)จะลบส่วนเวลาของวันที่ออกดังนั้นเราจึงเริ่มตอนเที่ยงคืน

ฉันจะถือว่าcreationdateและuseridคอลัมน์คอลัมน์ถูกจัดทำดัชนี

ฉันเพิ่งรู้ว่าสิ่งนี้ไม่ได้บอกผู้ใช้ทั้งหมดและจำนวนวันที่ติดต่อกันทั้งหมด แต่จะบอกให้คุณทราบว่าผู้ใช้รายใดจะเข้าเยี่ยมชมในจำนวนวันที่กำหนดนับจากวันที่คุณเลือก

วิธีแก้ไข:

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

ฉันได้ตรวจสอบสิ่งนี้แล้วและจะค้นหาผู้ใช้ทั้งหมดและวันที่ทั้งหมด มันขึ้นอยู่กับโซลูชันแรก (ตลก?) ของ Spencerแต่ของฉันได้ผล

อัปเดต: ปรับปรุงการจัดการวันที่ในโซลูชันที่สอง


ปิด แต่เราต้องการบางสิ่งที่เหมาะกับช่วงเวลา (n) วันใด ๆ ไม่ใช่ในวันที่เริ่มต้นคงที่
Jeff Atwood

0

สิ่งนี้ควรทำในสิ่งที่คุณต้องการ แต่ฉันไม่มีข้อมูลเพียงพอที่จะทดสอบประสิทธิภาพ สิ่งที่ CONVERT / FLOOR ที่ซับซ้อนคือการตัดส่วนเวลาออกจากฟิลด์วันที่และเวลา หากคุณใช้ SQL Server 2008 คุณสามารถใช้ CAST (x.CreationDate AS DATE)

ประกาศ @Range เป็น INT
SET @Range = 10

เลือก DISTINCT UserId, CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate)))
  จาก tblUserLogin a
มีที่ไหน
   (เลือก 1 
      จาก tblUserLogin b 
     WHERE a.userId = b.userId 
       และ (เลือก COUNT (DISTINCT (แปลง (DATETIME, FLOOR (CONVERT (FLOAT, CreationDate))))) 
              จาก tblUserLogin c 
             ที่ไหน c.userid = b.userid 
               และแปลง (DATETIME, FLOOR (CONVERT (FLOAT, c.CreationDate))) ระหว่างแปลง (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate))) และ CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate)) ) + @ ช่วง -1) = @ ช่วง)

สคริปต์การสร้าง

สร้างตาราง [dbo] [tblUserLogin] (
    [Id] [int] IDENTITY (1,1) ไม่เป็นโมฆะ,
    [UserId] [int] NULL,
    [CreationDate] [วันที่และเวลา] NULL
) ใน [หลัก]

โหดทีเดียว 26 วินาทีใน 406,624 แถว
Jeff Atwood

คุณตรวจสอบเพื่อมอบป้ายบ่อยเพียงใด หากเป็นเพียงวันละครั้งการตี 26 วินาทีในช่วงเวลาที่ช้าก็ไม่ได้แย่ขนาดนั้น แม้ว่าประสิทธิภาพจะช้าลงเมื่อโต๊ะเติบโตขึ้น หลังจากอ่านคำถามซ้ำแล้วซ้ำอีกเวลาอาจไม่เกี่ยวข้องเนื่องจากมีการบันทึกเพียงครั้งเดียวต่อวัน
Dave Barker

0

Spencer เกือบจะทำ แต่นี่ควรเป็นรหัสการทำงาน:

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

จากด้านบนของหัวของฉัน 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

ยังไม่ได้ทดสอบและเกือบจะต้องมีการแปลงสำหรับ MSSQL แต่ฉันคิดว่านั่นให้แนวคิดบางอย่าง


0

แล้วคนที่ใช้ตาราง Tally ล่ะ? เป็นไปตามแนวทางอัลกอริทึมที่มากขึ้นและแผนการดำเนินการก็เป็นเรื่องง่าย เติม tallyTable ด้วยตัวเลขตั้งแต่ 1 ถึง 'MaxDaysBehind' ที่คุณต้องการสแกนตาราง (เช่น 90 จะค้นหา 3 เดือนหลังเป็นต้น)

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

ปรับแต่งแบบสอบถามของบิลเล็กน้อย คุณอาจต้องตัดวันที่ก่อนจัดกลุ่มเพื่อนับการเข้าสู่ระบบเพียงหนึ่งครั้งต่อวัน ...

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

แก้ไขเพื่อใช้ DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) แทนการแปลง (char (10), CreationDate, 101)

@IDisposable ฉันต้องการใช้ datepart ก่อนหน้านี้ แต่ฉันขี้เกียจเกินไปที่จะค้นหาไวยากรณ์ดังนั้นฉันจึงคิดว่า id ใช้การแปลงแทน ฉันรู้ว่ามันมีผลกระทบอย่างมากขอบคุณ! ตอนนี้ฉันรู้.


การตัดทอน SQL DATETIME เป็นวันที่เท่านั้นทำได้ดีที่สุดกับ DATEADD (dd, DATEDIFF (dd, 0, UH.CreationDate), 0)
IDisposable

(ข้างต้นทำงานโดยใช้ความแตกต่างของทั้งวันระหว่าง 0 (เช่น 1900-01-01 00: 00: 00.000) แล้วบวกความแตกต่างทั้งวันกลับไปเป็น 0 (เช่น 1900-01-01 00:00:00) ส่งผลให้ส่วนเวลาของ DATETIME ถูกยกเลิก)
IDisposable

0

สมมติว่าสคีมาเป็นดังนี้:

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

สิ่งนี้จะดึงช่วงที่ติดกันจากลำดับวันที่ที่มีช่องว่าง

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
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.