En uzun öneki bulmak için algoritma


11

İki masam var.

Birincisi önek içeren bir tablo

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

İkincisi, telefon numaralarına sahip arama kayıtları

number        time
834353212     10
834321242     20
834312345     30

Her kayıt için öneklerden en uzun önek bulmak bir komut dosyası yazmak ve bu gibi üçüncü tabloya tüm bu verileri yazmak gerekir:

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

834353212 numarası için '8'i kırpmalı ve sonra önek tablosundan en uzun kodu bulmalıyız, 3435.
Her zaman önce' 8 '' i düşürmeliyiz ve önek başlangıçta olmalıdır.

Bu görevi uzun zaman önce çok kötü bir şekilde çözdüm. Her kayıt için çok fazla sorgu yapan korkunç perl betiğiydi. Bu komut dosyası:

  1. Çağrılar tablosundan bir sayı alın, döngüde uzunluktan (sayı) 1 => $ önekine kadar alt dize yapın

  2. Sorguyu yapın: kodun '$ prefix' gibi olduğu öneklerden count (*) seçin

  3. Count> 0 ise, ilk önekleri alın ve tabloya yazın

İlk sorun sorgu sayıları - bu call_records * length(number). İkinci sorun LIKEifadelerdir. Korkarım bunlar yavaş.

İkinci sorunu şu şekilde çözmeye çalıştım:

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

Bu, her sorguyu hızlandırır, ancak genel olarak sorunu çözmez.

Şu anda 20k önek ve 170k numaram var ve eski çözümüm kötü. Döngüsüz yeni bir çözüme ihtiyacım var gibi görünüyor.

Her çağrı kaydı için sadece bir sorgu veya bunun gibi bir şey.


2
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.
dezso

Evet. Haklısın. '8' hakkında yazmayı unutmuştum. Teşekkür ederim.
Korjavin Ivan

2
ön ekin başında olması gerekir, değil mi?
dezso

Evet. İkinci yerden. 8 $ önek $ numaraları
Korjavin Ivan

Masalarınızın asıl özelliği nedir? 100 bin rakam? Kaç önek var?
Erwin Brandstetter

Yanıtlar:


21

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 ONSQL 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 DESCen 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_trgmiyi 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 WHEREbazılarını veya tümünü gereksiz cümleleri bırakın ve WHEREaş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 DEFAULTdeğ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.


Hala kontrol ediyorum, ama mükemmel görünüyor! Fikriniz operatör gibi "ters" - parlak. Neden bu kadar aptaldım; (
Korjavin Ivan

5
Whoah! bu oldukça düzenleme. keşke tekrar oy verebilseydim.
swasheck

3
İnanılmaz cevabınızdan son iki yıldan daha fazlasını öğreniyorum. Birkaç saat karşı 17-30 ms döngü çözümümde? Bu bir sihir.
Korjavin Ivan

1
@KorjavinIvan: Belgelendirildiği gibi, 2k önek / 17k sayıların kurulumunu azalttım. Ama bu oldukça iyi ölçeklenmeli ve test makinem küçük bir sunucuydu. Bu yüzden gerçek hayat durumunuzla bir saniyenin altında kalmalısınız.
Erwin Brandstetter

1
Güzel cevap ... dimitri'nin önek uzantısını biliyor musunuz ? Bunu test senaryolarının karşılaştırmasına ekleyebilir misiniz?
MatheusOl

0

Bir S dizesi, T iff T dizisinin S ve SZ arasında bir önekidir; burada Z, sözlükbilimsel olarak diğer dizelerden daha büyüktür (örneğin, veri kümesindeki mümkün olan en uzun telefon numarasını aşmak için yeterli 9'a sahip 99999999 veya bazen 0xFF çalışacaktır).

Herhangi bir T için en uzun ortak önek aynı zamanda sözlükbilimsel olarak maksimaldir, bu nedenle ve max tarafından basit bir grup onu bulur.

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

Bu yavaşsa, büyük olasılıkla hesaplanan ifadelerden kaynaklanır, bu nedenle p.code || '999999' kodunu kendi dizininde vb. İçeren bir sütuna düzenlemeyi de deneyebilirsiniz.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.