Trova le righe in cui la sequenza intera contiene una data sottosequenza


9

Problema

Nota: mi riferisco alle sequenze matematiche , non al meccanismo delle sequenze di PostgreSQL .

Ho una tabella che rappresenta sequenze di numeri interi. La definizione è:

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

Il mio obiettivo è trovare le righe usando una data sottosequenza. Vale a dire, le righe in cui il sequencecampo è una sequenza che contiene la sottosequenza data (nel mio caso, la sequenza è ordinata).

Esempio

Supponiamo che la tabella contenga i seguenti dati:

+----+-------+------------+-------------------------------+
| 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}     |
+----+-------+------------+-------------------------------+

Quindi, se la sottosequenza data è {12, 742, 225, 547}, voglio trovare la riga 2.

Allo stesso modo, se la sottosequenza data è {3, 17, 25, 377}, voglio trovare la riga 1 e la riga 4.

Infine, se la sottosequenza data è {12, 4, 3, 25, 377}, allora non ci sono righe restituite.

indagini

Innanzitutto, non sono del tutto sicuro che rappresentare sequenze con un tipo di dati array sia saggio. Anche se questo sembra appropriato alla situazione; Temo che renda la gestione più complicata. Forse è meglio rappresentare le sequenze in modo diverso, usando un modello di relazioni con un'altra tabella.

Allo stesso modo, penso di espandere le sequenze usando la unnestfunzione array e quindi aggiungere i miei criteri di ricerca. Tuttavia, il numero di termini nella sequenza essendo variabile non vedo come farlo.

So che è anche possibile tagliare la mia sequenza in sottosequenza usando la subarrayfunzione del modulo intarray ma non vedo come mi avvantaggia la mia ricerca.

vincoli

Anche se al momento il mio modello è ancora in fase di sviluppo, la tabella dovrebbe essere composta da molte sequenze, tra 50.000 e 300.000 righe. Quindi ho un forte vincolo di prestazioni.

Nel mio esempio ho usato numeri interi relativamente piccoli. In pratica, è possibile che questi numeri interi diventino molto più grandi, fino a traboccare bigint. In una situazione del genere, penso che il migliore sia memorizzare numeri come stringhe (poiché non è necessario eseguire queste sequenze di operazioni matematiche). Tuttavia, optando per questa soluzione, ciò rende impossibile utilizzare il modulo intarray , menzionato sopra.


Se riescono a traboccare, bigintè necessario utilizzare numericcome tipo per memorizzarli. È molto più lento e richiede comunque più spazio.
Craig Ringer,

@CraigRinger Perché usare numerice non una stringa ( textad esempio)? Non ho bisogno di eseguire operazioni matematiche sulle mie sequenze.
mlpo,

2
Perché è più compatto e per molti versi più veloce di text, e ti impedisce di archiviare dati non numerici fasulli. Dipende, se stai solo eseguendo l'I / O, potresti volere del testo per ridurre l'elaborazione dell'I / O.
Craig Ringer,

@CraigRinger In effetti, il tipo è più coerente. Per quanto riguarda le prestazioni, proverò quando avrò trovato il modo di fare la mia ricerca.
mlpo,

2
@CraigRinger Potrebbe funzionare se l'ordine non ha importanza. Ma qui, le sequenze sono ordinate. Esempio: SELECT ARRAY[12, 4, 3, 17, 25, 377, 456, 25] @> ARRAY[12, 4, 3, 25, 377];restituirà vero, perché l'ordine non è considerato da questo operatore.
mlpo,

Risposte:


3

Se stai cercando miglioramenti significativi delle prestazioni della risposta di Dnoeth , considera l'utilizzo di una funzione C nativa e la creazione dell'operatore appropriato.

Ecco un esempio per le matrici int4. ( Una variante di matrice generica e lo script SQL corrispondente ).

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

Ora puoi filtrare le righe in questo modo.

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

Ho condotto un piccolo esperimento per scoprire quanto è più veloce questa soluzione.

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"

Quindi, è circa 16 volte più veloce. Se non è abbastanza, puoi aggiungere il supporto per gli indici GIN o GiST, ma questo sarà un compito molto più difficile.


