SQL для визначення мінімальних послідовних днів доступу?


125

Наступна таблиця історії користувачів містить один запис на кожен день, коли певний користувач має доступ до веб-сайту (за 24-годинний UTC). Він має багато тисяч записів, але лише один запис на день на кожного користувача. Якщо користувач не отримав доступ до веб-сайту за цей день, запис не буде створено.

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

Що я шукаю - це SQL-запит на цій таблиці з хорошою продуктивністю , який підказує мені, які користувачі користувалися веб-сайтом протягом (n) безперервних днів, не пропускаючи день.

Іншими словами, скільки користувачів мають (n) записів у цій таблиці з послідовними (день-до-дня або після-день) датами ? Якщо в послідовності відсутній якийсь день, послідовність порушується і повинна знову перезапуститись на 1; ми шукаємо користувачів, які досягли безперервної кількості днів тут без прогалин.

Будь-яка схожість між цим запитом і певним значком переповнення стека , звичайно, випадкова .. :)


Я отримав знак ентузіаста після 28 (<30) днів членства. Містика.
Кирило Васильович Лядвінський

3
Чи зберігається ваша дата як UTC? Якщо так, то що станеться, якщо один день жителя ЦА відвідує сайт о 8 ранку, а наступного дня о 20:00? Хоча він / вона відвідує дні поспіль у Тихоокеанському часовому поясі, він не буде записаний як такий у БД, оскільки БД зберігає часи як UTC.
Хлопець

Джефф / Джеррод - чи можете ви перевірити meta.stackexchange.com/questions/865/… будь ласка?
Роб Фарлі

Відповіді:


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

Редагувати:

[Джефф Етвуд] Це прекрасне швидке рішення і заслуговує на те, щоб його прийняти, але рішення Роб Фарлі також відмінне і, мабуть, навіть швидше (!). Перевірте це теж!


@Artem: Це те, що я спочатку думав, але коли я думав про це, якщо у вас індекс на (UserId, CreationDate), записи відображатимуться послідовно в індексі, і він повинен працювати добре.
Мехрдад Афшарі

Підсумуючи це, я отримую результати за ~ 15 секунд на 500k рядків.
Jim T

4
Обрізати CreateionDate до днів у всіх цих тестах (лише праворуч або ти вбиваєш SARG) за допомогою DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) Це працює, віднімаючи введені дати від нуля - який Microsoft SQL Server інтерпретує як 1900-01-01 00:00:00 і дає кількість днів. Потім це значення повторно додається до нульової дати, даючи ту саму дату з усіченим часом.
Ідентифікатор

1
все, що я можу вам сказати, без змін IDisposable розрахунок невірний . Я особисто підтвердив ці дані. Деякі користувачі з пробілами в 1 день НЕОБХІДНО отримують значок неправильно.
Джефф Етвуд

3
Цей запит може пропустити візит, який відбувається о 23: 59: 59.5 - як щодо того, щоб змінити його на:, ON uh2.CreationDate >= uh1.CreationDate AND uh2.CreationDate < DATEADD(dd, DATEDIFF(dd, 0, uh1.CreationDate) + @days, 0)щоб означати "Ще не на 31 день пізніше". Також означає, що ви можете пропустити обчислення @секунд.
Роб Фарлі

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  

Ідея полягає в тому, що якщо у нас є список днів (як число) та число рядків, то пропущені дні зсуву між цими двома списками зростають трохи більше. Тому ми шукаємо діапазон, який має послідовне зміщення.

Ви можете використати "ЗАМОВИТИ НА NumConsecutiveDays DESC" наприкінці цього або сказати "ВІДПОВІДНІЙ кількість (*)> 14" для порогу ...

Я цього ще не перевіряв - просто записую це вгору. Сподіваємось, працює в SQL2005 і далі.

... і мені дуже допоможе індекс на ім'я таблиці (UserID, CreationDate)

Відредаговано: Вимикається зміщення - це зарезервоване слово, тому я використовував TheOffset замість цього.

Відредаговано: Пропозиція використовувати COUNT (*) є дуже вірною - я мав би це зробити в першу чергу, але насправді не думав. Раніше замість цього використовували dateiff (day, min (CreationDate), max (CreationDate)).

Роб


1
о, ви також повинні додати; перед с ->; с
Младен Прайдич

2
Младен - ні, попередню заяву слід закінчити крапкою з двократкою. ;) Джефф - Гаразд, замість [Offset]. Я думаю, що офсет - це застережене слово. Як я вже говорив, я цього не перевіряв.
Роб Фарлі

1
Просто повторюю себе, бо це часто зустрічається питання. Обрізати CreateionDate до днів у всіх цих тестах (лише з правого боку або ти вбиваєш SARG) за допомогою DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) Це працює, віднімаючи додану дату від нуля - який Microsoft SQL Server інтерпретує як 1900-01-01 00:00:00 і дає кількість днів. Потім це значення повторно додається до нульової дати, даючи ту саму дату з усіченим часом.
Ідентифікатор

