Il modo più veloce per contare il numero di intervalli di date copre ciascuna data delle serie


12

Ho una tabella (in PostgreSQL 9.4) che assomiglia a questa:

CREATE TABLE dates_ranges (kind int, start_date date, end_date date);
INSERT INTO dates_ranges VALUES 
    (1, '2018-01-01', '2018-01-31'),
    (1, '2018-01-01', '2018-01-05'),
    (1, '2018-01-03', '2018-01-06'),
    (2, '2018-01-01', '2018-01-01'),
    (2, '2018-01-01', '2018-01-02'),
    (3, '2018-01-02', '2018-01-08'),
    (3, '2018-01-05', '2018-01-10');

Ora voglio calcolare per le date date e per ogni tipo, in quante righe dates_rangescadono da ciascuna data. Gli zeri potrebbero essere eventualmente omessi.

Risultato desiderato:

+-------+------------+----+
|  kind | as_of_date |  n |
+-------+------------+----+
|     1 | 2018-01-01 |  2 |
|     1 | 2018-01-02 |  2 |
|     1 | 2018-01-03 |  3 |
|     2 | 2018-01-01 |  2 |
|     2 | 2018-01-02 |  1 |
|     3 | 2018-01-02 |  1 |
|     3 | 2018-01-03 |  1 |
+-------+------------+----+

Ho escogitato due soluzioni, una con LEFT JOINeGROUP BY

SELECT
kind, as_of_date, COUNT(*) n
FROM
    (SELECT d::date AS as_of_date FROM generate_series('2018-01-01'::timestamp, '2018-01-03'::timestamp, '1 day') d) dates
LEFT JOIN
    dates_ranges ON dates.as_of_date BETWEEN start_date AND end_date
GROUP BY 1,2 ORDER BY 1,2

e uno con LATERAL, che è leggermente più veloce:

SELECT
    kind, as_of_date, n
FROM
    (SELECT d::date AS as_of_date FROM generate_series('2018-01-01'::timestamp, '2018-01-03'::timestamp, '1 day') d) dates,
LATERAL
    (SELECT kind, COUNT(*) AS n FROM dates_ranges WHERE dates.as_of_date BETWEEN start_date AND end_date GROUP BY kind) ss
ORDER BY kind, as_of_date

Mi chiedo: è un modo migliore per scrivere questa query? E come includere coppie tipo-data con 0 conteggi?

In realtà ci sono alcuni tipi distinti, periodo fino a cinque anni (1800 date) e ~ 30k righe nella dates_rangestabella (ma potrebbe crescere in modo significativo).

Non ci sono indici Per essere precisi nel mio caso è il risultato di una query secondaria, ma ho voluto limitare la domanda a un problema, quindi è più generale.


Cosa fare se gli intervalli nella tabella sono non sovrapposti o toccanti. Ad esempio, se hai un intervallo in cui (tipo, inizio, fine) = (1,2018-01-01,2018-01-15)e (1,2018-01-20,2018-01-25)vuoi tenerne conto nel determinare quante date sovrapposte hai?
Evan Carroll,

Sono anche confuso perché il tuo tavolo è piccolo? Perché non lo è 2018-01-31o 2018-01-30o 2018-01-29quando ci sono tutti nella prima gamma?
Evan Carroll,

Le date di @EvanCarroll in generate_seriessono parametri esterni - non necessariamente coprono tutti gli intervalli nella dates_rangestabella. Per quanto riguarda la prima domanda, suppongo di non capirlo: le righe dates_rangessono indipendenti, non voglio determinare la sovrapposizione.
BartekCh

Risposte:


4

La seguente query funziona anche se "zero mancanti" sono OK:

select *
from (
  select
    kind,
    generate_series(start_date, end_date, interval '1 day')::date as d,
    count(*)
  from dates_ranges
  group by 1, 2
) x
where d between date '2018-01-01' and date '2018-01-03'
order by 1, 2;

ma non è più veloce della lateralversione con il piccolo set di dati. Potrebbe ridimensionarsi meglio, poiché non è richiesto alcun join, ma la versione precedente si aggrega su tutte le righe, quindi potrebbe perdere di nuovo.

La query seguente cerca di evitare lavori inutili rimuovendo qualsiasi serie che non si sovrapponga comunque:

select
  kind,
  generate_series(greatest(start_date, date '2018-01-01'), least(end_date, date '2018-01-03'), interval '1 day')::date as d,
  count(*)
from dates_ranges
where (start_date, end_date + interval '1 day') overlaps (date '2018-01-01', date '2018-01-03' + interval '1 day')
group by 1, 2
order by 1, 2;

- e devo usare l' overlapsoperatore! Si noti che è necessario aggiungere interval '1 day'a destra poiché l'operatore di sovrapposizione considera i periodi di tempo come aperti a destra (il che è abbastanza logico perché una data viene spesso considerata come un timestamp con componente temporale di mezzanotte).


Bello, non sapevo che generate_seriespotesse essere usato così. Dopo alcuni test ho le seguenti osservazioni. La tua query si ridimensiona davvero bene con la lunghezza dell'intervallo selezionato - praticamente non c'è alcuna differenza tra il periodo di 3 e 10 anni. Tuttavia, per periodi più brevi (1 anno) le mie soluzioni sono più veloci: suppongo che la ragione sia che ci sono intervalli molto lunghi dates_ranges(come 2010-2100), che stanno rallentando la tua richiesta. Limitare start_datee end_dateall'interno della query interna dovrebbe aiutare però. Devo fare qualche altro test.
BartekCh

6

E come includere coppie tipo-data con 0 conteggi?

Crea una griglia di tutte le combinazioni, quindi LATERAL unisciti al tuo tavolo, in questo modo:

