使用 int8range 连接 2 个大型 postgres 表不能很好地扩展

Posted

技术标签:

【中文标题】使用 int8range 连接 2 个大型 postgres 表不能很好地扩展【英文标题】:Joining 2 large postgres tables using int8range not scaling well 【发布时间】:2015-10-13 14:01:18 【问题描述】:

我想将 IP 路由表信息加入 IP whois 信息。我正在使用 Amazon 的 RDS,这意味着我不能使用 Postgres ip4r 扩展,因此我使用 int8range 类型来表示 IP 地址范围,并使用 gist 索引。

我的表格如下所示:

=> \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 表包含不到 60 万行,而 netblock_details 包含大约 825 万行。两个表中有重叠的范围,但对于 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)

DISTINCT ON 似乎更有效,所以我继续使用那个。当我对完整数据集运行查询时,大约等待 24 小时后出现磁盘空间不足错误。我创建了一个 routing_details_small 表,其中包含完整 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 表上运行得很好而且很快,但是扩展性很差。

关于如何改进我的查询的建议,或者甚至我可以进行的架构更改,以便此查询能够在整个数据集上有效地工作?

【问题讨论】:

您在填充这些表后是否运行了ANALYZE routing_details; ANALYZE netblock_details?另外:当您(暂时)省略 ORDER BY 时,查询计划会发生什么? "external sort Disk: 608kB",你对work_mem的设置是什么?您需要更多内存以避免磁盘排序。 SET work_mem TO '....'; @joop 我没有,但只是再次尝试并得到相同的结果。当我省略订单时,这里是查询计划gist.github.com/coderholic/92ae27c405cd25e45fa6 - 它很快,但结果不正确 @FrankHeikens 使用 RDS 默认值 1MB。更改为 100MB,这使得前几个示例查询更快,但仍然没有足够的内存用于完整数据集 我的猜测是 , upper(n.range) - lower(n.range); 被视为一组函数,使其无法被外部查询索引。尝试在内部查询中计算它可能有助于/避免排序。 (避免外部查询中的SELECT * 也有帮助) 【参考方案1】:

使用 LATERAL 加入(在 routing_details 中找到每行的最小匹配项):

What is the difference between LATERAL and a subquery in PostgreSQL?

当前的数据库设计

这些查询的唯一相关索引是:

"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 中每行只有很少的比赛获胜 - 但它需要 很多work_mem 进行大排序。为大查询设置大约 200 MB。在同一事务中使用SET LOCAL

Configuration parameter work_mem in PostgreSQL on Linux

此查询需要尽可能多的排序内存。与DISTINCT ON 不同,您不应该看到 Postgres 交换到磁盘进行排序,work_mem 的设置还不错。所以EXPLAIN ANALYZE 输出中没有这样的行:

Sort Method: external merge Disk: 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 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 倍。但是对于数百万行仍然不够快。

SQL Fiddle.


技术详解(以b-tree索引为例,但查询原理与GiST索引类似):

Optimize GROUP BY query to retrieve latest record per user

对于DISTINCT ON 变体:

Select first row in each GROUP BY group?

高级解决方案

上述解决方案随routing_details 中的行数线性扩展,但随着netblock_details 中的匹配数而恶化。它终于回到了我的身边:我们之前在 dba.SE 上解决了这个问题,采用了一种更复杂的方法,产生了非常出色的性能:

Can spatial index help a “range - order by - limit” query

链接答案中的frequency在这里扮演ip_max - n.ip_min/upper(range) - lower(range)的角色。

【讨论】:

如果将范围长度添加到索引会消除排序步骤,我会感到惊讶。使用这个多列索引查看EXPLAIN ANALYZE 的输出会很有趣。我认为这与 index 刚刚range 时相同:gist.github.com/coderholic/9e90311f9323b543aef2 很高兴被证明是错误的。 @VladimirBaranov:我在 joop 的设置上测试了两个索引,多列索引更快。但是,我无法使用更真实的测试设置来重现结果。也许我错误地使用了 pg 9.5(它为 GiST 索引引入了仅索引扫描)。因此,我放弃了多列 GiST 索引的建议。 (顺便说一句,我从来没有说过它会“消除”排序步骤。)LATERAL 查询在所有测试中都是最快的,从长远来看。添加一个更简单的替代方案,没有范围和 GiST ...和小提琴,您可以在其中看到 EXPLAIN 输出。 如果您能看看我修改后的答案,我将不胜感激,我建议分两步走,并写下您的想法。要点 - 我认为单个索引不会有太大帮助,但是针对不同目的使用两个不同索引的两个步骤应该可以解决问题。【参考方案2】:

我最初考虑的是横向连接,就像其他建议的方法一样(例如,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;

当你在 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 &lt;= r.ip_min,但随后您将继续读取满足此条件的所有行,并对每个这样的行执行第二次检查 @987654335 @。平均而言(如果数据分布均匀),您必须读取netblock_details 表的一半行。优化器可以选择先使用索引搜索n.ip_max &gt;= r.ip_max,然后再应用第二个过滤器n.ip_min &lt;= 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_detailsrouting_details 的每个范围。你说routing_details 中的每个条目在netblock_details 中有多个条目,从 3 到大约 10。所以,这个连接的结果可能有大约 600 万行(不是太多)

routing_details 范围对连接结果进行排序/分区,并为每个此类范围从netblock_details 中找到最佳(最小)覆盖范围。


我的想法是反转查询。我建议不要从较小的routing_details 表中为每一行从较大的netblock_details 中查找所有覆盖范围,而是从较大的netblock_details 中的每一行中查找从较小的routing_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
);

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

我们需要routing_details 上的索引(ip_min, ip_max)。这里的主要内容是ip_min 上的索引。索引中的第二列有助于消除查找ip_max 值的需要;它对树搜索没有帮助。

请注意,横向子查询没有LIMIT。这还不是最终结果。此查询应返回约 6M 行。将此结果保存在临时表中。 为(r_ID, n_length, n_ID) 上的临时表添加索引。 n_ID 再次只是为了删除额外的查找。我们需要一个索引来避免对每个r_ID 的数据进行排序。

最后一步

对于来自routing_details 的每一行,找到具有最小n_lengthn_ID。现在我们可以以“正确”的顺序使用横向连接。这里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 ON netblock_details.ID = t.n_ID

这里的子查询应该是对索引的查找,而不是扫描。由于LIMIT 1,优化器应该足够聪明,只进行查找并返回第一个找到的结果,因此您将在 6M 行临时表中进行 60 万次索引查找。


原始答案(我将仅保留范围图):

你能“作弊”吗?

如果我正确理解您的查询,对于每个routing_details.range 你想找到一个最小的netblock_details.range,它覆盖/大于routing_details.range

鉴于以下示例,其中r 是路由范围,n1, ..., n8 是网络块范围,正确答案是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 

您的query that took 14 hours 显示索引扫描需要 6 毫秒,但按范围长度排序需要 80 毫秒。

通过这种搜索,没有简单的一维数据排序。您将n.startn.endn.length 一起使用。但是,也许您的数据不是那么通用,或者返回稍微不同的结果是可以的。

例如,如果返回n6 是可以的,它的运行速度会更快。 n6start最大的覆盖范围:

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

或者,您可以选择n7,它具有最小的end

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

这种搜索将使用n.start(或n.end)上的简单索引,无需额外排序。


第二种完全不同的方法。范围有多大/多长?如果它们相对较短(数字很少),那么您可以尝试将它们存储为明确的整数列表,而不是范围。 例如,范围 [5-8] 将存储为 4 行:(5, 6, 7, 8)。使用这种存储模型,可能更容易找到范围的交叉点。

【讨论】:

不,它必须是最小范围。请参阅en.wikipedia.org/wiki/Longest_prefix_match 了解原因。范围也可以很大。 数据分布是否以任何方式显着倾斜?例如,netblock 范围均匀分布在 1 和 100,000,000 之间,但 99% 的routing 范围在 100 和 1000 之间。或者,长度分布非常不平衡。 这是一本有趣的读物,无论如何都值得一票。布丁的证据在吃。你能让它工作吗?它的表现如何?我看到了一些障碍。您可以从小提琴中获取我的测试设置以快速开始。无论哪种方式,进一步考虑这一点,它回到我身边:我们之前已经解决了这个问题:dba.stackexchange.com/a/22500/3684 哇,更新后的答案中提供的 2 个查询在 90 秒内给出了正确的结果,而之前的最佳答案是 14 小时!太棒了! @BenDowling,我很高兴它有所帮助。我试图写下我的方法的理由,并希望它足够清楚。底线:正确索引是强大的东西,但索引本身并不神奇,它有助于了解它是如何工作的,它能做什么,不能做什么。【参考方案3】:

我不知道这是否适用于真实数据。候选人选择被挤进了内部循环,这对我来说似乎很好。在测试时,它给出了两次索引扫描(加上一次用于反连接),避免了最终排序/唯一。它似乎确实给出了相同的结果。

-- 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)
        ;

更新:(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;

【讨论】:

似乎没有给出正确的结果。在我的 routing_details_small 表中,我有 250k 行,但此查询的输出有超过 260k 行。对于 routing_details 表中的每一行,输出中应该只有一行。 也许您的 routing_details(小)表中有重复的范围? 是的,事实证明我愿意。 赞成单独提供测试用例!也很有趣的查询。不过,DISTINCT ONLATERAL 在我的本地测试中更快。【参考方案4】:

我没有很好的答案给你,因为我不熟悉 gist 索引,但我有点感兴趣,所以我看了一下你的解释计划。有几件事很突出:

1) 您的计划是使用嵌套循环连接,即使在 250K 示例中也是如此。它是 seq 扫描较大的表,并在较小的表上进行查找。这意味着它在较小的表上执行 800 万次索引查找,占用时间超过 148 秒。令我感到奇怪的是,随着routing_details_small 表大小的增加,这会显着减慢。就像我说的,我不熟悉 gist 索引,但我会尝试使用 set enable_nestloop to false; 看看你是否可以让它进行某种排序的合并/哈希连接。

2) 不同的是在最后被执行。它只需要一小部分时间(约 11 秒),但这也意味着您可能需要做一些额外的工作。看起来 distinct 将结果记录数从超过 100 万减少到 250K,所以我会尝试早点尝试。我不确定您是否得到重复,因为routing_details_small 表中有多个条目用于netblock,或者netblock_details 表对于给定的网络块有多个匹配项。如果是前者,您可以加入只有唯一路由详细信息的子查询。如果是后者,请尝试我将要提到的事情:

3) 稍微结合前两个观察结果,您可以尝试从 routing_details_small 的 seq 扫描中进行部分连接(连接子查询)。这应该只会导致 600K 索引扫描。类似的东西(假设 postgres 9.4): SELECT * FROM routing_details_small r, LATERAL (SELECT * FROM netblock_details n WHERE r.range <@ n.range LIMIT 1) nb;

【讨论】:

对于 routing_details 中的每个条目,netblock_details 中有多个条目,从 3 到 10 左右不等。一旦我添加了一个小修改以确保我得到最窄的内容,您在 (3) 中提出的查询似乎有效通过添加 order 子句进行匹配。将运行一些额外的测试来确认。 另外,我使用的是 postgres 9.3,但查询似乎仍然有效。如果针对 9.3 运行此程序,我应该注意什么? 不 - 我只是认为是 9.4 添加了 LATERAL,但我可能弄错了。很高兴听到它有帮助! 在完整数据集上完成查询大约需要 14 小时。任何进一步的优化可用? gist.github.com/coderholic/9e90311f9323b543aef2

以上是关于使用 int8range 连接 2 个大型 postgres 表不能很好地扩展的主要内容,如果未能解决你的问题,请参考以下文章

Oracle 更新基于 2 个其他表的连接

链表-141-环形链表

链表-141-环形链表

大型网站数据库系统,怎么连接那么多并发数量的?

flume+kafka集群解决某著名联锁大型超市超过25年POS线下收单系统变实时系统的典型案例

连接大型机上的 UNIX 文件