【问题标题】:Efficient retrieval of overlapping IP range records via a single point通过单点高效检索重叠 IP 范围记录
【发布时间】:2011-04-09 08:25:22
【问题描述】:

我有一个包含数百万 IP 范围记录(分别为 start_num 和 end_num)的表,我需要通过单个 IP 地址对其进行查询,以便返回与该点重叠的所有范围。查询本质上是:

SELECT start_num
       , end_num
       , other_data_col 
FROM ip_ranges 
WHERE :query_ip BETWEEN start_num and end_num;

该表在 start_num 上有 8 个范围分区,并在 (start_num, end_num) 上有一个本地复合索引。称之为 UNQ_RANGE_IDX。已针对表和索引收集统计信息。

查询按预期对 UNQ_RANGE_IDX 索引进行索引范围扫描,并且在某些情况下执行得非常好。它表现良好的情况是接近 IP 地址空间的底部(即类似 4.4.10.20),而在高端时性能较差。 (即 200.2.2.2)我确信问题在于,在低端,优化器可以修剪包含适用范围的分区之上的所有分区,因为 start_num 上的范围分区提供了必要的信息修剪。在 IP 频谱的顶端进行查询时,它无法修剪较低的分区,因此会产生读取附加索引分区的 I/O。这可以在跟踪执行时通过 CR_BUFFER_GETS 的数量来验证。

实际上,满足查询的范围不会在任何分区中,而是在 query_ip 所在的分区或紧接其下方或上方的分区,因为范围大小不会大于 A 类和每个分区每个都涵盖许多 A 类。我可以通过在 where 子句中指定该信息让 Oracle 使用该信息,但有没有办法通过统计、直方图或自定义/域索引将此类信息传达给 Oracle?在搜索涵盖特定日期的日期范围时,似乎会有一种常见的解决方案/方法来解决此类问题。

我正在寻找使用 Oracle 及其功能来解决此问题的解决方案,但也欢迎其他解决方案类型。我已经想到了一些在 Oracle 范围之外可行的方法,但我希望有一种更好的索引、统计数据收集或分区方法来解决问题。

要求的信息:

CREATE TABLE IP_RANGES (
    START_NUM NUMBER NOT NULL, 
    END_NUM   NUMBER NOT NULL,
    OTHER     NUMBER NOT NULL,
    CONSTRAINT START_LTE_END CHECK (START_NUM <= END_NUM)
)
PARTITION BY RANGE(START_NUM)
(
    PARTITION part1 VALUES LESS THAN(1090519040) TABLESPACE USERS,
    PARTITION part2 VALUES LESS THAN(1207959552) TABLESPACE USERS
    ....<snip>....
    PARTITION part8 VALUES LESS THAN(MAXVALUE) TABLESPACE USERS
);

CREATE UNIQUE INDEX IP_RANGES_IDX ON IP_RANGES(START_NUM, END_NUM, OTHER) LOCAL NOLOGGING;

ALTER TABLE IP_RANGES ADD CONSTRAINT PK_IP_RANGE 
PRIMARY KEY(START_NUM, END_NUM, OTHER) USING INDEX IP_RANGES_IDX;

为范围分区选择的截止值没有什么特别之处。它们只是 A 类地址,其中每个分区的范围数相当于大约 1M 条记录。

【问题讨论】:

  • start_num和end_num的数据类型是什么?
  • start_num 和 end_num 的类型为 NUMBER
  • 表和每个分区有多少行?
  • 可以分享一下分区脚本吗?我想看看范围和其他细节。另外为什么要本地分区索引?为什么不是全局分区索引?
  • 表中的行数略低于 8M,每个分区大约有 1M 行。

标签: sql database oracle database-design


【解决方案1】:

我过去也遇到过类似的问题;我的优势是我的范围是不同的。我有几个 IP_RANGES 表,每个表都针对特定的上下文,最大的是大约 1000 万条记录,未分区。

我拥有的每个表都是索引组织的,主键是 (END_NUM, START_NUM)。我在 (START_NUM, END_NUM) 上也有一个唯一索引,但在这种情况下没有使用它。

使用随机 IP 地址 (1234567890),您的查询大约需要 132k 次一致获取。

以下查询在 10.2.0.4 上返回 4-10 次一致的获取(取决于 IP)。

select *
  from ip_ranges outr
 where :ip_addr between outr.num_start and outr.num_end
   and outr.num_end = (select /*+ no_unnest */
                              min(innr.num_end)
                             from ip_ranges innr
                            where innr.num_end >= :ip_addr);
