先日投稿した「NULLを排除した設計①」の続きです。
若干変更
前回の投稿から若干変更です。せっかくCLOBカラムを定義したので、I/O負荷を高くするため値を設定する際に4000バイト長のデータにして格納することにしました。(LOBカラムへの正規の値格納方法ではないのですが、簡単に4000バイトのデータを設定しました。)
SQL> update CUSTOMERS_1 set REMARKS = rpad('0',4000,'0') where mod(CUST_ID,5 ) = 0; 10000行が更新されました。 SQL> update CUSTOMERS_1 set REMARKS = rpad('1',4000,'1') where mod(CUST_ID,10) = 1; 5000行が更新されました。 SQL> update CUSTOMERS_1 set REMARKS = rpad('2',4000,'2') where mod(CUST_ID,20) = 2; 2500行が更新されました。 SQL> update CUSTOMERS_1 set REMARKS = rpad('3',4000,'3') where mod(CUST_ID,50) = 3; 1000行が更新されました。 SQL> commit; コミットが完了しました。 SQL> select 2 CUST_ID 3 ,nvl(DBMS_LOB.SUBSTR(REMARKS, 10, 1),'Null') REMARKS 4 ,DBMS_LOB.GETLENGTH(REMARKS) LENGTH 5 from CUSTOMERS_1 where CUST_ID < 21 6 order by CUST_ID; CUST_ID REMARKS LENGTH ---------- --------------- ---------- 1 1111111111 4000 2 2222222222 4000 3 3333333333 4000 4 Null 5 0000000000 4000 6 Null 7 Null 8 Null 9 Null 10 0000000000 4000 11 1111111111 4000 CUST_ID REMARKS LENGTH ---------- --------------- ---------- 12 Null 13 Null 14 Null 15 0000000000 4000 16 Null 17 Null 18 Null 19 Null 20 0000000000 4000 20行が選択されました。 SQL> select 2 nvl(DBMS_LOB.SUBSTR(REMARKS, 10, 1),'Null') REMARKS 3 ,count(*) 4 from 5 CUSTOMERS_1 6 group by 7 DBMS_LOB.SUBSTR(REMARKS, 10, 1) 8 order by 9 1; REMARKS COUNT(*) --------------- ---------- 0000000000 10000 1111111111 5000 2222222222 2500 3333333333 1000 Null 31500
パフォーマンス比較
テストは同じSQL文をそれぞれ3回連続で実行し、一番経過時間が短かった結果を採用しました。
まずは全件検索(結果:50,000件)
①-1 単純カラム追加(NULLを排除しない設計)
SQL> select 2 CUST_ID 3 ,CUST_FNAME 4 ,CUST_LNAME 5 ,SEX 6 ,REMARKS 7 from 8 CUSTOMERS_1; 50000行が選択されました。 経過: 00:00:30.41 実行計画 ---------------------------------------------------------- Plan hash value: 2813274236 --------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | --------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 50000 | 3857K| 140 (0)| 00:00:02 | | 1 | TABLE ACCESS FULL| CUSTOMERS_1 | 50000 | 3857K| 140 (0)| 00:00:02 | --------------------------------------------------------------------------------- 統計 ---------------------------------------------------------- 0 recursive calls 0 db block gets 198074 consistent gets 148000 physical reads 0 redo size 24824948 bytes sent via SQL*Net to client 17644472 bytes received via SQL*Net from client 124002 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 50000 rows processed
②-1 テーブル追加(Nullを排除した設計)
SQL> select 2 C.CUST_ID 3 ,C.CUST_FNAME 4 ,C.CUST_LNAME 5 ,C.SEX 6 ,R.REMARKS 7 from 8 CUSTOMERS_2 C left outer join CUST_2_REMARKS R 9 on C.CUST_ID = R.CUST_ID; 50000行が選択されました。 経過: 00:00:30.31 実行計画 ---------------------------------------------------------- Plan hash value: 2466772454 ------------------------------------------------------------------------------------------------ | Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time | ------------------------------------------------------------------------------------------------ | 0 | SELECT STATEMENT | | 50000 | 7373K| | 361 (1)| 00:00:05 | |* 1 | HASH JOIN RIGHT OUTER| | 50000 | 7373K| 2248K| 361 (1)| 00:00:05 | | 2 | TABLE ACCESS FULL | CUST_2_REMARKS | 18500 | 2023K| | 40 (0)| 00:00:01 | | 3 | TABLE ACCESS FULL | CUSTOMERS_2 | 50000 | 1904K| | 90 (0)| 00:00:02 | ------------------------------------------------------------------------------------------------ Predicate Information (identified by operation id): --------------------------------------------------- 1 - access("C"."CUST_ID"="R"."CUST_ID"(+)) 統計 ---------------------------------------------------------- 0 recursive calls 0 db block gets 198137 consistent gets 148000 physical reads 0 redo size 24824948 bytes sent via SQL*Net to client 17644472 bytes received via SQL*Net from client 124002 SQL*Net roundtrips to/from client 1 sorts (memory) 0 sorts (disk) 50000 rows processed
このクエリーはOUTER JOINで書くのがポイントです。
コスト値は単純カラム追加の方が半分以下なのですが、physical readsは両者全く同じで、経過時間もほとんど同じです。
②のコストが大きいのはハッシュ・ジョインのためですが、パフォーマンス的には遜色のない結果となっています。
IS NOT NULL検索はどうか?(結果:18,500件)
①-2 単純カラム追加(NULLを排除しない設計)
SQL> select 2 CUST_ID 3 ,CUST_FNAME 4 ,CUST_LNAME 5 ,SEX 6 ,REMARKS 7 from 8 CUSTOMERS_1 9 where REMARKS is not null; 18500行が選択されました。 経過: 00:00:24.06 実行計画 ---------------------------------------------------------- Plan hash value: 2813274236 --------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | --------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 18500 | 1427K| 143 (2)| 00:00:02 | |* 1 | TABLE ACCESS FULL| CUSTOMERS_1 | 18500 | 1427K| 143 (2)| 00:00:02 | --------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter("REMARKS" IS NOT NULL) 統計 ---------------------------------------------------------- 0 recursive calls 0 db block gets 166771 consistent gets 148000 physical reads 0 redo size 20641347 bytes sent via SQL*Net to client 17297972 bytes received via SQL*Net from client 92502 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 18500 rows processed
②-2 テーブル追加(Nullを排除した設計)
SQL> select 2 C.CUST_ID 3 ,C.CUST_FNAME 4 ,C.CUST_LNAME 5 ,C.SEX 6 ,R.REMARKS 7 from 8 CUSTOMERS_2 C inner join CUST_2_REMARKS R 9 on C.CUST_ID = R.CUST_ID; 18500行が選択されました。 経過: 00:00:23.60 実行計画 ---------------------------------------------------------- Plan hash value: 2209842270 --------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time | --------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 18500 | 2728K| | 361 (1)| 00:00:05 | |* 1 | HASH JOIN | | 18500 | 2728K| 2248K| 361 (1)| 00:00:05 | | 2 | TABLE ACCESS FULL| CUST_2_REMARKS | 18500 | 2023K| | 40 (0)| 00:00:01 | | 3 | TABLE ACCESS FULL| CUSTOMERS_2 | 50000 | 1904K| | 90 (0)| 00:00:02 | --------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - access("C"."CUST_ID"="R"."CUST_ID") 統計 ---------------------------------------------------------- 0 recursive calls 0 db block gets 166835 consistent gets 148000 physical reads 0 redo size 20641347 bytes sent via SQL*Net to client 17297972 bytes received via SQL*Net from client 92502 SQL*Net roundtrips to/from client 1 sorts (memory) 0 sorts (disk) 18500 rows processed
このクエリーはINNER JOINで書くのがポイントですが、全件検索と同様に②-1と較べて遜色ないというか、むしろ若干よい結果が出ました。(これは偶然だと思いますが。)
IS NULL検索はどうか?(結果:31,500件)
③-1 単純カラム追加(NULLを排除しない設計)
SQL> select 2 CUST_ID 3 ,CUST_FNAME 4 ,CUST_LNAME 5 ,SEX 6 ,REMARKS 7 from 8 CUSTOMERS_1 9 where REMARKS is null; 31500行が選択されました。 経過: 00:00:06.56 実行計画 ---------------------------------------------------------- Plan hash value: 2813274236 --------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | --------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 31500 | 2430K| 143 (2)| 00:00:02 | |* 1 | TABLE ACCESS FULL| CUSTOMERS_1 | 31500 | 2430K| 143 (2)| 00:00:02 | --------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter("REMARKS" IS NULL) 統計 ---------------------------------------------------------- 0 recursive calls 0 db block gets 31745 consistent gets 0 physical reads 0 redo size 4184175 bytes sent via SQL*Net to client 346972 bytes received via SQL*Net from client 31502 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 31500 rows processed
③-2 テーブル追加(Nullを排除した設計)
SQL> select 2 CUST_ID 3 ,CUST_FNAME 4 ,CUST_LNAME 5 ,SEX 6 ,null REMARKS 7 from 8 CUSTOMERS_2 9 where not exists ( 10 select 11 1 12 from 13 CUST_2_REMARKS 14 where CUSTOMERS_2.CUST_ID = CUST_2_REMARKS.CUST_ID 15 ); 31500行が選択されました。 経過: 00:00:00.75 実行計画 ---------------------------------------------------------- Plan hash value: 2401287113 ---------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ---------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 31500 | 1353K| 93 (4)| 00:00:02 | | 1 | NESTED LOOPS ANTI | | 31500 | 1353K| 93 (4)| 00:00:02 | | 2 | TABLE ACCESS FULL| CUSTOMERS_2 | 50000 | 1904K| 90 (0)| 00:00:02 | |* 3 | INDEX UNIQUE SCAN| PK_CUST_2_REMARKS | 6845 | 34225 | 0 (0)| 00:00:01 | ---------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 3 - access("CUSTOMERS_2"."CUST_ID"="CUST_2_REMARKS"."CUST_ID") 統計 ---------------------------------------------------------- 0 recursive calls 0 db block gets 6386 consistent gets 0 physical reads 0 redo size 1577675 bytes sent via SQL*Net to client 23561 bytes received via SQL*Net from client 2101 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 31500 rows processed
このクエリーはNOT EXIST句を使って書くのがポイントですが、テーブル分割した方が圧倒的にパフォーマンスがよいです!
しかし、実はこれには落とし穴があります。IS NULL条件なので結果のREMARKSカラムは必ずNullになります。③-1は以下のように書き換えが可能です。
SQL> select 2 CUST_ID 3 ,CUST_FNAME 4 ,CUST_LNAME 5 ,SEX 6 ,null REMARKS 7 from 8 CUSTOMERS_1 9 where REMARKS is null; 31500行が選択されました。 経過: 00:00:00.71 実行計画 ---------------------------------------------------------- Plan hash value: 2813274236 --------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | --------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 31500 | 2430K| 143 (2)| 00:00:02 | |* 1 | TABLE ACCESS FULL| CUSTOMERS_1 | 31500 | 2430K| 143 (2)| 00:00:02 | --------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter("REMARKS" IS NULL) 統計 ---------------------------------------------------------- 0 recursive calls 0 db block gets 2531 consistent gets 0 physical reads 0 redo size 1577675 bytes sent via SQL*Net to client 23561 bytes received via SQL*Net from client 2101 SQL*Net roundtrips to/from client 0 sorts (memory) 0 sorts (disk) 31500 rows processed
律儀にREMARKSカラムをクエリーに入れてしまうと、パフォーマンスが極端に悪くなります。
こうして見ると設計の違いでパフォーマンスに特筆すべき差はないので、「単純なカラム追加でよいではないか。」という意見もあるかと思いますが、Nullを排除した設計の利点を次回考察していきたいと思います。
(続く)