Selecione com eficiência o início e o fim de vários intervalos contíguos na consulta do Postgresql


19

Eu tenho cerca de um bilhão de linhas de dados em uma tabela com um nome e um número inteiro no intervalo de 1 a 288. Para um determinado nome , todo int é único e nem todo número possível do intervalo está presente - portanto, existem lacunas.

Esta consulta gera um exemplo de caso:

--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")

Gostaria de gerar uma tabela de pesquisa com uma linha para cada nome e sequência de números inteiros contíguos. Cada linha conteria:

name - o valor do início da coluna de nome - o primeiro número inteiro no final da sequência contígua - o valor final no período de sequência contígua - end - start + 1


Esta consulta gera saída de exemplo para o exemplo acima:

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

Como tenho muitas linhas, mais eficiente é melhor. Dito isto, só preciso executar esta consulta uma vez, portanto não é um requisito absoluto.

Desde já, obrigado!

Editar:

Devo acrescentar que as soluções PL / pgSQL são bem-vindas (por favor, explique quaisquer truques extravagantes - ainda sou novo no PL / pgSQL).


Eu encontraria uma maneira de processar a tabela em pedaços pequenos o suficiente (talvez colocando o "nome" em N buckets ou pegando a primeira / última letra do nome), para que uma espécie se encaixe na memória. É provável que a varredura da tabela em várias tabelas seja mais rápida do que deixar uma classificação se espalhar no disco. Depois disso, eu usava as funções de janelas. Além disso, não esqueça de explorar padrões nos dados. Talvez a maior parte do "nome" tenha realmente uma contagem de 288 valores; nesse caso, você pode excluir esses valores do processo principal. Final de divagações aleatórias :)

ótimo - e bem-vindo ao site. Você teve alguma sorte com as soluções fornecidas?
31412 Jack Douglas

obrigado. Na verdade, mudei de projeto logo após postar esta pergunta (e logo depois mudei de emprego), para nunca ter a chance de testar essas soluções. o que devo fazer em relação à seleção de uma resposta nesse caso?
Stew

Respostas:


9

Que tal usar with recursive

vista de teste:

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");

inquerir:

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;

resultado:

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

Eu ficaria interessado em saber como isso funciona na sua tabela de bilhões de linhas.


Se o desempenho for um problema, jogar com as configurações de work_mem pode ajudar a melhorar o desempenho.
9608 Frank-Heikens

7

Você pode fazer isso com funções de janelas. A idéia básica é usar as funções de janelas leade lagjanelas para puxar as linhas à frente e atrás da linha atual. Então, podemos calcular se temos o início ou o fim da sequência:

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)

(Usei uma visualização para facilitar a lógica abaixo). Agora sabemos se a linha é um começo ou um fim. Temos que recolher isso em linha:

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)

Parece correto para mim :)


3

Outra solução de função de janela. Nenhuma idéia sobre eficiência, adicionei o plano de execução no final (embora com tão poucas linhas, provavelmente não tenha muito valor). Se você quiser brincar: Teste do SQL-Fiddle

Tabela e dados:

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)
  ;

Inquerir:

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
 ; 

Plano de consulta

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

No SQL Server, eu adicionaria mais uma coluna chamada 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")

Eu usaria uma restrição CHECK para garantir que previousInt <int e uma restrição FK (name, previousInt) se refiram a (name, intIn) e mais algumas restrições para garantir a integridade dos dados à prova d'água. Feito isso, selecionar lacunas é trivial:

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

Para acelerar, posso criar um índice filtrado que inclua apenas lacunas. Isso significa que todas as suas lacunas são pré-computadas, portanto, as seleções são muito rápidas e as restrições garantem a integridade de seus dados pré-computados. Estou usando bastante essas soluções, elas estão por todo o meu sistema.


1

Você pode procurar pelo método 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/

Basicamente:

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.

Eu acho esse desempenho melhor:

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

um plano aproximado:

  • Selecione o mínimo para cada nome (agrupar por nome)
  • Selecione o mínimo2 para cada nome, onde min2> min1 e não existe (subconsulta: SEL min2-1).
  • Sel max val1> min val1 onde max val1 <min val2.

Repita de 2. até que não ocorra mais atualização. A partir daí, torna-se complicado, górdio, com o agrupamento de no máximo minutos e minutos máx. Eu acho que eu iria para uma linguagem de programação.

PS: Uma boa tabela de amostra com alguns valores de amostra seria boa, e poderia ser usada por todos, para que nem todos criem seus dados de teste do zero.


0

Esta solução é inspirada na resposta de nate c usando funções de janelas e a cláusula OVER. Curiosamente, essa resposta é revertida para subconsultas com referências externas. É possível concluir a consolidação da linha usando outro nível de funções de janelas. Pode não parecer muito bonito, mas presumo que seja mais eficiente, pois utiliza a lógica interna das poderosas funções de janelas.

Percebi pela solução da nate que o conjunto inicial de linhas já produzia os sinalizadores necessários para 1) selecionar os valores de intervalo inicial e final AND 2) para eliminar as linhas extras no meio. A consulta aninhou subconsultas com duas profundidades apenas devido a limitações das funções de janela, que restringem como os aliases da coluna podem ser usados. Logicamente, eu poderia ter produzido os resultados com apenas uma subconsulta aninhada.

Algumas outras notas : O código a seguir é para SQLite3. O dialeto SQLite é derivado do postgresql, portanto é muito semelhante e pode até funcionar inalterado. Adicionei restrição de enquadramento às cláusulas OVER, pois as funções lag()e lead()precisam apenas de uma janela de linha única, antes e depois respectivamente (portanto, não havia necessidade de manter o conjunto padrão de todas as linhas anteriores). Também optei pelos nomes firste, lastcomo a palavra endé reservada.

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

Os resultados são exatamente como as outras respostas, como se espera:

 name | first | last | span
------+-------+------+------
 bar  |     1 |    3 |   3
 foo  |     2 |    4 |   3
 foo  |    10 |   11 |   2
 foo  |    13 |   13 |   1
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.