SQL để xác định số ngày truy cập tối thiểu?


125

Bảng Lịch sử người dùng sau đây chứa một bản ghi cho mỗi ngày một người dùng nhất định đã truy cập một trang web (trong khoảng thời gian UTC 24 giờ). Nó có nhiều ngàn bản ghi, nhưng chỉ có một bản ghi mỗi ngày cho mỗi người dùng. Nếu người dùng không truy cập trang web cho ngày hôm đó, sẽ không có bản ghi nào được tạo.

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

Điều tôi đang tìm kiếm là một truy vấn SQL trên bảng này với hiệu suất tốt , cho tôi biết những người dùng nào đã truy cập trang web trong (n) ngày liên tục mà không bỏ lỡ một ngày.

Nói cách khác, có bao nhiêu người dùng có (n) bản ghi trong bảng này với các ngày liên tiếp (ngày trước hoặc ngày sau) ? Nếu bất kỳ ngày nào bị thiếu trong chuỗi, chuỗi bị hỏng và sẽ khởi động lại ở 1; chúng tôi đang tìm kiếm những người dùng đã đạt được số ngày liên tục ở đây mà không có khoảng trống.

Tất cả sự tương đồng giữa truy vấn này và huy hiệu Stack Overflow cụ thể hoàn toàn là ngẫu nhiên, tất nhiên .. :)


Tôi đã nhận được huy hiệu người đam mê sau 28 (<30) ngày thành viên. Thần bí.
Kirill V. Lyadvinsky

3
Ngày của bạn có được lưu dưới dạng UTC không? Nếu vậy, điều gì xảy ra nếu một cư dân CA truy cập trang web vào lúc 8 giờ sáng một ngày và sau đó 8 giờ tối ngày hôm sau? Mặc dù anh ấy / cô ấy truy cập vào những ngày liên tiếp trong Múi giờ Thái Bình Dương, nhưng nó sẽ không được ghi lại như vậy trong DB vì DB đang lưu trữ thời gian như UTC.
Guy

Jeff / Jarrod - bạn có thể kiểm tra meta.stackexchange.com/questions/865/ không?
Rob Farley

Câu trả lời:


69

Câu trả lời rõ ràng là:

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

BIÊN TẬP:

Được rồi đây là câu trả lời nghiêm túc của tôi:

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

BIÊN TẬP:

[Jeff Atwood] Đây là một giải pháp nhanh tuyệt vời và xứng đáng được chấp nhận, nhưng giải pháp của Rob Farley cũng rất tuyệt vời và thậm chí còn nhanh hơn (!). Vui lòng kiểm tra nó quá!


@Artem: Đó là những gì tôi nghĩ ban đầu nhưng khi tôi nghĩ về nó, nếu bạn có một chỉ mục trên (UserId, CreationDate), các bản ghi sẽ hiển thị liên tiếp trong chỉ mục và nó sẽ hoạt động tốt.
Mehrdad Afshari

Upvote cho cái này, tôi sẽ nhận được kết quả sau ~ 15 giây trên 500k hàng.
Jim T

4
Rút ngắn CreationDate xuống còn vài ngày trong tất cả các thử nghiệm này (chỉ ở phía bên phải hoặc bạn giết SARG) bằng DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) Điều này hoạt động bằng cách trừ ngày được cung cấp từ 0 - mà Microsoft SQL Server diễn giải là 1900-01-01 00:00:00 và đưa ra số ngày. Giá trị này sau đó được thêm lại vào ngày số 0 mang lại cùng ngày với thời gian bị cắt ngắn.
IDis Dùng

1
tất cả những gì tôi có thể nói với bạn là, nếu không có sự thay đổi của IDis, thì phép tính không chính xác . Cá nhân tôi đã xác nhận dữ liệu bản thân mình. Một số người dùng với 1 lỗ hổng ngày SẼ nhận được huy hiệu không chính xác.
Jeff Atwood

3
Truy vấn này có khả năng bỏ lỡ một lượt truy cập xảy ra vào lúc 23: 59: 59.5 - làm thế nào về việc thay đổi nó thành : ON uh2.CreationDate >= uh1.CreationDate AND uh2.CreationDate < DATEADD(dd, DATEDIFF(dd, 0, uh1.CreationDate) + @days, 0), có nghĩa là "Chưa đến ngày thứ 31 sau". Cũng có nghĩa là bạn có thể bỏ qua tính toán @seconds.
Rob Farley

147

Còn về (và vui lòng đảm bảo câu lệnh trước kết thúc bằng dấu chấm phẩy):

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  

Ý tưởng là nếu chúng ta có danh sách các ngày (dưới dạng số) và một hàng số, thì những ngày bị bỏ lỡ sẽ làm cho phần bù giữa hai danh sách này lớn hơn một chút. Vì vậy, chúng tôi đang tìm kiếm một phạm vi có một sự bù phù hợp.

