MySQL: группировка по последовательным дням и группам подсчета
у меня есть таблица базы данных, которая содержит возвраты пользователей в городах. Мне нужно знать, сколько дней пользователь был в городе, а затем, сколько посещений пользователь сделал в городе (визит состоит из последовательных дней, проведенных в городе).
Итак, рассмотрим у меня следующую таблицу (упрощенную, содержащую только DATETIME
s-тот же пользователь и город):
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
Я думаю, вы должны рассмотреть возможность изменения структуры базы данных. Вы можете добавить посещения таблиц и 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 в текущие данные).