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

Вместо того, чтобы объяснить его со словами, вот пример. Это - версия Схемы функции факториала:

(define (factorial x)
  (if (= x 0) 1
      (* x (factorial (- x 1)))))

Вот версия факториала, который является рекурсивным хвостом:

(define factorial
  (letrec ((fact (lambda (x accum)
                   (if (= x 0) accum
                       (fact (- x 1) (* accum x))))))
    (lambda (x)
      (fact x 1))))

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

124
задан Community 23 May 2017 в 12:25
поделиться

18 ответов

Ответ очевиден:

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

EDIT:

Хорошо, вот мой серьезный ответ:

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

EDIT:

[Джефф Этвуд] Это отличное быстрое решение и заслуживает того, чтобы его приняли, но решение Роба Фарли также превосходно и, возможно, даже быстрее (!). Пожалуйста, проверьте это тоже!

69
ответ дан 24 November 2019 в 01:06
поделиться

при условии, что схема выглядит примерно так:

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
0
ответ дан 24 November 2019 в 01:06
поделиться

Немного подправил запрос Билла. Возможно, вам придется усечь дату перед группировкой, чтобы подсчитать только один вход в день ...

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, чтобы использовать DATEADD (dd, DATEDIFF (dd, 0, CreationDate), 0) вместо convert (char (10), CreationDate , 101).

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

0
ответ дан 24 November 2019 в 01:06
поделиться

Вне всяких сомнений, 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
ответ дан 24 November 2019 в 01:06
поделиться

Как насчет того, чтобы использовать таблицы 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
ответ дан 24 November 2019 в 01:06
поделиться

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

DECLARE @Range as INT
SET @Range = 10

SELECT DISTINCT UserId, CONVERT(DATETIME, FLOOR(CONVERT(FLOAT, a.CreationDate)))
  FROM tblUserLogin a
WHERE EXISTS
   (SELECT 1 
      FROM tblUserLogin b 
     WHERE a.userId = b.userId 
       AND (SELECT COUNT(DISTINCT(CONVERT(DATETIME, FLOOR(CONVERT(FLOAT, CreationDate))))) 
              FROM tblUserLogin c 
             WHERE c.userid = b.userid 
               AND CONVERT(DATETIME, FLOOR(CONVERT(FLOAT, c.CreationDate))) BETWEEN CONVERT(DATETIME, FLOOR(CONVERT(FLOAT, a.CreationDate))) and CONVERT(DATETIME, FLOOR(CONVERT(FLOAT, a.CreationDate)))+@Range-1) = @Range)

Сценарий создания

CREATE TABLE [dbo].[tblUserLogin](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [UserId] [int] NULL,
    [CreationDate] [datetime] NULL
) ON [PRIMARY]
0
ответ дан 24 November 2019 в 01:06
поделиться
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

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

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

1
ответ дан 24 November 2019 в 01:06
поделиться

Спенсер почти сделал это, но это должен быть рабочий код:

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
ответ дан 24 November 2019 в 01:06
поделиться

Я использовал простой math, чтобы определить, кто последовательно заходил на сайт. Это свойство заключается в том, что разница в днях между первым и последним доступом должна быть равна количеству записей в журнале таблицы доступа.

Вот сценарий 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
ответ дан 24 November 2019 в 01:06
поделиться

Что-то вроде этого?

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
ответ дан 24 November 2019 в 01:06
поделиться

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

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

Другая идея, которую я '

2
ответ дан 24 November 2019 в 01:06
поделиться

Вы можете использовать рекурсивный 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
ответ дан 24 November 2019 в 01:06
поделиться

Похоже, вы могли бы воспользоваться тем фактом, что для непрерывности в течение n дней потребуется n строк.

Примерно так:

SELECT users.UserId, count(1) as cnt
FROM users
WHERE users.CreationDate > now() - INTERVAL 30 DAY
GROUP BY UserId
HAVING cnt = 30
4
ответ дан 24 November 2019 в 01:06
поделиться

Если это так важно для вас, создайте это событие и заведите таблицу, чтобы предоставить вам эту информацию. Не нужно убивать машину всеми этими сумасшедшими запросами.

2
ответ дан 24 November 2019 в 01:06
поделиться

Выполнение этого с один-единственный запрос SQL кажется мне слишком сложным. Позвольте мне разбить этот ответ на две части. Запустите ежедневное задание cron, которое проверяет каждого пользователя, вошедшего в систему сегодня, и затем увеличивает счетчик, если он есть, или устанавливает его на 0, если нет.

  • Что вам следует делать сейчас:
    - Экспортируйте эту таблицу на сервер, на котором не работает ваш сайт и который некоторое время не понадобится. ;)
    - Отсортируйте по пользователю, затем по дате.
    - пройти по порядку, держать счетчик ...
  • 3
    ответ дан 24 November 2019 в 01:06
    поделиться

    Некоторый красиво выразительный 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);
            }
        }
    }
    
    6
    ответ дан 24 November 2019 в 01:06
    поделиться

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

    После добавления этого столбца запрос будет очевиден:

    if exists(select * from table
              where LongestStreak >= 30 and UserId = @UserId)
       -- award the Woot badge.
    
    18
    ответ дан 24 November 2019 в 01:06
    поделиться

    Как насчет (и убедитесь, что предыдущий оператор закончился полу- двоеточие):

    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)

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

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

    147
    ответ дан 24 November 2019 в 01:06
    поделиться
    Другие вопросы по тегам:

    Похожие вопросы: