text
İlgili sütunlar için veri türü varsayıyorum .
CREATE TABLE prefix (code text, name text, price int);
CREATE TABLE num (number text, time int);
"Basit" Çözüm
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;
Anahtar unsurlar:
DISTINCT ON
SQL standardının Postgres uzantısıdır DISTINCT
. SO ile ilgili bu cevapta kullanılan sorgu tekniği için ayrıntılı bir açıklama bulun .
ORDER BY p.code DESC
en uzun eşleşmeyi seçer, çünkü '1234'
sonra '123'
sıralanır (artan sırada).
Basit SQL Fiddle .
Dizin olmadan sorgu çok uzun bir süre çalışırdı (bitmesini görmek için beklemiyordu). Bunu hızlı yapmak için dizin desteğine ihtiyacınız var. Ek modül tarafından sağlanan trigram indeksleri pg_trgm
iyi bir adaydır. GIN ve GiST indeksi arasında seçim yapmak zorundasınız. Sayıların ilk karakteri sadece gürültüdür ve indeksten hariç tutulabilir ve ek olarak fonksiyonel bir indeks haline getirilebilir.
Testlerimde, fonksiyonel bir trigram GIN endeksi bir trigram GiST indeksi (beklendiği gibi) üzerinde yarışı kazandı:
CREATE INDEX num_trgm_gin_idx ON num USING gin (right(number, -1) gin_trgm_ops);
Gelişmiş dbfiddle burada .
Tüm test sonuçları, kurulumu azaltılmış yerel bir Postgres 9.1 test kurulumundan alınmıştır: 17k sayıları ve 2k kodları:
- Toplam çalışma süresi: 1719.552 ms (trigram GiST)
- Toplam çalışma süresi: 912.329 ms (trigram GIN)
Çok daha hızlı
Denemesi başarısız oldu text_pattern_ops
Dikkat dağıtan ilk gürültü karakterini göz ardı ettikten sonra, temel sol bağlantılı desen eşleşmesine iner . Bu nedenle işlevsel bir B-ağacı dizini işleç sınıfıtext_pattern_ops
(sütun türü varsayarak text
) ile denedim .
CREATE INDEX num_text_pattern_idx ON num(right(number, -1) text_pattern_ops);
Bu , tek bir arama terimi olan doğrudan sorgular için mükemmel bir şekilde çalışır ve trigram endeksinin karşılaştırıldığında kötü görünmesini sağlar:
SELECT * FROM num WHERE right(number, -1) LIKE '2345%'
- Toplam çalışma süresi: 3.816 ms (trgm_gin_idx)
- Toplam çalışma süresi: 0.147 ms (text_pattern_idx)
Ancak , sorgu planlayıcısı iki tabloya katılmak için bu dizini dikkate almaz. Bu sınırlamayı daha önce görmüştüm. Bunun için henüz anlamlı bir açıklamam yok.
Kısmi / fonksiyonel B-ağacı indeksleri
Alternatif olarak, kısmi dizinleri olan kısmi dizelerde eşitlik kontrolleri kullanılır. Bu , a JOIN
.
Tipik olarak yalnızca sınırlı sayıda different lengths
önekimiz olduğu için, burada kısmi dizinlerle sunulanlara benzer bir çözüm oluşturabiliriz .
Diyelim ki 1 ile 5 karakter arasında değişen önekler var . Her farklı önek uzunluğu için bir tane olmak üzere bir dizi kısmi işlev dizini oluşturun:
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;
Bunlar kısmi dizinler olduğu için, hepsi birlikte tek bir tam dizinden çok daha büyüktür.
Numaralar için eşleşen dizinler ekleyin (baştaki gürültü karakterini dikkate alarak):
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;
Bu dizinler yalnızca bir alt dize tutarken ve kısmi olsa da, her biri tablonun çoğunu veya tamamını kapsar. Böylece, tek bir toplam endeksinden çok daha büyükler - uzun sayılar hariç. Ve yazma işlemleri için daha fazla iş empoze ediyorlar. İnanılmaz hızın maliyeti budur .
Bu maliyet sizin için çok yüksekse (yazma performansı önemlidir / çok fazla yazma işlemi / disk alanı sorunu), bu dizinleri atlayabilirsiniz. Geri kalanı hala daha hızlı, eğer olabildiğince hızlı değilse ...
Sayılar hiçbir zaman daha kısa değilse n
, karakterlerden WHERE
bazılarını veya tümünü gereksiz cümleleri bırakın ve WHERE
aşağıdaki tüm sorgulardan karşılık gelen maddeyi de bırakın .
Özyinelemeli CTE
Şimdiye kadar yapılan tüm kurulumlarda, özyinelemeli bir CTE ile çok zarif bir çözüm umuyordum :
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;
- Toplam çalışma süresi: 1045.115 ms
Ancak, bu sorgu kötü olmasa da - bir trigram GIN endeksi ile basit sürüm kadar iyi performans gösterir - hedeflediğim şeyi sunmuyor. Yinelemeli terim yalnızca bir kez planlanır, bu nedenle en iyi dizinleri kullanamaz. Sadece özyinelemesiz terim olabilir.
BİRLİK TÜMÜ
Az sayıda özyineleme ile uğraştığımızdan, bunları tekrarlayarak heceleyebiliriz. Bu, her biri için optimize edilmiş planlara izin verir. (Yine de, zaten başarılı olan sayıların yinelemeli olarak hariç tutulmasını kaybediyoruz. Bu nedenle , özellikle daha geniş bir dizi önek uzunluğu aralığı için hala iyileştirme için bir yer var)):
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;
- Toplam çalışma süresi: 57.578 ms (!!)
Sonunda bir atılım!
SQL işlevi
Bunu bir SQL işlevine sarmak, tekrarlanan kullanım için sorgu planlama ek yükünü kaldırır:
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$;
Telefon etmek:
SELECT * FROM f_longest_prefix_sql();
- Toplam çalışma süresi: 17.138 ms (!!!)
Dinamik SQL ile PL / pgSQL işlevi
Bu plpgsql işlevi yukarıdaki özyinelemeli CTE'ye çok benzer, ancak dinamik SQL EXECUTE
, sorguyu her yineleme için yeniden planlanmaya zorlar. Şimdi tüm uyarlanmış endekslerden faydalanıyor.
Ayrıca bu, herhangi bir önek uzunluğu aralığı için de geçerlidir. İşlev aralık için iki parametre alır, ancak DEFAULT
değerlerle hazırladım , bu yüzden açık parametreler olmadan da çalışır:
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$;
Son adım bir işleve kolayca sarılamaz.
Ya sadece şöyle deyin:
SELECT DISTINCT ON (1)
number, code
FROM f_longest_prefix_prefix2() x
ORDER BY number, code DESC;
- Toplam çalışma süresi: 27.413 ms
Veya sarmalayıcı olarak başka bir SQL işlevi kullanın:
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$;
Telefon etmek:
SELECT * FROM f_longest_prefix3();
- Toplam çalışma süresi: 37.622 ms
Gerekli planlama yükü nedeniyle biraz daha yavaş. Ancak SQL'den daha çok yönlü ve daha uzun önekler için daha kısa.
code
İlk tabloda daha sonra önek ile aynı olup olmadığından emin değilim . Lütfen açıklığa kavuşturabilir misiniz? Örnek verilerin ve istenen çıktının (sorununuzu takip etmenin daha kolay olması için) bazı düzeltmeleri de memnuniyetle karşılanacaktır.