Bạn có thể sử dụng "ĐẶT HÀNG B NumNG NumCons liên tụcDays DESC" ở cuối phần này hoặc nói "HAVING Count (*)> 14" cho một ngưỡng ...

Tôi đã không kiểm tra điều này mặc dù - chỉ viết nó ra khỏi đỉnh đầu của tôi. Hy vọng hoạt động trong SQL2005 trở đi.

... và sẽ được trợ giúp rất nhiều bởi một chỉ mục trên tablename (UserID, CreationDate)

Đã chỉnh sửa: Hóa ra Offset là một từ dành riêng, vì vậy tôi đã sử dụng ThePackset thay thế.

Đã chỉnh sửa: Đề xuất sử dụng COUNT (*) là rất hợp lệ - Tôi nên thực hiện điều đó ngay từ đầu nhưng không thực sự suy nghĩ. Trước đây, nó đã sử dụng dateiff (ngày, min (CreationDate), max (CreationDate)) để thay thế.

Cướp


1
oh bạn cũng nên thêm; trước với ->; với
Mladen Prajdic

2
Mladen - không, bạn nên kết thúc câu lệnh trước bằng dấu chấm phẩy. ;) Jeff - Ok, thay vào đó, đặt [Offset]. Tôi đoán Offset là một từ dành riêng. Như tôi đã nói, tôi đã không thử nó.
Rob Farley

1
Chỉ cần lặp lại bản thân mình, bởi vì đây là một vấn đề thường thấy. Rút ngắn CreationDate xuống còn vài ngày trong tất cả các thử nghiệm này (chỉ ở phía bên phải hoặc bạn giết SARG) bằng DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) Điều này hoạt động bằng cách trừ ngày được cung cấp từ 0 - mà Microsoft SQL Server diễn giải là 1900-01-01 00:00:00 và đưa ra số ngày. Giá trị này sau đó được thêm lại vào ngày số 0 mang lại cùng ngày với thời gian bị cắt ngắn.
IDis Dùng

1
IDis Dùng - yup, tôi thường làm điều đó bản thân mình. Tôi chỉ không lo lắng về việc làm nó ở đây. Sẽ không nhanh hơn việc chuyển nó thành int, nhưng có thể linh hoạt đếm giờ, tháng, bất cứ điều gì.
Rob Farley

1
Tôi cũng vừa mới viết một bài đăng trên blog về việc giải quyết vấn đề này với DENSE_RANK (). tinyurl.com/denserank
Rob Farley

18

Nếu bạn có thể thay đổi lược đồ bảng, tôi khuyên bạn nên thêm một cột LongestStreakvào bảng mà bạn đã đặt thành số ngày liên tiếp kết thúc với CreationDate. Thật dễ dàng để cập nhật bảng vào thời điểm đăng nhập (tương tự như những gì bạn đang làm, nếu không có hàng nào tồn tại của ngày hiện tại, bạn sẽ kiểm tra xem có hàng nào tồn tại cho ngày hôm trước không. Nếu đúng, bạn sẽ tăng LongestStreaktrong hàng mới, nếu không, bạn sẽ đặt nó thành 1.)

Truy vấn sẽ rõ ràng sau khi thêm cột này:

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

1
+1 Tôi cũng có suy nghĩ tương tự, nhưng với một trường bit (IsCons liên tiếp) sẽ là 1 nếu có bản ghi cho ngày hôm trước, nếu không thì 0.
Fredrik Mörk

7
chúng ta sẽ không thay đổi lược đồ cho việc này
Jeff Atwood

Và IsCons liên tục có thể là một cột được tính toán được xác định trong bảng UserHistory. Bạn cũng có thể biến nó thành một cột được tính toán (lưu trữ) được vật chất hóa được tạo khi hàng được chèn IFF (nếu và CHỈ nếu) bạn luôn chèn các hàng theo thứ tự thời gian.
IDis Dùng

(vì NOBODY sẽ thực hiện CHỌN *, chúng tôi biết việc thêm cột được tính toán này sẽ không ảnh hưởng đến các kế hoạch truy vấn trừ khi cột được tham chiếu ... đúng không?!?)
IDis Dùng

3
nó chắc chắn là một giải pháp hợp lệ nhưng nó không phải là những gì tôi yêu cầu. Vì vậy, tôi cho nó một "ngón tay cái đi ngang" ..
Jeff Atwood

6

Một số SQL biểu cảm độc đáo dọc theo dòng:

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

Giả sử bạn có một hàm tổng hợp do người dùng xác định một cái gì đó dọc theo dòng (hãy coi chừng đây là lỗi):

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

Có vẻ như bạn có thể tận dụng thực tế là để liên tục trong n ngày sẽ yêu cầu phải có n hàng.

