【问题标题】:Representing IPv4/IPv6 addresses in Oracle在 Oracle 中表示 IPv4/IPv6 地址
【发布时间】:2011-06-24 10:30:17
【问题描述】:

在 Oracle 中,表示网络地址的适当数据类型或技术是什么,哪些地址可能是 IPv4 或 IPv6?

背景:我正在转换一个记录网络活动的表,使用 PostgreSQL inet data type 构建以在同一个表中保存 v4 和 v6 地址。

但是,没有行包含 v4 和 v6 地址。 (也就是说,一条记录要么来自机器的 v4 堆栈,要么来自机器的 v6 堆栈。)

【问题讨论】:

  • 您是否按 IP 地址对该表进行聚合/搜索?您希望在 1 年内有多少行?
  • 按 IP 聚合和搜索,是的。行数,可能数亿。 (你有小/中/大的推荐吗?)
  • 如果您希望我收到通知,请在您的 cmets 中使用 @jachguate 表示法。对于您所说的,我认为更好的方法是@Alain 的第一个答案。

标签: oracle ipv6 ipv4


【解决方案1】:

在 Oracle 中,什么是合适的 数据类型或技术 表示网络地址,其中 地址可以是 IPv4 或 IPv6

有两种方法:

  1. 仅用于存储。
  2. 存储常规表示

仅用于存储。 IPV4 地址应该是一个整数(32 位就足够了)。对于 IP V6,128 位,INTEGER(类似于 Number(38))就可以了。当然,那是存储。该方法认为表示是应用程序的问题。

如果采取相反的策略,即存储常规表示,则需要确保 IP V4 和 IPV6 地址只有一种常规(字符串)表示。它以 ipV4 闻名。至于IPV6,也有标准格式。

我更喜欢第一种策略。在最坏的情况下,您可以采用混合方法(尽管不是酸性的)并将二进制和 ascii 表示并排存储,并以“优先级”为二进制值。

没有行同时包含 v4 和 v6 但是地址。

IPV6 格式的 IPV4 地址的标准表示是:::ffff:192.0.2.128

我不知道上下文,但我会保留 2 列,一列用于 IPV4,另一列用于不同的 ipV6 地址。

更新
在@sleepyMonad 的好评之后,我想指出的是,最好使用 INTEGER 数据类型而不是 Number 数据类型,这将很高兴地容纳可能的最高值用 128 位整数 'ff...ff' 表示(需要39 个十进制数字)。 38 是从 0 到 9 的 10 的最高幂,可以在 128 位上编码,但仍然可以插入 2**128 - 1 的最大无符号值(十进制 340282366920938463463374607431768211455)。这是一个小测试来说明这种可能性。

create table test (
  id integer primary key,
  ipv6_address_bin INTEGER );

