Algoritmo per trovare il prefisso più lungo


11

Ho due tavoli.

Il primo è una tabella con prefissi

code name price
343  ek1   10
3435 nt     4
3432 ek2    2

Il secondo è il registro delle chiamate con numeri di telefono

number        time
834353212     10
834321242     20
834312345     30

Ho bisogno di scrivere uno script che trovi il prefisso più lungo tra i prefissi per ogni record e scrivere tutti questi dati sulla terza tabella, in questo modo:

 number        code   ....
 834353212     3435
 834321242     3432
 834312345     343

Per il numero 834353212 dobbiamo tagliare '8', quindi trovare il codice più lungo dalla tabella dei prefissi, il suo 3435.
Dobbiamo sempre rilasciare il primo '8' e il prefisso deve essere all'inizio.

Ho risolto questo compito molto tempo fa, con un pessimo modo. È stato un terribile script perl che fa molte domande per ogni disco. Questo script:

  1. Prendi un numero dalla tabella delle chiamate, esegui la sottostringa dalla lunghezza (numero) a 1 => $ prefisso nel ciclo

  2. Esegui la query: seleziona count (*) dai prefissi in cui codice come '$ prefix'

  3. Se conta> 0, prendi i primi prefissi e scrivi nella tabella

Il primo problema è il conteggio delle query: lo è call_records * length(number). Il secondo problema sono le LIKEespressioni. Temo che siano lenti.

Ho provato a risolvere il secondo problema:

CREATE EXTENSION pg_trgm;
CREATE INDEX prefix_idx ON prefix USING gist (code gist_trgm_ops);

Questo accelera ogni query, ma non ha risolto il problema in generale.

Ora ho 20k prefissi e 170k numeri e la mia vecchia soluzione è pessima. Sembra che abbia bisogno di una nuova soluzione senza loop.

Solo una query per ogni record di chiamata o qualcosa del genere.


2
Non sono davvero sicuro che codenella prima tabella sia uguale al prefisso successivo. Potresti chiarirlo, per favore? E anche il fissaggio dei dati di esempio e l'output desiderato (in modo che sia più facile seguire il tuo problema) saranno i benvenuti.
dezso,

Sì. Hai ragione. Mi sono dimenticato di scrivere su "8". Grazie.
Korjavin Ivan,

2
il prefisso deve essere all'inizio, giusto?
dezso,

Sì. Dal secondo posto. 8 $ prefisso $ numeri
Korjavin Ivan

Qual è la cardinalità dei tuoi tavoli? 100k numeri? Quanti prefissi?
Erwin Brandstetter,

Risposte:


21

Sto assumendo il tipo di dati textper le colonne pertinenti.

CREATE TABLE prefix (code text, name text, price int);
CREATE TABLE num (number text, time int);

Soluzione "semplice"

SELECT DISTINCT ON (1)
       n.number, p.code
FROM   num n
JOIN   prefix p ON right(n.number, -1) LIKE (p.code || '%')
ORDER  BY n.number, p.code DESC;

Elementi chiave:

DISTINCT ONè un'estensione Postgres dello standard SQL DISTINCT. Trova una spiegazione dettagliata della tecnica di query utilizzata in questa risposta correlata su SO .
ORDER BY p.code DESCseleziona la corrispondenza più lunga, perché '1234'ordina dopo '123'(in ordine crescente).

Fiddle SQL semplice .

