2011-08-17 14 views
8

Ho una tabella di database che contiene i controlli di ciascun utente nelle città. Ho bisogno di sapere quanti giorni un utente è stato in una città, e poi, quante visite ha fatto un utente in una città (una visita consiste di giorni consecutivi trascorsi in una città).MySQL: raggruppa per giorni consecutivi e gruppi di conteggio

Quindi, prendere in considerazione ho la tabella seguente (semplificata, contenente solo i DATETIME s - stesso utente e città):

 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 

Il numero di giorni questo utente è stato quello di questa città sarebbe (30.06, 01.07, 02.07, 01.08, 05.08, 06.08).

ho pensato di fare questo usando SELECT COUNT(id) FROM table GROUP BY DATE(datetime)

Poi, per il numero di visite l'utente ha fatto a questa città, la query deve restituire (30.06-02.07, 01.08, 05.08 -06,08).

Il problema è che non ho idea di come creare questa query.

Qualsiasi aiuto sarebbe molto apprezzato!

risposta

10

È possibile trovare il primo giorno di ogni visita, trovando checkin dove non c'era di arrivo il giorno prima.

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 

Ci sono diverse parti importanti in questa ricerca.

Prima di tutto, ogni check-in viene aggiunto a qualsiasi check-in del giorno precedente. Ma dal momento che si tratta di un join esterno, se non è stato effettuato il check-in il giorno precedente, il lato destro del join avrà i risultati NULL. Il filtro WHERE si verifica dopo il join, quindi mantiene solo quei check-in dal lato sinistro dove non ce ne sono dal lato destro. LEFT OUTER JOIN/WHERE IS NULL è davvero utile per trovare dove le cose non sono.

Quindi conta distinto le date di check-in per assicurarsi che non contino due volte se l'utente ha effettuato il check-in più volte il primo giorno della visita. (In realtà ho aggiunto quella parte in modifica, quando ho individuato l'errore possibile.)

Modifica: ho appena riletto la query proposta per la prima domanda. La tua query ti farà ottenere il numero di check-in in una certa data, invece del conteggio delle date. Penso che tu voglia invece qualcosa del genere:

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

Per quanto riguarda il primo aspetto ... Non riesco a capire completamente il tuo suggerimento ... È possibile per dare qualche dettaglio in più? Grazie! Per quanto riguarda il secondo, la mia domanda è giusta, a condizione che non contate l'utente e la città, come menzionato nella mia domanda. – linkyndy

+0

Spiacente, ho presupposto che il risultato per "quanti giorni un utente è stato in una città" dovrebbe apparire (user_id, count_of_days). – Simon

+0

Grazie per i dettagli. Con diverse regolazioni per adattarsi alla mia tabella del database reale, la tua query funziona come un incantesimo. Grazie ancora! – linkyndy

0

per un primo sub-task:

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

Penso che dovresti prendere in considerazione la modifica della struttura del database. È possibile aggiungere visite alla tabella e visit_id nella tabella dei check-in. Ogni volta che si desidera registrare un nuovo check-up, controllare se vi è alcun check-in un giorno indietro. Se sì, aggiungi un nuovo check-in con visit_id dal check-in di ieri.In caso contrario, aggiungi nuove visite alle visite e nuovo checkin con new visit_id.

allora si potrebbe ottenere i tuoi dati in una query con qualcosa di simile: SELECT COUNT(id) AS number_of_days, COUNT(DISTINCT visit_id) number_of_visits FROM checkin GROUP BY user, city

Non è molto ottimale, ma comunque meglio che non fare nulla con struttura attuale e funzionerà. Inoltre, se i risultati possono essere query separate, funzionerà molto velocemente.

Ma ovviamente gli svantaggi sono che è necessario modificare la struttura del database, eseguire altri script e convertire i dati correnti in una nuova struttura (ad esempio, sarà necessario aggiungere visit_id ai dati correnti).

+0

Grazie per la risposta, ma vorrei attenermi alla mia attuale struttura del database, almeno per ora. Inoltre, quando inserirò, avrò bisogno di fare ulteriori operazioni, poiché un giorno potrebbe avere più check-in, quindi non è così semplice con "controlla se c'è un check-in un giorno indietro". Questo tipo di manipolazione dei dati può essere effettuato anche in PHP con la struttura del database fornita, ma stavo cercando una query per fare questo lavoro, in quanto è più pulito e conveniente. – linkyndy

3

cercare di applicare questo codice per il vostro compito -

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

Spiegazione:

Per capire come funziona cerchiamo di controllare la sottoquery, eccolo.

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; 

Come si vede la query restituisce tutte le righe ed esegue il posizionamento per il numero di visite. Questo è noto metodo di classificazione basato su variabili, si noti che le righe sono ordinate per campi utente e data. Questa query calcola visite degli utenti, e le uscite dati successivo set dove days colonna fornisce rango per il numero di visite -

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

Poi gruppo questo insieme di dati dall'utente e utilizzare le funzioni di aggregazione: 'COUNT (DISTINCT (DATA (dt))) '- conta il numero di giorni ' MAX (giorni) '- il numero di visite, è un valore massimo per il campo days dalla nostra sottoquery.

Questo è tutto;)

+0

Sembra piuttosto complicato ... puoi darci qualche dettaglio in più sul tuo codice? Apprezzerebbe! – linkyndy

+0

Ho aggiunto alcuni dettagli. – Devart

+0

Grazie per i dettagli. È piuttosto triste che non possa dare la generosità a due risposte. Tuttavia, ho scelto l'altra risposta in quanto la query è un po 'più semplice. Sono davvero dispiaciuto e voglio ringraziarti ancora per la tua risposta! – linkyndy

1

Come campione di dati fornito dal Devart, l'interno "PreQuery" funziona con variabili SQL. Per impostazione predefinita di @LUser su -1 (probabile ID utente inesistente), il test IF() verifica eventuali differenze tra l'ultimo utente e il corrente. Appena un nuovo utente, ottiene un valore di 1 ... Inoltre, se l'ultima data è superiore a 1 giorno dalla nuova data del check-in, ottiene il valore 1. Quindi, le colonne successive reimpostano il valore @LUser e @LDate sul valore del record in entrata appena testato rispetto al ciclo successivo. Quindi, la query esterna li riassume e li conteggia per i risultati finali corretti per il set di dati Devart di

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

Grazie per la tua risposta e per chiarirla! – linkyndy

+0

Felice di aiutare ... ha ottenuto la soluzione esatta di cui avevi bisogno (quindi anche l'inserimento delle informazioni sull'ID utente, per aiutare). – DRapp

+0

Lo ha fatto, peccato che solo una risposta possa essere accettata e premiata ... – linkyndy

Problemi correlati