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

у меня есть таблица базы данных, которая содержит возвраты пользователей в городах. Мне нужно знать, сколько дней пользователь был в городе, а затем, сколько посещений пользователь сделал в городе (визит состоит из последовательных дней, проведенных в городе).

Итак, рассмотрим у меня следующую таблицу (упрощенную, содержащую только DATETIMEs-тот же пользователь и город):

      datetime
-------------------
2011-06-30 12:11:46
2011-07-01 13:16:34
2011-07-01 15:22:45
2011-07-01 22:35:00
2011-07-02 13:45:12
2011-08-01 00:11:45
2011-08-05 17:14:34
2011-08-05 18:11:46
2011-08-06 20:22:12

количество дней этот пользователь был в этом городе будет 6 (30.06, 01.07, 02.07, 01.08, 05.08, 06.08).

Я думал сделать это с помощью SELECT COUNT(id) FROM table GROUP BY DATE(datetime)

затем, для количества посещений этого пользователя в этом городе, запрос должен вернуться 3 (30.06-02.07, 01.08, 05.08-06.08).

проблема в том, что я понятия не имею, как я должен построить этот запрос.

любой помощь будет высоко оценена!

5 ответов


вы можете найти первый день каждого посещения, найдя проверки, где не было проверки накануне.

select count(distinct date(start_of_visit.datetime))
from checkin start_of_visit
left join checkin previous_day
    on start_of_visit.user = previous_day.user
    and start_of_visit.city = previous_day.city
    and date(start_of_visit.datetime) - interval 1 day = date(previous_day.datetime)
where previous_day.id is null

в этом запросе есть несколько важных частей.

во-первых, каждая проверка присоединяется к любой проверке с предыдущего дня. Но поскольку это внешнее соединение, если вчера не было проверки, правая сторона соединения будет иметь NULL результаты. The WHERE фильтрация происходит после соединения, поэтому она сохраняет только те проверки слева с той стороны, где их нет с правой стороны. LEFT OUTER JOIN/WHERE IS NULL очень удобно для поиска, где вещи не.

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

Edit: я просто перечитал ваш предлагаемый запрос для первого вопроса. Ваш запрос даст вам количество checkins на заданную дату, а не количество дат. Я думаю, вы хотите что-то вроде этого:

select count(distinct date(datetime))
from checkin
where user='some user' and city='some city'

попробуйте применить этот код к вашей задаче -

CREATE TABLE visits(
  user_id INT(11) NOT NULL,
  dt DATETIME DEFAULT NULL
);

INSERT INTO visits VALUES 
  (1, '2011-06-30 12:11:46'),
  (1, '2011-07-01 13:16:34'),
  (1, '2011-07-01 15:22:45'),
  (1, '2011-07-01 22:35:00'),
  (1, '2011-07-02 13:45:12'),
  (1, '2011-08-01 00:11:45'),
  (1, '2011-08-05 17:14:34'),
  (1, '2011-08-05 18:11:46'),
  (1, '2011-08-06 20:22:12'),
  (2, '2011-08-30 16:13:34'),
  (2, '2011-08-31 16:13:41');


SET @i = 0;
SET @last_dt = NULL;
SET @last_user = NULL;

SELECT v.user_id,
  COUNT(DISTINCT(DATE(dt))) number_of_days,
  MAX(days) number_of_visits
FROM
  (SELECT user_id, dt
        @i := IF(@last_user IS NULL OR @last_user <> user_id, 1, IF(@last_dt IS NULL OR (DATE(dt) - INTERVAL 1 DAY) > DATE(@last_dt), @i + 1, @i)) AS days,
        @last_dt := DATE(dt),
        @last_user := user_id
   FROM
     visits
   ORDER BY
     user_id, dt
  ) v
GROUP BY
  v.user_id;

----------------
Output:

+---------+----------------+------------------+
| user_id | number_of_days | number_of_visits |
+---------+----------------+------------------+
|       1 |              6 |                3 |
|       2 |              2 |                1 |
+---------+----------------+------------------+

объяснение:

чтобы понять, как это работает, давайте проверим подзапрос, вот он.

SET @i = 0;
SET @last_dt = NULL;
SET @last_user = NULL;


