我有一个包含数百万个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个记录。
答案 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;
/