Първоначално мислех за странично свързване, както при други предложени подходи (например последната заявка от 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)
. С този модел на съхранение може да е по-лесно да намерите пресечни точки на диапазони.