Encuentra filas donde la secuencia entera contiene una subsecuencia dada


9

Problema

Nota: Me refiero a las secuencias matemáticas , no al mecanismo de secuencias de PostgreSQL .

Tengo una tabla que representa secuencias de enteros. La definición es:

CREATE TABLE sequences
(
  id serial NOT NULL,
  title character varying(255) NOT NULL,
  date date NOT NULL,
  sequence integer[] NOT NULL,
  CONSTRAINT "PRIM_KEY_SEQUENCES" PRIMARY KEY (id)
);

Mi objetivo es encontrar filas usando una subsecuencia dada. Es decir, las filas donde el sequencecampo es una secuencia que contiene la subsecuencia dada (en mi caso, la secuencia está ordenada).

Ejemplo

Supongamos que la tabla contiene los siguientes datos:

+----+-------+------------+-------------------------------+
| id | title |    date    |           sequence            |
+----+-------+------------+-------------------------------+
|  1 | BG703 | 2004-12-24 | {1,3,17,25,377,424,242,1234}  |
|  2 | BG256 | 2005-05-11 | {5,7,12,742,225,547,2142,223} |
|  3 | BD404 | 2004-10-13 | {3,4,12,5698,526}             |
|  4 | BK956 | 2004-08-17 | {12,4,3,17,25,377,456,25}     |
+----+-------+------------+-------------------------------+

Entonces, si la subsecuencia dada es {12, 742, 225, 547}, quiero encontrar la fila 2.

Del mismo modo, si la subsecuencia dada es {3, 17, 25, 377}, quiero encontrar la fila 1 y la fila 4.

Finalmente, si la subsecuencia dada es {12, 4, 3, 25, 377}, entonces no hay filas devueltas.

Investigaciones

Primero, no estoy completamente seguro de que representar secuencias con un tipo de datos de matriz sea inteligente. Aunque esto parece apropiado para la situación; Me temo que hace que el manejo sea más complicado. Quizás sea mejor representar las secuencias de manera diferente, utilizando un modelo de relaciones con otra tabla.

De la misma manera, pienso en expandir las secuencias usando la unnestfunción de matriz y luego agregar mis criterios de búsqueda. Sin embargo, el número de términos en la secuencia es variable. No veo cómo hacerlo.

Sé que también es posible cortar mi secuencia en subsecuencia usando la subarrayfunción del módulo intarray , pero no veo cómo me beneficia en mi búsqueda.

Restricciones

Incluso si en este momento mi modelo todavía se está desarrollando, la tabla está compuesta de muchas secuencias, entre 50,000 y 300,000 filas. Entonces tengo una fuerte restricción de rendimiento.

En mi ejemplo usé enteros relativamente pequeños. En la práctica, es posible que estos enteros se vuelvan mucho más grandes, hasta desbordarse bigint. En tal situación, creo que lo mejor es almacenar números como cadenas (ya que no es necesario realizar estas secuencias de operaciones matemáticas). Sin embargo, al optar por esta solución, esto hace que sea imposible usar el módulo intarray , mencionado anteriormente.


Si pueden desbordarse bigint, debe usarlos numericcomo tipo para almacenarlos. Sin embargo, es mucho más lento y ocupa mucho más espacio.
Craig Ringer

@CraigRinger ¿Por qué usar numericy no una cadena ( textpor ejemplo)? No necesito realizar operaciones matemáticas en mis secuencias.
mlpo

2
Porque es más compacto y en muchos sentidos más rápido que text, y evita que almacene datos no numéricos falsos. Depende, si solo está haciendo E / S, es posible que desee que el texto reduzca el procesamiento de E / S.
Craig Ringer

@CraigRinger De hecho, el tipo es más consistente. En cuanto al rendimiento, probaré cuando haya encontrado una manera de hacer mi búsqueda.
mlpo