Vì vậy, một cái gì đó như:

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

vâng, chúng ta có thể cổng nó bởi số lượng hồ sơ, chắc chắn .. nhưng đó chỉ loại bỏ một số khả năng, như chúng ta có thể có 120 ngày kể từ khi quý khách đến thăm qua nhiều năm với rất nhiều khoảng trống hàng ngày
Jeff Atwood

1
Được rồi, nhưng một khi bạn bị cuốn vào việc trao giải cho trang này, bạn chỉ cần chạy nó một lần mỗi ngày. Tôi nghĩ trong trường hợp đó, một cái gì đó như trên sẽ làm nên chuyện. Để bắt kịp, tất cả những gì bạn cần làm là biến mệnh đề WHERE thành cửa sổ trượt bằng cách sử dụng GIỮA.
Hóa đơn

1
mỗi lần chạy nhiệm vụ là không trạng thái và độc lập; nó không có kiến ​​thức về các lần chạy trước ngoài bảng trong câu hỏi
Jeff Atwood

3

Làm điều này với một truy vấn SQL có vẻ quá phức tạp đối với tôi. Hãy để tôi phá vỡ câu trả lời này thành hai phần.

  1. Những gì bạn nên làm cho đến bây giờ và nên bắt đầu làm ngay bây giờ:
    Chạy một công việc định kỳ hàng ngày để kiểm tra mọi người dùng mà anh ta đã đăng nhập ngày hôm nay và sau đó tăng bộ đếm nếu anh ta có hoặc đặt nó thành 0 nếu anh ta không.
  2. Những gì bạn nên làm bây giờ:
    - Xuất bảng này sang máy chủ không chạy trang web của bạn và sẽ không cần thiết trong một thời gian. ;)
    - Sắp xếp nó theo người dùng, sau đó ngày.
    - đi qua nó một cách tuần tự, giữ một bộ đếm ...

chúng ta có thể viết mã vào truy vấn và vòng lặp, đó là .. dary tôi nói .. tầm thường. Tôi tò mò về cách duy nhất của SQL tại thời điểm này.
Jeff Atwood

2

Nếu điều này rất quan trọng với bạn, hãy lấy sự kiện này và lái một bảng để cung cấp cho bạn thông tin này. Không cần phải giết máy với tất cả những truy vấn điên rồ đó.


2

Bạn có thể sử dụng CTE đệ quy (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 có một chương hoàn chỉnh về vấn đề này trong SQL dành cho Smarties (gọi nó là Chạy và Chuỗi). Tôi không có cuốn sách đó ở nhà, vì vậy khi tôi đi làm ... Tôi thực sự sẽ trả lời điều này. (giả sử bảng lịch sử được gọi là dbo.UserHistory và số ngày là @Days)

Một khách hàng tiềm năng khác là từ blog của SQL Team đang chạy

Ý tưởng khác mà tôi đã có, nhưng không có máy chủ SQL tiện dụng để làm việc ở đây là sử dụng CTE với ROW_NUMBER được phân vùng như thế này:

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

Ở trên có khả năng là CÁCH HARDER hơn nó phải có, nhưng để lại như một tiếng tích tắc khi bạn có một số định nghĩa khác về "chạy" hơn là chỉ ngày.


2

Một vài tùy chọn SQL Server 2012 (giả sử N = 100 bên dưới).

;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 

Mặc dù với dữ liệu mẫu của tôi, cách sau đây hiệu quả hơn

;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

Cả hai đều dựa vào các ràng buộc được nêu trong câu hỏi rằng có nhiều nhất một bản ghi mỗi ngày cho mỗi người dùng.


1

Một cái gì đó như thế này?

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

Tôi đã sử dụng một thuộc tính toán học đơn giản để xác định người liên tiếp truy cập trang web. Thuộc tính này là bạn nên có chênh lệch ngày giữa lần truy cập đầu tiên và lần cuối bằng số lượng bản ghi trong nhật ký bảng truy cập của bạn.

Đây là tập lệnh SQL mà tôi đã thử nghiệm trong Oracle DB (nó cũng hoạt động trong các DB khác):

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

Kịch bản chuẩn bị bảng:

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

Tuyên bố cast(convert(char(11), @startdate, 113) as datetime)loại bỏ phần thời gian của ngày để chúng tôi bắt đầu vào nửa đêm.

Tôi cũng sẽ cho rằng creationdateuserid cột được lập chỉ mục.

Tôi chỉ nhận ra rằng điều này sẽ không cho bạn biết tất cả người dùng và tổng số ngày liên tiếp của họ. Nhưng sẽ cho bạn biết những người dùng nào sẽ truy cập vào một số ngày nhất định kể từ ngày bạn chọn.

Giải pháp sửa đổi:

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

Tôi đã kiểm tra cái này và nó sẽ truy vấn tất cả người dùng và tất cả các ngày. Nó dựa trên giải pháp (trò đùa?) Của Spencer , nhưng của tôi hoạt động.

Cập nhật: cải thiện việc xử lý ngày trong giải pháp thứ hai.


đóng, nhưng chúng tôi cần một cái gì đó hoạt động trong bất kỳ (n) ngày nào, không phải vào ngày bắt đầu cố định
Jeff Atwood

0

Điều này sẽ làm những gì bạn muốn nhưng tôi không có đủ dữ liệu để kiểm tra hiệu quả. Công cụ CONVERT / FLOOR phức tạp là loại bỏ phần thời gian khỏi trường datetime. Nếu bạn đang sử dụng SQL Server 2008 thì bạn có thể sử dụng CAST (x.CreationDate AS DATE).

KHAI THÁC @ Thay đổi như INT
THIẾT LẬP @Range = 10

CHỌN DISTINCT UserId, CHUYỂN ĐỔI (DATETIME, FLOOR (CHUYỂN ĐỔI (FLOAT, a.CreationDate)))
  TỪ tblUserLogin a
Ở ĐÂU
   (CHỌN 1 
      TỪ tblUserLogin b 
     Ở đâu a.userId = b.userId 
       VÀ (CHỌN QUỐC GIA (DISTINCT (CHUYỂN ĐỔI (DATETIME, FLOOR (CHUYỂN ĐỔI (FLOAT, CreationDate))))) 
              TỪ tblUserLogin c 
             Ở đâu c.userid = b.userid 
               VÀ CHUYỂN ĐỔI (DATETIME, FLOOR (CONVERT (FLOAT, c.CreationDate))) GIỮA CHUYỂN ĐỔI (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate)) và CONVERT (DATETIME, FLO ) + @ Phạm vi-1) = @ Thay đổi)