Senza indice, la query verrebbe eseguita per un tempo molto lungo (non vedo l'ora di vederlo finire). Per renderlo veloce, è necessario il supporto dell'indice. Gli indici di trigramma che hai citato, forniti dal modulo aggiuntivo pg_trgmsono un buon candidato. Devi scegliere tra l'indice GIN e GiST. Il primo carattere dei numeri è solo il rumore e può essere escluso dall'indice, rendendolo inoltre un indice funzionale.
Nei miei test, un indice GIN trigramma funzionale ha vinto la gara su un indice GiST trigramma (come previsto):

CREATE INDEX num_trgm_gin_idx ON num USING gin (right(number, -1) gin_trgm_ops);

Dbfiddle avanzato qui .

Tutti i risultati dei test provengono da un'installazione di test Postgres 9.1 locale con un'impostazione ridotta: numeri 17k e codici 2k:

  • Durata totale: 1719.552 ms (trigram GiST)
  • Durata totale: 912.329 ms (trigram GIN)

Molto più veloce ancora

Tentativo fallito con text_pattern_ops

Una volta ignorato il primo carattere di disturbo che distrae, si riduce alla corrispondenza del modello ancorato di base. Pertanto ho provato un indice B-tree funzionale con la classe operatoretext_pattern_ops (assumendo il tipo di colonna text).

CREATE INDEX num_text_pattern_idx ON num(right(number, -1) text_pattern_ops);

Questo funziona in modo eccellente per le query dirette con un singolo termine di ricerca e rende l'indice del trigramma un aspetto negativo in confronto:

SELECT * FROM num WHERE right(number, -1) LIKE '2345%'
  • Durata totale: 3.816 ms (trgm_gin_idx)
  • Durata totale: 0.147 ms (text_pattern_idx)

Tuttavia , il pianificatore di query non prenderà in considerazione questo indice per l'unione di due tabelle. Ho visto questa limitazione prima. Non ho ancora una spiegazione significativa per questo.

Indici B-tree parziali / funzionali

L'alternativa è usare controlli di uguaglianza su stringhe parziali con indici parziali. Questo può essere usato in a JOIN.

Dato che di solito abbiamo un numero limitato di different lengthsprefissi, possiamo costruire una soluzione simile a quella presentata qui con indici parziali.

Supponiamo che abbiamo prefissi che vanno da 1 a 5 caratteri. Crea un numero di indici funzionali parziali, uno per ogni distinta lunghezza del prefisso:

CREATE INDEX prefix_code_idx5 ON prefix(code) WHERE length(code) = 5;
CREATE INDEX prefix_code_idx4 ON prefix(code) WHERE length(code) = 4;
CREATE INDEX prefix_code_idx3 ON prefix(code) WHERE length(code) = 3;
CREATE INDEX prefix_code_idx2 ON prefix(code) WHERE length(code) = 2;
CREATE INDEX prefix_code_idx1 ON prefix(code) WHERE length(code) = 1;

Poiché si tratta di indici parziali , tutti insieme sono appena più grandi di un singolo indice completo.

Aggiungi indici corrispondenti per i numeri (tenendo conto del carattere di disturbo principale):

CREATE INDEX num_number_idx5 ON num(substring(number, 2, 5)) WHERE length(number) >= 6;
CREATE INDEX num_number_idx4 ON num(substring(number, 2, 4)) WHERE length(number) >= 5;
CREATE INDEX num_number_idx3 ON num(substring(number, 2, 3)) WHERE length(number) >= 4;
CREATE INDEX num_number_idx2 ON num(substring(number, 2, 2)) WHERE length(number) >= 3;
CREATE INDEX num_number_idx1 ON num(substring(number, 2, 1)) WHERE length(number) >= 2;

Mentre questi indici contengono solo una sottostringa ciascuno e sono parziali, ognuno copre la maggior parte o tutta la tabella. Quindi sono molto più grandi insieme di un singolo indice totale, ad eccezione dei numeri lunghi. E impongono più lavoro per le operazioni di scrittura. Questo è il costo per una velocità incredibile.

Se quel costo è troppo alto per te (le prestazioni di scrittura sono importanti / troppe operazioni di scrittura / spazio su disco un problema), puoi saltare questi indici. Il resto è ancora più veloce, se non abbastanza veloce come potrebbe essere ...

Se i numeri non sono mai più brevi dei ncaratteri, elimina le WHEREclausole ridondanti da alcune o tutte e rilascia la WHEREclausola corrispondente da tutte le query seguenti.

CTE ricorsivo

Con tutto il setup finora speravo in una soluzione molto elegante con un CTE ricorsivo :

WITH RECURSIVE cte AS (
   SELECT n.number, p.code, 4 AS len
   FROM   num n
   LEFT    JOIN prefix p
            ON  substring(number, 2, 5) = p.code
            AND length(n.number) >= 6  -- incl. noise character
            AND length(p.code) = 5

   UNION ALL 
   SELECT c.number, p.code, len - 1
   FROM    cte c
   LEFT   JOIN prefix p
            ON  substring(number, 2, c.len) = p.code
            AND length(c.number) >= c.len+1  -- incl. noise character
            AND length(p.code) = c.len
   WHERE    c.len > 0
   AND    c.code IS NULL
   )
SELECT number, code
FROM   cte
WHERE  code IS NOT NULL;
  • Durata totale: 1045.115 ms

Tuttavia, mentre questa query non è male - funziona bene quanto la versione semplice con un indice GIN trigramma - non offre ciò a cui stavo puntando. Il termine ricorsivo è pianificato una sola volta, quindi non può utilizzare gli indici migliori. Solo il termine non ricorsivo può.

UNION ALL

Dato che abbiamo a che fare con un numero limitato di ricorsioni, possiamo semplicemente spiegarle in modo iterativo. Ciò consente piani ottimizzati per ciascuno di essi. (Tuttavia, perdiamo l'esclusione ricorsiva di numeri già riusciti. Quindi c'è ancora spazio per miglioramenti, specialmente per una gamma più ampia di lunghezze di prefisso)):