-- Let's enter 2**128 - 1 in the nueric field
insert into test (id, ipv6_address_bin) values ( 1, to_number ( 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX') ) ;

-- retrieve it to make sure it's not "truncated".
select to_char ( ipv6_address_bin, 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX' ) from test where id = 1 ;
-- yields 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'

select to_char ( ipv6_address_bin ) from test where id = 1 ;
-- yields 340282366920938463463374607431768211455

select LOG(2, ipv6_address_bin) from test where id = 1 ;
-- yields 128

select LOG(10, ipv6_address_bin) from test where id = 1 ;
-- yields > 38

【讨论】:

  • 完全同意 IPv4 和 IPv6 值的单独列。
  • 如果您有 ipv4 或 ipv6 但从来没有两者,请使用对应的 ipv6 存储 ipv4 地址
  • @ThiefMaster。是的,确实,因为 ::ffff:a.b.c.d 范围是专门为这种情况保留的。
  • @Alain Pannetier:因为 2^128 有 39 位十进制数字,所以它如何适合 NUMBER(38) 并不明显。
  • @Alain Pannetier。我试过你的代码,你是对的,它有效。描述函数的输出似乎是错误的。我查询了 user_tab_columns,它给了我 data_precision NULL 和 INTEGER 列和 38 的 NUMBER(38) 列。当我有更多时间时,我会进一步调查。我认为你不应该依赖这个未记录的特性。
【解决方案2】:

以 RAW 格式存储。

RAW是变长字节数组,所以....

  • 只需将 IPv4 视为 4 个字节的数组
  • 和 IPv6 作为 16 个字节的数组

...并将其中任何一个直接存储在 RAW(16) 中。


RAW 可以被索引,可以是 PK、UNIQUE 或 FOREIGN KEY,因此您可以使用 VARCHAR2 或 INT/NUMBER/DECIMAL 执行通常可以执行的任何操作,但转换和存储开销更少。

为了说明 INT 相对于 RAW 的存储开销,请考虑以下示例:

CREATE TABLE IP_TABLE (
    ID INT PRIMARY KEY,
    IP_RAW RAW(16), 
    IP_INT INT
);

INSERT INTO IP_TABLE (ID, IP_RAW, IP_INT) VALUES (
    1,
    HEXTORAW('FFFFFFFF'),
    TO_NUMBER('FFFFFFFF', 'XXXXXXXX')
);

INSERT INTO IP_TABLE (ID, IP_RAW, IP_INT) VALUES (
    2,
    HEXTORAW('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF'),
    TO_NUMBER('FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF', 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX')
);

SELECT VSIZE(IP_RAW), VSIZE(IP_INT), IP_TABLE.*  FROM IP_TABLE;

结果(Oracle 10.2下):

table IP_TABLE created.
1 rows inserted.
1 rows inserted.
VSIZE(IP_RAW)          VSIZE(IP_INT)          ID                     IP_RAW                           IP_INT                 
---------------------- ---------------------- ---------------------- -------------------------------- ---------------------- 
4                      6                      1                      FFFFFFFF                         4294967295             
16                     21                     2                      FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF 340282366920938463463374607431768211455 

【讨论】:

  • RAW 的另一个好处:您可以使用UTL_RAW.BIT_ANDUTL_RAW.BIT_OR,以防您使用子网划分。
【解决方案3】:

@Alain Pannetier(因为我还不能发表评论): 根据http://download.oracle.com/docs/cd/B19306_01/server.102/b14200/sql_elements001.htm#i54335,ANSI INTEGER 数据类型映射到 Oracle 中的 NUMBER(38)。 在表格下方,您可以找到 NUMBER 仅提供 126 位二进制精度的信息,这对于 128 位 IPv6 地址来说是不够的。 最大值可能会很好地存储,但有些地址会被舍入到下一个较低的地址。

内部数字格式为 ROUND((length(p)+s)/2))+1 (http://download.oracle.com/docs/cd/B19306_01/server.102/b14220/datatype.htm#i16209)。

更新: 再次摆弄这个问题后,我现在找到了一种解决方案,可以对包含 IPv6 地址的网络进行高性能查询:将 IPv6 地址和子网掩码存储在 RAW(16 ) 列并使用 UTL_RAW.BIT_AND 进行比较:

SELECT name, DECODE(UTL_RAW.BIT_AND('20010DB8000000000000000000000001', ipv6_mask), ipv6_net, 1, 0)
FROM ip_net
WHERE ipv6_net IS NOT NULL;

【讨论】:

    【解决方案4】:

    您也可以使用自定义的 oracle 对象。

    SQL>set SERVEROUTPUT on
    SQL>drop table test;
    
    Table dropped.
    
    SQL>drop type body inaddr;
    
    Type body dropped.
    
    SQL>drop type inaddr;
    
    Type dropped.
    
    SQL>create type inaddr as object
      2  ( /* TODO enter attribute and method declarations here */
      3  A number(5),
      4  B number(5),
      5  C number(5),
      6  D number(5),
      7  E number(5),
      8  F number(5),
      9  G number(5),
     10  H NUMBER(5),
     11  MAP MEMBER FUNCTION display RETURN VARCHAR2,
     12  MEMBER FUNCTION toString( SELF IN INADDR , CONTRACT BOOLEAN DEFAULT TRUE) RETURN VARCHAR2,
     13  CONSTRUCTOR FUNCTION INADDR(SELF IN OUT NOCOPY INADDR, INADDRASSTRING VARCHAR2)  RETURN SELF AS RESULT
     14  
     15  ) NOT FINAL;
     16  /
    
    SP2-0816: Type created with compilation warnings
    
    SQL>
    SQL>
    SQL>CREATE TYPE BODY INADDR AS
      2  
      3  MAP MEMBER FUNCTION display RETURN VARCHAR2
      4  IS BEGIN
      5  return tostring(FALSE);
      6  END;
      7  
      8  
      9  MEMBER FUNCTION TOSTRING( SELF IN  INADDR , CONTRACT BOOLEAN DEFAULT TRUE) RETURN VARCHAR2 IS
     10  IP4 VARCHAR2(6) := 'FM990';
     11  ip6 varchar2(6) := 'FM0XXX';
     12    BEGIN
     13  IF CONTRACT THEN
     14    ip6 := 'FMXXXX';
     15  end if;
     16  
     17  IF CONTRACT AND A =0 AND B=0 AND C = 0 AND D=0 AND E =0 AND F = 65535 THEN --ipv4
     18      RETURN  '::FFFF:'||TO_CHAR(TRUNC(G/256),'FM990.')||TO_CHAR(MOD(G,256),'FM990.')||TO_CHAR(TRUNC(H/256),'FM990.')||TO_CHAR(MOD(H,256),'FM990');
     19  ELSE
     20      RETURN
     21  TO_CHAR(A,ip6)||':'||
     22  TO_CHAR(B,IP6)||':'||
     23  TO_CHAR(C,ip6)||':'||
     24  TO_CHAR(D,ip6)||':'||
     25  TO_CHAR(E,ip6)||':'||
     26  TO_CHAR(F,ip6)||':'||
     27  TO_CHAR(G,ip6)||':'||
     28  TO_CHAR(H,ip6);
     29  end if;
     30    end;
     31  
     32      CONSTRUCTOR FUNCTION inaddr(SELF IN OUT NOCOPY inaddr, inaddrasstring VARCHAR2)
     33                                 RETURN SELF AS RESULT IS
     34      begin
     35          if instr(inaddrasstring,'.') > 0 then
     36            --ip4
     37  null;
     38              a := 0;
     39              B := 0;
     40              C := 0;
     41              D := 0;
     42              E := 0;
     43              F := TO_NUMBER('FFFF', 'XXXX');
     44              G := TO_NUMBER(TO_CHAR(TO_NUMBER(REGEXP_SUBSTR(INADDRASSTRING,'([0-9]{1,3}).',1,1,'i',1),'999'),'FM0X')
     45  ||TO_CHAR(TO_NUMBER(REGEXP_SUBSTR(INADDRASSTRING,'([0-9]{1,3}).',1,2,'i',1),'999'),'FM0X')
     46  ,'XXXX');
     47              h := TO_NUMBER(TO_CHAR(TO_NUMBER(REGEXP_SUBSTR(INADDRASSTRING,'([0-9]{1,3}).',1,3,'i',1),'999'),'FM0X')
     48  ||TO_CHAR(TO_NUMBER(REGEXP_SUBSTR(INADDRASSTRING,'([0-9]{1,3})',1,4,'i',1),'999'),'FM0X')
     49  ,'XXXX');
     50  
     51          ELSIF instr(inaddrasstring,':') > 0 then
     52              --ip6
     53              a := TO_NUMBER(REGEXP_SUBSTR(inaddrasstring,'([0-9a-fA-F]{1,4})',1,1,'i',1),'XXXX');
     54              b := TO_NUMBER(REGEXP_SUBSTR(inaddrasstring,'([0-9a-fA-F]{1,4})',1,2,'i',1),'XXXX');
     55              c := TO_NUMBER(REGEXP_SUBSTR(inaddrasstring,'([0-9a-fA-F]{1,4})',1,3,'i',1),'XXXX');
     56              d := TO_NUMBER(REGEXP_SUBSTR(inaddrasstring,'([0-9a-fA-F]{1,4})',1,4,'i',1),'XXXX');
     57              E := TO_NUMBER(REGEXP_SUBSTR(inaddrasstring,'([0-9a-fA-F]{1,4})',1,5,'i',1),'XXXX');
     58              f := TO_NUMBER(REGEXP_SUBSTR(inaddrasstring,'([0-9a-fA-F]{1,4})',1,6,'i',1),'XXXX');
     59              g := TO_NUMBER(REGEXP_SUBSTR(inaddrasstring,'([0-9a-fA-F]{1,4})',1,7,'i',1),'XXXX');
     60              H := TO_NUMBER(REGEXP_SUBSTR(inaddrasstring,'([0-9a-fA-F]{1,4})',1,8,'i',1),'XXXX');
     61          end if;
     62  
     63          RETURN;
     64      END;
     65  end;
     66  /
    
    Type body created.
    
    SQL>
    SQL>create table test
      2  (id integer primary key,
      3  address inaddr);
    
    Table created.
    
    SQL>
    SQL>select * from test;
    
    no rows selected
    
    SQL>
    SQL>
    SQL>insert into test values (1, INADDR('fe80:0000:0000:0000:0202:b3ff:fe1e:8329') );
    
    1 row created.
    
    SQL>INSERT INTO TEST VALUES (2, INADDR('192.0.2.128') );
    
    1 row created.
    
    SQL>insert into test values (3, INADDR('20.0.20.1') );
    
    1 row created.
    
    SQL>insert into test values (4, INADDR('fe80:0001:0002:0003:0202:b3ff:fe1e:8329') );
    
    1 row created.
    
    SQL>insert into test values (5, INADDR('fe80:0003:0002:0003:0202:b3ff:fe1e:8329') );
    
    1 row created.
    
    SQL>INSERT INTO TEST VALUES (6, INADDR('fe80:0003:0001:0003:0202:b3ff:fe1e:8329') );
    
    1 row created.
    
    SQL>INSERT INTO TEST VALUES (7, INADDR('fe80:0003:0001:0003:0202:b3ff:fe1e:8328') );
    
    1 row created.
    
    SQL>INSERT INTO TEST VALUES (8, INADDR('dead:beef:f00d:cafe:dea1:aced:b00b:1234') );
    
    1 row created.
    
    SQL>
    SQL>COLUMN INET_ADDRESS_SHORT FORMAT A40
    SQL>column inet_address_full format a40
    SQL>
    SQL>select t.address.toString() inet_address_short, t.address.display( ) inet_address_full
      2  from test T
      3  order by t.address ;
    
    INET_ADDRESS_SHORT                       INET_ADDRESS_FULL
    ---------------------------------------- ----------------------------------------
    ::FFFF:20.0.20.1                         0000:0000:0000:0000:0000:FFFF:1400:1401
    ::FFFF:192.0.2.128                       0000:0000:0000:0000:0000:FFFF:C000:0280
    DEAD:BEEF:F00D:CAFE:DEA1:ACED:B00B:1234  DEAD:BEEF:F00D:CAFE:DEA1:ACED:B00B:1234
    FE80:0:0:0:202:B3FF:FE1E:8329            FE80:0000:0000:0000:0202:B3FF:FE1E:8329
    FE80:1:2:3:202:B3FF:FE1E:8329            FE80:0001:0002:0003:0202:B3FF:FE1E:8329
    FE80:3:1:3:202:B3FF:FE1E:8328            FE80:0003:0001:0003:0202:B3FF:FE1E:8328
    FE80:3:1:3:202:B3FF:FE1E:8329            FE80:0003:0001:0003:0202:B3FF:FE1E:8329
    FE80:3:2:3:202:B3FF:FE1E:8329            FE80:0003:0002:0003:0202:B3FF:FE1E:8329
    
    8 rows selected.
    
    SQL>spool off
    

    我只是在最后一小时把它放在一起(同时自学了一些东西),所以我确信它可以改进。如果我进行更新,我会在这里重新发布它们

    【讨论】:

    • 根据RFC 5952地址FE80:0:0:0:202:B3FF:FE1E:8329应表示为fe80::202:b3ff:fe1e:8329
    • 公平点.. toString 方法中的额外 regexp_replace 可以解决这个问题.. regexp_replace('FE80:0:0:0:202:B3FF:FE1E:8329',':[0:]*:','::')
    • 它会如何得到2001:db8:0:0:1:0:0:1“错误”,因为只有一组 0 可以减少(它应该是最长的一组)
    • 我认为您不能仅使用正则表达式将 0:0 的系列缩短到 :: - 至少在 Oracle 中不能,因为它不支持反向引用,请参阅此解决方案:stackoverflow.com/questions/7043983/…
    • 我写了这个通用包:stackoverflow.com/questions/43221720/…函数Canonical_IPv6做一个正确的输出。
    【解决方案5】:

    我希望将 IP 地址存储在字符串中,格式由 SYS_CONTEXT ('USERENV', 'IP_ADDRESS') 返回

    参考 11g 中的 SYS_CONTEXT,仅将默认返回值长度描述为 256 字节,并没有描述精确的“IP_ADDRESS”上下文的返回值大小。

    在文档Oracle Database and IPv6 Statement of Direction 中描述:

    Oracle 数据库 11g 第 2 版支持标准 IPv6 地址 由 RFC2732 指定的符号。一个 128 位的 IP 地址一般是 表示为 8 组 4 个十六进制数字,以“:”符号作为 组分隔符。每组中的前导零被删除。为了 例如,1080:0:0:0:8:800:200C:417A 将是有效的 IPv6 地址。一 或多个连续的零字段可以选择用 “::“ 分隔器。例如,1080::8:800:200C:417A。

    从这个注释我更喜欢制作列 IP_ADDRESS varchar2(39) 以允许存储 8 个 4 位数字组和这些组之间的 7 个分隔符。

    【讨论】:

    • 使用 VARCHAR2 会产生一些麻烦。 1080::8:800:200C:417A1080::8:800:32.12.65.1221080::8:800:200c:417a1080:0:0:0:8:800:200C:417A1080:0000:0000:0000:0008:0800:200C:417A 都是同一 IPv6 IP 地址的合法表示(请参阅 RFC 5952RFC 4291)。除非您强制执行某种格式,否则如果您必须在 WHERE 子句中使用它,您就会遇到问题。
    • 取决于格式 VARCHAR2(39) 是不够的,在最坏的情况下,您可能有 45 个字符长的 1080:1234:1234:1234:0008:5800:132.212.165.122
    • 对于来自 Oracle 的地址,只要他们始终如一地遵守上面引用的承诺就足够了。对于其他来源的地址你是对的,谢谢。
    【解决方案6】:

    Oracle 文档确实声明 INTEGER 是 NUMBER(38) 的别名,但这可能是一个错字,因为上面的段落指出:

    NUMBER(p,s) 其中:p 是精度... Oracle 保证 数字的可移植性,精度高达 20 base-100 位, 这相当于 39 或 40 位十进制数字,具体取决于 小数点的位置。

    所以 NUMBER 可以存储 39 到 40 位数字,而 INTEGER 可能是 NUMBER(最大精度)而不是 NUMBER(38) 的别名。这就是提供的示例有效的原因(如果您将 INTEGER 更改为 NUMBER,它也有效)。

    【讨论】:

    • 这如何回答这个问题?
    【解决方案7】:

    可能性是:

    • 存储为字符串,即VARCHAR2(例如1080::8:800:200c:417a
    • 存储为数值
      • NUMBER 数据类型
      • INTEGER数据类型
    • 存储为RAW
      • 一个 RAW 值,即分别用于 IPv4 或 IPv6 的 RAW(4)RAW(16)
      • 4 x RAW(1) 或 8 x RAW(2) 分别用于 IPv4 或 IPv6

    我建议使用 RAW 值,因为

    • 如果您使用字符串,那么您必须考虑不同格式的 IPv6。

      1080::8:800:200C:417A
      1080::8:800:200c:417a
      1080::8:800:32.12.65.122
      1080:0:0:0:8:800:200C:417A
      1080:0:0:0:0008:0800:200C:417A
      1080:0000:0000:0000:0008:0800:200C:417A
      

      是同一 IPv6 IP 地址的所有合法表示。您的应用程序需要强制使用通用格式才能正确使用,例如在WHERE 条件下使用。

    • NUMBER/INTEGER 值在没有转换为人类可读格式的情况下毫无意义。不能在 PL/SQL 中使用 INTEGER 数据类型

      i INTEGER := 2**128-1; -- i.e. ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff
      
      -> ORA-06502: PL/SQL: numeric or value error: number precision too large. 
      
    • 如果您必须使用子网划分,则不能使用函数 BITAND - 它还支持最多 2^127 的数字

    • 您可以使用UTL_RAW 函数UTL_RAW.BIT_ANDUTL_RAW.BIT_COMPLEMENTUTL_RAW.BIT_OR 进行子网操作。

    • 如果您必须处理真正 大量 数据(我指的是数十亿行),将 IP 地址拆分为多个 RAW 值可能会有所帮助,即4 x RAW(1) 或 8 x RAW(2)。这样的列将被预定为Bitmap-Indexes,您将节省大量磁盘空间并提高性能。

    【讨论】:

    猜你喜欢
    • 2013-05-13
    • 2013-09-18
    • 1970-01-01
    • 2012-12-25
    • 1970-01-01
    • 1970-01-01
    • 2018-01-23
    • 1970-01-01
    • 2011-02-16
    相关资源
    最近更新 更多