Tạo kịch bản

TẠO BẢNG [dbo]. [TblUserLogin] (
    [Id] [int] IDENTITY (1,1) KHÔNG NULL,
    [Người dùng] [int] NULL,
    [CreationDate] [datetime] NULL
) TRÊN [CHÍNH HÃNG]

khá tàn bạo. 26 giây trên 406.624 hàng.
Jeff Atwood

Bạn có thường xuyên kiểm tra để trao huy hiệu không? Nếu chỉ một lần một ngày thì cú đánh 26 giây trong khoảng thời gian chậm dường như không tệ. Mặc dù, hiệu suất sẽ chậm lại khi bảng phát triển. Sau khi đọc lại câu hỏi tước thời gian có thể không liên quan vì chỉ có một bản ghi mỗi ngày.
Dave Barker

0

Spencer gần như đã làm điều đó, nhưng đây phải là mã làm việc:

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

Tắt đầu tôi, 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

Chưa được kiểm tra và gần như chắc chắn cần một số chuyển đổi cho MSSQL, nhưng tôi nghĩ rằng điều đó đưa ra một số ý tưởng.


0

Làm thế nào về một người sử dụng bảng Tally? Nó tuân theo một cách tiếp cận thuật toán hơn, và kế hoạch thực hiện là một cách dễ dàng. Dân số tallyTable với các số từ 1 đến 'MaxDaysBehind' mà bạn muốn quét bảng (tức là 90 sẽ tìm kiếm 3 tháng sau, v.v.).

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

Tinh chỉnh truy vấn của Bill một chút. Bạn có thể phải cắt ngắn ngày trước khi nhóm để chỉ đếm một lần đăng nhập mỗi ngày ...

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

EDITED sử dụng DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) thay vì convert (char (10), CreationDate, 101).

@IDis Dùng một lần Tôi đã tìm cách sử dụng datepart trước đó nhưng tôi quá lười để tìm cú pháp nên tôi đã tìm id sử dụng convert thay thế. Tôi không biết nó có tác động đáng kể Cảm ơn! bây giờ tôi biết.


Cắt bỏ một DATETIME SQL cho đến nay chỉ được thực hiện tốt nhất với DATEADD (đ, DATEDIFF (đ, 0, UH.CreationDate), 0)
IDisposable

(cách trên hoạt động bằng cách lấy chênh lệch trong cả ngày giữa 0 (ví dụ: 1900-01-01 00: 00: 00.000) và sau đó thêm chênh lệch đó trong cả ngày trở về 0 (ví dụ: 1900-01-01 00:00:00) Điều này dẫn đến kết quả là phần thời gian của DATETIME bị loại bỏ)
IDis Dùng vào

0

giả sử một lược đồ đi như sau:

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

điều này sẽ trích xuất các phạm vi liền kề từ một chuỗi ngày với các khoảng trống.

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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.