SELECT DISTINCT ON (1) number, code
FROM  (
   SELECT n.number, p.code
   FROM   num n
   JOIN   prefix p
            ON  substring(number, 2, 5) = p.code
            AND length(n.number) >= 6  -- incl. noise character
            AND length(p.code) = 5
   UNION ALL 
   SELECT n.number, p.code
   FROM   num n
   JOIN   prefix p
            ON  substring(number, 2, 4) = p.code
            AND length(n.number) >= 5
            AND length(p.code) = 4
   UNION ALL 
   SELECT n.number, p.code
   FROM   num n
   JOIN   prefix p
            ON  substring(number, 2, 3) = p.code
            AND length(n.number) >= 4
            AND length(p.code) = 3
   UNION ALL 
   SELECT n.number, p.code
   FROM   num n
   JOIN   prefix p
            ON  substring(number, 2, 2) = p.code
            AND length(n.number) >= 3
            AND length(p.code) = 2
   UNION ALL 
   SELECT n.number, p.code
   FROM   num n
   JOIN   prefix p
            ON  substring(number, 2, 1) = p.code
            AND length(n.number) >= 2
            AND length(p.code) = 1
   ) x
ORDER BY number, code DESC;
  • Durata totale: 57.578 ms (!!)

Una svolta, finalmente!

Funzione SQL

L'avvolgimento in una funzione SQL rimuove l'overhead di pianificazione della query per un uso ripetuto:

CREATE OR REPLACE FUNCTION f_longest_prefix()
  RETURNS TABLE (number text, code text) LANGUAGE sql AS
$func$
SELECT DISTINCT ON (1) number, code
FROM  (
   SELECT n.number, p.code
   FROM   num n
   JOIN   prefix p
            ON  substring(number, 2, 5) = p.code
            AND length(n.number) >= 6  -- incl. noise character
            AND length(p.code) = 5
   UNION ALL 
   SELECT n.number, p.code
   FROM   num n
   JOIN   prefix p
            ON  substring(number, 2, 4) = p.code
            AND length(n.number) >= 5
            AND length(p.code) = 4
   UNION ALL 
   SELECT n.number, p.code
   FROM   num n
   JOIN   prefix p
            ON  substring(number, 2, 3) = p.code
            AND length(n.number) >= 4
            AND length(p.code) = 3
   UNION ALL 
   SELECT n.number, p.code
   FROM   num n
   JOIN   prefix p
            ON  substring(number, 2, 2) = p.code
            AND length(n.number) >= 3
            AND length(p.code) = 2
   UNION ALL 
   SELECT n.number, p.code
   FROM   num n
   JOIN   prefix p
            ON  substring(number, 2, 1) = p.code
            AND length(n.number) >= 2
            AND length(p.code) = 1
   ) x