---------------------------------------------------------------------------------------------------
| Id  | Operation                     | Name              | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |                   |     1 |    70 |     6   (0)| 00:00:01 |
|*  1 |  INDEX RANGE SCAN             | IP_RANGES_PK      |     1 |    70 |     3   (0)| 00:00:01 |
|   2 |   SORT AGGREGATE              |                   |     1 |     7 |            |          |
|   3 |    FIRST ROW                  |                   |   471K|  3223K|     3   (0)| 00:00:01 |
|*  4 |     INDEX RANGE SCAN (MIN/MAX)| IP_RANGES_PK      |   471K|  3223K|     3   (0)| 00:00:01 |
---------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - access("OUTR"."NUM_END"= (SELECT /*+ NO_UNNEST */ MIN("INNR"."NUM_END") FROM
              "IP_RANGES" "INNR" WHERE "INNR"."NUM_END">=TO_NUMBER(:IP_ADDR)) AND
              "OUTR"."NUM_START"<=TO_NUMBER(:IP_ADDR))
       filter("OUTR"."NUM_END">=TO_NUMBER(:IP_ADDR))
   4 - access("INNR"."NUM_END">=TO_NUMBER(:IP_ADDR))


Statistics
----------------------------------------------------------
          0  recursive calls
          0  db block gets
          7  consistent gets
          0  physical reads
          0  redo size
        968  bytes sent via SQL*Net to client
        492  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          1  rows processed

NO_UNNEST 提示是 key;它告诉 Oracle 运行该子查询一次,而不是对每一行运行一次,并且它对要在外部查询中使用的索引进行相等性测试。

【讨论】:

  • 感谢您的回复,但这在我的情况下不起作用。在原始查询中返回 4 行的查询在您的提案中不返回任何内容。这是因为范围重叠,您的内部查询可能会返回与外部范围无关的 end_num。例如,一个 IP(比如 4.4.4.4)可能在一个巨大的 a 类范围 4.0.0.0-4.255.255.255 中,但前面有一个较小的 4.4.5.0-255 范围,这将由您的内部查询返回。除非 end_nums 都相同,否则您的查询将永远不会返回多行
  • 我把它放在那里以防其他人遇到我遇到的问题,如果存在非重叠行,它确实解决了快速找到 a 匹配行的调整问题范围。我刚刚想到的是 Spatial 中的一些选项(特别是使用 SDO_POINT / SDO_LINE、R-Tree 索引和 COVERS 运算符)可能会有所帮助,但 Spatial 需要花钱,而且我没有获得许可。跨度>
【解决方案2】:

我建议你把你的 800 万行表变成一个更大的表。 Google 的 IP(目前对我而言)即将推出

“66.102.011.104”

您将一条记录存储为“66.102.011”及其所属的相应范围。实际上,您为每个“aaa.bbb.ccc”存储至少一条记录。您最终可能会得到一张可能大 5 倍的表,但您可以每次只需几个逻辑 IO 即可查明相关记录,而不是分区扫描的成百上千。

我怀疑您拥有的任何数据无论如何都会有点过时(因为世界各地的各个机构发布/重新发布范围),因此不应该每天/每周重新生成该表的调整一个大问题。

【讨论】:

  • 这基本上也是我想做的事情。我无法真正改变表格的格式化方式,因为它也链接了太多东西,但我考虑过使用类似的方法来制作一个基本上用作现有表格索引的表格。基本上,创建一个非唯一索引,其中包含 c 类和现有表中与该 c 类重叠的所有记录。然后更改 SQL 以查询此表中与 query_ip c-class 重叠的所有范围,然后从该小子集中过滤掉不与该特定 IP 重叠的任何范围。
  • 我唯一的缺点是我必须管理这个表,因为范围是从现有表中添加和删除的。如果没有提供允许 Oracle 完成所有工作的其他解决方案,我将尝试这样做。谢谢。 +1
【解决方案3】:

我看到的问题是本地分区索引,正如你所说,Oracle 似乎没有有效地修剪分区列表。您可以尝试使用全球索引吗?本地分区索引不适用于 OLTP 查询。在我们的环境中,我们不使用任何本地分区索引。

【讨论】:

  • 性能表现与本地分区索引相同。
  • 你的意思是全局分区索引吗?
  • 全局和本地分区索引提供相同类型的性能。无论分区是本地的还是全局的,问题仍然是优化器无法根据 start_num 和 end_num 之间的 :query_ip 修剪分区。
  • 如果你有全局分区索引并且如果 CBO 使用索引修剪应该自动发生。是否可以提供具有较低和较高 IP 范围值的自动跟踪输出?你现在的版本是什么?
  • 你可以尝试查询为 SELECT start_num , end_num , other_data_col FROM ip_ranges WHERE :query_ip BETWEEN start_num and end_num AND start_num >= :query_ip