1
Ідентифікатор - так, я часто це роблю сам. Я просто не хвилювався про це, роблячи це тут. Це не було б швидше, ніж кидати його до int, але має гнучкість рахувати години, місяці, що завгодно.
Роб Фарлі

1
Я щойно писав допис у блозі про вирішення цього питання також із DENSE_RANK (). tinyurl.com/denserank
Роб Фарлі

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
ми не збираємось змінювати схему для цього
Джефф Етвуд

І IsConsecutive може бути обчисленим стовпцем, визначеним у таблиці UserHistory. Ви також можете зробити його матеріалізованим (збереженим) обчисленим стовпцем, який створюється, коли рядок вставляється IFF (якщо і ТОЛЬКО якщо), ви завжди вставляєте рядки в хронологічному порядку.
Ідентифікатор

(тому що NOBODY зробить SELECT *, ми знаємо, що додавання цього обчисленого стовпця не вплине на плани запитів, якщо на посилання не буде посилання на стовпець ... правильно хлопці?!?)
IDisposable

3
це, безумовно, правильне рішення, але це не те, про що я просив. Тож я даю йому "великі пальці набік" ..
Джефф Етвуд

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 у розсувне вікно за допомогою BETWEEN.
Білл

1
кожен запуск завдання є бездержавним та самостійним; вона не має знань про попередні прогони, окрім таблиці у питанні
Джефф Етвуд

3

Зробити це за допомогою одного запиту SQL мені здається надто складним. Дозвольте розбити цю відповідь на дві частини.

  1. Що ви мали б робити до цього часу і почати робити зараз:
    Запускайте щоденну роботу cron, яка перевіряє кожного користувача, наскільки він увійшов сьогодні, а потім збільшує лічильник, якщо він має або встановлює його до 0, якщо він цього не зробив.
  2. Що вам слід зробити зараз:
    - Експортуйте цю таблицю на сервер, який не працює на вашому веб-сайті і не знадобиться якийсь час. ;)
    - Сортуйте його за користувачем та датою.
    - пройдіть це послідовно, тримайте лічильник ...

ми можемо написати код на запит і цикл, це .. я кажу, що тривожно. Мені цікаво лише про SQL на даний момент.
Джефф Етвуд

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

Джо Селко має повну главу з цього приводу в SQL для Smarties (називаючи це «Руни та послідовності»). У мене немає цієї книги вдома, тож коли я прийду на роботу, я справді відповім на це. (якщо припустити, що таблиця історії називається 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

Сказане вище, ймовірно, ШЛЯХІШЕ, ніж це має бути, але залишається головним мозком, коли у вас є якесь інше визначення "пробігу", а не лише дати.


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 (він також повинен працювати і в інших БД):

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

Я перевірив це, і він буде запитувати всіх користувачів і всі дати. Він заснований на першому (жарт?) Рішенні Спенсера , але міна працює.

Оновлення: покращено обробку дат у другому рішенні.


близько, але нам потрібно щось, що працює на будь-який (n) день, а не на фіксовану дату початку
Джефф Етвуд

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 
     ДЕ a.userId = b.userId 
       І (ВИБІРТЕ КУХНУ (DISTINCT (CONVERT (DATETIME, FLOOR) (CONVERT (FLOAT, CreationDate))))) 
              ВІД tblUserLogin c 
             ДЕ c.userid = b.userid 
               І КОНВЕРТ (DATETIME, FLOOR (CONVERT (FLOAT, c.CreationDate))) MED CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate))) та CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a.Creation) ) + @ Діапазон-1) = @ Ранг)

Сценарій створення

СТВОРИТИ ТАБЛИЦЮ [dbo]. [TblUserLogin] (
    [Id] [int] ІДЕНТИЧНІСТЬ (1,1) NOT NULL,
    [UserId] [int] NULL,
    [CreationDate] [datetime] NULL
) НА [ПЕРШИЙ]

досить жорстокий. 26 секунд через 406 624 рядки.
Джефф Етвуд

Як часто ви перевіряєте нагородження значка? Якщо це лише один раз на день, то уповільнення 26 секунд у повільному періоді не здається таким поганим. Хоча продуктивність сповільнюватиметься у міру зростання таблиці. Після повторного читання питання про зняття часу може виявитися нерелевантним, оскільки в день є лише один запис.
Дейв Баркер

0

Спенсер майже це зробив, але це повинен бути робочий код:

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 раніше, але я був занадто ледачий, щоб шукати синтаксис, тому я зрозумів, що використання ідентифікатора замість цього конвертувати. Я знаю, що це справило значний вплив. Дякую! Тепер я знаю.


Обрізання 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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.