2011-08-17 54 views
8

Tengo una tabla de base de datos que contiene los registros de cada usuario en las ciudades. Necesito saber cuántos días ha estado un usuario en una ciudad y, luego, cuántas visitas ha hecho un usuario a una ciudad (una visita consiste en días consecutivos en una ciudad).MySQL: grupo por días consecutivos y grupos de recuento

lo tanto, considerar tengo el siguiente cuadro (simplificado, que contiene sólo los DATETIME s - el mismo usuario y de la ciudad):

 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 

El número de días que haya estado en esta ciudad habría (30,06, 01,07, 02,07, 01,08, 05,08, 06,08).

se me ocurrió hacer esto utilizando SELECT COUNT(id) FROM table GROUP BY DATE(datetime)

Entonces, para el número de visitas que este usuario ha hecho a esta ciudad, la consulta debe devolver (30.06-02.07, 01.08, 05.08 -06.08).

El problema es que no tengo idea de cómo debo crear esta consulta.

¡Cualquier ayuda sería muy apreciada!

Respuesta

10

puede encontrar el primer día de cada visita al encontrar confirmaciones donde no había checkin el día anterior.

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 

hay varias partes importantes a esta consulta.

Primero, cada registro se une a cualquier registro desde el día anterior. Pero como se trata de una combinación externa, si no se registró el día anterior, el lado derecho de la unión tendrá NULL resultados. El filtro WHERE se produce después de la unión, por lo que solo conserva esos registros del lado izquierdo donde no los hay del lado derecho. LEFT OUTER JOIN/WHERE IS NULL es realmente útil para encontrar donde las cosas no son.

Luego cuenta distinto fechas de verificación para asegurarse de que no se cuente dos veces si el usuario ingresó varias veces el primer día de la visita. (De hecho, agregué esa parte en la edición, cuando detecté el posible error).

Editar: Acabo de volver a leer la consulta propuesta para la primera pregunta. Su consulta le proporcionará el número de registros en una fecha determinada, en lugar de un recuento de fechas. Creo que quieres algo como esto en su lugar:

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

En cuanto al primer aspecto ... Me parece que no puede entender por completo su sugerencia ... ¿Es posible para dar algunos detalles más? ¡Gracias! Respecto al segundo, mi consulta es correcta, siempre que no cuente el usuario y la ciudad, como se menciona en mi pregunta. – linkyndy

+0

Lo siento, asumí que el resultado de "cuántos días ha estado un usuario en una ciudad" debería ser similar a (user_id, count_of_days). – Simon

+0

Gracias por los detalles. Con varios ajustes para adaptarse a mi tabla de base de datos real, su consulta funciona como un encanto. ¡Gracias de nuevo! – linkyndy

0

para una primera sub-tareas:

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

Creo que deberías considerar cambiar la estructura de la base de datos. Puede agregar visitas a la mesa y visitar_id en su tabla de registros. Cada vez que desee registrar un nuevo registro, verifique si hay algún registro el día anterior. En caso afirmativo, agregue un nuevo registro con visit_id del registro de ayer.De lo contrario, agregue una nueva visita a las visitas y un nuevo registro con la nueva visita_id.

Posteriormente, se podría obtener sus datos en una consulta con algo así: SELECT COUNT(id) AS number_of_days, COUNT(DISTINCT visit_id) number_of_visits FROM checkin GROUP BY user, city

No es muy óptimo, pero mejor que no hacer nada con la estructura actual y se ponga a funcionar. Además, si los resultados pueden ser consultas separadas, funcionará muy rápido.

Pero, por supuesto, los inconvenientes son que tendrá que cambiar la estructura de la base de datos, hacer más scripts y convertir los datos actuales a una nueva estructura (es decir, deberá agregar visit_id a los datos actuales).

+0

Gracias por su respuesta, pero me gustaría mantener mi estructura de base de datos actual, al menos por ahora. Además, tendré que hacer algunas operaciones adicionales al insertar, ya que un día puede tener múltiples registros, por lo que no es tan simple con el "control si hay algún registro en el día anterior". Este tipo de manipulación de datos también se puede hacer en PHP con la estructura de la base de datos proporcionada, pero estaba buscando una consulta para hacer este trabajo, ya que es más limpio y conveniente. – linkyndy

3

intenta aplicar este código a su tarea -

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

Explicación:

Para entender cómo funciona vamos a ver la subconsulta, aquí está.

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; 

Como ve, la consulta devuelve todas las filas y realiza una clasificación para el número de visitas. Este es un método de clasificación conocido basado en variables, tenga en cuenta que las filas están ordenadas por los campos de usuario y fecha. Esta consulta calcula visitas de los usuarios, y salidas próximo conjunto de datos donde days columna proporciona el rango para el número de visitas -

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

Luego grupo de este conjunto de datos por el usuario y utilizar funciones de agregado: 'COUNT (DISTINCT (FECHA (dt))) '- cuenta el número de días ' MAX (días) '- el número de visitas, es un valor máximo para el campo days de nuestra subconsulta.

Eso es todo;)

+0

Parece bastante complicado ... ¿podría darnos más detalles sobre su código? ¡Apreciaría! – linkyndy

+0

He agregado algunos detalles. – Devart

+0

Gracias por los detalles. Es bastante triste que no pueda darle la recompensa a dos respuestas. Sin embargo, elegí la otra respuesta ya que la consulta es un poco más simple. ¡Realmente lo siento y quiero agradecerles nuevamente por su respuesta! – linkyndy

1

Como muestra de datos proporcionada por Devart, el interior "la consulta previa" trabaja con variables de SQL. Al establecer de forma predeterminada el @LUser en -1 (probable identificación de usuario inexistente), la prueba IF() verifica cualquier diferencia entre el último usuario y el actual. Tan pronto como un nuevo usuario, obtiene un valor de 1 ... Además, si la última fecha es más de 1 día desde la nueva fecha de check-in, obtiene un valor de 1. Luego, las columnas subsiguientes reinician el @LUser y @LDate al valor del registro entrante que se acaba de probar para el siguiente ciclo. A continuación, la consulta externa sólo les resume y las cuenta de los resultados correctos final por el conjunto de datos de 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 
+0

¡Gracias por su respuesta y por aclararla! – linkyndy

+0

Me complace ayudar ... ¿obtuvo la solución exacta que necesitaba (por lo tanto, la inclusión de la información de identificación del usuario también, para ayudar). – DRapp

+0

Lo hizo, lástima que solo una respuesta puede ser aceptada y recompensada ... – linkyndy