【解决方案4】:

您能否指出您的 IP 范围是否有任何统一或有序的特征?例如,我通常希望 IP 范围位于 2 的幂边界上。是不是这种情况,所以我们可以假设所有范围都有一个隐式网络掩码,以 m 个开头,然后是 n 个零,其中 m + n = 32?

如果是这样,应该有一种方法可以利用这些知识并“步入”范围。是否可以使用掩码位数 (0-32) 或块大小 (1 到 2^32) 在计算值上添加索引?

仅使用 start_num 从掩码 0 到 32 查找 32 比使用 BETWEEN start_num AND end_num 进行扫描要快。

此外,您是否考虑过位算术作为检查匹配的一种可能方法(同样,仅当范围表示以 2 的幂大小均匀定位的块时)。

【讨论】:

    【解决方案5】:

    首先,您的性能要求是什么?

    您的分区有一个明确的起始值和结束值,可以从 ALL_PARTITIONS(或硬编码)确定并在函数中使用(下面的概念,但您需要修改它以向前/向后移动一个分区)。

    你应该能够编码

    SELECT * FROM ip_ranges
    WHERE :query_ip BETWEEN start_num and end_num
    AND start_num between get_part_start(:query_ip) and get_part_end(:query_ip);
    

    应该能够将其锁定到特定分区。但是,如果按照您的建议,您只能将其锁定到八个分区中的三个,那仍将是一次 BIG 扫描。我正在发布另一个更激进的答案,这可能更合适。

    create or replace function get_part_start (i_val in number) 
                                  return number deterministic is
      cursor c_1 is 
        select high_value from all_tab_partitions
        where table_name = 'IP_RANGES'
        order by table_owner, table_name;
      type tab_char is table of varchar2(20) index by pls_integer;
      type tab_num is table of number index by pls_integer;
      t_char  tab_char;
      t_num   tab_num;
      v_ind   number;
    begin
      open c_1;
      fetch c_1 bulk collect into t_char;
      close c_1;
      --
      for i in 1..t_char.last loop
        IF t_char(i) != 'MAXVALUE' THEN
          t_num(to_number(t_char(i))) := null;
        END IF;
      end loop;
      --
      IF i_val > t_num.last then
        return t_num.last;
      ELSIF i_val < t_num.first then
        return 0;
      END IF;
      v_ind := 0;
      WHILE i_val >= t_num.next(v_ind) loop
        v_ind := t_num.next(v_ind);
        exit when v_ind is null;
      END LOOP;
      return v_ind;
    end;
    /
    

    【讨论】:

      【解决方案6】:

      您现有的分区不起作用,因为 Oracle 通过 start_num 访问表的本地索引分区,它必须检查每个可能匹配的分区。

      另一种解决方案,假设没有范围跨越 A 类,将按 trunc(start_num / power(256,3)) 列出分区 - 第一个八位字节。可能值得将其拆分为一列(通过触发器填充)并将其作为过滤列添加到您的查询中。

      然后,假设均匀分布,您的大约 1000 万行将分散成大约 40000 行,这可能会更快阅读。


      我运行了下面讨论的用例,假设没有范围跨越 A 类网络。

      create table ip_ranges
       (start_num         number           not null, 
        end_num           number           not null, 
        start_first_octet number           not null,
         ...
        constraint start_lte_end check (start_num <= end_num), 
        constraint check_first_octet check (start_first_octet = trunc(start_num / 16777216) )
      )
      partition by list ( start_first_octet )
      (
      partition p_0 values (0),
      partition p_1 values (1),
      partition p_2 values (2),
      ...
      partition p_255 values (255)
      );
      
      -- run data population script, ordered by start_num, end_num
      
      create index ip_ranges_idx01 on ip_ranges (start_num, end_num) local;
      
      begin 
        dbms_stats.gather_table_stats (ownname => user, tabname => 'IP_RANGES', cascade => true);
      end;
      /
      

      使用上面的基本查询仍然表现不佳,因为它无法进行有效的分区消除:

      ----------------------------------------------------------------------------------------------------------------------
      | Id  | Operation                          | Name            | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
      ----------------------------------------------------------------------------------------------------------------------
      |   0 | SELECT STATEMENT                   |                 | 25464 |  1840K|   845   (1)| 00:00:05 |       |       |
      |   1 |  PARTITION LIST ALL                |                 | 25464 |  1840K|   845   (1)| 00:00:05 |     1 |   256 |
      |   2 |   TABLE ACCESS BY LOCAL INDEX ROWID| IP_RANGES       | 25464 |  1840K|   845   (1)| 00:00:05 |     1 |   256 |
      |*  3 |    INDEX RANGE SCAN                | IP_RANGES_IDX01 |   825 |       |   833   (1)| 00:00:05 |     1 |   256 |
      ----------------------------------------------------------------------------------------------------------------------
      
      Predicate Information (identified by operation id):
      ---------------------------------------------------
      
         3 - access("END_NUM">=TO_NUMBER(:IP_ADDR) AND "START_NUM"<=TO_NUMBER(:IP_ADDR))
             filter("END_NUM">=TO_NUMBER(:IP_ADDR))
      
      
      Statistics
      ----------------------------------------------------------
               15  recursive calls
                0  db block gets
           141278  consistent gets
            94469  physical reads
                0  redo size
             1040  bytes sent via SQL*Net to client
              492  bytes received via SQL*Net from client
                2  SQL*Net roundtrips to/from client
                0  sorts (memory)
                0  sorts (disk)
                1  rows processed
      

      但是,如果我们添加条件以允许 Oracle 专注于单个分区,则会产生巨大的差异:

      SQL> select * from ip_ranges
        2   where :ip_addr between start_num and end_num
        3     and start_first_octet = trunc(:ip_addr / power(256,3));
      
      ----------------------------------------------------------------------------------------------------------------------
      | Id  | Operation                          | Name            | Rows  | Bytes | Cost (%CPU)| Time     | Pstart| Pstop |
      ----------------------------------------------------------------------------------------------------------------------
      |   0 | SELECT STATEMENT                   |                 |   183 | 13542 |   126   (2)| 00:00:01 |       |       |
      |   1 |  PARTITION LIST SINGLE             |                 |   183 | 13542 |   126   (2)| 00:00:01 |   KEY |   KEY |
      |   2 |   TABLE ACCESS BY LOCAL INDEX ROWID| IP_RANGES       |   183 | 13542 |   126   (2)| 00:00:01 |   KEY |   KEY |
      |*  3 |    INDEX RANGE SCAN                | IP_RANGES_IDX01 |     3 |       |   322   (1)| 00:00:02 |   KEY |   KEY |
      ----------------------------------------------------------------------------------------------------------------------
      
      Predicate Information (identified by operation id):
      ---------------------------------------------------
      
         3 - access("END_NUM">=TO_NUMBER(:IP_ADDR) AND "START_NUM"<=TO_NUMBER(:IP_ADDR))
             filter("END_NUM">=TO_NUMBER(:IP_ADDR))
      
      
      Statistics
      ----------------------------------------------------------
               15  recursive calls
                0  db block gets
                7  consistent gets
                0  physical reads
                0  redo size
             1040  bytes sent via SQL*Net to client
              492  bytes received via SQL*Net from client
                2  SQL*Net roundtrips to/from client
                0  sorts (memory)
                0  sorts (disk)
                1  rows processed
      

      【讨论】:

      • 我们不能只在第一个八位字节上创建全局分区函数索引吗?
      • 那么在(start_num, end_num) partition by (trunc(start_num / power(256, 3))) 上创建一个索引并将查询更改为包含and trunc(start_num / power(256, 3)) = trunc(:ip_addr / power(256,3))?这可能行得通——我还没有测试过——但它看起来很聪明,我的个人规则之一是尽可能避免使用全局(ly 分区)索引;它们排除了太多有效的分区表操作。在我看来,这似乎是一种双重间接的聪明,这种事情会驱使下一个可怜的混蛋试图理解 Sonova ^&!%@ 在想什么。
      • (该死的 600 个字符限制!)OP 说“为范围分区选择的截止值没有什么特别之处。它们只是一个类地址,其中每个分区的范围数大约等于100 万条记录。”如果范围不跨越 A 类记录,为什么不修复分区方案,而不是通过强制索引来解决它?
      • 我不认为 A 级分区除了使分区更小之外没有多大作用。如果优化器不在上层 IP 上扫描所有分区,那会更好。如果我查询 205.xxx 的 IP,无论是 start_num 上的所有分区,还是 start_num 的 A 类“低于”205.xxx 的 start_num 所在的分区,都可能有一个超出我的查询 IP 的 end_num,因此需要被退回。我可以在 start_num 上添加一个额外的过滤器来限制它可以向后看多远,但是如果在某处插入了异常大的范围,则可能找不到它。
      • 如果 Oracle 知道基于统计信息或分区 1-5 没有足够大的 end_nums 以包含与查询匹配的记录,那就太好了,但我不确定是否有可能实现这一点.空间可能有用,但我认为我们也没有这种选择。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-11-07
      • 2011-06-03
      • 2012-07-09
      相关资源
      最近更新 更多