ORDER BY number, code DESC
$func$;

Chiamata:

SELECT * FROM f_longest_prefix_sql();
  • Durata totale: 17.138 ms (!!!)

Funzione PL / pgSQL con SQL dinamico

Questa funzione plpgsql è molto simile al CTE ricorsivo sopra, ma l'SQL dinamico EXECUTEimpone che la query venga riprogrammata per ogni iterazione. Ora utilizza tutti gli indici personalizzati.

Inoltre, funziona per qualsiasi intervallo di lunghezze di prefisso. La funzione accetta due parametri per l'intervallo, ma l'ho preparato con DEFAULTvalori, quindi funziona anche senza parametri espliciti:

CREATE OR REPLACE FUNCTION f_longest_prefix2(_min int = 1, _max int = 5)
  RETURNS TABLE (number text, code text) LANGUAGE plpgsql AS
$func$
BEGIN
FOR i IN REVERSE _max .. _min LOOP  -- longer matches first
   RETURN QUERY EXECUTE '
   SELECT n.number, p.code
   FROM   num n
   JOIN   prefix p
            ON  substring(n.number, 2, $1) = p.code
            AND length(n.number) >= $1+1  -- incl. noise character
            AND length(p.code) = $1'
   USING i;
END LOOP;
END
$func$;

Il passaggio finale non può essere facilmente inserito nell'unica funzione. O chiamalo semplicemente così:

SELECT DISTINCT ON (1)
       number, code
FROM   f_longest_prefix_prefix2() x
ORDER  BY number, code DESC;
  • Durata totale: 27.413 ms

Oppure usa un'altra funzione SQL come wrapper:

CREATE OR REPLACE FUNCTION f_longest_prefix3(_min int = 1, _max int = 5)
  RETURNS TABLE (number text, code text) LANGUAGE sql AS
$func$
SELECT DISTINCT ON (1)
       number, code
FROM   f_longest_prefix_prefix2($1, $2) x
ORDER  BY number, code DESC
$func$;

Chiamata:

SELECT * FROM f_longest_prefix3();
  • Durata totale: 37.622 ms

Un po 'più lento a causa del sovraccarico di pianificazione richiesto. Ma più versatile di SQL e più corto per prefissi più lunghi.


Sto ancora controllando, ma sembra eccellente! La tua idea "inversa" come un operatore - geniale. Perché ero così stupido; (
Korjavin Ivan

5
Whoah! questa è piuttosto la modifica. vorrei poter votare di nuovo.
swasheck,

3
Ho imparato dalla tua straordinaria risposta più che negli ultimi due anni. 17-30 ms contro diverse ore nella mia soluzione loop? Questa è una magia.
Korjavin Ivan,

1
@KorjavinIvan: Beh, come documentato, ho provato con una configurazione ridotta di prefissi 2k / numeri 17k. Ma questo dovrebbe ridimensionarsi abbastanza bene e la mia macchina di prova era un server minuscolo. Quindi dovresti rimanere ben al di sotto di un secondo nel tuo caso di vita reale.
Erwin Brandstetter,

1
Bella risposta ... Conosci l' estensione del prefisso del dimitri ? Potresti includerlo nel confronto dei casi di test?
MatheusOl

0

Una stringa S è un prefisso di una stringa T se T è compreso tra S e SZ dove Z è lessicograficamente più grande di qualsiasi altra stringa (ad es. 99999999 con abbastanza 9 per superare il numero di telefono più lungo possibile nel set di dati, oppure a volte 0xFF funzionerà).

Il prefisso comune più lungo per ogni dato T è anche lessicograficamente massimo, quindi un gruppo semplice di e max lo troverà.

select n.number, max(p.code) 
from prefixes p
join numbers n 
on substring(n.number, 2, 255) between p.code and p.code || '99999999'
group by n.number

Se questo è lento, è probabilmente dovuto alle espressioni calcolate, quindi puoi anche provare a materializzare p.code || '999999' in una colonna della tabella dei codici con il suo indice, ecc.

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.