通过单点有效检索重叠的IP范围记录

时间:2010-09-07 18:11:55

标签: sql database oracle database-design

我有一个包含数百万个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;

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

6 个答案:

答案 0 :(得分:2)

过去我遇到过类似的问题;我的优势是我的范围很明显。我有几个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提示是;它告诉Oracle运行该子查询一次,而不是每行运行一次,并且它给出了在外部查询中使用的索引的相等性测试。

答案 1 :(得分:1)

我建议你将800万行表变成一张更大的表。 谷歌的IP(目前对我而言)即将出现

“66.102.011.104”

您将一条记录存储为“66.102.011”及其所属的相应范围。实际上,您为每个“aaa.bbb.ccc”存储至少一条记录。你可能最终得到的表可能是表的五倍,但是你可以用每次只有几个逻辑IO来确定相关记录,而不是分区扫描的数百/数千。

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

答案 2 :(得分:0)

我看到的问题是本地分区索引,正如您所说的看起来甲骨文不会有效地修剪分区列表。你能试试全球指数吗?对于OLTP查询,本地分区索引不能很好地扩展。在我们的环境中,我们不使用任何本地分区索引。

答案 3 :(得分:0)

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

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

32只使用start_num从掩码0到32搜索比使用BETWEEN start_num和end_num的扫描更快。

另外,您是否考虑过比特算术作为检查匹配的可能方法(仅当范围表示以2的幂为单位的均匀定位的块时)。

答案 4 :(得分:0)

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

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

假设均匀分布,你的~10米行将分散到大约40k行,这可能要快得多阅读。


我运行了下面讨论的用例,假设没有范围跨越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

答案 5 :(得分:0)

首先,您的表现要求是什么?

您的分区具有明确的起始值和结束值,可以从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);

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

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