Seleziona in modo efficiente l'inizio e la fine di più intervalli contigui nella query Postgresql


19

Ho circa un miliardo di righe di dati in una tabella con un nome e un numero intero nell'intervallo 1-288. Per un determinato nome , ogni int è univoco e non tutti i possibili numeri interi nell'intervallo sono presenti, quindi esistono degli spazi vuoti.

Questa query genera un caso di esempio:

--what I have:
SELECT *
FROM ( VALUES ('foo', 2),
              ('foo', 3),
              ('foo', 4),
              ('foo', 10),
              ('foo', 11),
              ('foo', 13),
              ('bar', 1),
              ('bar', 2),
              ('bar', 3)
     ) AS baz ("name", "int")

Vorrei generare una tabella di ricerca con una riga per ogni nome e sequenza di numeri interi contigui. Ciascuna di queste righe dovrebbe contenere:

nome - il valore del nome colonna
inizio - il primo intero nella sequenza contigua
fine - il valore finale della sequenza contigua
campata - fine - inizio + 1

Questa query genera un output di esempio per l'esempio precedente:

--what I need:
SELECT * 
FROM ( VALUES ('foo', 2, 4, 3),
              ('foo', 10, 11, 2),
              ('foo', 13, 13, 1),
              ('bar', 1, 3, 3)
     ) AS contiguous_ranges ("name", "start", "end", span)

Perché ho così tante file, più efficiente è meglio. Detto questo, devo eseguire questa query una sola volta, quindi non è un requisito assoluto.

Grazie in anticipo!

Modificare:

Dovrei aggiungere che le soluzioni PL / pgSQL sono benvenute (per favore spiegate qualsiasi trucco di fantasia - Sono ancora nuovo a PL / pgSQL).


Vorrei trovare un modo per elaborare la tabella in blocchi abbastanza piccoli (magari tagliando il "nome" in N secchi o prendendo la prima / ultima lettera del nome), in modo che un ordinamento si adatti alla memoria. È probabile che la scansione della tabella di più tabelle sia più rapida rispetto alla possibilità che un ordinamento si riversi sul disco. Una volta che l'avessi fatto, avrei usato le funzioni di windowing. Inoltre, non dimenticare di sfruttare i modelli nei dati. Forse la maggior parte del "nome" ha effettivamente un conteggio di 288 valori, nel qual caso è possibile escludere tali valori dal processo principale. Fine del

fantastico - e benvenuto nel sito. Hai avuto fortuna con le soluzioni fornite?
Jack Douglas,

grazie. In realtà ho cambiato i progetti poco dopo aver pubblicato questa domanda (e poco dopo, ho cambiato lavoro), quindi non ho mai avuto la possibilità di testare queste soluzioni. cosa devo fare in termini di selezione di una risposta in tal caso?
Spezzatino il

Risposte:


9

Che ne dici di usare with recursive

vista di prova:

create view v as 
select *
from ( values ('foo', 2),
              ('foo', 3),
              ('foo', 4),
              ('foo', 10),
              ('foo', 11),
              ('foo', 13),
              ('bar', 1),
              ('bar', 2),
              ('bar', 3)
     ) as baz ("name", "int");

query:

with recursive t("name", "int") as ( select "name", "int", 1 as span from v
                                     union all
                                     select "name", v."int", t.span+1 as span
                                     from v join t using ("name")
                                     where v."int"=t."int"+1 )
select "name", "start", "start"+span-1 as "end", span
from( select "name", ("int"-span+1) as "start", max(span) as span
      from ( select "name", "int", max(span) as span 
             from t
             group by "name", "int" ) z
      group by "name", ("int"-span+1) ) z;

risultato:

 name | start | end | span
------+-------+-----+------
 foo  |     2 |   4 |    3
 foo  |    13 |  13 |    1
 bar  |     1 |   3 |    3
 foo  |    10 |  11 |    2
(4 rows)

Sarei interessato a sapere come si comporta sul tuo miliardo di righe.


Se le prestazioni sono un problema, giocare con le impostazioni di work_mem potrebbe aiutare a migliorare le prestazioni.
Frank Heikens,

7