Sembra interessante, tuttavia uso le stringhe o il tipo numericper rappresentare i miei dati perché potrebbero traboccare bigint. Potrebbe essere opportuno modificare la risposta in modo che corrisponda ai vincoli della domanda. Ad ogni modo, farò una performance comparativa che posterò qui.
mlpo,

Non sono sicuro che sia una buona pratica incollare grandi blocchi di codice in risposte poiché si suppone che siano minimi e verificabili. Una versione generica dell'array di questa funzione è quattro volte più lunga e piuttosto ingombrante. L'ho anche testato con numerice texte il miglioramento variava da 20 a 50 volte a seconda della lunghezza degli array.
Slonopotamus

Sì, tuttavia è necessario che le risposte abbiano risposto alle domande :-). Qui, mi sembra che una risposta conforme ai vincoli sia interessante (perché questo aspetto fa parte della domanda). Tuttavia, potrebbe non essere necessario proporre una versione generica. Solo una versione con stringhe o numeric.
mlpo,

Ad ogni modo, ho aggiunto la versione per array generici poiché sarebbe quasi la stessa per qualsiasi tipo di dati a lunghezza variabile. Ma se sei davvero preoccupato per le prestazioni, dovresti rimanere con tipi di dati di dimensioni fisse come bigint.
Slonopotamus,

Mi piacerebbe farlo. Il problema è che alcune delle mie sequenze traboccano ben oltre bigint, quindi sembra che non abbia scelta. Ma se hai un'idea, sono interessato :).
mlpo,

1

È possibile trovare facilmente la sottosequenza quando si eseguono il cast degli array in stringhe e si sostituiscono le parentesi graffe con virgole:

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

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

Fai lo stesso per l'array che stai cercando e aggiungi un carattere iniziale e finale %:

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

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

Ora lo confronti usando LIKE:

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

Modificare:

Fiddle sta di nuovo lavorando.

Se le matrici sono normalizzate in una riga per valore, è possibile applicare la logica basata su set:

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

ndeve essere sequenziale, senza duplicati, senza spazi vuoti. Ora unisciti a valori comuni e sfrutta il fatto che le sequenze sono sequenziali :-)

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;

Infine conta il numero di righe con lo stesso manichino e controlla se è il numero corretto:

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

Prova un indice sulle sequenze (val, id, n).


Ho anche considerato questa soluzione in seguito. Ma vedo diversi problemi che sembrano piuttosto fastidiosi: prima di tutto temo che questa soluzione sia molto inefficiente, dobbiamo lanciare ogni array di ogni riga prima di fare un modello di ricerca. È possibile considerare la memorizzazione di sequenze in un TEXTcampo ( varcharè una cattiva idea secondo me, le sequenze possono essere lunghe, come i numeri, quindi la dimensione è piuttosto imprevedibile), per evitare il cast; ma non è ancora possibile utilizzare gli indici per migliorare le prestazioni (inoltre utilizzare un campo stringa non sembra necessariamente giudizioso, vedere il commento di @CraigRinger sopra).
mlpo,

@mlpo: quali sono le tue aspettative sulle prestazioni? Per poter utilizzare un indice è necessario normalizzare la sequenza in una riga per valore, applicare una divisione relazionale e infine verificare se l'ordine è corretto. Nel tuo esempio 25esiste due volte id=4, è davvero possibile? Quante corrispondenze esistono in media / al massimo per una sequenza cercata?
Dnoeth,

Una sequenza può contenere più volte lo stesso numero. Ad esempio {1, 1, 1, 1, 12, 2, 2, 12, 12, 1, 1, 5, 4}è del tutto possibile. Per quanto riguarda il numero di corrispondenze, si ritiene che le sottosequenze utilizzate limitino il numero di risultati. Tuttavia, alcune sequenze sono molto simili e talvolta può essere interessante utilizzare una sottosequenza più breve per ottenere più risultati. Stimo che il numero di corrispondenze per la maggior parte dei casi è compreso tra 0 e 100. Con sempre la possibilità che occasionalmente la sottosequenza corrisponda a molte sequenze quando è breve o molto comune.
mlpo,

@mlpo: ho aggiunto una soluzione basata su set e sarei molto interessato a un confronto delle prestazioni :-)
dnoeth

@ypercube: Questa è stata solo una rapida aggiunta per restituire un risultato più significativo :-) Ok, è orribile, lo cambierò.
Dnoeth
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.