Объединение 2 больших таблиц postgres с использованием int8range не хорошо масштабируется
Я хотел бы присоединиться к информации таблицы маршрутизации IP к информации whois IP. Я использую RDS Amazon, что означает, что я не могу использовать Postgres ip4r расширение, и поэтому я вместо этого использую int8range типы для представления диапазонов IP-адресов, с суть индексы.
мои таблицы выглядят так:
=> d routing_details
Table "public.routing_details"
Column | Type | Modifiers
----------+-----------+-----------
asn | text |
netblock | text |
range | int8range |
Indexes:
"idx_routing_details_netblock" btree (netblock)
"idx_routing_details_range" gist (range)
=> d netblock_details
Table "public.netblock_details"
Column | Type | Modifiers
------------+-----------+-----------
range | int8range |
name | text |
country | text |
source | text |
Indexes:
"idx_netblock_details_range" gist (range)
полная таблица routing_details содержит чуть менее 600K строк, а netblock_details содержит около 8.25 M строк. Есть перекрывающиеся диапазоны в обеих таблицах, но для каждого диапазона в таблице routing_details я хочу получить одно лучшее (наименьшее) совпадение из таблицы netblock_details.
Я придумал 2 разных запроса, которые, я думаю, вернут точные данные, один с помощью оконных функций и один с помощью DISTINCT ON:
EXPLAIN SELECT DISTINCT ON (r.netblock) *
FROM routing_details r JOIN netblock_details n ON r.range <@ n.range
ORDER BY r.netblock, upper(n.range) - lower(n.range);
QUERY PLAN
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------
Unique (cost=118452809778.47..118477166326.22 rows=581300 width=91)
Output: r.asn, r.netblock, r.range, n.range, n.name, n.country, r.netblock, ((upper(n.range) - lower(n.range)))
-> Sort (cost=118452809778.47..118464988052.34 rows=4871309551 width=91)
Output: r.asn, r.netblock, r.range, n.range, n.name, n.country, r.netblock, ((upper(n.range) - lower(n.range)))
Sort Key: r.netblock, ((upper(n.range) - lower(n.range)))
-> Nested Loop (cost=0.00..115920727265.53 rows=4871309551 width=91)
Output: r.asn, r.netblock, r.range, n.range, n.name, n.country, r.netblock, (upper(n.range) - lower(n.range))
Join Filter: (r.range <@ n.range)
-> Seq Scan on public.routing_details r (cost=0.00..11458.96 rows=592496 width=43)
Output: r.asn, r.netblock, r.range
-> Materialize (cost=0.00..277082.12 rows=8221675 width=48)
Output: n.range, n.name, n.country
-> Seq Scan on public.netblock_details n (cost=0.00..163712.75 rows=8221675 width=48)
Output: n.range, n.name, n.country
(14 rows) -> Seq Scan on netblock_details n (cost=0.00..163712.75 rows=8221675 width=48)
EXPLAIN VERBOSE SELECT * FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY r.range ORDER BY UPPER(n.range) - LOWER(n.range)) AS rank
FROM routing_details r JOIN netblock_details n ON r.range <@ n.range
) a WHERE rank = 1 ORDER BY netblock;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------------------
Sort (cost=118620775630.16..118620836521.53 rows=24356548 width=99)
Output: a.asn, a.netblock, a.range, a.range_1, a.name, a.country, a.rank
Sort Key: a.netblock
-> Subquery Scan on a (cost=118416274956.83..118611127338.87 rows=24356548 width=99)
Output: a.asn, a.netblock, a.range, a.range_1, a.name, a.country, a.rank
Filter: (a.rank = 1)
-> WindowAgg (cost=118416274956.83..118550235969.49 rows=4871309551 width=91)
Output: r.asn, r.netblock, r.range, n.range, n.name, n.country, row_number() OVER (?), ((upper(n.range) - lower(n.range))), r.range
-> Sort (cost=118416274956.83..118428453230.71 rows=4871309551 width=91)
Output: ((upper(n.range) - lower(n.range))), r.range, r.asn, r.netblock, n.range, n.name, n.country
Sort Key: r.range, ((upper(n.range) - lower(n.range)))
-> Nested Loop (cost=0.00..115884192443.90 rows=4871309551 width=91)
Output: (upper(n.range) - lower(n.range)), r.range, r.asn, r.netblock, n.range, n.name, n.country
Join Filter: (r.range <@ n.range)
-> Seq Scan on public.routing_details r (cost=0.00..11458.96 rows=592496 width=43)
Output: r.asn, r.netblock, r.range
-> Materialize (cost=0.00..277082.12 rows=8221675 width=48)
Output: n.range, n.name, n.country
-> Seq Scan on public.netblock_details n (cost=0.00..163712.75 rows=8221675 width=48)
Output: n.range, n.name, n.country
(20 rows)
The DISTINCT ON кажется немного более эффективным,поэтому я продолжил с этим. Когда я запускаю запрос против полного набора данных, я получаю из дискового пространства ошибка после 24 часов ожидания. Я создал таблицу routing_details_small с подмножеством из N строк полной таблицы routing_details, чтобы попытаться понять, что происходит.
С N=1000
=> EXPLAIN ANALYZE SELECT DISTINCT ON (r.netblock) *
-> FROM routing_details_small r JOIN netblock_details n ON r.range <@ n.range
-> ORDER BY r.netblock, upper(n.range) - lower(n.range);
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Unique (cost=4411888.68..4453012.20 rows=999 width=90) (actual time=124.094..133.720 rows=999 loops=1)
-> Sort (cost=4411888.68..4432450.44 rows=8224705 width=90) (actual time=124.091..128.560 rows=4172 loops=1)
Sort Key: r.netblock, ((upper(n.range) - lower(n.range)))
Sort Method: external sort Disk: 608kB
-> Nested Loop (cost=0.41..1780498.29 rows=8224705 width=90) (actual time=0.080..101.518 rows=4172 loops=1)
-> Seq Scan on routing_details_small r (cost=0.00..20.00 rows=1000 width=42) (actual time=0.007..1.037 rows=1000 loops=1)
-> Index Scan using idx_netblock_details_range on netblock_details n (cost=0.41..1307.55 rows=41124 width=48) (actual time=0.063..0.089 rows=4 loops=1000)
Index Cond: (r.range <@ range)
Total runtime: 134.999 ms
(9 rows)
С N=100000
=> EXPLAIN ANALYZE SELECT DISTINCT ON (r.netblock) *
-> FROM routing_details_small r JOIN netblock_details n ON r.range <@ n.range
-> ORDER BY r.netblock, upper(n.range) - lower(n.range);
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Unique (cost=654922588.98..659034941.48 rows=200 width=144) (actual time=28252.677..29487.380 rows=98992 loops=1)
-> Sort (cost=654922588.98..656978765.23 rows=822470500 width=144) (actual time=28252.673..28926.703 rows=454856 loops=1)
Sort Key: r.netblock, ((upper(n.range) - lower(n.range)))
Sort Method: external merge Disk: 64488kB
-> Nested Loop (cost=0.41..119890431.75 rows=822470500 width=144) (actual time=0.079..24951.038 rows=454856 loops=1)
-> Seq Scan on routing_details_small r (cost=0.00..1935.00 rows=100000 width=96) (actual time=0.007..110.457 rows=100000 loops=1)
-> Index Scan using idx_netblock_details_range on netblock_details n (cost=0.41..725.96 rows=41124 width=48) (actual time=0.067..0.235 rows=5 loops=100000)
Index Cond: (r.range <@ range)
Total runtime: 29596.667 ms
(9 rows)
С N=250000
=> EXPLAIN ANALYZE SELECT DISTINCT ON (r.netblock) *
-> FROM routing_details_small r JOIN netblock_details n ON r.range <@ n.range
-> ORDER BY r.netblock, upper(n.range) - lower(n.range);
QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Unique (cost=1651822953.55..1662103834.80 rows=200 width=144) (actual time=185835.443..190143.266 rows=247655 loops=1)
-> Sort (cost=1651822953.55..1656963394.18 rows=2056176250 width=144) (actual time=185835.439..188779.279 rows=1103850 loops=1)
Sort Key: r.netblock, ((upper(n.range) - lower(n.range)))
Sort Method: external merge Disk: 155288kB
-> Nested Loop (cost=0.28..300651962.46 rows=2056176250 width=144) (actual time=19.325..177403.913 rows=1103850 loops=1)
-> Seq Scan on netblock_details n (cost=0.00..163743.05 rows=8224705 width=48) (actual time=0.007..8160.346 rows=8224705 loops=1)
-> Index Scan using idx_routing_details_small_range on routing_details_small r (cost=0.28..22.16 rows=1250 width=96) (actual time=0.018..0.018 rows=0 loops=8224705)
Index Cond: (range <@ n.range)
Total runtime: 190413.912 ms
(9 rows)
против полной таблицы с 600k строками запрос завершается с ошибкой примерно через 24 часа после запуска дискового пространства, что, по-видимому, вызвано внешним шагом слияния. Так это запрос работает хорошо и очень быстро с небольшой таблицей routing_details, но масштабируется очень плохо.
предложения по улучшению моего запроса или, возможно, даже изменения схемы, которые я мог бы сделать, чтобы этот запрос эффективно работал над полным набором данных?
4 ответов
я изначально думал о боковом соединении, как и в других предложенных подходах (например, последний запрос Эрвина Брандштеттера, где он использует простой int8
тип данных и простые индексы), но не хотел писать его в ответе, потому что думал, что это не очень эффективно. Когда вы пытаетесь найти все netblock
диапазоны, которые охватывают данный диапазон, индекс не очень помогает.
я повторю запрос Erwin Brandstetter здесь:
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;
когда вы имейте индекс на netblock_details, например:
CREATE INDEX netblock_details_ip_min_max_idx ON 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. Таким образом, результат этого соединения может иметь ~6M строк (не слишком много)заказать / разделить результат соединения по
routing_details
диапазоны и для каждого такого диапазона найдите лучший (самый маленький) диапазон покрытия отnetblock_details
.
моя идея состоит в том, чтобы изменить запрос. Вместо того, чтобы найти все диапазоны покрытия от большего netblock_details
для каждой строки из небольших routing_details
таблица я предлагаю найти все меньшие диапазоны от меньшего routing_details
для каждой строки из больших netblock_details
.
два этапа
для каждой строки из больших netblock_details
найти все диапазоны от routing_details
что внутри
у меня нет действительно хорошего ответа для вас, потому что я не знаком с индексами gist, но мне интересно, поэтому я немного посмотрел на ваш план объяснения. Несколько вещей выделялись:
1) Ваш план использует вложенное соединение цикла, даже в Примере 250K. Это seq, сканирующий большую таблицу и выполняющий поиск на меньшей. Это означает, что он выполняет 8 миллионов поисков индекса на меньшем столе, занимая более 148 секунд. Мне странно, что это замедляется. значительно с увеличением размера routing_details_small
таблица. Как я уже сказал, Я не знаком с индексами gist, но я бы экспериментировал с set enable_nestloop to false;
чтобы увидеть, можете ли вы заставить его сделать какое-то сортированное слияние/хэш-соединение.
2) в конце выполняется distinct. Это занимает довольно малую часть времени (~11 секунд), но это также означает, что вы можете делать немного дополнительной работы. Похоже, что distinct снижает результирующее количество записей с более чем 1 миллиона до 250K, поэтому я бы экспериментировал с его попыткой раньше. Я не уверен, что вы получаете дубликаты, потому что в routing_details_small
в таблице netblock
или netblock_details
таблица имеет несколько совпадений для данного netblock. Если первый, вы можете присоединиться к подзапросу только с уникальными деталями маршрутизации. Если последнее, попробуйте то, что я собираюсь упомянуть:
3) несколько комбинируя предыдущие два наблюдения, вы можете попробовать выполнить частичное соединение (присоединение к подзапросу) из сканирования seq на routing_details_small. Это должно привести только к сканированию индекса 600K. Что-то вроде (предполагая postgres 9.4):
SELECT * FROM routing_details_small r,
LATERAL (SELECT * FROM netblock_details n WHERE r.range <@ n.range LIMIT 1) nb;
использовать LATERAL
вступить (найти наименьший матч в строке в routing_details
):
текущий дизайн БД
единственным соответствующим индексом для этих запросов является:
"idx_netblock_details_range" gist (range)
другие индексы здесь не имеют значения.
запрос
SELECT * -- only select columns you need to make it faster
FROM routing_details r
, LATERAL (
SELECT *
FROM netblock_details n
WHERE n.range @> r.range
ORDER BY upper(n.range) - lower(n.range)
LIMIT 1
) n
SQL Fiddle более реалистичные тестовые данные.
как и в ваших исходных запросах, строки из routing_details
без каких-либо совпадений в netblock_details
удаляются из результата.
производительность зависит от распределения данных. Это должно быть лучше со многими матчами. DISTINCT ON
может выиграть только с очень небольшим количеством матчей в строке в routing_details
- но он должен большое of work_mem
для большого рода. Сделайте это около 200 МБ для большого запроса. Использовать SET LOCAL
в то же транзакция:
запрос будет не нужно как можно больше сортировать память. В отличие от DISTINCT ON
вы не должны видеть замену Postgres на диск для сортировки с наполовину приличной настройкой для work_mem
. Так что нет такой строки в EXPLAIN ANALYZE
выход:
Sort Method: external merge Disk: 155288kB
проще DB дизайн
на второй взгляд, я протестировал упрощенный дизайн с помощью plain int8
столбцы для нижней и верхней границы вместо типов диапазона и простого индекса btree:
CREATE TABLE routing_details ( -- SMALL table
ip_min int8
, ip_max int8
, asn text
, netblock text
);
CREATE TABLE netblock_details ( -- BIG table
ip_min int8
, ip_max int8
, name text
, country text
, source text
);
CREATE INDEX netblock_details_ip_min_max_idx ON netblock_details
(ip_min, ip_max DESC NULLS LAST);
сортировка второго столбца индекса DESC NULLS LAST
важно!
запрос
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;
та же основная техника. В моих тестах это было ~ 3 раза быстрее, чем первый подход. Но все равно недостаточно быстро для миллионов рядов.
подробное объяснение метода (примеры с индексами b-дерева, но принцип запроса аналогичен для индекса GiST):
и DISTINCT ON
вариант:
передовая решение
над решениями масштабируется линейно с числовыми строками в routing_details
, но ухудшается с количеством матчей в netblock_details
. Наконец до меня дошло: у нас есть решил это раньше на dba.SE с более сложным подходом, дающим в значительной степени превосходную производительность:
frequency
в связанном ответ играет роль ip_max - n.ip_min
/ upper(range) - lower(range)
здесь.
Я не знаю, работает ли это на реальных данных. Отбор кандидатов втиснут во внутреннюю петлю, что мне кажется хорошим. При тестировании он дал два сканирования индекса (плюс один для анти-соединения), избегая окончательной сортировки/уникальности. Похоже, это дает эквивалентные результаты.
-- EXPLAIN ANALYZE
SELECT *
FROM routing_details r
JOIN netblock_details n ON r.range <@ n.range
-- We want the smallest overlapping range
-- Use "Not exists" to suppress overlapping ranges
-- that are larger than n
-- (this should cause an antijoin)
WHERE NOT EXISTS(
SELECT * FROM netblock_details nx
WHERE r.range <@ nx.range -- should enclose r
AND n.range <> nx.range -- but differ from n
AND (nx.range <@ n.range -- and overlap n, or be larger
OR upper(nx.range) - lower(nx.range) < upper(n.range) - lower(n.range)
OR (upper(nx.range) - lower(nx.range) = upper(n.range) - lower(n.range) AND lower(nx.range) > lower(n.range) )
)
)
ORDER BY r.netblock
-- not needed any more
-- , upper(n.range) - lower(n.range)
;
UPDATE: (FWIW) в качестве бонуса, мой тестовый набор данных
CREATE Table routing_details
( asn text
, netblock text
, range int8range
);
-- Indexes:
CREATE INDEX idx_routing_details_netblock ON routing_details (netblock);
CREATE INDEX idx_routing_details_range ON routing_details USING gist(range) ;
CREATE Table netblock_details
( range int8range
, name text
, country text
, source text
);
-- Indexes:
CREATE INDEX idx_netblock_details_range ON netblock_details USING gist(range);
-- the smaller table
INSERT INTO routing_details(range,netblock)
SELECT int8range(gs, gs+13), 'block_' || gs::text
FROM generate_series(0,1000000, 11) gs
;
-- the larger table
INSERT INTO netblock_details(range,name)
SELECT int8range(gs, gs+17), 'name_' || gs::text
FROM generate_series(0,1000000, 17) gs
;
INSERT INTO netblock_details(range,name)
SELECT int8range(gs, gs+19), 'name_' || gs::text
FROM generate_series(0,1000000, 19) gs
;
INSERT INTO netblock_details(range,name)
SELECT int8range(gs, gs+23), 'name_' || gs::text
FROM generate_series(0,1000000, 23) gs
;
VACUUM ANALYZE routing_details;
VACUUM ANALYZE netblock_details;