2
@CraigRinger Puede funcionar si el orden no importa. Pero aquí, las secuencias están ordenadas. Ejemplo: SELECT ARRAY[12, 4, 3, 17, 25, 377, 456, 25] @> ARRAY[12, 4, 3, 25, 377];devolverá verdadero, porque el orden no es considerado por este operador.
mlpo

Respuestas:


3

Si está buscando mejoras significativas en el rendimiento de la respuesta de dnoeth , considere usar una función C nativa y crear el operador apropiado.

Aquí hay un ejemplo para las matrices int4. ( Una variante de matriz genérica y el script SQL correspondiente ).

Datum
_int_sequence_contained(PG_FUNCTION_ARGS)
{
    return DirectFunctionCall2(_int_contains_sequence,
                               PG_GETARG_DATUM(1),
                               PG_GETARG_DATUM(0));
}

Datum
_int_contains_sequence(PG_FUNCTION_ARGS)
{
    ArrayType  *a = PG_GETARG_ARRAYTYPE_P(0);
    ArrayType  *b = PG_GETARG_ARRAYTYPE_P(1);
    int         na, nb;
    int32      *pa, *pb;
    int         i, j;

    na = ArrayGetNItems(ARR_NDIM(a), ARR_DIMS(a));
    nb = ArrayGetNItems(ARR_NDIM(b), ARR_DIMS(b));
    pa = (int32 *) ARR_DATA_PTR(a);
    pb = (int32 *) ARR_DATA_PTR(b);

    /* The naive searching algorithm. Replace it with a better one if your arrays are quite large. */
    for (i = 0; i <= na - nb; ++i)
    {
        for (j = 0; j < nb; ++j)
            if (pa[i + j] != pb[j])
                break;

        if (j == nb)
            PG_RETURN_BOOL(true);
    }

    PG_RETURN_BOOL(false);
}
CREATE FUNCTION _int_contains_sequence(_int4, _int4)
RETURNS bool
AS 'MODULE_PATHNAME'
LANGUAGE C STRICT IMMUTABLE;

CREATE FUNCTION _int_sequence_contained(_int4, _int4)
RETURNS bool
AS 'MODULE_PATHNAME'
LANGUAGE C STRICT IMMUTABLE;

CREATE OPERATOR @@> (
  LEFTARG = _int4,
  RIGHTARG = _int4,
  PROCEDURE = _int_contains_sequence,
  COMMUTATOR = '<@@',
  RESTRICT = contsel,
  JOIN = contjoinsel
);

CREATE OPERATOR <@@ (
  LEFTARG = _int4,
  RIGHTARG = _int4,
  PROCEDURE = _int_sequence_contained,
  COMMUTATOR = '@@>',
  RESTRICT = contsel,
  JOIN = contjoinsel
);

Ahora puede filtrar filas como esta.

SELECT * FROM sequences WHERE sequence @@> '{12, 742, 225, 547}'

He realizado un pequeño experimento para encontrar qué tan rápida es esta solución.

CREATE TEMPORARY TABLE sequences AS
SELECT array_agg((random() * 10)::int4) AS sequence, g1 AS id
FROM generate_series(1, 100000) g1
  CROSS JOIN generate_series(1, 30) g2
GROUP BY g1;
EXPLAIN ANALYZE SELECT * FROM sequences
WHERE        translate(cast(sequence as text), '{}',',,')
 LIKE '%' || translate(cast('{1,2,3,4}'as text), '{}',',,') || '%'

"Seq Scan on sequences  (cost=0.00..7869.42 rows=28 width=36) (actual time=2.487..334.318 rows=251 loops=1)"
"  Filter: (translate((sequence)::text, '{}'::text, ',,'::text) ~~ '%,1,2,3,4,%'::text)"
"  Rows Removed by Filter: 99749"
"Planning time: 0.104 ms"
"Execution time: 334.365 ms"
EXPLAIN ANALYZE SELECT * FROM sequences WHERE sequence @@> '{1,2,3,4}'

