Suchen Sie Zeilen, in denen eine ganzzahlige Sequenz eine bestimmte Teilsequenz enthält


9

Problem

Hinweis: Ich beziehe mich auf die mathematischen Sequenzen , nicht auf den Sequenzmechanismus von PostgreSQL .

Ich habe eine Tabelle, die Folgen von ganzen Zahlen darstellt. Die Definition lautet:

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

Mein Ziel ist es, Zeilen mit einer bestimmten Teilsequenz zu finden. Das heißt, die Zeilen, in denen das sequenceFeld eine Sequenz ist, die die angegebene Teilsequenz enthält (in meinem Fall ist die Sequenz geordnet).

Beispiel

Angenommen, die Tabelle enthält folgende Daten:

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

Wenn also die gegebene Teilsequenz ist {12, 742, 225, 547}, möchte ich die Zeile 2 finden.

In ähnlicher Weise {3, 17, 25, 377}möchte ich , wenn die gegebene Teilsequenz ist , die Zeile 1 und die Zeile 4 finden.

Wenn die angegebene Teilsequenz lautet {12, 4, 3, 25, 377}, werden keine Zeilen zurückgegeben.

Untersuchungen

Erstens bin ich mir nicht ganz sicher, ob es sinnvoll ist, Sequenzen mit einem Array-Datentyp darzustellen. Obwohl dies der Situation angemessen erscheint; Ich fürchte, es macht die Handhabung komplizierter. Vielleicht ist es besser, die Sequenzen anders darzustellen, indem Sie ein Beziehungsmodell mit einer anderen Tabelle verwenden.

Auf die gleiche Weise denke ich darüber nach, die Sequenzen mithilfe der unnestArray-Funktion zu erweitern und dann meine Suchkriterien hinzuzufügen. Da die Anzahl der Begriffe in der Sequenz jedoch variabel ist, sehe ich nicht, wie das geht.

Ich weiß, dass es auch möglich ist, meine Sequenz mithilfe der subarrayFunktion des Intarray- Moduls in Teilsequenzen zu schneiden, aber ich sehe nicht, wie es mir bei meiner Suche zugute kommt.

Einschränkungen

Auch wenn sich mein Modell derzeit noch in der Entwicklung befindet, soll die Tabelle aus vielen Sequenzen zwischen 50.000 und 300.000 Zeilen bestehen. Ich habe also eine starke Leistungsbeschränkung.

In meinem Beispiel habe ich relativ kleine ganze Zahlen verwendet. In der Praxis ist es möglich, dass diese ganzen Zahlen bis zum Überlauf viel größer werden bigint. In einer solchen Situation ist es meiner Meinung nach am besten, Zahlen als Zeichenfolgen zu speichern (da diese Sequenzen mathematischer Operationen nicht ausgeführt werden müssen). Wenn Sie sich jedoch für diese Lösung entscheiden, ist es unmöglich, das oben erwähnte Intarray- Modul zu verwenden .


Wenn sie überlaufen können bigint, sollten Sie sie numericals Typ zum Speichern verwenden. Es ist viel langsamer und nimmt viel mehr Platz ein.
Craig Ringer

@CraigRinger Warum verwenden numericund nicht eine Zeichenfolge ( textzum Beispiel)? Ich muss keine mathematischen Operationen an meinen Sequenzen ausführen.
mlpo

2
Weil es kompakter und in vielerlei Hinsicht schneller als textist und Sie daran hindert, falsche nicht numerische Daten zu speichern. Abhängig davon, ob Sie nur E / A ausführen, möchten Sie möglicherweise, dass Text die E / A-Verarbeitung reduziert.
Craig Ringer

@CraigRinger In der Tat ist der Typ konsistenter. In Bezug auf die Leistung werde ich testen, wann ich einen Weg gefunden habe, meine Suche durchzuführen.
mlpo

2
@CraigRinger Es könnte funktionieren, wenn die Reihenfolge keine Rolle spielt. Aber hier sind die Sequenzen geordnet. Beispiel: SELECT ARRAY[12, 4, 3, 17, 25, 377, 456, 25] @> ARRAY[12, 4, 3, 25, 377];Gibt true zurück, da die Reihenfolge von diesem Operator nicht berücksichtigt wird.
mlpo

Antworten:


3

Wenn Sie nach signifikanten Leistungsverbesserungen für die Antwort von dnoeth suchen , sollten Sie eine native C-Funktion verwenden und den entsprechenden Operator erstellen.

Hier ist ein Beispiel für int4-Arrays. ( Eine generische Array-Variante und das entsprechende SQL-Skript ).

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

Jetzt können Sie Zeilen wie diese filtern.

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

Ich habe ein kleines Experiment durchgeführt, um herauszufinden, wie viel schneller diese Lösung ist.

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"

Es ist also ungefähr 16 mal schneller. Wenn dies nicht ausreicht, können Sie Unterstützung für GIN- oder GiST-Indizes hinzufügen. Dies ist jedoch eine viel schwierigere Aufgabe.


Klingt interessant, aber ich verwende entweder Zeichenfolgen oder den Typ numeric, um meine Daten darzustellen, da sie möglicherweise überlaufen bigint. Es kann sinnvoll sein, Ihre Antwort so zu bearbeiten, dass sie den Einschränkungen der Frage entspricht. Wie auch immer, ich werde eine vergleichende Performance machen, die ich hier posten werde.
mlpo

