Sto assumendo il tipo di dati text
per 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 DESC
seleziona 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_trgm
sono 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 lengths
prefissi, 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 n
caratteri, elimina le WHERE
clausole ridondanti da alcune o tutte e rilascia la WHERE
clausola 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 EXECUTE
impone 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 DEFAULT
valori, 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;
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();
Un po 'più lento a causa del sovraccarico di pianificazione richiesto. Ma più versatile di SQL e più corto per prefissi più lunghi.
code
nella 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.