Puoi farlo con le funzioni di windowing. L'idea di base è quella di utilizzare leade lagfunzioni windowing per tirare righe avanti e dietro la riga corrente. Quindi possiamo calcolare se abbiamo l'inizio o la fine della sequenza:

create temp view temp_view as
    select
        n,
        val,
        (lead <> val + 1 or lead is null) as islast,
        (lag <> val - 1 or lag is null) as isfirst,
        (lead <> val + 1 or lead is null) and (lag <> val - 1 or lag is null) as orphan
    from
    (
        select
            n,
            lead(val, 1) over( partition by n order by n, val),
            lag(val, 1) over(partition by n order by n, val ),
            val
        from test
        order by n, val
    ) as t
;  
select * from temp_view;
 n  | val | islast | isfirst | orphan 
-----+-----+--------+---------+--------
 bar |   1 | f      | t       | f
 bar |   2 | f      | f       | f
 bar |   3 | t      | f       | f
 bar |  24 | t      | t       | t
 bar |  42 | t      | t       | t
 foo |   2 | f      | t       | f
 foo |   3 | f      | f       | f
 foo |   4 | t      | f       | f
 foo |  10 | f      | t       | f
 foo |  11 | t      | f       | f
 foo |  13 | t      | t       | t
(11 rows)

(Ho usato una vista in modo che la logica sarà più facile da seguire di seguito.) Quindi ora sappiamo se la riga è un inizio o una fine. Dobbiamo comprimerlo in riga:

select
    n as "name",
    first,
    coalesce (last, first) as last,
    coalesce (last - first + 1, 1) as span
from
(
    select
    n,
    val as first,
    -- this will not be excellent perf. since were calling the view
    -- for each row sequence found. Changing view into temp table 
    -- will probably help with lots of values.
    (
        select min(val)
        from temp_view as last
        where islast = true
        -- need this since isfirst=true, islast=true on an orphan sequence
        and last.orphan = false
        and first.val < last.val
        and first.n = last.n
    ) as last
    from
        (select * from temp_view where isfirst = true) as first
) as t
;

 name | first | last | span 
------+-------+------+------
 bar  |     1 |    3 |    3
 bar  |    24 |   24 |    1
 bar  |    42 |   42 |    1
 foo  |     2 |    4 |    3
 foo  |    10 |   11 |    2
 foo  |    13 |   13 |    1
(6 rows)

Mi sembra corretto :)


3

Un'altra soluzione per la funzione finestra. Nessuna idea sull'efficienza, ho aggiunto il piano di esecuzione alla fine (anche se con così poche righe, probabilmente non ha molto valore). Se vuoi giocare: test SQL-Fiddle

Tabella e dati:

CREATE TABLE baz
( name VARCHAR(10) NOT NULL
, i INT  NOT NULL
, UNIQUE  (name, i)
) ;

INSERT INTO baz
  VALUES 
    ('foo', 2),
    ('foo', 3),
    ('foo', 4),
    ('foo', 10),
    ('foo', 11),
    ('foo', 13),
    ('bar', 1),
    ('bar', 2),
    ('bar', 3)
  ;

Query:

SELECT a.name     AS name
     , a.i        AS start
     , b.i        AS "end"
     , b.i-a.i+1  AS span
FROM
      ( SELECT name, i
             , ROW_NUMBER() OVER (PARTITION BY name ORDER BY i) AS rn
        FROM baz AS a
        WHERE NOT EXISTS
              ( SELECT * 
                FROM baz AS prev
                WHERE prev.name = a.name
                  AND prev.i = a.i - 1
              ) 
      ) AS a
    JOIN
      ( SELECT name, i 
             , ROW_NUMBER() OVER (PARTITION BY name ORDER BY i) AS rn
        FROM baz AS a
        WHERE NOT EXISTS
              ( SELECT * 
                FROM baz AS next
                WHERE next.name = a.name
                  AND next.i = a.i + 1
              )
      ) AS b
    ON  b.name = a.name
    AND b.rn  = a.rn
 ; 

Piano di query

