Sql 使用int8range连接2个大型postgres表扩展性不好
我想将IP路由表信息加入IP whois信息。我使用的是Amazon的RDS,这意味着我不能使用Postgres扩展,因此我使用类型来表示IP地址范围,并使用索引 我的桌子是这样的:Sql 使用int8range连接2个大型postgres表扩展性不好,sql,postgresql,amazon-web-services,amazon-rds,Sql,Postgresql,Amazon Web Services,Amazon Rds,我想将IP路由表信息加入IP whois信息。我使用的是Amazon的RDS,这意味着我不能使用Postgres扩展,因此我使用类型来表示IP地址范围,并使用索引 我的桌子是这样的: => \d routing_details Table "public.routing_details" Column | Type | Modifiers ----------+-----------+----------- asn | text | net
=> \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)
CREATE INDEX netblock_details_ip_min_max_idx ON netblock_details
(ip_min, ip_max DESC NULLS LAST);
完整的routing_details表包含不到60万行,而netblock_details包含大约825万行。这两个表中都有重叠的范围,但对于routing_details表中的每个范围,我希望从netblock_details表中获得单个最佳最小匹配
我提出了两个不同的查询,我认为它们将返回准确的数据,一个使用窗口函数,另一个使用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)
这种独特的方式似乎更有效,所以我继续使用这种方式。当我对完整的数据集运行查询时,经过大约24小时的等待后,我得到一个磁盘空间不足错误。我创建了一个routing_details_小表,其中包含完整routing_details表中N行的子集,以尝试了解发生了什么
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表中运行良好,速度非常快,但扩展性非常差
关于如何改进我的查询的建议,或者甚至是我可以进行的模式更改,以便此查询能够在完整的数据集上有效地工作?我没有很好的答案给您,因为我不熟悉gist索引,但我有点感兴趣,所以我看了一下您的解释计划。有几件事很突出: 1您的计划使用嵌套循环联接,即使在250K示例中也是如此。它是seq扫描较大的表,并查找较小的表。这意味着它要在较小的表上查找800万个索引,耗时超过148秒。我感到奇怪的是,随着routing_details_小表大小的增加,这种速度会显著减慢。正如我所说,我不熟悉gist索引,但我会尝试将enable_nestloop设置为false;看看是否可以让它进行某种排序的合并/哈希连接 2在末尾执行distinct。这只需要相当小的一部分时间——11秒,但这也意味着你可能会做一些额外的工作。看起来distinct将记录的数量从100多万条减少到了25万条,所以我会尝试早些尝试。我不确定您是否得到了重复项,因为一个netblock的routing_details_小表中有多个条目,或者netblock_details表中有多个匹配项。如果是前者,则可以只使用唯一的路由细节连接子查询。如果是后者,请尝试我将要提到的内容: 3在某种程度上结合前两个观察结果,您可以尝试从路由_details _small上的seq扫描对子查询进行部分连接。这只会导致600K索引扫描。类似于假设postgres 9.4: 从布线详细信息中选择*,
横向选择*FROM netblock\u details n其中r.range我最初考虑的是横向连接,就像其他建议的方法一样,例如,Erwin Brandstetter的最后一个查询,他使用简单的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;
在OlogN中,您可以在netblock_details表中快速找到扫描的起始点—最大n.ip_min小于r.ip_min,或者最小n.ip_max大于r.ip_max。但是,接下来您必须扫描/读取索引/表的其余部分,并对每一行执行第二部分检查并过滤掉大多数行
换句话说,这个索引有助于快速找到满足第一个搜索条件的起始行:n.ip_min=r.ip_max first,然后应用第二个过滤器n.ip_min我不知道这是否在实际数据上执行。候选人的选择被压缩到内部循环中,这对我来说很好。在测试中,它给出了两个索引扫描加上一个反连接扫描,从而避免了最终的排序/唯一性。它似乎给出了相同的结果
-- 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)
;
使用横向连接查找布线详细信息中每行的最小匹配:
当前数据库设计
这些查询的唯一相关索引是:
"idx_netblock_details_range" gist (range)
其他索引在这里是不相关的
查询
具有更真实的测试数据
与原始查询一样,将从结果中删除路由\u详细信息中与netblock\u详细信息不匹配的行
性能取决于数据分布。这在许多比赛中应该是优越的。在m上不同
在路线细节上,每排只有很少几场比赛,我们就赢了,但这需要大量的工作来完成。对于大型查询,将其设置为大约200 MB。在同一事务中使用SET LOCAL:
此查询不需要那么多排序内存。与DISTINCT ON不同的是,您不应该看到Postgres将工作记忆设置为半正常的排序切换到磁盘。所以解释分析输出中没有这样的行:
排序方法:外部合并磁盘:155288kB
更简单的数据库设计
再看一眼,我测试了一个简化的设计,用普通的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最后排序是至关重要的
查询
同样的基本技术。在我的测试中,这比第一种方法快约3倍。但速度仍然不够快,无法容纳数百万行
对使用b树索引的技术示例进行了详细说明,但GiST索引的查询原则类似:
对于不同的ON变量:
高级解决方案
上述解决方案随路由_详细信息中的行数线性扩展,但随netblock_详细信息中的匹配数而恶化。我终于想起来了:我们以前在dba.SE上用一种更复杂的方法解决了这个问题,这种方法可以产生更高的性能:
链接答案中的频率在此处扮演ip_max-n.ip_min/upperrange-lowerrange的角色。是否运行了分析路由详细信息;填充这些表后分析netblock\u详细信息?另外:当您临时忽略ORDER BY时,queryplan会发生什么情况?外部排序磁盘:608kB,您的work\u mem设置是什么?您需要更多内存以避免磁盘排序。将work_mem设置为“..”@乔普:我没有,只是再试了一次,结果还是一样。当我省略order by时,这里是查询计划-它很快,但是结果很好incorrect@FrankHeikens使用的RDS默认值为1MB。更改为100MB,这使得前几个示例查询速度更快,但仍然没有足够的内存用于完整的数据集。我猜,uppern.range-lowern.range;被视为一组函数,使其对外部查询不可索引。尝试在内部查询中计算它可能有助于/避免排序。避免外部查询中的SELECT*可能会有所帮助,因为对于路由详细信息中的每个条目,netblock\u详细信息中有多个条目,从3到大约10。当我添加一个小的修改以确保通过添加一个order子句获得最窄的匹配时,您在3中提出的查询似乎可以工作。我将运行一些额外的测试来确认。此外,我正在使用postgres 9.3,但查询似乎仍然有效。如果在9.3版上运行,我应该注意什么?不-我只是认为是9.4版增加了侧向力,但我可能错了。很高兴听到有帮助!在完整数据集上完成查询大约需要14小时。有没有进一步的优化?似乎没有给出正确的结果。在我的routing_details_小表中,我有25000行,但是这个查询的输出有超过260k行。对于routing_details表中的每一行,输出中应该只有一行。也许在routing_details小表中有重复的范围?是的,事实证明我有。投票支持单独提供测试用例!这个问题也很有趣。虽然在我的本地测试中速度更快,但它必须是最小的范围。看看原因。范围也可能非常大。数据分布是否有明显的偏差?例如,netblock范围平均分布在1到100000000之间,但99%的路由范围在100到1000之间。或者,长度的分布是非常偏斜的。这是一篇有趣的文章,无论如何值得一投。布丁的证据就在于吃。你能做到吗?它的表现如何?我看到了一些障碍。你可以把我的测试设置从小提琴快速开始。不管是哪种方式,进一步思考后,我想到:我们以前解决过这个问题:哇,更新答案中提供的两个查询在90秒内给出了正确的结果,而之前的最佳答案是14小时!太棒了@本多林,我很高兴这有帮助。我试图写下我的方法的理由,希望它足够清楚。一句话:正确的索引是强大的东西,但索引本身并不是魔法,它有助于了解它是如何工作的,它能做什么,它不能做什么。如果将范围长度添加到索引中会消除排序步骤,我会感到惊讶。使用此多列索引查看EXPLAIN ANALYSE的输出会很有趣。我认为这和索引只有一个范围时是一样的:我很高兴被证明是错误的。@VladimirBaranov:我在joop的设置中测试了这两个索引,多列索引更快。但是,我无法用更真实的测试设置重现结果。也许我错误地使用了PG9.5,它引入了GiST索引的索引扫描。因此,我放弃了使用Multicl的建议
umn GiST索引。顺便说一句,我从未说过它会消除排序步骤。横向查询在所有测试中都是最快的,从长远来看。添加一个没有范围和要点的更简单的替代方案。。。如果你能看看我修改后的答案,我将不胜感激,我建议采用两步方法,并写出你的想法。主要的一点是,我不认为一个索引有多大帮助,但是有两个步骤和两个不同的索引用于不同的目的应该可以做到这一点。
|---|
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
n.start <= r.start AND n.end >= r.end
order by n.start desc
limit 1
n.start <= r.start AND n.end >= r.end
order by n.end
limit 1
-- 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)
;
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;
"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
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);
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;