Mam dwa stoliki.
Pierwszy to tabela z prefiksami
code name price
343 ek1 10
3435 nt 4
3432 ek2 2
Drugi to zapis połączeń z numerami telefonów
number time
834353212 10
834321242 20
834312345 30
Potrzebuję napisać skrypt, który znajdzie najdłuższy prefiks z prefiksów dla każdego rekordu, i zapisz wszystkie te dane do trzeciej tabeli, jak poniżej:
number code ....
834353212 3435
834321242 3432
834312345 343
Dla numeru 834353212 musimy przyciąć „8”, a następnie znaleźć najdłuższy kod z tabeli prefiksów, czyli 3435.
Zawsze musimy upuścić najpierw „8”, a prefiks musi znajdować się na początku.
Rozwiązałem to zadanie dawno temu, bardzo źle. To był okropny skrypt perla, który wykonuje wiele zapytań dla każdego rekordu. Ten skrypt:
Weź numer z tabeli wywołań, wykonaj podciąg od długości (liczby) do prefiksu 1 => $ w pętli
Wykonaj zapytanie: wybierz count (*) z prefiksów, w których kod taki jak „$ prefix”
- Jeśli liczba> 0, weź pierwsze prefiksy i zapisz w tabeli
Pierwszym problemem jest liczba zapytań - to call_records * length(number)
. Drugi problem to LIKE
wyrażenia. Obawiam się, że są powolne.
Próbowałem rozwiązać drugi problem przez:
CREATE EXTENSION pg_trgm;
CREATE INDEX prefix_idx ON prefix USING gist (code gist_trgm_ops);
Przyspiesza to każde zapytanie, ale ogólnie nie rozwiązuje problemu.
Mam teraz 20 000 prefiksów i 170 000 numerów, a moje stare rozwiązanie jest złe. Wygląda na to, że potrzebuję nowego rozwiązania bez pętli.
Tylko jedno zapytanie dla każdego rekordu połączenia lub coś takiego.
źródło
code
w pierwszej tabeli jest taki sam jak przedrostek później. Czy możesz to wyjaśnić? A niektóre poprawki przykładowych danych i pożądanych danych wyjściowych (aby łatwiej było śledzić problem) będą również mile widziane.Odpowiedzi:
Zakładam typ danych
text
dla odpowiednich kolumn.„Proste” rozwiązanie
Kluczowe elementy:
DISTINCT ON
jest rozszerzeniem standardu SQL PostgresDISTINCT
. Znajdź szczegółowe wyjaśnienie zastosowanej techniki zapytań w tej pokrewnej odpowiedzi na SO .ORDER BY p.code DESC
wybiera najdłuższe dopasowanie, ponieważ'1234'
sortuje po'123'
(w kolejności rosnącej).Proste skrzypce SQL .
Bez indeksu zapytanie działałoby przez bardzo długi czas (nie czekało, aż zakończy się). Aby zrobić to szybko, potrzebujesz obsługi indeksu. Wspomniane indeksy trygramu dostarczone przez moduł dodatkowy
pg_trgm
są dobrym kandydatem. Musisz wybrać między indeksem GIN i GiST. Pierwszy znak liczb to po prostu szum i można go wykluczyć z indeksu, co czyni go dodatkowo indeksem funkcjonalnym.W moich testach funkcjonalny indeks GIN trygramu wygrał wyścig z indeksem GiST trigramu (zgodnie z oczekiwaniami):
Zaawansowane dbfiddle tutaj .
Wszystkie wyniki testów pochodzą z lokalnej instalacji testowej Postgres 9.1 ze zmniejszoną konfiguracją: 17 tys. Numerów i 2 tys. Kodów:
Jeszcze szybciej
Nieudana próba z
text_pattern_ops
Kiedy zignorujemy rozpraszającą uwagę pierwszą postać hałasu, sprowadza się ona do podstawowego dopasowania zakotwiczonego w lewo wzoru. Dlatego próbowałem funkcjonalnego indeksu B-drzewa z klasą operatora
text_pattern_ops
(zakładając typ kolumnytext
).Działa to doskonale w przypadku bezpośrednich zapytań z jednym wyszukiwanym terminem i sprawia, że indeks trigram wygląda źle w porównaniu:
Jednak planista zapytań nie uwzględni tego indeksu przy łączeniu dwóch tabel. Widziałem to ograniczenie wcześniej. Nie mam jeszcze sensownego wyjaśnienia tego.
Częściowe / funkcjonalne indeksy B-drzewa
Alternatywą jest użycie kontroli równości częściowych ciągów z częściowymi indeksami. To może być używany w
JOIN
.Ponieważ zazwyczaj mamy tylko ograniczoną liczbę
different lengths
prefiksów, możemy zbudować rozwiązanie podobne do prezentowanego tutaj z częściowymi indeksami.Powiedzmy, że mamy prefiksy od 1 do 5 znaków. Utwórz kilka częściowych indeksów funkcjonalnych, po jednym dla każdej odrębnej długości prefiksu:
Ponieważ są to indeksy częściowe , wszystkie razem są niewiele większe niż pojedynczy pełny indeks.
Dodaj pasujące indeksy dla liczb (biorąc pod uwagę wiodący znak szumu):
Chociaż te indeksy zawierają tylko podciąg i są częściowe, każdy obejmuje większość lub całość tabeli. Są więc znacznie większe niż pojedynczy indeks całkowity - z wyjątkiem długich liczb. I nakładają więcej pracy na operacje zapisu. To koszt niesamowitej prędkości.
Jeśli ten koszt jest dla Ciebie zbyt wysoki (ważna jest wydajność zapisu / zbyt wiele operacji zapisu / problem z miejscem na dysku), możesz pominąć te indeksy. Reszta jest jeszcze szybsza, jeśli nie tak szybka, jak mogłaby być ...
Jeśli liczby nigdy nie są krótsze niż
n
znaki, usuń zbędneWHERE
klauzule z niektórych lub wszystkich, a także usuń odpowiedniąWHERE
klauzulę z wszystkich kolejnych zapytań.Rekurencyjne CTE
Przy całej dotychczasowej konfiguracji liczyłem na bardzo eleganckie rozwiązanie z rekurencyjnym CTE :
Jednak chociaż to zapytanie nie jest złe - działa tak dobrze, jak prosta wersja z indeksem GIN trygramu - nie zapewnia tego, do czego dążyłem. Termin rekurencyjny jest planowany tylko raz, więc nie można użyć najlepszych indeksów. Tylko termin nierekurencyjny może.
UNION ALL
Ponieważ mamy do czynienia z niewielką liczbą rekurencji, możemy po prostu przeliterować je iteracyjnie. Pozwala to zoptymalizować plany dla każdego z nich. (Tracimy jednak rekursywne wykluczanie już udanych liczb. Więc jest jeszcze miejsce na ulepszenia, szczególnie w przypadku szerszego zakresu długości prefiksów)):
Wreszcie przełom!
Funkcja SQL
Zawijanie tego w funkcję SQL usuwa narzut związany z planowaniem zapytań do ponownego użycia:
Połączenie:
Funkcja PL / pgSQL z dynamicznym SQL
Ta funkcja plpgsql jest podobna do rekurencyjnej CTE powyżej, ale dynamiczny SQL
EXECUTE
wymusza ponowne zaplanowanie zapytania dla każdej iteracji. Teraz wykorzystuje wszystkie dostosowane indeksy.Dodatkowo działa to dla dowolnego zakresu długości prefiksów. Funkcja przyjmuje dwa parametry dla zakresu, ale przygotowałem ją z
DEFAULT
wartościami, więc działa również bez wyraźnych parametrów:Ostatni krok nie może być łatwo opakowany w jedną funkcję. Albo nazwij to tak:
Lub użyj innej funkcji SQL jako opakowania:
Połączenie:
Trochę wolniej z powodu wymaganego narzutu związanego z planowaniem. Ale bardziej wszechstronny niż SQL i krótszy dla dłuższych prefiksów.
źródło
Ciąg S jest prefiksem ciągu T iff T jest między S i SZ, gdzie Z jest leksykograficznie większy niż jakikolwiek inny ciąg (np. 99999999 z liczbą 9, aby przekroczyć możliwie najdłuższy numer telefonu w zestawie danych, lub czasami 0xFF będzie działać).
Najdłuższy wspólny przedrostek dla dowolnego T jest również leksykograficznie maksymalny, więc znajdzie go prosta grupa według i max.
Jeśli jest to powolne, prawdopodobnie wynika to z obliczeń, więc możesz także spróbować zmaterializować p.code || '999999' w kolumnie tabeli kodów z własnym indeksem itp.
źródło