日別アーカイブ: 2017/01/16

IPアドレスの管理方法を考える①

ネットワーク・アドレスの格納を考える

構成管理データベースを考える時、ネットワーク管理情報を適切に格納することは重要である。Oracle以外のデータベースでは以下のように専用のデータ型や関数を提供している。

PostgreSQLの場合

PostgreSQLでは、IPv4アドレス、IPv6アドレス、MACアドレスを格納するデータ型を提供している。(8.9. ネットワークアドレス型

MySQLの場合

MySQLではIPv4 ネットワークアドレスのドット区切り表現を文字列から10進数、あるいはその逆の変換を実行する関数を提供している。(IPv6 ネットワークアドレスの変換関数も用意されている。)
inet_aton()
inet_ntoa()

Oracleでネットワークアドレス変換関数を作ってみる

翻って、Oracleでは悲しいくらいにネットワーク情報の格納手段が乏しい。ネットで検索したら以下の中国語サイトに面白い情報があったので、早速引用し試してみる。
Oracle SQL 模拟MySQL的inet_aton()和inet_ntoa()

inet_aton()

SQL> select
  2   to_number(regexp_replace(ip, '([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})', '\1')) * 16777216 +
  3   to_number(regexp_replace(ip, '([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})', '\2')) * 65536 +
  4   to_number(regexp_replace(ip, '([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})', '\3')) * 256 +
  5   to_number(regexp_replace(ip, '([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})', '\4'))   as IP_NUMBER
  6  from
  7   (select '192.168.11.1' as ip from dual);

 IP_NUMBER
----------
3232238337

dual表に任意のIPアドレスを指定すると、10進数に変換してくれるクエリーである。

さらに、OTN掲示板で見つけた「10進数を2進数に変換する」のスレッドから引用した、n進数に変換するTO_BASE関数(元ネタはAskTomらしい)で2進数に表示し直してみる。
つまり32bitで表現されるIPv4アドレスを32バイトの2進数文字列で表示する。

変換関数TO_BASE定義

CREATE OR REPLACE FUNCTION to_base( p_dec in number, p_base in number )
RETURN VARCHAR2
DETERMINISTIC
IS
  l_str   VARCHAR2(255) DEFAULT NULL;
  l_num   NUMBER        DEFAULT p_dec;
  l_hex   VARCHAR2(50)  DEFAULT '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ';
BEGIN
  IF ( trunc(p_dec) <> p_dec OR p_dec < 0 ) then
     raise PROGRAM_ERROR;
  END IF;

  LOOP
    l_str := substr( l_hex, mod(l_num,p_base)+1, 1 ) || l_str;
    l_num := trunc( l_num/p_base );
    EXIT WHEN ( l_num = 0 );
  END LOOP;

  RETURN l_str;
END to_base;
/

IPアドレスの10進数表現を2進数に変換

SQL> col "BIN_8bit" for a8
SQL> select 3232238337 as "DEC",to_base(3232238337,2) as "BIN_8bit" from dual;

       DEC BIN_8bit  (参考:オクテットを10進数に変換)
---------- --------
3232238337 11000000   192
           10101000   168
           00001011    11
           00000001     1

inet_ntoa()

同様にIPアドレスの10進数表現をドット区切り表現に変換するクエリーは以下となる。

SQL> select
  2   trunc(ip/16777216)            ||'.'||
  3   trunc(mod(ip,16777216)/65536) ||'.'||
  4   trunc(mod(ip,65536)/256)      ||'.'||
  5   trunc(mod(ip,256))            as IP_ADDRESS
  6  from
  7   (select 3232238337 as ip from dual);

IP_ADDRESS
------------
192.168.11.1

PL/SQLによるinet_aton関数の実装

CREATE OR REPLACE FUNCTION INET_ATON (
  ip_addr IN VARCHAR2)
RETURN NUMBER
DETERMINISTIC
IS
BEGIN
  RETURN(
    to_number(regexp_replace(ip_addr, '([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})', '\1')) * 16777216 +
    to_number(regexp_replace(ip_addr, '([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})', '\2')) * 65536 +
    to_number(regexp_replace(ip_addr, '([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})', '\3')) * 256 +
    to_number(regexp_replace(ip_addr, '([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})', '\4'))
  );
END;
/

実行例

SQL> select INET_ATON('192.168.11.1') IP_NUMBER from dual;

 IP_NUMBER
----------
3232238337

PL/SQLによるinet_ntoa関数の実装

CREATE OR REPLACE FUNCTION INET_NTOA (
  ip_number IN NUMBER)
RETURN VARCHAR2
DETERMINISTIC
IS
BEGIN
  RETURN(
    trunc(ip_number/16777216)            ||'.'||
    trunc(mod(ip_number,16777216)/65536) ||'.'||
    trunc(mod(ip_number,65536)/256)      ||'.'||
    trunc(mod(ip_number,256))
  );
END;
/

実行例

SQL> select INET_NTOA(3232238337) IP_ADDRESS from dual;

IP_ADDRESS
----------------
192.168.11.1

ネットワーク・アドレス管理表を考える

上で作成した関数を利用して、IPアドレスを管理する表の実装を検討する。

CREATE TABLE addr_tbl
(ip_number NUMBER
,ip_binary VARCHAR2(128) GENERATED ALWAYS AS (substr(TO_BASE(ip_number,2),1,32)) VIRTUAL
,ip_addr   VARCHAR2(60)  GENERATED ALWAYS AS (substr(INET_NTOA(ip_number),1,15)) VIRTUAL);

ALTER TABLE ADDR_TBL ADD CONSTRAINT PK_ADDR_TBL PRIMARY KEY (IP_NUMBER);
  • IPアドレスはinet_aton関数で10進数表現した値を「ip_number」列に格納し、この列が主キーとなる。
  • さらに2進数表現した「ip_binary」列を仮想列で作成している。
  • 仮想列をユーザ定義関数で作成する場合、DETERMINISTICな関数である必要がある。
  • 人間に最もなじみの深いドット区切り表現も同様に「ip_addr」列として仮想列で作成する。
  • つまり実際にデータが格納されるのは「ip_number」列のみで、「ip_binary」列と「ip_addr」列は仮想列として関数により導出される。
SQL> CREATE TABLE addr_tbl
  2  (ip_number NUMBER
  3  ,ip_binary VARCHAR2(128) GENERATED ALWAYS AS (substr(TO_BASE(ip_number,2),1,32)) VIRTUAL
  4  ,ip_addr   VARCHAR2(60)  GENERATED ALWAYS AS (substr(INET_NTOA(ip_number),1,15)) VIRTUAL);

Table created.

SQL> ALTER TABLE ADDR_TBL ADD CONSTRAINT PK_ADDR_TBL PRIMARY KEY (IP_NUMBER);

Table altered.

SQL> desc ADDR_TBL
 Name          Null?    Type
 ------------- -------- ----------------------------
 IP_NUMBER     NOT NULL NUMBER
 IP_BINARY              VARCHAR2(128)
 IP_ADDR                VARCHAR2(60)

仮想列の定義内容は以下のように確認する。
2進数表現列は32バイトあれば足りるのだが、仮想列を定義する場合はVARCHAR2型128バイトと大きめに定義する必要がある。(これはいろいろ試行錯誤して確認した。)
同様にドット区切り表現列も15バイトではなく60バイトで定義する必要がある。

SQL> SELECT
  2   TABLE_NAME,COLUMN_NAME,DATA_TYPE||'('||DATA_LENGTH||')' "Type"
  3  ,VIRTUAL_COLUMN, DATA_DEFAULT
  4  FROM USER_TAB_COLS WHERE TABLE_NAME='ADDR_TBL';

TABLE_NAME   COLUMN_NAME  Type            VIR DATA_DEFAULT
------------ ------------ --------------- --- --------------------------------------------------
ADDR_TBL     IP_NUMBER    NUMBER(22)      NO
ADDR_TBL     IP_BINARY    VARCHAR2(128)   YES SUBSTR("TEST"."TO_BASE"("IP_NUMBER",2),1,32)
ADDR_TBL     IP_ADDR      VARCHAR2(60)    YES SUBSTR("TEST"."INET_NTOA"("IP_NUMBER"),1,15)

テストデータの格納

データはIP_NUMBER列のみにInsertするだけでよいことと、INET_ATON関数を使用してドット区切り表現でInsertしていることに注目!

SQL> insert into addr_tbl(IP_NUMBER) values (INET_ATON('192.168.11.1'));

1 row created.

SQL> insert into addr_tbl(IP_NUMBER) values (INET_ATON('192.168.11.2'));

1 row created.

SQL> insert into addr_tbl(IP_NUMBER) values (INET_ATON('192.168.11.3'));

1 row created.

SQL> insert into addr_tbl(IP_NUMBER) values (INET_ATON('192.168.11.4'));

1 row created.

SQL> insert into addr_tbl(IP_NUMBER) values (INET_ATON('192.168.11.5'));

1 row created.

SQL> insert into addr_tbl(IP_NUMBER) values (INET_ATON('192.168.21.1'));

1 row created.

SQL> insert into addr_tbl(IP_NUMBER) values (INET_ATON('192.168.21.2'));

1 row created.

SQL> insert into addr_tbl(IP_NUMBER) values (INET_ATON('192.168.21.3'));

1 row created.

SQL> insert into addr_tbl(IP_NUMBER) values (INET_ATON('192.168.21.4'));

1 row created.

SQL> insert into addr_tbl(IP_NUMBER) values (INET_ATON('192.168.21.5'));

1 row created.

SQL> commit;

Commit complete.

テーブル内容の表示

SQL> col IP_ADDR for a18
SQL> col IP_BINARY for a8
SQL> select IP_NUMBER,IP_ADDR,IP_BINARY from ADDR_TBL;

 IP_NUMBER IP_ADDR            IP_BINAR
---------- ------------------ --------
3232238337 192.168.11.1       11000000
                              10101000
                              00001011
                              00000001

3232238338 192.168.11.2       11000000
                              10101000
                              00001011
                              00000010

3232238339 192.168.11.3       11000000
                              10101000
                              00001011
                              00000011

3232238340 192.168.11.4       11000000
                              10101000
                              00001011
                              00000100

3232238341 192.168.11.5       11000000
                              10101000
                              00001011
                              00000101

3232240897 192.168.21.1       11000000
                              10101000
                              00010101
                              00000001

3232240898 192.168.21.2       11000000
                              10101000
                              00010101
                              00000010

3232240899 192.168.21.3       11000000
                              10101000
                              00010101
                              00000011

3232240900 192.168.21.4       11000000
                              10101000
                              00010101
                              00000100

3232240901 192.168.21.5       11000000
                              10101000
                              00010101
                              00000101


10 rows selected.

指定されたアドレス範囲のIPアドレスを表示させる

SQL> set lines 140
SQL> set autot on
SQL> select IP_ADDR from ADDR_TBL
  2  where IP_NUMBER between INET_ATON('192.168.11.0') 
                         and INET_ATON('192.168.11.255');

IP_ADDR
------------------
192.168.11.1
192.168.11.2
192.168.11.3
192.168.11.4
192.168.11.5

Execution Plan
----------------------------------------------------------
Plan hash value: 1140719244

---------------------------------------------------------------------------------
| Id  | Operation         | Name        | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |             |     1 |    45 |     0   (0)| 00:00:01 |
|*  1 |  FILTER           |             |       |       |            |          |
|*  2 |   INDEX RANGE SCAN| PK_ADDR_TBL |     1 |    45 |     0   (0)| 00:00:01 |
---------------------------------------------------------------------------------

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

   1 - filter("INET_ATON"('192.168.11.0')<="INET_ATON"('192.168.11.255')) 2 - access("IP_NUMBER">="INET_ATON"('192.168.11.0') AND
              "IP_NUMBER"<="INET_ATON"('192.168.11.255'))
Note
-----
   - dynamic sampling used for this statement (level=2)

Statistics
----------------------------------------------------------
         58  recursive calls
          0  db block gets
         18  consistent gets
          0  physical reads
          0  redo size
        676  bytes sent via SQL*Net to client
        519  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          5  rows processed

アドレス範囲条件の指定はドット区切り表現で行っているが、inet_aton()を介して10進数に変換しているのでインデックスレンジ検索となっていることがわかる。

検索条件にドット区切り表現をそのまま使うと…

SQL> select IP_NUMBER,IP_ADDR,IP_BINARY from ADDR_TBL
  2  where IP_ADDR = '192.168.11.5';

 IP_NUMBER IP_ADDR            IP_BINAR
---------- ------------------ --------
3232238341 192.168.11.5       11000000
                              10101000
                              00001011
                              00000101

Execution Plan
----------------------------------------------------------
Plan hash value: 3135829337

------------------------------------------------------------------------------
| Id  | Operation         | Name     | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------
|   0 | SELECT STATEMENT  |          |     1 |   111 |     3   (0)| 00:00:01 |
|*  1 |  TABLE ACCESS FULL| ADDR_TBL |     1 |   111 |     3   (0)| 00:00:01 |
------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter("IP_ADDR"='192.168.11.5')

Note
-----
   - dynamic sampling used for this statement (level=2)

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

「ip_addr」列にインデックスは設定されていないので、実行計画はフル・テーブル・スキャンとなる。

「ip_addr」列にインデックスを作成してみる

仮想列のよいところは、データがなくてもインデックスが作成できることである。これはファンクション・インデックスに似ている。
そこで、「ip_addr」列にユニーク・インデックスを作成してみる。

SQL> CREATE UNIQUE INDEX IX_IP_ADDR ON ADDR_TBL (IP_ADDR);

Index created.

SQL> select IP_NUMBER,IP_ADDR,IP_BINARY from ADDR_TBL
  2  where IP_ADDR = '192.168.11.5';

 IP_NUMBER IP_ADDR            IP_BINAR
---------- ------------------ --------
3232238341 192.168.11.5       11000000
                              10101000
                              00001011
                              00000101
Execution Plan
----------------------------------------------------------
Plan hash value: 3058182370

------------------------------------------------------------------------------------------
| Id  | Operation                   | Name       | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT            |            |     1 |   111 |     1   (0)| 00:00:01 |
|   1 |  TABLE ACCESS BY INDEX ROWID| ADDR_TBL   |     1 |   111 |     1   (0)| 00:00:01 |
|*  2 |   INDEX UNIQUE SCAN         | IX_IP_ADDR |     1 |       |     0   (0)| 00:00:01 |
------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   2 - access("IP_ADDR"='192.168.11.5')

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

今度は、インデックスによる単一行検索となった。