PostgreSQL
 sql >> Baza danych >  >> RDS >> PostgreSQL

Dołączanie do 2 dużych stołów postgres przy użyciu int8range nie skaluje się dobrze

Początkowo myślałem o łączeniu bocznym, jak w innych sugerowanych podejściach (na przykład ostatnie zapytanie Erwina Brandstettera, w którym używa on prostego int8 typ danych i proste indeksy), ale nie chciałem tego pisać w odpowiedzi, ponieważ myślałem, że nie jest to zbyt wydajne. Gdy próbujesz znaleźć wszystkie netblock zakresy, które obejmują dany zakres, indeks niewiele pomaga.

Tutaj powtórzę zapytanie Erwina Brandstettera:

SELECT *  -- only select columns you need to make it faster
FROM   routing_details r
     , LATERAL (
   SELECT *
   FROM   netblock_details n
   WHERE  n.ip_min <= r.ip_min
   AND    n.ip_max >= r.ip_max
   ORDER  BY n.ip_max - n.ip_min
   LIMIT  1
   ) n;

Jeśli masz indeks na netblock_details, tak:

CREATE INDEX netblock_details_ip_min_max_idx ON netblock_details 
(ip_min, ip_max DESC NULLS LAST);

możesz szybko (w O(logN) ) znajdź punkt początkowy skanowania w netblock_details tabela - albo maksymalna wartość n.ip_min czyli mniej niż r.ip_min lub minimalny n.ip_max to więcej niż r.ip_max . Ale potem musisz zeskanować/odczytać resztę indeksu/tabeli i dla każdego wiersza wykonać drugą część sprawdzenia i odfiltrować większość wierszy.

Innymi słowy, ten indeks pomaga szybko znaleźć początkowy wiersz, który spełnia pierwsze kryteria wyszukiwania:n.ip_min <= r.ip_min , ale wtedy będziesz kontynuować odczytywanie wszystkich wierszy spełniających te kryteria i dla każdego takiego wiersza wykonaj drugie sprawdzenie n.ip_max >= r.ip_max . Średnio (jeśli dane mają równomierny rozkład) będziesz musiał przeczytać połowę wierszy netblock_details stół. Optymalizator może użyć indeksu do wyszukiwania n.ip_max >= r.ip_max najpierw, a potem zastosuj drugi filtr n.ip_min <= r.ip_min , ale nie możesz używać tego indeksu do jednoczesnego stosowania obu filtrów.

Wynik końcowy:dla każdego wiersza z routing_details przeczytamy połowę wierszy z netblock_details . 600K * 4M =2 400 000 000 000 wierszy. Jest 2 razy lepszy niż produkt kartezjański. Możesz zobaczyć ten numer (produkt kartezjański) na wyjściu EXPLAIN ANALYZE w pytaniu.

592,496 * 8,221,675 = 4,871,309,550,800

Nic dziwnego, że podczas próby zmaterializowania i posortowania zapytań zabrakło miejsca na dysku.

Ogólny proces wysokiego poziomu prowadzący do końcowego wyniku:

  • dołącz do dwóch stołów (nie znajdując jeszcze najlepszego dopasowania). W najgorszym przypadku jest to produkt kartezjański, w najlepszym obejmuje wszystkie zakresy od netblock_details dla każdego zakresu od routing_details . Powiedziałeś, że jest wiele wpisów w netblock_details dla każdego wpisu w routing_details , od 3 do około 10. Tak więc wynik tego połączenia może mieć ~6 mln wierszy (nie za dużo)

  • uporządkuj/podziel wynik połączenia przez routing_details zakresy i dla każdego takiego zakresu znajdź najlepszy (najmniejszy) zakres obejmujący netblock_details .

Moim pomysłem jest odwrócenie zapytania. Zamiast szukać wszystkich obejmujących zakresy z większych netblock_details dla każdego wiersza z mniejszych routing_details tabela Proponuję znaleźć wszystkie mniejsze zakresy z mniejszych routing_details dla każdego wiersza z większego netblock_details .

Proces dwuetapowy

Dla każdego wiersza z większego netblock_details znajdź wszystkie zakresy z routing_details które znajdują się wewnątrz netblock zasięg.

W zapytaniach użyłbym następującego schematu. Dodałem klucz podstawowy ID do obu stołów.

CREATE TABLE routing_details (
ID        int
,ip_min   int8
,ip_max   int8
,asn      text
,netblock text
);

CREATE TABLE netblock_details (
ID        int
,ip_min   int8
,ip_max   int8
,name     text
,country  text
,source   text
);

SELECT
    netblock_details.ID AS n_ID
    ,netblock_details.ip_max - netblock_details.ip_min AS n_length
    ,r.ID AS r_ID
