PostgreSQL
 sql >> база данни >  >> RDS >> PostgreSQL

Свързването на 2 големи таблици на postgres с помощта на int8range не се мащабира добре

Първоначално мислех за странично свързване, както при други предложени подходи (например последната заявка от Erwin Brandstetter, където той използва прост int8 тип данни и прости индекси), но не исках да го напиша в отговора, защото смятах, че не е наистина ефективен. Когато се опитате да намерите всички netblock диапазони, които покриват дадения диапазон, индексът не помага много.

Ще повторя запитването на Erwin Brandstetter тук:

SELECT * -- изберете само колони, които трябва да направите по-бързи 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 ОГРАНИЧЕНИЕ 1 ) n; 

Когато имате индекс на netblock_details, като това:

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

можете бързо (в O(logN) ) намерете началната точка на сканирането в netblock_details таблица - или максималния n.ip_min това е по-малко от r.ip_min или минималния n.ip_max това е повече от r.ip_max . Но тогава трябва да сканирате/прочетете останалата част от индекса/таблицата и за всеки ред да направите втората част от проверката и да филтрирате повечето редове.

С други думи, този индекс помага за бързо намиране на началния ред, който отговаря на първите критерии за търсене:n.ip_min <=r.ip_min , но след това ще продължите да четете всички редове, които отговарят на този критерий и за всеки такъв ред изпълнете втората проверка n.ip_max>=r.ip_max . Средно (ако данните имат равномерно разпределение) ще трябва да прочетете половината от редовете на netblock_details маса. Оптимизаторът може да избере да използва индекс за търсене n.ip_max>=r.ip_max първо и след това приложете втори филтър n.ip_min <=r.ip_min , но не можете да използвате този индекс, за да приложите двата филтъра заедно.

Краен резултат:за всеки ред от routing_details ще прочетем половината редове от netblock_details . 600K * 4M =2 400 000 000 000 реда. Това е 2 пъти по-добро от декартовото произведение. Можете да видите това число (декартово произведение) в изхода на EXPLAIN ANALYZE във въпроса.

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

Нищо чудно, че вашите заявки изчерпват дисковото пространство, когато се опитвате да материализирате и сортирате това.

Цялостният процес на високо ниво за достигане до крайния резултат:

  • съединете две маси (без все още да намерите най-доброто съвпадение). В най-лошия случай това е декартов продукт, в най-добрия случай всичко покрива диапазони от netblock_details за всеки диапазон от routing_details . Казахте, че има множество записи в netblock_details за всеки запис в routing_details , всичко от 3 до около 10. Така че резултатът от това присъединяване може да има ~6 милиона реда (не твърде много)

  • ред/разделяне на резултата от присъединяването чрез routing_details диапазони и за всеки такъв диапазон намерете най-добрия (най-малък) покриващ диапазон от netblock_details .

Идеята ми е да обърна заявката. Вместо да намирате всички покриващи диапазони от по-големи netblock_details за всеки ред от по-малки routing_details Предлагам да намерите всички по-малки диапазони от по-малки routing_details за всеки ред от по-големи netblock_details .

Процес в две стъпки

За всеки ред от по-големи netblock_details намерете всички диапазони от routing_details които са вътре в netblock диапазон.

Бих използвал следната схема в заявките. Добавих първичен ключ ID към двете таблици.

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);ИЗБЕРЕТЕ netblock_details.ID AS n_ID ,netblock_details.ip_max - netblock_details.ip_min AS n_length ,r.ID AS r_IDFROM 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 -- обърнете внимание как routing_details.ip_min е ограничен от двете страни -- това би направило възможно сканирането само (надяваме се) на малка -- част от таблицата вместо на пълната или половината таблица И routing_details.ip_max <=netblock_details.ip_max -- това клауза гарантира, че целият диапазон на маршрутизиране -- е вътре в диапазона на мрежовия блок) AS r ON true 

Имаме нужда от индекс на routing_details на (ip_min, ip_max) . Основното нещо тук е индексът на ip_min . Наличието на втора колона в индекса помага, като елиминира необходимостта от извършване на търсене на стойността на ip_max; не помага при търсенето в дърво.

Имайте предвид, че страничната подзаявка няма LIMIT . Все още не е окончателният резултат. Тази заявка трябва да върне ~6 милиона реда. Запазете този резултат във временна таблица. Добавете индекс към временната таблица на (r_ID, n_length, n_ID) . n_ID отново е само за премахване на допълнителни търсения. Имаме нужда от индекс, но избягвайте сортирането на данните за всеки r_ID .

Последна стъпка

За всеки ред от routing_details намерете n_ID с най-малката n_length . Сега можем да използваме страничното съединение в "правилен" ред. Тук temp таблицата е резултат от предишната стъпка с индекс .

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 НА netblock_details.ID =t.n_ID 

Тук подзаявката трябва да бъде търсене на индекса, а не сканиране. Оптимизаторът трябва да е достатъчно умен, за да извърши само търсенето и да върне първия намерен резултат поради LIMIT 1 , така че ще имате 600K търсения на индекс в 6M таблица с временни редове.

Оригинален отговор (ще го запазя само за диаграмата на диапазоните):

Можете ли да "мамите"?

Ако съм разбрал правилно вашата заявка, за всеки routing_details.range искате да намерите най-малкия netblock_details.range който покрива/е по-голям от routing_details.range .

Имайки предвид следния пример, където r е диапазон на маршрутизиране и n1, ..., n8 са диапазони на мрежови блокове, правилният отговор е n5 .

<предварителен |---| n1 |------------------| n2 |--------------| n3 |-----| n4 |------------------| n5 |-------------------------------------------| n6 |---------------------------| n7 |-----| n8 |------------| r начало endn.start <=r.start И n.end>=r.endorder от n.lengthlimit 1

Вашата заявка, която отне 14 часа показва, че сканирането на индекса отне 6ms, но сортирането по дължина на диапазона отне 80ms.

При този вид търсене няма просто 1D подреждане на данните. Вие използвате n.start заедно с n.end и заедно с n.length . Но може би вашите данни не са толкова общи или е добре да върнете малко по-различен резултат.

Например, ако е добре да се върне n6 в резултат на това може да работи много по-бързо. n6 е обхващащият диапазон, който има най-голямо начало :

n.start <=r.start И n.end>=r.endorder by n.start desclimit 1  

Или можете да изберете n7 , който има най-малък end :

n.start <=r.start AND n.end>=r.endorder by n.endlimit 1  

Този тип търсене ще използва обикновен индекс на n.start (или n.end ) без допълнително сортиране.

Втори, напълно различен подход. Колко големи/дълги са диапазоните? Ако са сравнително кратки (малко числа), можете да опитате да ги съхраните като изричен списък от цели числа, вместо диапазон. Например диапазон [5-8] ще се съхраняват като 4 реда:(5, 6, 7, 8) . С този модел на съхранение може да е по-лесно да намерите пресечни точки на диапазони.



  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Как да получите динамичен изглед за 12 работни дни в Postgresql?

  2. Защо SQL идентификационните последователности излизат от синхрон (по-специално с помощта на Postgres)?

  3. Как да получа предупредителни съобщения за процедури на Postgresql?

  4. За разбиране на заявки в PHP PG -подготвени оператори

  5. Как да получа броя на неделята на текущия месец в psql?