sql >> Databáze >  >> RDS >> PostgreSQL

Spojení 2 velkých postgresových tabulek pomocí int8range není dobře škálovatelné

Původně jsem uvažoval o bočním spojení jako v jiných navrhovaných přístupech (například poslední dotaz Erwina Brandstettera, kde používá jednoduchý int8 datový typ a jednoduché indexy), ale nechtěl jsem to psát do odpovědi, protože jsem si myslel, že to není opravdu efektivní. Když se pokusíte najít všechny netblock rozsahy, které pokrývají daný rozsah, index moc nepomůže.

Zopakuji zde dotaz 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;

Když máte index na netblock_details, takhle:

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

můžete rychle (v O(logN) ) vyhledejte počáteční bod skenování v netblock_details tabulka - buď maximální n.ip_min to je méně než r.ip_min nebo minimální n.ip_max to je více než r.ip_max . Pak ale musíte naskenovat/přečíst zbytek indexu/tabulky a pro každý řádek provést druhou část kontroly a odfiltrovat většinu řádků.

Jinými slovy, tento index pomáhá rychle najít počáteční řádek, který splňuje první vyhledávací kritéria:n.ip_min <= r.ip_min , ale pak budete pokračovat ve čtení všech řádků, které splňují tato kritéria, a pro každý takový řádek proveďte druhou kontrolu n.ip_max >= r.ip_max . V průměru (pokud mají data rovnoměrné rozložení) budete muset přečíst polovinu řádků netblock_details stůl. Optimalizátor se může rozhodnout použít k vyhledávání index n.ip_max >= r.ip_max nejprve a poté použijte druhý filtr n.ip_min <= r.ip_min , ale tento index nemůžete použít k použití obou filtrů společně.

Konečný výsledek:pro každý řádek z routing_details přečteme polovinu řádků z netblock_details . 600K * 4M =2 400 000 000 000 řádků. Je 2krát lepší než kartézský produkt. Toto číslo (kartézský součin) můžete vidět ve výstupu EXPLAIN ANALYZE v otázce.

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

Není divu, že vašim dotazům dochází místo na disku, když se to pokoušíte zhmotnit a seřadit.

Celkový proces na vysoké úrovni pro dosažení konečného výsledku:

  • připojte se ke dvěma stolům (aniž byste zatím našli nejlepší shodu). V horším případě je to kartézský produkt, v lepším případě to všechno pokrývá rozsahy od netblock_details pro každý rozsah z routing_details . Řekl jste, že v netblock_details je více položek pro každý záznam v routing_details , cokoliv od 3 do přibližně 10. Takže výsledek tohoto spojení by mohl mít ~6M řádků (ne příliš mnoho)

  • objednejte/rozdělte výsledek spojení pomocí routing_details rozsahy a pro každý takový rozsah najděte nejlepší (nejmenší) rozsah pokrytí z netblock_details .

Můj nápad je obrátit dotaz. Místo hledání všech pokrývajících rozsahů z větších netblock_details pro každý řádek z menších routing_details tabulka Doporučuji najít všechny menší rozsahy z menších routing_details pro každý řádek z větších netblock_details .

Dvoufázový proces

Pro každý řádek z větších netblock_details najít všechny rozsahy z routing_details které jsou uvnitř netblock rozsah.

V dotazech bych použil následující schéma. Přidal jsem primární klíč ID k oběma tabulkám.

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

Potřebujeme index na routing_details na (ip_min, ip_max) . Hlavní věc je zde index na ip_min . Mít druhý sloupec v indexu pomáhá tím, že odpadá nutnost vyhledávat hodnotu ip_max; to nepomůže při hledání stromu.

Všimněte si, že boční dílčí dotaz nemá LIMIT . Ještě to není konečný výsledek. Tento dotaz by měl vrátit ~6 milionů řádků. Uložte tento výsledek do dočasné tabulky. Přidejte index do dočasné tabulky na (r_ID, n_length, n_ID) . n_ID je opět jen odstranit další vyhledávání. Potřebujeme index a vyhneme se řazení dat pro každé r_ID .

Poslední krok

Pro každý řádek z routing_details vyhledejte n_ID s nejmenším n_length . Nyní můžeme použít boční spojení ve "správném" pořadí. Zde temp tabulka je výsledkem předchozího kroku s indexem .

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

Zde by měl být dílčí dotaz hledáním indexu, nikoli skenováním. Optimalizátor by měl být dostatečně chytrý, aby provedl pouze vyhledávání a vrátil první nalezený výsledek, protože LIMIT 1 , takže budete mít 600 000 hledání indexu v 6M řádkové dočasné tabulce.

Původní odpověď (nechám si ji jen pro diagram rozsahů):

Umíš "podvádět"?

Pokud jsem správně pochopil váš dotaz, pro každý routing_details.range chcete najít nejmenší netblock_details.range který pokrývá/je větší než routing_details.range .

V následujícím příkladu, kde r je směrovací rozsah a n1, ..., n8 jsou rozsahy netblock, správná odpověď je 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 

Váš dotaz, který trval 14 hodin ukazuje, že indexové skenování trvalo 6 ms, ale řazení podle délky rozsahu trvalo 80 ms.

Při tomto druhu vyhledávání neexistuje jednoduché 1D řazení dat. Používáte n.start spolu s n.end a společně s n.length . Možná však vaše data nejsou tak obecná, nebo je v pořádku vrátit poněkud jiný výsledek.

Například pokud bylo v pořádku vrátit n6 ve výsledku by to mohlo fungovat mnohem rychleji. n6 je rozsah pokrytí, který má největší start :

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

Nebo můžete zvolit n7 , který má nejmenší end :

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

Tento druh vyhledávání by používal jednoduchý index na n.start (nebo n.end ) bez dalšího třídění.

Druhý, zcela odlišný přístup. Jak velké/dlouhé jsou rozsahy? Pokud jsou relativně krátké (málo čísel), můžete je zkusit uložit jako explicitní seznam celých čísel namísto rozsahu. Například rozsah [5-8] bude uloženo jako 4 řádky:(5, 6, 7, 8) . S tímto modelem úložiště může být snadnější najít průsečíky rozsahů.



  1. Oracle odečítá dny a minuty

  2. Řetězec RODBC je zkrácen

  3. zjištění, zda se blíží výročí za n dní v MySql

  4. Úlohy hybridní databáze OLTP/Analytics:Replikace dat MySQL do ClickHouse