FROM
    netblock_details
    INNER JOIN LATERAL
    (
        SELECT routing_details.ID
        FROM routing_details
        WHERE
            routing_details.ip_min >= netblock_details.ip_min
            AND routing_details.ip_min <= netblock_details.ip_max
            -- note how routing_details.ip_min is limited from both sides
            -- this would make it possible to scan only (hopefully) small
            -- portion of the table instead of full or half table
            AND routing_details.ip_max <= netblock_details.ip_max
            -- this clause ensures that the whole routing range
            -- is inside the netblock range
    ) AS r ON true

Potrzebujemy indeksu na routing_details na (ip_min, ip_max) . Najważniejszą rzeczą tutaj jest indeks na ip_min . Posiadanie drugiej kolumny w indeksie pomaga, eliminując potrzebę wyszukiwania wartości ip_max; to nie pomaga w przeszukiwaniu drzewa.

Zwróć uwagę, że podzapytanie nie ma LIMIT . To nie jest jeszcze ostateczny wynik. To zapytanie powinno zwrócić ~6 mln wierszy. Zapisz ten wynik w tabeli tymczasowej. Dodaj indeks do tabeli tymczasowej na (r_ID, n_length, n_ID) . n_ID to znowu tylko usunięcie dodatkowych wyszukiwań. Potrzebujemy indeksu, aby uniknąć sortowania danych dla każdego r_ID .

Ostatni krok

Dla każdego wiersza z routing_details znajdź n_ID z najmniejszą n_length . Teraz możemy użyć sprzężenia bocznego w „właściwej” kolejności. Tutaj temp tabela jest wynikiem poprzedniego kroku z indeksem .

SELECT
    routing_details.*
    ,t.n_ID
    ,netblock_details.*
FROM
    routing_details
    INNER JOIN LATERAL
    (
        SELECT temp.n_ID
        FROM temp
        WHERE temp.r_ID = routing_details.ID
        ORDER BY temp.n_length
        LIMIT 1
    ) AS t ON true
    INNER JOIN netblock_details ON netblock_details.ID = t.n_ID

Tutaj podzapytanie powinno być wyszukiwaniem indeksu, a nie skanowaniem. Optymalizator powinien być wystarczająco sprytny, aby wykonać tylko wyszukiwanie i zwrócić pierwszy znaleziony wynik, ponieważ LIMIT 1 , więc będziesz miał 600K wyszukiwań indeksu w tabeli tymczasowej rzędu 6M.

Oryginalna odpowiedź (zatrzymam ją tylko dla diagramu zakresów):

Czy możesz „oszukiwać”?

Jeśli dobrze zrozumiałem Twoje zapytanie, dla każdego routing_details.range chcesz znaleźć najmniejszy netblock_details.range który obejmuje/jest większy niż routing_details.range .

Biorąc pod uwagę następujący przykład, gdzie r to zakres routingu i n1, ..., n8 są zakresami bloków sieci, poprawną odpowiedzią jest n5 .

   |---|
   n1

     |------------------|
     n2

                           |---------------|
                           n3

                                          |-----|
                                          n4

                  |------------------|
                  n5

                     |--------------------------------------|
                     n6

        |---------------------------|
        n7

                      |-----|
                      n8

                      |------------|
                      r
                     start       end

n.start <= r.start AND n.end >= r.end
order by n.length
limit 1 

Twoje zapytanie, które zajęło 14 godzin pokazuje, że skanowanie indeksu zajęło 6 ms, ale sortowanie według długości zakresu zajęło 80 ms.

Przy tego rodzaju wyszukiwaniu nie ma prostego porządkowania danych 1D. Używasz n.start razem z n.end i razem z n.length . Ale może twoje dane nie są tak ogólne, lub możesz zwrócić nieco inny wynik.

Na przykład, jeśli można było zwrócić n6 w rezultacie może działać znacznie szybciej. n6 to zakres obejmujący największy start :

n.start <= r.start AND n.end >= r.end
order by n.start desc
limit 1 

Możesz też wybrać n7 , który ma najmniejszy end :

n.start <= r.start AND n.end >= r.end
order by n.end
limit 1 

Ten rodzaj wyszukiwania używałby prostego indeksu na n.start (lub n.end ) bez dodatkowego sortowania.

Drugie, zupełnie inne podejście. Jak duże/długie są zakresy? Jeśli są stosunkowo krótkie (kilka liczb), możesz spróbować zapisać je jako jawną listę liczb całkowitych zamiast zakresu. Na przykład zakres [5-8] będą przechowywane jako 4 wiersze:(5, 6, 7, 8) . Dzięki temu modelowi przechowywania może być łatwiej znaleźć punkty przecięcia zakresów.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Powrót z funkcji z parametrem OUT

  2. Docelowy czas przywracania Pgbackrest

  3. Jak UPSERT (MERGE, INSERT ... ON DUPLICATE UPDATE) w PostgreSQL?

  4. Dziwne zestawienie z postgresql

  5. Jaka jest kolejność rekordów w tabeli ze złożonym kluczem podstawowym?