Seleccione eficientemente el comienzo y el final de múltiples rangos contiguos en la consulta Postgresql

19

Tengo alrededor de mil millones de filas de datos en una tabla con un nombre y un número entero en el rango 1-288. Para un nombre dado , cada int es único, y no todos los enteros posibles en el rango están presentes, por lo que hay huecos.

Esta consulta genera un caso de ejemplo:

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

Me gustaría generar una tabla de búsqueda con una fila para cada nombre y secuencia de enteros contiguos. Cada una de esas filas contendría:

nombre - el valor del comienzo de la columna de nombre - el primer entero en el final de la secuencia contigua - el valor final en el intervalo de la secuencia contigua - final - inicio + 1


Esta consulta genera resultados de ejemplo para el ejemplo anterior:

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

Debido a que tengo tantas filas, más eficiente es mejor. Dicho esto, solo tengo que ejecutar esta consulta una vez, por lo que no es un requisito absoluto.

¡Gracias por adelantado!

Editar:

Debo agregar que las soluciones PL / pgSQL son bienvenidas (explique cualquier truco de fantasía: todavía soy nuevo en PL / pgSQL).

Estofado
fuente
Encontraría una manera de procesar la tabla en trozos lo suficientemente pequeños (tal vez haciendo un hash con el "nombre" en N cubos, o tomando la primera / última letra del nombre), de modo que una especie encaje en la memoria. Es probable que escanear la tabla varias tablas sea más rápido que dejar que una clasificación se derrame al disco. Una vez que tuviera eso, seguiría usando las funciones de ventanas. Además, no olvides explotar patrones en los datos. Tal vez la mayoría del "nombre" en realidad tiene un recuento de 288 valores, en cuyo caso podría excluir esos valores del proceso principal. Fin de divagación aleatoria :)
genial, y bienvenido al sitio. ¿Tuviste suerte con las soluciones proporcionadas?
Jack Douglas
gracias. En realidad, cambié los proyectos poco después de publicar esta pregunta (y poco después, cambié de trabajo), por lo que nunca tuve la oportunidad de probar estas soluciones. ¿Qué debo hacer en términos de seleccionar una respuesta en tal caso?
Estofado

Respuestas:

9

¿Qué hay de usar with recursive

vista de prueba:

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

consulta:

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)

Me interesaría saber cómo funciona eso en su tabla de mil millones de filas.

Jack Douglas
fuente
Si el rendimiento es un problema, jugar con la configuración de work_mem podría ayudar a mejorar el rendimiento.
Frank Heikens
7

Puede hacerlo con las funciones de ventanas. La idea básica es utilizar leady lagde ventanas funciones para tirar filas por delante y por detrás de la fila actual. Entonces podemos calcular si tenemos el inicio o el final de la secuencia:

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)

(Utilicé una vista para que la lógica sea más fácil de seguir a continuación). Ahora sabemos si la fila es un principio o un final. Tenemos que colapsar eso en fila:

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)

Me parece correcto :)


fuente
3

Otra solución de función de ventana. No tengo idea de la eficiencia, he agregado el plan de ejecución al final (aunque con tan pocas filas, probablemente no tenga mucho valor). Si quieres jugar: prueba de SQL-Fiddle

Tabla y datos:

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

Consulta:

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
 ; 

Plan 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)
ypercubeᵀᴹ
fuente
3

En SQL Server, agregaría una columna más llamada 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")

Usaría una restricción CHECK para asegurarme de que previousInt <int, y una restricción FK (name, previousInt) se refieren a (name, int), y un par de restricciones más para garantizar la integridad de los datos. Hecho esto, seleccionar huecos es trivial:

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

Para acelerarlo, podría crear un índice filtrado que incluiría solo huecos. Esto significa que todas sus brechas se calculan previamente, por lo que las selecciones son muy rápidas y las restricciones aseguran la integridad de sus datos calculados previamente. Estoy usando mucho tales soluciones, están en todo mi sistema.

Alaska
fuente
1

Puedes buscar el 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/

Básicamente:

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.

Creo que esta actuación mejor:

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
Carlos S
fuente
0

un plan aproximado:

  • Seleccione el mínimo para cada nombre (grupo por nombre)
  • Seleccione el mínimo2 para cada nombre, donde min2> min1 y no existe (subconsulta: SEL min2-1).
  • Sel max val1> min val1 donde max val1 <min val2.

Repita desde 2. hasta que no ocurra más actualización. A partir de ahí, se complica, Gordian, con una agrupación de máx. De minutos y mín. De máx. Supongo que elegiría un lenguaje de programación.

PD: Una buena tabla de muestra con algunos valores de muestra estaría bien, lo que podría ser utilizado por todos, por lo que no todos crean sus datos de prueba desde cero.


fuente
0

Esta solución está inspirada en nate c la respuesta usando las funciones de ventanas y la cláusula OVER. Curiosamente, esa respuesta vuelve a las subconsultas con referencias externas. Es posible completar la consolidación de filas utilizando otro nivel de funciones de ventanas. Puede que no parezca demasiado bonito, pero supongo que es más eficiente ya que utiliza la lógica incorporada de las potentes funciones de ventanas.

Me di cuenta de la solución de Nate que el conjunto inicial de filas ya prodigaba los indicadores necesarios para 1) seleccionar los valores de rango inicial y final Y 2) para eliminar las filas adicionales en el medio. La consulta tiene subconsultas anidadas dos profundas solo debido a las limitaciones de las funciones de ventanas, que restringen cómo se pueden usar los alias de columna. Lógicamente, podría haber producido los resultados con solo una subconsulta anidada.

Algunas otras notas : El siguiente es el código para SQLite3. El dialecto SQLite se deriva de postgresql, por lo que es muy similar e incluso podría funcionar sin modificaciones. Agregué restricciones de encuadre a las cláusulas OVER, ya que las funciones lag()y lead()solo necesitan una ventana de una sola fila, antes y después respectivamente (por lo que no era necesario mantener el conjunto predeterminado de todas las filas anteriores). También opté por los nombres firsty lastdado que la palabra endestá 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

Los resultados son como las otras respuestas, como se espera:

 name | first | last | span
------+-------+------+------
 bar  |     1 |    3 |   3
 foo  |     2 |    4 |   3
 foo  |    10 |   11 |   2
 foo  |    13 |   13 |   1
C Perkins
fuente