Merge Join (cost=442.74..558.76 rows=18 width=46)
Merge Cond: ((a.name)::text = (a.name)::text)
Join Filter: ((row_number() OVER (?)) = (row_number() OVER (?)))
-> WindowAgg (cost=221.37..238.33 rows=848 width=42)
-> Sort (cost=221.37..223.49 rows=848 width=42)
Sort Key: a.name, a.i
-> Merge Anti Join (cost=157.21..180.13 rows=848 width=42)
Merge Cond: (((a.name)::text = (prev.name)::text) AND (((a.i - 1)) = prev.i))
-> Sort (cost=78.60..81.43 rows=1130 width=42)
Sort Key: a.name, ((a.i - 1))
-> Seq Scan on baz a (cost=0.00..21.30 rows=1130 width=42)
-> Sort (cost=78.60..81.43 rows=1130 width=42)
Sort Key: prev.name, prev.i
-> Seq Scan on baz prev (cost=0.00..21.30 rows=1130 width=42)
-> Materialize (cost=221.37..248.93 rows=848 width=50)
-> WindowAgg (cost=221.37..238.33 rows=848 width=42)
-> Sort (cost=221.37..223.49 rows=848 width=42)
Sort Key: a.name, a.i
-> Merge Anti Join (cost=157.21..180.13 rows=848 width=42)
Merge Cond: (((a.name)::text = (next.name)::text) AND (((a.i + 1)) = next.i))
-> Sort (cost=78.60..81.43 rows=1130 width=42)
Sort Key: a.name, ((a.i + 1))
-> Seq Scan on baz a (cost=0.00..21.30 rows=1130 width=42)
-> Sort (cost=78.60..81.43 rows=1130 width=42)
Sort Key: next.name, next.i
-> Seq Scan on baz next (cost=0.00..21.30 rows=1130 width=42)

3

Su SQL Server, aggiungerei un'altra colonna denominata previousInt:

SELECT *
FROM ( VALUES ('foo', 2, NULL),
              ('foo', 3, 2),
              ('foo', 4, 3),
              ('foo', 10, 4),
              ('foo', 11, 10),
              ('foo', 13, 11),
              ('bar', 1, NULL),
              ('bar', 2, 1),
              ('bar', 3, 2)
     ) AS baz ("name", "int", "previousInt")

Vorrei usare un vincolo CHECK per assicurarmi che previousInt <int e un vincolo FK (name, previousInt) si riferiscano a (name, int) e un paio di altri vincoli per garantire l'integrità dei dati a tenuta stagna. Fatto ciò, selezionare le lacune è banale:

SELECT NAME, PreviousInt, Int from YourTable WHERE PreviousInt < Int - 1;

Per accelerarlo, potrei creare un indice filtrato che includa solo lacune. Ciò significa che tutti i gap sono pre-calcolati, quindi le selezioni sono molto veloci e i vincoli assicurano l'integrità dei dati pre-calcolati. Sto usando queste soluzioni molto, sono in tutto il mio sistema.


1

Puoi cercare il metodo Tabibitosan:

https://community.oracle.com/docs/DOC-915680
http://rwijk.blogspot.com/2014/01/tabibitosan.html
https://www.xaprb.com/blog/2006/03/22/find-contiguous-ranges-with-sql/

Fondamentalmente:

SQL> create table mytable (nr)
  2  as
  3  select 1 from dual union all
  4  select 2 from dual union all
  5  select 3 from dual union all
  6  select 6 from dual union all
  7  select 7 from dual union all
  8  select 11 from dual union all
  9  select 18 from dual union all
 10  select 19 from dual union all
 11  select 20 from dual union all
 12  select 21 from dual union all
 13  select 22 from dual union all
 14  select 25 from dual
 15  /

 Table created.

 SQL> with tabibitosan as
 2  ( select nr
 3         , nr - row_number() over (order by nr) grp
 4      from mytable
 5  )
 6  select min(nr)
 7       , max(nr)
 8    from tabibitosan
 9   group by grp
10   order by grp
11  /

   MIN(NR)    MAX(NR)
---------- ----------
         1          3
         6          7
        11         11
        18         22
        25         25

5 rows selected.

Penso che questa prestazione sia migliore:

