SQL для определения минимального количества последовательных дней доступа?

125

Следующая таблица истории пользователей содержит одну запись для каждого дня, когда данный пользователь заходил на веб-сайт (в 24-часовом периоде в формате UTC). В нем много тысяч записей, но только одна запись в день на пользователя. Если пользователь не заходил на веб-сайт в этот день, запись не создается.

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

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

Другими словами, сколько пользователей имеют (n) записей в этой таблице с последовательными датами (на день до или на день после) ? Если в последовательности пропущен какой-либо день, последовательность прерывается и должна возобновиться снова с 1; мы ищем пользователей, которые прожили здесь непрерывное количество дней без пропусков.

Любое сходство между этим запросом и конкретным значком Stack Overflow , конечно же, чисто случайное .. :)

Джефф Этвуд
источник
Я получил значок энтузиаста после 28 (<30) дней членства. Мистика.
Кирилл В. Лядвинский
3
Ваша дата хранится в формате UTC? Если да, то что произойдет, если резидент ЦС зайдет на сайт в 8:00 в один день, а затем в 20:00 на следующий день? Хотя он / она посещает несколько дней подряд в тихоокеанском часовом поясе, это не будет записано как таковое в БД, потому что БД хранит время в формате UTC.
Guy
Джефф / Джаррод - не могли бы вы проверить 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), записи будут отображаться в индексе последовательно, и он должен работать хорошо.
Mehrdad Afshari
Проголосуйте за это, я получаю результаты через ~ 15 секунд на 500 тыс. Строк.
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 день БУДУТ получать значок неправильно.
Джефф Этвуд,
3
Этот запрос может пропустить посещение, которое происходит в 23: 59: 59.5 - как насчет того, чтобы изменить его на:, ON uh2.CreationDate >= uh1.CreationDate AND uh2.CreationDate < DATEADD(dd, DATEDIFF(dd, 0, uh1.CreationDate) + @days, 0)чтобы означать «Еще не на 31-й день позже». Также означает, что вы можете пропустить расчет @seconds.
Роб Фарли
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 и далее.

... и мне бы очень помог индекс по имени таблицы (UserID, CreationDate)

Отредактировано: Оказалось, что Offset - зарезервированное слово, поэтому вместо этого я использовал TheOffset.

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

обкрадывать

Роб Фарли
источник
1
о, вы также должны добавить; перед with ->; with
Mladen Prajdic
2
Младен - нет, предыдущее утверждение следует заканчивать точкой с запятой. ;) Джефф - Хорошо, поставь вместо [Offset]. Думаю, смещение - зарезервированное слово. Как я уже сказал, я его не тестировал.
Роб Фарли
1
Просто повторяюсь, потому что это часто встречающаяся проблема. Сократите CreateionDate до дней во всех этих тестах (только с правой стороны, или вы убьете SARG), используя DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0). Это работает путем вычитания предоставленной даты из нуля, что Microsoft SQL Server интерпретирует как 1900-01-01 00:00:00 и дает количество дней. Затем это значение снова добавляется к нулевой дате, что дает ту же дату с усеченным временем.
IDisposable,
1
IDisposable - да, я сам часто так делаю. Я просто не беспокоился о том, что он делает это здесь. Это не будет быстрее, чем приведение его к типу 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.
Фредрик Мёрк
7
мы не собираемся менять схему для этого
Джефф Этвуд,
И IsConsecutive может быть вычисляемым столбцом, определенным в таблице UserHistory. Вы также можете сделать его материализованным (сохраненным) вычисляемым столбцом, который создается при вставке строки. IFF (если и ТОЛЬКО если) вы всегда вставляете строки в хронологическом порядке.
IDisposable,
(поскольку НИКТО не выполняет 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);
        }
    }
}
Joshuamck
источник
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.
Bill
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
OMG Пони
источник
2

У Джо Селко есть полная глава об этом в SQL for Smarties (называющая это Runs and Sequences). У меня дома нет этой книги, поэтому, когда я приду на работу ... Я отвечу на это. (предполагается, что таблица истории называется dbo.UserHistory, а количество дней - @Days)

Еще одно замечание - из блога команды SQL о пробегах.

Другая идея, которая у меня была, но у меня нет удобного 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

Вышеупомянутое, вероятно, НАМНОГО ТРУДЧЕ, чем должно быть, но оставлено как щекотка для мозга, когда у вас есть другое определение «пробежки», кроме свиданий.

IDisposable
источник
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

Я проверил это, и он будет запрашивать всех пользователей и все даты. Он основан на первом (шутливом?) Решении Спенсера , но мое работает.

Обновление: улучшена обработка даты во втором решении.

Стивен Перельсон
источник
близко, но нам нужно что-то, что работает для любого (n) дневного периода, а не для фиксированной даты начала
Джефф Этвуд,
0

Это должно делать то, что вы хотите, но у меня недостаточно данных для проверки эффективности. Запутанный материал CONVERT / FLOOR состоит в том, чтобы убрать временную часть из поля datetime. Если вы используете SQL Server 2008, вы можете использовать CAST (x.CreationDate AS DATE).

ОБЪЯВИТЬ @Range как INT
УСТАНОВИТЬ @Range = 10

ВЫБЕРИТЕ DISTINCT UserId, CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate)))
  ОТ tblUserLogin a
ГДЕ СУЩЕСТВУЕТ
   (ВЫБРАТЬ 1 
      ОТ tblUserLogin b 
     ГДЕ a.userId = b.userId 
       И (ВЫБРАТЬ СЧЕТЧИК (РАЗЛИЧНЫЙ (ПРЕОБРАЗОВАТЬ (ДАТАВРЕМЯ, ЭТАЖ (ПРЕОБРАЗОВАТЬ (ПЛАВАТЬ, Дата создания))))) 
              ОТ tblUserLogin c 
             ГДЕ c.userid = b.userid 
               AND CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, c.CreationDate))) BETWEEN CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate))) и CONVERT (DATETIME, FLOOR (CONVERT (FLOAT, a.CreationDate)) ) + @ Диапазон-1) = @ Диапазон)

Скрипт создания

СОЗДАТЬ ТАБЛИЦУ [dbo]. [TblUserLogin] (
    [Id] [int] IDENTITY (1,1) NOT NULL,
    [UserId] [int] NULL,
    [CreationDate] [datetime] NULL
) НА [ОСНОВНОЙ]
Дэйв Баркер
источник
довольно жестоко. 26 секунд в 406624 строках.
Джефф Этвуд,
Как часто вы проверяете получение значка? Если это только один раз в день, то 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, но я думаю, что это дает некоторые идеи.

Cebjyre
источник
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
Radu094
источник
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) вместо convert (char (10), CreationDate, 101).

@IDisposable Раньше я искал использовать datepart, но мне было лень искать синтаксис, поэтому я решил, что вместо него id использует convert. Я не знаю, что это оказало значительное влияние. Спасибо! теперь я знаю.

Jaskirat
источник
Усечение 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
Винсент Бак
источник