SELECT user_id, dt,
        @i := IF(@last_user IS NULL OR @last_user <> user_id, 1, IF(@last_dt IS NULL OR (DATE(dt) - INTERVAL 1 DAY) > DATE(@last_dt), @i + 1, @i)) AS 

days,
        @last_dt := DATE(dt) lt,
        @last_user := user_id lu
FROM
  visits
ORDER BY
  user_id, dt;

как вы видите, запрос возвращает все строки и выполняет ранжирование по количеству посещений. Это известный метод ранжирования на основе переменных, обратите внимание, что строки упорядочены по полям user и date. Этот запрос вычисляет посещения пользователей и выводит следующий набор данных, где предоставляет ранг по количеству посещений -

+---------+---------------------+------+------------+----+
| user_id | dt                  | days | lt         | lu |
+---------+---------------------+------+------------+----+
|       1 | 2011-06-30 12:11:46 |    1 | 2011-06-30 |  1 |
|       1 | 2011-07-01 13:16:34 |    1 | 2011-07-01 |  1 |
|       1 | 2011-07-01 15:22:45 |    1 | 2011-07-01 |  1 |
|       1 | 2011-07-01 22:35:00 |    1 | 2011-07-01 |  1 |
|       1 | 2011-07-02 13:45:12 |    1 | 2011-07-02 |  1 |
|       1 | 2011-08-01 00:11:45 |    2 | 2011-08-01 |  1 |
|       1 | 2011-08-05 17:14:34 |    3 | 2011-08-05 |  1 |
|       1 | 2011-08-05 18:11:46 |    3 | 2011-08-05 |  1 |
|       1 | 2011-08-06 20:22:12 |    3 | 2011-08-06 |  1 |
|       2 | 2011-08-30 16:13:34 |    1 | 2011-08-30 |  2 |
|       2 | 2011-08-31 16:13:41 |    1 | 2011-08-31 |  2 |
+---------+---------------------+------+------------+----+

затем мы группируем этот набор данных пользователем и используем агрегатные функции: 'COUNT (DISTINCT (DATE (dt)))' - количество дней 'MAX (days)' - количество посещений, это максимальное значение для days поле из нашего подзапроса.

вот и все;)


как образец данных, предоставленный Devart, внутренний "предварительный запрос" работает с переменными sql. По умолчанию @LUser равен -1 (вероятный несуществующий идентификатор пользователя), тест IF () проверяет наличие разницы между последним пользователем и текущим. Как только новый пользователь получает значение 1... Кроме того, если последняя дата больше 1 дня с новой даты регистрации заезда, она получает значение 1. Затем последующие столбцы сбрасывают @LUser и @LDate до значения входящей записи, только что протестированной против for очередной цикл. Затем внешний запрос просто суммирует их и подсчитывает их для конечных правильных результатов в наборе данных Devart

User ID    Distinct Visits   Total Days
1           3                 9
2           1                 2

select PreQuery.User_ID,
       sum( PreQuery.NextVisit ) as DistinctVisits,
       count(*) as TotalDays
   from
      (  select v.user_id,
               if( @LUser <> v.User_ID OR @LDate < ( date( v.dt ) - Interval 1 day ), 1, 0 ) as NextVisit,
               @LUser := v.user_id,
               @LDate := date( v.dt )
            from 
               Visits v,
               ( select @LUser := -1, @LDate := date(now()) ) AtVars 
            order by
               v.user_id,
               v.dt  ) PreQuery
    group by 
       PreQuery.User_ID

для первой подзадачи:

select count(*) 
from (
select TO_DAYS(p.d)
from p
group by TO_DAYS(p.d)
) t

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

тогда вы можете получить данные в одном запросе с чем-то вроде этого: SELECT COUNT(id) AS number_of_days, COUNT(DISTINCT visit_id) number_of_visits FROM checkin GROUP BY user, city

Это не очень оптимальный, но все же лучше, чем делать что-либо с текущей структурой и он будет работать. Также, если результаты могут быть отдельными запросами, он будет работать очень быстро.

но, конечно, вам нужно будет изменить структуру базы данных, сделать еще несколько сценариев и преобразовать текущие данные в новую структуру (т. е. вам нужно будет добавить visit_id в текущие данные).