SQL> r
  1  select min(nr) as range_start
  2    ,max(nr) as range_end
  3  from (-- our previous query
  4    select nr
  5      ,rownum
  6      ,nr - rownum grp
  7    from  (select nr
  8       from   mytable
  9       order by 1
 10      )
 11   )
 12  group by grp
 13* order by 1

RANGE_START  RANGE_END
----------- ----------
      1      3
      6      7
     11     11
     18     22
     25     25

0

un piano approssimativo:

  • Seleziona il minimo per ciascun nome, (raggruppa per nome)
  • Selezionare il minimo2 per ciascun nome, dove min2> min1 e non esiste (sottoquery: SEL min2-1).
  • Sel max val1> min val1 dove max val1 <min val2.

Ripeti dal 2. fino a quando non si verificano più aggiornamenti. Da lì diventa complicato, Gordian, con il raggruppamento su max di min e min di max. Immagino che sceglierei un linguaggio di programmazione.

PS: Una bella tabella di esempio con alcuni valori di esempio andrebbe bene, che potrebbe essere utilizzata da tutti, quindi non tutti creano i suoi dati di test da zero.


0

Questa soluzione si ispira alla risposta di nate c usando le funzioni di windowing e la clausola OVER. È interessante notare che quella risposta ritorna alle sottoquery con riferimenti esterni. È possibile completare il consolidamento delle righe utilizzando un altro livello di funzioni di finestratura. Potrebbe non sembrare troppo bello, ma presumo che sia più efficiente poiché utilizza la logica integrata delle potenti funzioni di finestre.

Mi sono reso conto dalla soluzione di nate che l'insieme iniziale di righe produceva già i flag necessari per 1) selezionare i valori dell'intervallo iniziale e finale E 2) per eliminare le righe extra nel mezzo. La query ha nidificato due subquery in profondità solo a causa delle limitazioni delle funzioni di windowing, che limitano il modo in cui è possibile utilizzare gli alias di colonna. Logicamente avrei potuto produrre i risultati con una sola query secondaria nidificata.

Alcune altre note : Di seguito è riportato il codice per SQLite3. Il dialetto SQLite deriva da postgresql, quindi è molto simile e potrebbe anche funzionare inalterato. Ho aggiunto la limitazione dell'inquadratura alle clausole OVER, poiché le funzioni lag()e lead()richiedono solo una finestra a riga singola, rispettivamente prima e dopo (quindi non è stato necessario mantenere il set predefinito di tutte le righe precedenti). Ho anche optato per i nomi firste lastpoiché la parola endè riservata.

create temp view test as 
with cte(name, int) AS (
select * from ( values ('foo', 2),
              ('foo', 3),
              ('foo', 4),
              ('foo', 10),
              ('foo', 11),
              ('foo', 13),
              ('bar', 1),
              ('bar', 2),
              ('bar', 3) ))
select * from cte;


SELECT name,
       int AS first, 
       endpoint AS last,
       (endpoint - int + 1) AS span
FROM ( SELECT name, 
             int, 
             CASE WHEN prev <> 1 AND next <> -1 -- orphan
                  THEN int
                WHEN next = -1 -- start of range
                  THEN lead(int) OVER (PARTITION BY name 
                                       ORDER BY int 
                                       ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING)
                ELSE null END
             AS endpoint
        FROM ( SELECT name, 
                   int,
                   coalesce(int - lag(int) OVER (PARTITION BY name 
                                                 ORDER BY int 
                                                 ROWS BETWEEN 1 PRECEDING AND CURRENT ROW), 
                            0) AS prev,
                   coalesce(int - lead(int) OVER (PARTITION BY name 
                                                  ORDER BY int 
                                                  ROWS BETWEEN CURRENT ROW AND 1 FOLLOWING),
                            0) AS next
              FROM test
            ) AS mark_boundaries
        WHERE NOT (prev = 1 AND next = -1) -- discard values within range
      ) as raw_ranges
WHERE endpoint IS NOT null
ORDER BY name, first

I risultati sono esattamente come le altre risposte, come ci si aspetta:

 name | first | last | span
------+-------+------+------
 bar  |     1 |    3 |   3
 foo  |     2 |    4 |   3
 foo  |    10 |   11 |   2
 foo  |    13 |   13 |   1
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.