"Seq Scan on sequences  (cost=0.00..5752.01 rows=282 width=36) (actual time=0.178..20.792 rows=251 loops=1)"
"  Filter: (sequence @@> '{1,2,3,4}'::integer[])"
"  Rows Removed by Filter: 99749"
"Planning time: 0.091 ms"
"Execution time: 20.859 ms"

Entonces, es aproximadamente 16 veces más rápido. Si no es suficiente, puede agregar soporte para índices GIN o GiST, pero esta será una tarea mucho más difícil.


Suena interesante, sin embargo, uso cadenas o el tipo numericpara representar mis datos porque pueden desbordarse bigint. Sería bueno editar su respuesta para que coincida con las restricciones de la pregunta. De todos modos, haré un rendimiento comparativo que publicaré aquí.
mlpo

No estoy seguro de si es una buena práctica pegar grandes bloques de código en las respuestas, ya que se supone que son mínimas y verificables. Una versión de matriz genérica de esta función es cuatro veces más larga y bastante engorrosa. También he probado con numericy texty la mejora varió de 20 a 50 veces dependiendo de la longitud de las matrices.
Slonopotamus

Sí, sin embargo, es necesario que las respuestas respondan a las preguntas :-). Aquí, me parece que una respuesta que cumpla con las restricciones es interesante (porque este aspecto es parte de la pregunta). Sin embargo, puede no ser necesario proponer una versión genérica. Solo una versión con cadenas o numeric.
mlpo

De todos modos, agregué la versión para matrices genéricas, ya que sería casi la misma para cualquier tipo de datos de longitud variable. Pero si realmente le preocupa el rendimiento, debe seguir con tipos de datos de tamaño fijo como bigint.
Slonopotamus

Me encantaria hacer eso. El problema es que algunas de mis secuencias se desbordan mucho más allá bigint, por lo que parece que no tengo otra opción. Pero si tienes una idea, estoy interesado :).
mlpo

1

Puede encontrar fácilmente la subsecuencia cuando convierte las matrices en cadenas y reemplaza las llaves con comas:

translate(cast(sequence as varchar(10000)), '{}',',,')

{1,3,17,25,377,424,242,1234} -> ',1,3,17,25,377,424,242,1234,'

Haga lo mismo para la matriz que está buscando y agregue un inicio y un final %:

'%' || translate(cast(searchedarray as varchar(10000)), '{}',',,') || '%'

{3, 17, 25, 377} -> '%,3,17,25,377,%'

Ahora lo compara usando LIKE:

WHERE        translate(cast(sequence      as varchar(10000)), '{}',',,')
 LIKE '%' || translate(cast(searchedarray as varchar(10000)), '{}',',,') || '%'

Editar:

Fiddle está trabajando de nuevo.

Si las matrices se normalizan en una fila por valor, puede aplicar la lógica basada en conjuntos:

CREATE TABLE sequences
( id int NOT NULL,
  n int not null,
  val numeric not null
);

insert into sequences values(  1, 1,1     );
insert into sequences values(  1, 2,3     );
insert into sequences values(  1, 3,17    );
insert into sequences values(  1, 4,25    );
insert into sequences values(  1, 5,377   );
insert into sequences values(  1, 6,424   );
insert into sequences values(  1, 7,242   );
insert into sequences values(  1, 8,1234  );
insert into sequences values(  2, 1,5     );
insert into sequences values(  2, 2,7     );
insert into sequences values(  2, 3,12    );
insert into sequences values(  2, 4,742   );
insert into sequences values(  2, 5,225   );
insert into sequences values(  2, 6,547   );
insert into sequences values(  2, 7,2142  );
insert into sequences values(  2, 8,223   );
insert into sequences values(  3, 1,3     );
insert into sequences values(  3, 2,4     );
insert into sequences values(  3, 3,12    );
insert into sequences values(  3, 4,5698  );
insert into sequences values(  3, 5,526   );          
insert into sequences values(  4, 1,12    );
insert into sequences values(  4, 2,4     );
insert into sequences values(  4, 3,3     );
insert into sequences values(  4, 4,17    );
insert into sequences values(  4, 5,25    );
insert into sequences values(  4, 6,377   );
insert into sequences values(  4, 7,456   );
insert into sequences values(  4, 8,25    );
insert into sequences values(  5, 1,12    );
insert into sequences values(  5, 2,4     );
insert into sequences values(  5, 3,3     );
insert into sequences values(  5, 4,17    );
insert into sequences values(  5, 5,17    );
insert into sequences values(  5, 6,25    );
insert into sequences values(  5, 7,377   );
insert into sequences values(  5, 8,456   );
insert into sequences values(  5, 9,25    );