Ich bin mir nicht sicher, ob es eine gute Praxis ist, große Codeblöcke in Antworten einzufügen, da diese minimal und überprüfbar sein sollen. Eine generische Array-Version dieser Funktion ist viermal länger und ziemlich umständlich. Ich habe es auch mit numericund getestet textund die Verbesserung lag je nach Länge der Arrays zwischen 20 und 50 Mal.
Slonopotamus

Ja, aber es ist notwendig, dass die Antworten Fragen beantworten :-). Hier scheint mir eine Antwort, die den Einschränkungen entspricht, interessant zu sein (weil dieser Aspekt ein Teil der Frage ist). Es ist jedoch möglicherweise nicht erforderlich, eine generische Version vorzuschlagen. Nur eine Version mit Strings oder numeric.
mlpo

Wie auch immer, ich habe die Version für generische Arrays hinzugefügt, da sie für jeden Datentyp mit variabler Länge nahezu gleich wäre. Wenn Sie sich jedoch wirklich Gedanken über die Leistung machen, sollten Sie sich an Datentypen mit fester Größe wie halten bigint.
Slonopotamus

Ich würde es lieben das zu tun. Das Problem ist, dass einige meiner Sequenzen weit darüber hinaus überlaufen bigint, so dass ich anscheinend keine Wahl habe. Aber wenn Sie eine Idee haben, bin ich interessiert :).
mlpo

1

Sie können die Teilsequenz leicht finden, wenn Sie die Arrays in Zeichenfolgen umwandeln und die geschweiften Klammern durch Kommas ersetzen:

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

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

Machen Sie dasselbe für das Array, nach dem Sie suchen, und fügen Sie ein führendes und ein nachfolgendes hinzu %:

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

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

Jetzt vergleichen Sie es mit LIKE:

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

Bearbeiten:

Fiddle arbeitet wieder.

Wenn die Arrays in einer Zeile pro Wert normalisiert sind, können Sie eine satzbasierte Logik anwenden:

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

nmuss sequentiell sein, keine Duplikate, keine Lücken. Schließen Sie sich nun gemeinsamen Werten an und nutzen Sie die Tatsache, dass Sequenzen sequentiell sind :-)

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;

Zählen Sie abschließend die Anzahl der Zeilen mit demselben Dummy und prüfen Sie, ob es sich um die richtige Anzahl handelt:

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

Versuchen Sie einen Index für Sequenzen (val, id, n).


Ich habe diese Lösung auch später in Betracht gezogen. Aber ich sehe einige Probleme, die ziemlich störend erscheinen: Zunächst befürchte ich, dass diese Lösung sehr ineffizient ist. Wir müssen jedes Array jeder Zeile umwandeln, bevor wir ein Suchmuster erstellen. Es ist möglich, das Speichern von Sequenzen in einem TEXTFeld in Betracht zu ziehen ( varcharist meiner Meinung nach eine schlechte Idee, Sequenzen können lang sein, da die Zahlen, so dass die Größe eher unvorhersehbar ist), um eine Umwandlung zu vermeiden. Es ist jedoch immer noch nicht möglich, Indizes zur Verbesserung der Leistung zu verwenden (außerdem scheint die Verwendung eines Zeichenfolgenfelds nicht unbedingt sinnvoll zu sein, siehe Kommentar von @CraigRinger oben).
mlpo

@mlpo: Was ist Ihre Leistungserwartung? Um einen Index verwenden zu können, müssen Sie die Sequenz in eine Zeile pro Wert normalisieren, eine relationale Division anwenden und schließlich prüfen, ob die Reihenfolge korrekt ist. In Ihrem Beispiel 25gibt es zweimal in id=4, ist das tatsächlich möglich? Wie viele Übereinstimmungen gibt es im Durchschnitt / Maximum für eine gesuchte Sequenz?
dnoeth

Eine Sequenz kann mehrmals dieselbe Nummer enthalten. Zum Beispiel {1, 1, 1, 1, 12, 2, 2, 12, 12, 1, 1, 5, 4}ist durchaus möglich. In Bezug auf die Anzahl der Übereinstimmungen wird normalerweise angenommen, dass die verwendeten Teilsequenzen die Anzahl der Ergebnisse begrenzen. Einige Sequenzen sind jedoch sehr ähnlich, und es kann manchmal interessant sein, eine kürzere Teilsequenz zu verwenden, um mehr Ergebnisse zu erzielen. Ich schätze, dass die Anzahl der Übereinstimmungen in den meisten Fällen zwischen 0 und 100 liegt. Es besteht immer die Möglichkeit, dass die Teilsequenz gelegentlich mit vielen Sequenzen übereinstimmt, wenn sie kurz oder sehr häufig ist.
mlpo

@mlpo: Ich habe eine satzbasierte Lösung hinzugefügt und wäre sehr an einem Leistungsvergleich interessiert :-)
dnoeth

@ypercube: Dies war nur eine kurze Ergänzung, um ein aussagekräftigeres Ergebnis zu erzielen :-) Ok, es ist schrecklich, ich werde es ändern.l
dnoeth
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.