SELECT k.kind, d.as_of_date, c.n
FROM  (SELECT DISTINCT kind FROM dates_ranges) k
CROSS  JOIN (
   SELECT d::date AS as_of_date
   FROM   generate_series(timestamp '2018-01-01', timestamp '2018-01-03', interval '1 day') d
   ) d
CROSS  JOIN LATERAL (
   SELECT count(*)::int AS n
   FROM   dates_ranges
   WHERE  kind = k.kind
   AND    d.as_of_date BETWEEN start_date AND end_date
   ) c
ORDER  BY k.kind, d.as_of_date;

Dovrebbe anche essere il più veloce possibile.

All'inizio avevo LEFT JOIN LATERAL ... on true, ma c'è un aggregato nella sottoquery c, quindi otteniamo sempre una riga e possiamo usare CROSS JOINanche. Nessuna differenza nelle prestazioni.

Se hai una tabella che contiene tutti i tipi rilevanti , usa quella invece di generare l'elenco con la subquery k.

Il cast a integerè facoltativo. Altrimenti ottieni bigint.

Gli indici sarebbero di aiuto, in particolare un indice a più colonne attivo (kind, start_date, end_date). Dal momento che stai basando su una sottoquery, questo potrebbe o non potrebbe essere possibile raggiungere.

L'uso di funzioni di restituzione come generate_series()nella SELECTlista non è generalmente consigliabile nelle versioni di Postgres prima delle 10 (a meno che non si sappia esattamente cosa si sta facendo). Vedere:

Se hai molte combinazioni con poche o nessuna riga, questo modulo equivalente potrebbe essere più veloce:

SELECT k.kind, d.as_of_date, count(dr.kind)::int AS n
FROM  (SELECT DISTINCT kind FROM dates_ranges) k
CROSS JOIN (
   SELECT d::date AS as_of_date
   FROM   generate_series(timestamp '2018-01-01', timestamp '2018-01-03', interval '1 day') d
   ) d
LEFT   JOIN dates_ranges dr ON dr.kind = k.kind
                           AND d.as_of_date BETWEEN dr.start_date AND dr.end_date
GROUP  BY 1, 2
ORDER  BY 1, 2;

Per quanto riguarda le funzioni di restituzione di set SELECTnell'elenco - ho letto che non è consigliabile, tuttavia sembra che funzioni bene, se esiste solo una di queste funzioni. Se sono sicuro che ce ne sarà solo uno, qualcosa potrebbe andare storto?
BartekCh

@BartekCh: un singolo SRF SELECTnell'elenco funziona come previsto. Forse aggiungi un commento per mettere in guardia contro l'aggiunta di un altro. Oppure spostalo FROMnell'elenco per iniziare con le versioni precedenti di Postgres. Perché rischiare complicazioni? (Anche questo è SQL standard e non confonderà le persone provenienti da altri RDBMS.)
Erwin Brandstetter,

1

Usando il daterangetipo

PostgreSQL ha un daterange. Usarlo è piuttosto semplice. A partire dai dati di esempio, ci spostiamo per utilizzare il tipo nella tabella.

BEGIN;
  ALTER TABLE dates_ranges ADD COLUMN myrange daterange;
  UPDATE dates_ranges
    SET myrange = daterange(start_date, end_date, '[]');
  ALTER TABLE dates_ranges
    DROP COLUMN start_date,
    DROP COLUMN end_date;
COMMIT;

-- Now you can create GIST index on it...
CREATE INDEX ON dates_ranges USING gist (myrange);

TABLE dates_ranges;
 kind |         myrange         
------+-------------------------
    1 | [2018-01-01,2018-02-01)
    1 | [2018-01-01,2018-01-06)
    1 | [2018-01-03,2018-01-07)
    2 | [2018-01-01,2018-01-02)
    2 | [2018-01-01,2018-01-03)
    3 | [2018-01-02,2018-01-09)
    3 | [2018-01-05,2018-01-11)
(7 rows)

Voglio calcolare per le date date e per ogni tipo, in quante righe da date_ranges cade ogni data.

Ora per interrogarlo, invertiamo la procedura e generiamo una serie di date, ma ecco il problema che la query stessa può usare l' @>operatore contenimento ( ) per verificare che le date siano nell'intervallo, usando un indice.

Nota che usiamo timestamp without time zone(per fermare i rischi dell'ora legale)

SELECT d1.kind, day::date, count(d2.kind)
FROM dates_ranges AS d1
CROSS JOIN LATERAL generate_series(
  lower(myrange)::timestamp without time zone,
  upper(myrange)::timestamp without time zone,
  '1 day'
) AS gs(day)
INNER JOIN dates_ranges AS d2
  ON d2.myrange @> day::date
GROUP BY d1.kind, day;

Qual è la sovrapposizione giornaliera dettagliata sull'indice.

Come bonus laterale, con il tipo daterange puoi interrompere l' inserimento di intervalli che si sovrappongono ad altri usando unEXCLUDE CONSTRAINT


Qualcosa non va nella tua query, sembra che stia contando le righe più volte, una JOINimmagino troppo.
BartekCh

@BartekCh no hai righe sovrapposte, puoi aggirare questo rimuovendo i range di sovrapposizione (suggerito) o usandocount(DISTINCT kind)
Evan Carroll

ma voglio righe sovrapposte. Ad esempio, per la 1data tipo 2018-01-01è entro le prime due righe da dates_ranges, ma la tua query dà 8.
BartekCh

o usandocount(DISTINCT kind) hai aggiunto la DISTINCTparola chiave lì?
Evan Carroll,

Sfortunatamente con la DISTINCTparola chiave non funziona ancora come previsto. Conta tipi distinti per ogni data, ma voglio contare tutte le righe di ogni tipo per ogni data.
BartekCh
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.