ndebe ser secuencial, sin duplicados, sin huecos. Ahora únete a valores comunes y explota el hecho de que las secuencias son secuenciales :-)

with searched (n,val) as (
  VALUES
   ( 1,3  ),
   ( 2,17 ),
   ( 3,25 ),
   ( 4,377)
)
select seq.id, 
   -- this will return the same result if the values from both tables are in the same order
   -- it's a meaningless dummy, but the same meaningless value for sequential rows 
   seq.n - s.n as dummy,
   seq.val,
   seq.n,
   s.n 
from sequences as seq join searched as s
on seq.val = s.val
order by seq.id, dummy, seq.n;

Finalmente cuente el número de filas con el mismo ficticio y verifique si es el número correcto:

with searched (n,val) as (
  VALUES
   ( 1,3  ),
   ( 2,17 ),
   ( 3,25 ),
   ( 4,377)
)
select distinct seq.id
from sequences as seq join searched as s
on seq.val = s.val
group by 
   seq.id,
   seq.n - s.n
having count(*) = (select count(*) from searched)
;

Pruebe un índice en secuencias (val, id, n).


También consideré esta solución después. Pero veo varios problemas que parecen bastante molestos: en primer lugar, me temo que esta solución es muy ineficiente, debemos convertir cada matriz de cada fila antes de hacer un patrón de búsqueda. Es posible considerar almacenar secuencias en un TEXTcampo ( varchares una mala idea en mi opinión, las secuencias pueden ser largas, como los números, por lo que el tamaño es bastante impredecible), para evitar el lanzamiento; pero todavía no es posible usar índices para mejorar el rendimiento (además, usar un campo de cadena no parece necesariamente juicioso, vea el comentario de @CraigRinger arriba).
mlpo

@mlpo: ¿Cuál es su expectativa de rendimiento? Para poder usar un índice, debe normalizar la secuencia en una fila por valor, aplicar una división relacional y finalmente verificar si el orden es correcto. En su ejemplo 25existe dos veces id=4, ¿es esto realmente posible? ¿Cuántas coincidencias existen en promedio / máximo para una secuencia buscada?
Dnoeth

Una secuencia puede contener varias veces el mismo número. Por ejemplo {1, 1, 1, 1, 12, 2, 2, 12, 12, 1, 1, 5, 4}es bastante posible. Con respecto al número de coincidencias, normalmente se piensa que las subsecuencias utilizadas limitan el número de resultados. Sin embargo, algunas secuencias son muy similares, y a veces puede ser interesante usar una subsecuencia más corta para obtener más resultados. Estimo que el número de coincidencias para la mayoría de los casos está entre 0 y 100. Siempre existe la posibilidad de que ocasionalmente la subsecuencia coincida con muchas secuencias cuando es corta o muy común.
mlpo

@mlpo: agregué una solución basada en conjuntos y estaría muy interesado en alguna comparación de rendimiento :-)
dnoeth

@ypercube: Esto fue sólo una adición rápida para devolver un resultado más significativo :-) Ok, es horrible, voy a cambiar it.l
dnoeth
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.