Oracle」カテゴリーアーカイブ

ORAエラーはいくつある?訂正しました。

今週の名言

「勇気とは不安を感じないことではない。勇気とは不安をコントロールすることだ。」
マーク・トウェイン

すみません、大嘘ついてました。(^^;

4月12日の投稿ORAエラーはいくつある?でORAエラーの数を数えてみたのですが、結果が大間違いであることがわかりました。

間違いの内容は既に記事を訂正していますのでそちらを見ていただければよいと思いますが、1ヶ月以上経っているのにこの件に対して誰からも何のツッコミもなかったということは、このブログもまだまだだなという悲しい現実を再認識させられました。

でも、他の人に指摘される前に自分で見つけられて正直ホッとしているというのも事実ですので、今後も自分できちんと検証し自信を持って記事を書いていくという方針は貫いていきたいと思います。

UTL_LMSパッケージを使う

最初に結論を書きますが、Oracleのエラー・メッセージを取得するにはUTL_LMS.GET_MESSAGEファンクションを使います。

このファンクションはかなり使えるので後で他の応用も紹介しますが、以前紹介したORAエラーの数を数えるのは以下のPL/SQLスクリプトで可能です。(環境はOracle 11.2.0.4)

SQL> set serveroutput on
SQL> DECLARE
  2   err_msg LONG;
  3   err_cod PLS_INTEGER;
  4   i       PLS_INTEGER;
  5   cnt     NUMBER :=0;
  6  BEGIN
  7   FOR err_cod IN 1..99999 LOOP
  8    i := UTL_LMS.GET_MESSAGE (err_cod, 'rdbms', 'ora', 'japanese', err_msg);
  9    IF err_msg not like 'Message%not found%' THEN
 10  --   DBMS_OUTPUT.PUT_LINE ('ORA-' || TO_CHAR (err_cod, 'FM00000') || ': ' || err_msg);
 11     cnt := cnt +1;
 12    END IF;
 13   END LOOP;
 14   DBMS_OUTPUT.PUT_LINE('Nuber of Error Code : ' || cnt);
 15  END;
 16  /

Nuber of Error Code : 18414

PL/SQL procedure successfully completed.

このスクリプト例ではあえてコメントアウトしていますが、11行目のコメントを外すとメッセージが得られます。

「ORAエラーはいくつある?」という問いに対しては18,414個という結果を答えとして挙げておきます。
(お時間のある方、是非数えてみてください。)

応用編:CRSエラーの数を数えてみる

SQLERRMファンクションは、ORAエラーのみを表示します(しかも余計な番号も含む)が、UTL_LMS.GET_MESSAGEファンクションは第2引数と第3引数を変えると、Oracle RDBMS以外の製品が出力するエラー・メッセージを取得することができます。(第4引数を変えると表示する言語を選択することができます。)

例えば、Oracle Grid Infrastructureのエラー・メッセージの場合は「crs」と「crs」を指定して以下のように実行します。(ハイライト行に注目)

SQL> DECLARE
  2   err_msg LONG;
  3   err_cod PLS_INTEGER;
  4   i       PLS_INTEGER;
  5   cnt     NUMBER :=0;
  6  BEGIN
  7   FOR err_cod IN 1..99999 LOOP
  8    i := UTL_LMS.GET_MESSAGE (err_cod, 'crs', 'crs', 'japanese', err_msg);
  9    IF err_msg not like 'Message%not found%' THEN
 10  --   DBMS_OUTPUT.PUT_LINE ('CRS-' || TO_CHAR (err_cod, 'FM00000') || ': ' || err_msg);
 11     cnt := cnt +1;
 12    END IF;
 13   END LOOP;
 14   DBMS_OUTPUT.PUT_LINE('Nuber of Error Code : ' || cnt);
 15  END;
 16  /

Nuber of Error Code : 1778

PL/SQL procedure successfully completed.

CRSエラーは1,778個あることがわかりました。

今回はここまで

カレンダーで遊んでみる②

今週の名言

「幸せを与えてくれるのは、富でも豪華さでもなく、穏やかさと仕事である。」
トーマス・ジェファーソン

各カレンダーを比べてみる

Oracle Database 11gR2 で和暦を含む7つのカレンダーをサポートしていることは前回紹介しました。(12cではさらに1つ追加)
私たち日本人は西暦(グレゴリオ暦)と和暦しか馴染みがないのですが、他のカレンダーを使う機会はまずないので、それらでちょっと遊んでみたいと思います。

2015年1月のカレンダーで比較する

例として、2015年1月を各カレンダーで表示させてみたいと思います。
1月1日のデータは作成しているので2日から31日までのデータを追加します。

SQL> INSERT INTO jcal VALUES (11,TO_DATE('20150102','YYYYMMDD'));

1 row created.

SQL> INSERT INTO jcal VALUES (12,TO_DATE('20150103','YYYYMMDD'));

1 row created.
....................................................................
SQL> INSERT INTO jcal VALUES (39,TO_DATE('20150130','YYYYMMDD'));

1 row created.

SQL> INSERT INTO jcal VALUES (40,TO_DATE('20150131','YYYYMMDD'));

1 row created.

SQL> COMMIT;

Commit complete.

それでは、6つのカレンダーを比較してみましょう。

SQL> SELECT
  2   id
  3  ,TO_CHAR(sdate,'YYYY.MM.DD', 'NLS_CALENDAR = ''Gregorian''')         "グレゴリオ暦"
  4  ,TO_CHAR(sdate,'YYYY.MM.DD', 'NLS_CALENDAR = ''Arabic Hijrah''')     "イスラム暦"
  5  ,TO_CHAR(sdate,'YYYY.MM.DD', 'NLS_CALENDAR = ''Persian''')           "ペルシャ暦"
  6  ,TO_CHAR(sdate,'eeyy.mm.dd', 'NLS_CALENDAR = ''Japanese Imperial''') "和暦"
  7  ,TO_CHAR(sdate,'eeyyy.mm.dd','NLS_CALENDAR = ''ROC Official''')      "台湾暦"
  8  ,TO_CHAR(sdate,'eeYYYY.MM.DD','NLS_CALENDAR = ''Thai Buddha''')      "タイ仏教暦"
  9  FROM jcal
 10  WHERE id = 1 OR id BETWEEN 11 AND 40
 11  ORDER BY sdate
 12 ;

 ID グレゴリオ暦  イスラム暦  ペルシャ暦  和暦          台湾暦             タイ仏教暦
--- ------------ ---------- ---------- ------------- ----------------- --------------------
  1 2015.01.01   1436.03.10 1393.10.11 平成27.01.01  中華民國104.01.01  พุทธศักราช2558.01.01
 11 2015.01.02   1436.03.11 1393.10.12 平成27.01.02  中華民國104.01.02  พุทธศักราช2558.01.02
 12 2015.01.03   1436.03.12 1393.10.13 平成27.01.03  中華民國104.01.03  พุทธศักราช2558.01.03
 13 2015.01.04   1436.03.13 1393.10.14 平成27.01.04  中華民國104.01.04  พุทธศักราช2558.01.04
 14 2015.01.05   1436.03.14 1393.10.15 平成27.01.05  中華民國104.01.05  พุทธศักราช2558.01.05
 15 2015.01.06   1436.03.15 1393.10.16 平成27.01.06  中華民國104.01.06  พุทธศักราช2558.01.06
 16 2015.01.07   1436.03.16 1393.10.17 平成27.01.07  中華民國104.01.07  พุทธศักราช2558.01.07
 17 2015.01.08   1436.03.17 1393.10.18 平成27.01.08  中華民國104.01.08  พุทธศักราช2558.01.08
 18 2015.01.09   1436.03.18 1393.10.19 平成27.01.09  中華民國104.01.09  พุทธศักราช2558.01.09
 19 2015.01.10   1436.03.19 1393.10.20 平成27.01.10  中華民國104.01.10  พุทธศักราช2558.01.10

 ID グレゴリオ暦  イスラム暦  ペルシャ暦  和暦          台湾暦             タイ仏教暦
--- ------------ ---------- ---------- ------------- ----------------- --------------------
 20 2015.01.11   1436.03.20 1393.10.21 平成27.01.11  中華民國104.01.11  พุทธศักราช2558.01.11
 21 2015.01.12   1436.03.21 1393.10.22 平成27.01.12  中華民國104.01.12  พุทธศักราช2558.01.12
 22 2015.01.13   1436.03.22 1393.10.23 平成27.01.13  中華民國104.01.13  พุทธศักราช2558.01.13
 23 2015.01.14   1436.03.23 1393.10.24 平成27.01.14  中華民國104.01.14  พุทธศักราช2558.01.14
 24 2015.01.15   1436.03.24 1393.10.25 平成27.01.15  中華民國104.01.15  พุทธศักราช2558.01.15
 25 2015.01.16   1436.03.25 1393.10.26 平成27.01.16  中華民國104.01.16  พุทธศักราช2558.01.16
 26 2015.01.17   1436.03.26 1393.10.27 平成27.01.17  中華民國104.01.17  พุทธศักราช2558.01.17
 27 2015.01.18   1436.03.27 1393.10.28 平成27.01.18  中華民國104.01.18  พุทธศักราช2558.01.18
 28 2015.01.19   1436.03.28 1393.10.29 平成27.01.19  中華民國104.01.19  พุทธศักราช2558.01.19
 29 2015.01.20   1436.03.29 1393.10.30 平成27.01.20  中華民國104.01.20  พุทธศักราช2558.01.20

 ID グレゴリオ暦  イスラム暦  ペルシャ暦  和暦          台湾暦             タイ仏教暦
--- ------------ ---------- ---------- ------------- ----------------- --------------------
 30 2015.01.21   1436.03.30 1393.11.01 平成27.01.21  中華民國104.01.21  พุทธศักราช2558.01.21
 31 2015.01.22   1436.04.01 1393.11.02 平成27.01.22  中華民國104.01.22  พุทธศักราช2558.01.22
 32 2015.01.23   1436.04.02 1393.11.03 平成27.01.23  中華民國104.01.23  พุทธศักราช2558.01.23
 33 2015.01.24   1436.04.03 1393.11.04 平成27.01.24  中華民國104.01.24  พุทธศักราช2558.01.24
 34 2015.01.25   1436.04.04 1393.11.05 平成27.01.25  中華民國104.01.25  พุทธศักราช2558.01.25
 35 2015.01.26   1436.04.05 1393.11.06 平成27.01.26  中華民國104.01.26  พุทธศักราช2558.01.26
 36 2015.01.27   1436.04.06 1393.11.07 平成27.01.27  中華民國104.01.27  พุทธศักราช2558.01.27
 37 2015.01.28   1436.04.07 1393.11.08 平成27.01.28  中華民國104.01.28  พุทธศักราช2558.01.28
 38 2015.01.29   1436.04.08 1393.11.09 平成27.01.29  中華民國104.01.29  พุทธศักราช2558.01.29
 39 2015.01.30   1436.04.09 1393.11.10 平成27.01.30  中華民國104.01.30  พุทธศักราช2558.01.30

 ID グレゴリオ暦  イスラム暦  ペルシャ暦  和暦          台湾暦             タイ仏教暦
--- ------------ ---------- ---------- ------------- ----------------- --------------------
 40 2015.01.31   1436.04.10 1393.11.11 平成27.01.31  中華民國104.01.31  พุทธศักราช2558.01.31

31 rows selected.

西暦と年だけが異なるもの、月日も異なるもの、いろいろな違いがあることがわかります。
タイ仏教暦などはなんて書いてあるかもわかりませんね。

各カレンダーの紀元を調べてみる

それでは、各カレンダーで「1年1月1日」というデータを作成して、西暦(グレゴリオ暦)で確認してみましょう。

イスラム暦

イスラム暦に関してはOracle Databaseでは「Arabic Hijrah(イスラム暦)」と「English Hijrah(英語版イスラム暦)」をサポートしていますが、両者の正確な違いは確認していません。
今回は「Arabic Hijrah(イスラム暦)」を使います。
ちなみにWikipediaでは次のように紹介されています。
イスラム暦(ヒジュラ暦)

SQL> ALTER SESSION SET NLS_CALENDAR='Arabic Hijrah';

Session altered.

SQL> INSERT INTO jcal VALUES (41,TO_DATE('00010101','YYYYMMDD'));

1 row created.

SQL> COMMIT;

Commit complete.

SQL> SELECT
  2   id
  3  ,TO_CHAR(sdate,'YYYY.MM.DD', 'NLS_CALENDAR = ''Gregorian''') "A.D."
  4  FROM jcal
  5  WHERE id = 41;

 ID A.D.
--- ----------
 41 0622.07.16

上記Wikipediaのリンクには「預言者ムハンマドがマッカからマディーナへ聖遷(ヒジュラ)したユリウス暦622年を「ヒジュラの年」と定めヒジュラ暦元年とする新たな暦を制定した。なお、ヒジュラがあったとされる正確な日付は同622年7月16日(ユリウス通日1948439日)である。」との記述があります。

ペルシャ暦

これまたWikipediaからの引用になりますが次のリンクに解説があります。
ペルシャ暦(イラン暦)

SQL> ALTER SESSION SET NLS_CALENDAR='Persian';

Session altered.

SQL> INSERT INTO jcal VALUES (42,TO_DATE('00010101','YYYYMMDD'));

1 row created.

SQL> COMMIT;

Commit complete.

SQL> SELECT
  2   id
  3  ,TO_CHAR(sdate,'YYYY.MM.DD', 'NLS_CALENDAR = ''Gregorian''') "A.D."
  4  FROM jcal
  5  WHERE id = 42;

 ID A.D.
--- ----------
 42 0622.03.21

紀元はヒジュラ暦と同じ年ですが、年初がグレゴリオ暦の3月21日となっているところが特徴です。

台湾暦

台湾暦とは「民国紀元」と呼ばれ、中華民国が成立された1912年を紀元とする暦です。
民国紀元(Wikipedia)
中華民国は、1911年の辛亥革命の結果、アジアで史上初の共和制国家として成立しました。
「台湾暦」というくらいなので中国(中華人民共和国)でこの暦法は使用されておらず、西暦が用いられているそうです。

SQL> ALTER SESSION SET NLS_CALENDAR='ROC Official';

Session altered.

SQL> INSERT INTO jcal VALUES (43,TO_DATE('00010101','YYYYMMDD'));

1 row created.

SQL> COMMIT;

Commit complete.

SQL> SELECT
  2   id
  3  ,TO_CHAR(sdate,'YYYY.MM.DD', 'NLS_CALENDAR = ''Gregorian''') "A.D."
  4  FROM jcal
  5  WHERE id = 43;

 ID A.D.
--- ----------
 43 1912.01.01

ちなみに、民国紀元の年は日本の大正(元年から15年のみ)あるいは北朝鮮の主体暦(チュチェ暦-金日成の生誕年が紀元)と偶然同じになっています。
北朝鮮のOracle使いは裏技で台湾暦を使っているかもしれません。(冗談)
また、台湾では「中華民國99年」から「中華民國100年」(西暦2011年)にかけて、年データを一部のシステムにおいて2桁で管理していたことによる、いわゆる「民国100年問題」というどこかで聞いたことがある問題が起きたそうです。

タイ仏教暦

これもWikipediaからの受け売りですが、タイ仏教暦は「仏滅紀元(Wikipedia)」とも呼ばれ、紀元はお釈迦様が入滅(死去)された年だそうです。
この年は宗教上の伝来によるものとされているようで、学術的に言われている年とは一致しないようですが、Oracleの仕様上はこの年が採用されています。

他の暦法と違って、(グレゴリオ暦)の紀元前から始まっているので、日付書式も若干異なっていることに注意してください。

SQL> ALTER SESSION SET NLS_CALENDAR='Thai Buddha';

Session altered.

SQL> INSERT INTO jcal VALUES (44,TO_DATE('00010101','YYYYMMDD'));

1 row created.

SQL> COMMIT;

Commit complete.

SQL> SELECT
  2   id
  3  ,TO_CHAR(sdate,'BC YYYY.MM.DD', 'NLS_CALENDAR = ''Gregorian''') sdate
  4  FROM jcal
  5  WHERE id = 44;

 ID SDATE
--- --------------------
 44 紀元前 0542.01.01

日付書式BC/ADについて

上の問い合わせで日付書式「BC」は「紀元前」と日本語で表示されましたが、これは以下のように「NLS_DATE_LANGUAGE」パラメータが「JAPANESE」であることによります。
このパラメータは「NLS_LANGUAGE」パラメータから導出され、さらに「NLS_LANGUAGE」パラメータは環境変数「NLS_LANG」の先頭に指定された言語によって決まります。

SQL> SELECT PARAMETER,VALUE FROM NLS_SESSION_PARAMETERS
  2  WHERE PARAMETER LIKE 'NLS_%LANGUAGE';

PARAMETER                      VALUE
------------------------------ ------------------------------
NLS_LANGUAGE                   JAPANESE
NLS_DATE_LANGUAGE              JAPANESE

SQL> !env|grep NLS_LANG
NLS_LANG=Japanese_Japan.AL32UTF8

また、NLS_DATE_LANGUAGE=JAPANESEの環境においては、以下のように日付書式「BC」は日付書式「AD」と同じ結果を返す、つまり日付データが西暦0年よりも前であればどちらも「紀元前」と表示されるようです。

SQL> SELECT
  2   id
  3  ,TO_CHAR(sdate,'AD YYYY.MM.DD', 'NLS_CALENDAR = ''Gregorian''') sdate
  4  FROM jcal
  5  WHERE id = 44;

 ID SDATE
--- --------------------
 44 紀元前 0542.01.01

SQL> SELECT
  2   id
  3  ,TO_CHAR(sdate,'AD YYYY.MM.DD', 'NLS_CALENDAR = ''Gregorian''') sdate
  4  FROM jcal
  5  WHERE id = 43;

 ID SDATE
--- --------------------
 43 西暦 1912.01.01

SQL> SELECT
  2   id
  3  ,TO_CHAR(sdate,'BC YYYY.MM.DD', 'NLS_CALENDAR = ''Gregorian''') sdate
  4  FROM jcal
  5  WHERE id = 43;

 ID SDATE
--- --------------------
 43 西暦 1912.01.01

前回から2回に渡ってカレンダーでいろいろ遊んでみましたがいかがでしたでしょうか?

このように日付データはDATE型で格納しておけば、様々な日付書式が使えるだけでなく、「2月30日」のような明らかに不正なデータが混入する危険を防ぐことができるため大変有益です。

ところが、未だに文字型8桁のようなフォーマットで日付データを保持しているシステムを見ることがあります。
どうしてそのような設計になるのか理由はわかりませんが、わずかなパフォーマンス上のメリットを追求するあまりデータの正確性を犠牲にするような設計は根本的に間違っていると思います。

終わり

カレンダーで遊んでみる①

今週の名言

「知識に対する投資は常に一番の利益を生み出す」
- ベンジャミン・フランクリン -

Oracleと暦法

以下はOracleデータベースがサポートする暦法(カレンダー)です。

Oracle® Databaseグローバリゼーション・サポート・ガイド
11gリリース2 (11.2)
B56307-04
NLS_CALENDAR 参照

  • Arabic Hijrah(イスラム歴)
  • English Hijrah(英語版イスラム歴)
  • Gregorian(グレゴリオ暦)
  • Japanese Imperial(日本の元号暦)
  • Persian(ペルシャ暦)
  • ROC Official(台湾暦)
  • Thai Buddha(タイ仏教暦)
  • Ethiopian(エチオピア歴、12c〜)

こんなにも多くのカレンダーを使うことができるというのも驚きですが、我々日本人にとって元号歴がサポートされているというのは非常にありがたいことです。(でも、どの年号からサポートされているのでしょうか?まさか「大化」というとこはないでしょうが。)

昭和天皇が崩御された時、私は某官公庁で大型汎用計算機の仕事をしていました。(Oracleアーキテクチャをどのように理解するか 参照)

官公庁における報告書等は基本的に和暦が使用されますので、元号が「昭和」から「平成」に変わるということは非常に大変でした。

西暦から和暦に変換するファンクションを作成していたのですが、「平成01年」ではなくて「平成年」と表示させてくれという仕様変更があり大慌てで修正したことを今でもよく覚えています。

その当時の苦労から比べると、今回紹介する和暦変換は非常に簡単ですので、是非業務に活用していただければと思います。

検証環境

今回はOracle11g R2(Oracle Linux6)の環境を使用します。
NLS_LANGは日本語表示の設定であることを確認します。

SQL> select * from v$version;

BANNER
--------------------------------------------------------------------------------
Oracle Database 11g Enterprise Edition Release 11.2.0.4.0 - 64bit Production
PL/SQL Release 11.2.0.4.0 - Production
CORE	11.2.0.4.0	Production
TNS for Linux: Version 11.2.0.4.0 - Production
NLSRTL Version 11.2.0.4.0 - Production

SQL> !uname -rs
Linux 3.8.13-44.el6uek.x86_64

SQL> !env|grep NLS_LANG
NLS_LANG=Japanese_Japan.AL32UTF8

テーブル作成とデータ作成

日付データを格納する単純なテーブルを作成し、特定の日付データ(西暦)を挿入します。

SQL> CREATE TABLE jcal (id NUMBER,sdate DATE);

Table created.

SQL> INSERT INTO jcal VALUES (1,to_date('20150101','yyyymmdd'));

1 row created.

SQL> INSERT INTO jcal VALUES (2,to_date('20141231','yyyymmdd'));

1 row created.

SQL> INSERT INTO jcal VALUES (3,to_date('19890108','yyyymmdd'));

1 row created.

SQL> INSERT INTO jcal VALUES (4,to_date('19890107','yyyymmdd'));

1 row created.

SQL> INSERT INTO jcal VALUES (5,to_date('19261225','yyyymmdd'));

1 row created.

SQL> INSERT INTO jcal VALUES (6,to_date('19261224','yyyymmdd'));

1 row created.

SQL> INSERT INTO jcal VALUES (7,to_date('19120730','yyyymmdd'));

1 row created.

SQL> INSERT INTO jcal VALUES (8,to_date('19120729','yyyymmdd'));

1 row created.

SQL> INSERT INTO jcal VALUES (9,to_date('18680908','yyyymmdd'));

1 row created.

SQL> INSERT INTO jcal VALUES (10,to_date('18680907','yyyymmdd'));

1 row created.

SQL> COMMIT;

Commit complete.

SQL> SELECT
  2   id
  3  ,TO_CHAR(sdate,'yyyy.mm.dd') "A.D."
  4  FROM jcal
  5  ORDER BY sdate
  6 ;

  ID A.D.
---- ----------
  10 1868.09.07
   9 1868.09.08
   8 1912.07.29
   7 1912.07.30
   6 1926.12.24
   5 1926.12.25
   4 1989.01.07
   3 1989.01.08
   2 2014.12.31
   1 2015.01.01

10 rows selected.

10件のデータが作成されました。

和暦変換した日付を表示させる

とりあえず和暦変換した日付を表示させましょう。
ポイントは「NLS_CALENDAR=’Japanese Imperial’」の指定です。

SQL> SELECT
  2   id
  3  ,TO_CHAR(sdate,'yyyy.mm.dd') "A.D."
  4  ,TO_CHAR(sdate,'eyy.mm.dd','nls_calendar = ''Japanese Imperial''') "WAREKI-S"
  5  FROM jcal
  6  ORDER BY sdate
  7 ;
,TO_CHAR(sdate,'eyy.mm.dd','nls_calendar = ''Japanese Imperial''') "WAREKI-S"
 *
ERROR at line 4:
ORA-01863: この年は現行カレンダではサポートされていません

おっと!いきなりエラーとなってしまいました。

サポートされるのは、明治元年9月8日以降

明治天皇が即位されたのは、1898年9月8日(慶応4年9月8日)で、この日を「明治元年9月8日」としたということです。
また、遡って「慶応4年1月1日」を「明治元年1月1日」と正式に定めたそうです。(Wikipedia「明治」参照。)

「NLS_CALENDAR=’Japanese Imperial’」では、1898年1月1日から9月7日までを、和暦(明治元年)で表示させようとするとエラーになるようです。(当然「慶応」以前の元号はサポートしていません。)

SQL> SELECT
  2   id
  3  ,TO_CHAR(sdate,'yyyy.mm.dd') "A.D."
  4  ,TO_CHAR(sdate,'eyy.mm.dd','nls_calendar = ''Japanese Imperial''') "WAREKI-S"
  5  FROM jcal
  6  ORDER BY sdate
  7 ;
,TO_CHAR(sdate,'eyy.mm.dd','nls_calendar = ''Japanese Imperial''') "WAREKI-S"
 *
ERROR at line 4:
ORA-01863: この年は現行カレンダではサポートされていません

日付範囲を指定し直して再挑戦

今度は、「1898年9月7日」のデータを除外して再挑戦してみましょう。

SQL> SELECT
  2   id
  3  ,TO_CHAR(sdate,'yyyy.mm.dd') "A.D."
  4  ,TO_CHAR(sdate,'eyy.mm.dd','nls_calendar = ''Japanese Imperial''') "WAREKI-S"
  5  FROM jcal
  6  WHERE id < 10
  7  ORDER BY sdate
  8 ;

  ID A.D.       WAREKI-S
---- ---------- ----------
   9 1868.09.08 M01.09.08
   8 1912.07.29 M45.07.29
   7 1912.07.30 T01.07.30
   6 1926.12.24 T15.12.24
   5 1926.12.25 S01.12.25
   4 1989.01.07 S64.01.07
   3 1989.01.08 H01.01.08
   2 2014.12.31 H26.12.31
   1 2015.01.01 H27.01.01

9 rows selected.

エラーとなる和暦変換データがないので、クエリーは正常な結果を返します。
「eyy」の日付書式は「年号(短縮表記)+年(2桁)」を示します。

年号(漢字表記)を表示させる

「eeyy」の日付書式は「年号(漢字表記)+年(2桁)」を示します。

SQL> SELECT
  2   id
  3  ,TO_CHAR(sdate,'yyyy.mm.dd') "A.D."
  4  ,TO_CHAR(sdate,'eyy.mm.dd','nls_calendar = ''Japanese Imperial''') "WAREKI-S"
  5  ,TO_CHAR(sdate,'eeyy"年"mm"月"dd"日"','nls_calendar = ''Japanese Imperial''') "WAREKI-L"
  6  FROM jcal
  7  WHERE id < 10
  8  ORDER BY sdate
  9 ;

  ID A.D.       WAREKI-S   WAREKI-L
---- ---------- ---------- ------------------
   9 1868.09.08 M01.09.08  明治01年09月08日
   8 1912.07.29 M45.07.29  明治45年07月29日
   7 1912.07.30 T01.07.30  大正01年07月30日
   6 1926.12.24 T15.12.24  大正15年12月24日
   5 1926.12.25 S01.12.25  昭和01年12月25日
   4 1989.01.07 S64.01.07  昭和64年01月07日
   3 1989.01.08 H01.01.08  平成01年01月08日
   2 2014.12.31 H26.12.31  平成26年12月31日
   1 2015.01.01 H27.01.01  平成27年01月01日

9 rows selected.

応用編:「01年」を「元年」と表示させる

DECODE関数を使って、「01年」のみ「元年」と表示させてみます。

SQL> SELECT
  2   id
  3  ,TO_CHAR(sdate,'yyyy.mm.dd') "A.D."
  4  ,TO_CHAR(sdate,'eyy.mm.dd','nls_calendar = ''Japanese Imperial''') "WAREKI-S"
  5  ,DECODE(TO_CHAR(sdate,'yy"','nls_calendar = ''Japanese Imperial''')
  6        ,'01',TO_CHAR(sdate,'ee"元年"mm"月"dd"日"','nls_calendar = ''Japanese Imperial''')
  7        ,     TO_CHAR(sdate,'eeyy"年"mm"月"dd"日"','nls_calendar = ''Japanese Imperial''')) "WAREKI-L"
  8  FROM jcal
  9  WHERE id < 10
 10  ORDER BY sdate
 11 ;

  ID A.D.       WAREKI-S   WAREKI-L
---- ---------- ---------- ------------------
   9 1868.09.08 M01.09.08  明治元年09月08日
   8 1912.07.29 M45.07.29  明治45年07月29日
   7 1912.07.30 T01.07.30  大正元年07月30日
   6 1926.12.24 T15.12.24  大正15年12月24日
   5 1926.12.25 S01.12.25  昭和元年12月25日
   4 1989.01.07 S64.01.07  昭和64年01月07日
   3 1989.01.08 H01.01.08  平成元年01月08日
   2 2014.12.31 H26.12.31  平成26年12月31日
   1 2015.01.01 H27.01.01  平成27年01月01日

9 rows selected.

曜日も表示させる

SQL> SELECT
  2   id
  3  ,TO_CHAR(sdate,'yyyy.mm.dd') "A.D."
  4  ,TO_CHAR(sdate,'eyy.mm.dd','nls_calendar = ''Japanese Imperial''') "WAREKI-S"
  5  ,DECODE(TO_CHAR(sdate,'yy"','nls_calendar = ''Japanese Imperial''')
  6        ,'01',TO_CHAR(sdate,'ee"元年"mm"月"dd"日"','nls_calendar = ''Japanese Imperial''')
  7        ,     TO_CHAR(sdate,'eeyy"年"mm"月"dd"日"','nls_calendar = ''Japanese Imperial''')) "WAREKI-L"
  8  ,TO_CHAR(sdate,'day') day
  9  FROM jcal
 10  WHERE id < 10
 11  ORDER BY sdate
 12 ;

  ID A.D.       WAREKI-S   WAREKI-L           DAY
---- ---------- ---------- ------------------ --------
   9 1868.09.08 M01.09.08  明治元年09月08日    火曜日
   8 1912.07.29 M45.07.29  明治45年07月29日    月曜日
   7 1912.07.30 T01.07.30  大正元年07月30日    火曜日
   6 1926.12.24 T15.12.24  大正15年12月24日    金曜日
   5 1926.12.25 S01.12.25  昭和元年12月25日    土曜日
   4 1989.01.07 S64.01.07  昭和64年01月07日    土曜日
   3 1989.01.08 H01.01.08  平成元年01月08日    日曜日
   2 2014.12.31 H26.12.31  平成26年12月31日    水曜日
   1 2015.01.01 H27.01.01  平成27年01月01日    木曜日

9 rows selected.

セッション単位で暦法を変更する

NLS_CALENDARは、「TO_CHAR」SQL関数だけでなく、ALTER SESSION、初期化パラメータおよび環境変数で指定することが可能です。
ここでは、ALTER SESSIONで変更する方法を示します。

SQL> ALTER SESSION SET NLS_CALENDAR='Japanese Imperial';

Session altered.

SQL> SELECT
  2   id
  3  ,TO_CHAR(sdate,'eyy.mm.dd') "WAREKI-S"
  4  ,DECODE(TO_CHAR(sdate,'yy"')
  5        ,'01',TO_CHAR(sdate,'ee"元年"mm"月"dd"日"')
  6        ,     TO_CHAR(sdate,'eeyy"年"mm"月"dd"日"')) "WAREKI-L"
  7  ,TO_CHAR(sdate,'day') day
  8  FROM jcal
  9  WHERE id < 10
 10  ORDER BY sdate
 11 ;

  ID WAREKI-S   WAREKI-L           DAY
---- ---------- ------------------ --------
   9 M01.09.08  明治元年09月08日    火曜日
   8 M45.07.29  明治45年07月29日    月曜日
   7 T01.07.30  大正元年07月30日    火曜日
   6 T15.12.24  大正15年12月24日    金曜日
   5 S01.12.25  昭和元年12月25日    土曜日
   4 S64.01.07  昭和64年01月07日    土曜日
   3 H01.01.08  平成元年01月08日    日曜日
   2 H26.12.31  平成26年12月31日    水曜日
   1 H27.01.01  平成27年01月01日    木曜日

9 rows selected.

このテーマは1回限りの小ネタのつもりでしたが、ちょっと面白くなってきたので次回も遊んでみたいと思います。

続く

12c新機能「Identity Column」の検証⑤

今週の名言

「無知を恐れてはいけない。偽りの知識を恐れよ。」
ブレーズ・パスカル

「独立・依存エンティティ」をIdentity Columnで実装する

独立・依存エンティティそれでは、前回紹介した「独立・依存エンティティ(テーブル)」をIdentity Columnで実装してみましょう。
まずは、親表であるT_PAR表から作成します。Identity Columnのオプションは「ALWAYS」です。
忘れずに主キーも作成します。

SQL> CREATE TABLE t_par
  2  (pid  NUMBER GENERATED ALWAYS AS IDENTITY
  3  ,pval VARCHAR2(10)
  4  );

表が作成されました。

SQL> ALTER TABLE t_par ADD CONSTRAINT pk_t_par PRIMARY KEY (pid);

表が変更されました。

次に子表となるT_CHD表を作成します。親表とほぼ同じですが、PID列に参照整合性制約(外部キー)を作成するのがポイントです。

SQL> CREATE TABLE t_chd
  2  (cid  NUMBER GENERATED ALWAYS AS IDENTITY
  3  ,pid  NUMBER NOT NULL
  4  ,cval VARCHAR2(10)
  5  );

表が作成されました。

SQL> ALTER TABLE t_chd ADD CONSTRAINT pk_t_chd PRIMARY KEY (cid);

表が変更されました。

SQL> ALTER TABLE t_chd ADD CONSTRAINT fk_t_pid FOREIGN KEY (pid)
  2                        REFERENCES t_par(pid);

表が変更されました。

このタイミングで、表とともに作成された順序の名前も確認しておきましょう。

SQL> SELECT TABLE_NAME,COLUMN_NAME,SEQUENCE_NAME
  2  FROM USER_TAB_IDENTITY_COLS
  3  ORDER BY TABLE_NAME;

TABLE_NAME      COLUMN_NAME     SEQUENCE_NAME
--------------- --------------- ---------------
T_CHD           CID             ISEQ$$_91944
T_PAR           PID             ISEQ$$_91941

レコード作成スクリプト

連続したレコードを作成する以下のPL/SQLスクリプトを実行します。
ハイライト行に示すように、上で確認した順序名を直接指定しています。

SQL> get ins.sql
  1  DECLARE
  2   i NUMBER;
  3   j NUMBER;
  4  BEGIN
  5   FOR i IN 1..5 LOOP
  6    INSERT INTO t_par(pval) VALUES ('Parent');
  7    FOR j IN 1..5 LOOP
  8     INSERT INTO t_chd(pid,cval) VALUES (ISEQ$$_91941.currval,'Child');
  9    END LOOP;
 10    COMMIT;
 11   END LOOP;
 12* END;
SQL> @ins.sql

PL/SQLプロシージャが正常に完了しました。

SQL> SELECT * FROM t_par;

       PID PVAL
---------- ----------
         1 Parent
         2 Parent
         3 Parent
         4 Parent
         5 Parent

SQL> SELECT * FROM t_chd;

       CID        PID CVAL
---------- ---------- ----------
         1          1 Child
         2          1 Child
         3          1 Child
................................
        23          5 Child
        24          5 Child
        25          5 Child

25行が選択されました。

親レコード1行毎に子レコードが5行作成され、全部で親レコード5行、子レコード25行が作成されていることがわかります。

共有プールをフラッシュさせてみる

順序が生成したシーケンス番号は、共有プール上に「CACHE_SIZE」パラメータで指定された数だけキャッシュされています。
デフォルト値は20となっており、次に生成されるシーケンス番号は以下のSQL文の「LAST_NUMBER」列で確認することができます。

SQL> SELECT SEQUENCE_NAME,INCREMENT_BY,CACHE_SIZE,LAST_NUMBER
  2  FROM USER_SEQUENCES;

SEQUENCE_NAME   INCREMENT_BY CACHE_SIZE LAST_NUMBER
--------------- ------------ ---------- -----------
ISEQ$$_91941               1         20          21
ISEQ$$_91944               1         20          41

ここで、共有プールをフラッシュさせるとどのようになるかを確認します。

SQL> ALTER SYSTEM FLUSH SHARED_POOL;

システムが変更されました。

再度、レコード作成スクリプトを実行し新たな25行をInsertします。

SQL> @ins.sql

PL/SQLプロシージャが正常に完了しました。

SQL> SELECT * FROM t_par;

       PID PVAL
---------- ----------
         1 Parent
         2 Parent
         3 Parent
         4 Parent
         5 Parent
        21 Parent
        22 Parent
        23 Parent
        24 Parent
        25 Parent

10行が選択されました。

SQL> SELECT * FROM t_chd;

       CID        PID CVAL
---------- ---------- ----------
         1          1 Child
         2          1 Child
         3          1 Child
................................
        23          5 Child
        24          5 Child
        25          5 Child
        41         21 Child
        42         21 Child
        43         21 Child
................................
        63         25 Child
        64         25 Child
        65         25 Child

50行が選択されました。

上で確認した「LAST_NUMBER」列の値にシーケンス番号が飛んで採番されていることがわかります。(PID=21,CID=41)
飛び番は発生しますが重複が発生しているわけではないので、問題なくInsertできます。

レコード作成スクリプト改

「ISEQ$$_91941」のようにシステムが生成した順序の名前は、プログラムの中で使用するにはあまり適切ではありません。
できれば以下のハイライト行のように、開発者が認識できる順序の名前を使用できることが望ましいと考えます。

SQL> get ins1.sql
  1  DECLARE
  2   i NUMBER;
  3   j NUMBER;
  4  BEGIN
  5   FOR i IN 1..5 LOOP
  6    INSERT INTO t_par(pval) VALUES ('Parent');
  7    FOR j IN 1..5 LOOP
  8     INSERT INTO t_chd(pid,cval) VALUES (SEQ_PAR.currval,'Child');
  9    END LOOP;
 10    COMMIT;
 11   END LOOP;
 12* END;

順序は改名できない

テーブル名は「ALTER TABLE <テーブル名> RENAME <新テーブル名>」で改名することができます。
しかし、残念ながら順序は「ALTER SEQUENCE」文で名称を変更することはできません

改名はできませんが、シノニムを作成することで「別名」を使用することができます。
順序名を再確認し、早速シノニムを作成してみましょう。

SQL> SELECT SEQUENCE_NAME,INCREMENT_BY,CACHE_SIZE,LAST_NUMBER
  2  FROM USER_SEQUENCES;

SEQUENCE_NAME   INCREMENT_BY CACHE_SIZE LAST_NUMBER
--------------- ------------ ---------- -----------
ISEQ$$_91941               1         20          41
ISEQ$$_91944               1         20          81

SQL> CREATE SYNONYM seq_par FOR ISEQ$$_91941;

シノニムが作成されました。

レコード作成スクリプト改を実行する

それでは、順序名をシノニムに置き換えたスクリプトを実行してみます。

SQL> @ins1.sql

PL/SQLプロシージャが正常に完了しました。

SQL> SELECT * FROM t_par;

       PID PVAL
---------- ----------
         1 Parent
         2 Parent
         3 Parent
         4 Parent
         5 Parent
        21 Parent
        22 Parent
        23 Parent
        24 Parent
        25 Parent
        26 Parent
        27 Parent
        28 Parent
        29 Parent
        30 Parent

15行が選択されました。

SQL> SELECT * FROM t_chd;

       CID        PID CVAL
---------- ---------- ----------
         1          1 Child
         2          1 Child
         3          1 Child
................................
        63         25 Child
        64         25 Child
        65         25 Child
        66         26 Child
        67         26 Child
        68         26 Child
................................
        88         30 Child
        89         30 Child
        90         30 Child

75行が選択されました。

問題なく、新規レコードが作成されました。

もし、Identity Columnを使用しなければ

Oracle12c以前の環境で、同様のレコード作成スクリプトを書くと以下のようになります。
ハイライト行を冗長と見るか否かがIdentity Columnを使う判断基準となるのではないでしょうか?

SQL> get ins2.sql
  1  DECLARE
  2   i NUMBER;
  3   j NUMBER;
  4  BEGIN
  5   FOR i IN 1..5 LOOP
  6    INSERT INTO t_par(pid,pval) VALUES (SEQ_PAR.nextval,'Parent');
  7    FOR j IN 1..5 LOOP
  8     INSERT INTO t_chd(cid,pid,cval) VALUES (SEQ_CHD.nextval,SEQ_PAR.currval,'Child');
  9    END LOOP;
 10    COMMIT;
 11   END LOOP;
 12* END;

Identity Columnは使える機能か?

私がIdentity Columnという新機能を知った時、今回検証した親子関係のテーブルにおいて問題なく使用できるのであれば、プログラムを簡単にすることで開発効率品質を向上させることができる注目すべき機能である予感がしました。

上で確認したように、一旦テーブルを作成して順序名を確認してからシノニム(別名)を作成するという、ちょっと面倒な手順を踏めば何とか使えそうな気がしますが、このままでは今一な感じもします。

せめて、以下のようにテーブル作成時に任意の順序名を指定できる仕様であればよいのですが。。。

SQL> CREATE TABLE t_par
  2  (pid  NUMBER GENERATED ALWAYS AS IDENTITY "SEQ_PAR"
  3  ,pval VARCHAR2(10)
  4  );

Identity Columnは、現時点では他RDBMSからの移行を容易にするために実装された機能だと認識しています。

しかし、将来的により使いやすいように改良されることを期待しつつ、このシリーズを終えたいと思います。

終わり

12c新機能「Identity Column」の検証④

今週の名言

「幸福になる秘訣は快楽を得ようとひたすらに努力することではなく、努力そのもののうちに快楽を見出すことである。」
アンドレ・ジット

マスタ/ディテール関係の実装を考える

今回は検証の前にデータ構造設計の話をしておきたいと思います。

売上-売上明細のように1対多のカーディナリティを持つ2つのテーブル(正確にはエンティティと表現すべきですが、物理的に実装するところまでを述べるのでテーブルと呼称しつつ、エンティティという表現も適宜使い分けることにします。)のリレーションシップをマスタ/ディテール関係と呼んだりすることがあります。

簡単に言うと「親子関係」であり、ER図(IE表記)で表現すると以下のようになります。(図1)

M-D1

図1

  • 親表(T_PAR)の主キー(PK)はPIDであり、子表(T_CHD)の主キーの一部を成すことで親子関係を表現することができる。
  • この図では子表(T_CHD)の主キー(PK)は「PID + CSEQ(連番)」であり、ユニーク(一意)性を担保する必要がある。
  • IE表記において、子表を角の丸い四角形で表現した場合、親表に対する「従属エンティティ」あるいは「依存エンティティ」であることを意味する。
  • つまり、子表の主キーの一部であるPIDは必ず存在する必要がある。(Not Null制約)

この親子関係は、次のようにも表現することができます。(図2)
M-D2

図2

  • 子表(T_CHD)の主キー(PK)はCIDのみであり、このカラムだけで一意性を担保できる。
  • 主キー項目以外に、子表には親表のPID(外部キー、FK)を持つ必要があり、かつこのカラムはNot Nullである必要がある。
  • 子表を角がある四角形で表現した場合、「独立エンティティ」あるいは「非依存エンティティ」であることを意味する。
  • つまり、PID列がNullであっても子レコードは存在できるが、前述のようにPID列は必ずNot Nullであるので論理的に従属関係にある。

今検証しているIdentity Columnを活用してマスタ/ディテール関係を実装しようとすると、必然的に図2の形になります。

従属的な親子関係を表現するためには必ずしも図1のような「従属エンティティ」を使う必要はないと思います。

子レコードが親レコードに依存しているということと、主キーの一部に親レコードの主キー値を持たなければならないというのは必ずしも同じでないということです。

重要なのは「親レコードを指す主キー値がNullであってはならない。」ということなのです。

親-子-孫関係を考える

図1、図2どちらでもよい、というのはある意味悩ましい事実です。

そこでこの問題をより掘り下げるために、親-子-孫の関係を考えてみましょう。

従属エンティティは先祖をたどることができる

ER3

図3

図3を見れば明らかですが、従属エンティティは孫エンティティから先祖である親エンティティを特定することができます。つまり、主キーに「PID」というカラムを持つテーブルは、すべて親エンティティに従属することがわかります。

例えばPID単位でパーティショニングを行うような場合、これは大きなメリットと言えるかもしれません。

ただし、主キーが複数のカラムで構成されるデメリットも当然あります。親エンティティの主キー構成が万一変更されるような場合、従属するすべてのエンティティに影響が及びます。

主キーは本来変更されない前提で設計されるのですが、未来永劫変更されない保証はありません。

また、設計の初期段階で親エンティティの主キー構成がなかなか決まらないと子エンティティの設計もなかなか決まらないという恐れもあります。(こちらの方が多いかもしれません。)

独立エンティティは構造がシンプル

ER4

図4

一方、独立エンティティは1つの主キー項目とそれ以外の項目というデータ構造は、親・子・孫で変わることはありません。
主キー項目は文字通りID(IDentity)項目であり、行を一意に識別することができます。そしてOracleデータベースの場合、シーケンス・オブジェクト(Sequence、順序)が生成する重複のない値を使用します。

この図をよく見ると、孫エンティティには親エンティティの主キーであるPIDを保持するカラムがありません。従って自分の先祖を簡単にたどることができません。

しかし、これを単純にデメリットと捉えることは間違いだと思います。実際にクエリを記述する場合、ジョイン(結合)するテーブルは2つだけですので、例えば子と孫テーブルをジョインする場合、その上の親テーブルを意識する必要はありません。ジョインの結果得られた結果セットと親テーブルをジョインするからです。

むしろ孫テーブルにPIDを持つと、親・孫という間違ったジョインを記述してしまう恐れがあります。そのような場合重複行が発生してしまい、無理矢理distinctで重複行を排除するようなことをしてしまうのです。これにより不要なFull Scanが発生するようなこととなり、ミスをミスで隠蔽するという最悪のSQL文を書いてしまっていたというケースをよく見かけます。

従属エンティティのメリットとしてパーティショニングがたやすい、ということを挙げましたが独立エンティティでも工夫次第ではパーティション化をすることができます。これは簡単な発想で解決できますので、是非考えてみてください。

「独立・依存」エンティティをIdentity Columnで実装する

独立した子エンティティでありながら外部キー項目をNot Nullとする設計を「独立・依存エンティティ」と呼びたいと思います。

一概に決め付けることは厳に慎むべきですが、設計時の混乱や運用中の性能問題を数多く見てきた経験から、この「独立・依存エンティティ」の考え方は有効な解決策となり得ると実感しています。

実際にデータ構造のリファクタリングを成功させた例では、この基本的な考え方に沿って設計を進めました。

ID列は複数のカラムからなる主キーとは違い、代替キーとかサロゲートキーと呼ばれます。何が何でもサロゲートキーにしなければならないと主張するつもりはありません。しかし、どんなキーが自然にユニークとなる識別キー(ナチュラルキー)になるかはよく考える必要があります。

私は、世の中の設計における混乱は、識別キーと検索キーを混同していることによることも多いと思います。

。。。

と前置きのつもりで書いていたら長くなってしまいましたので、「独立・依存エンティティ」をIdentity Columnで実装するというテーマの検証は次回に回したいと思います。

結論から言うと、この機能はまだまだイケてないと思うこともあるので、その辺の紹介も含め次回で検証したいと思います。

続く

 

12c新機能「Identity Column」の検証③

今週の名言

「事を行うにあたって、いつから始めようかなどと考えているときには、すでに遅れをとっているのだ。」
クインティリアヌス

前々回のおさらい

前回は別のテーマだったので、前々回の内容をおさらいしておきます。

  1. SQLトレースの設定
    • テーブル作成時に内部的にどのようなSQLが実行されているかを確かめるためにSQLトレースを事前に設定しておきます。
  2. Identity Column付きテーブルの作成(2種類)
    • 「ALWAYS」と「BY DEFAULT ON NULL」それぞれのオプションでテーブルを作成します。
  3. USER_TAB_IDENTITY_COLSの確認
    • TABLE_NAME列とSEQUENCE_NAME列からテーブルと順序の関係を確認します。
  4. テーブルの削除
    • 2つのテーブルのうち1つをパージ・オプション付き、もう片方をパージ・オプションなしで削除します。
  5. SQLトレースの終了
    • SQLトレースの取得を終了し、1 〜 5 の間に実行されたSQLの情報を確認する準備を行います。
  6. USER_TAB_IDENTITY_COLSの定義を確認する。
    • SQLトレースの確認を行うため、ビューを構成する実表を予め確かめておきます。
    • 「sys.idnseq$」表および「sys.col$」表が調査対象であることを確認しました。

sys.idnseq$

SQLトレースを直接確認する前にtkprofで整形した結果から調査対象の実表を検索します。

tkprofの結果から

まず、Identity Columnの実装で最も重要と思われる sys.idnseq$表を探すと以下のInsert文が見つかりました。

SQL ID: cdzktrqhtspgg Plan Hash: 0

insert into idnseq$(obj#, intcol#, seqobj#, startwith)
values
 (:1, :2, :3, :4)

call     count       cpu    elapsed       disk      query    current        rows
------- ------  -------- ---------- ---------- ---------- ----------  ----------
Parse        2      0.00       0.00          0          0          0           0
Execute      2      0.00       0.00          0          2          6           2
Fetch        0      0.00       0.00          0          0          0           0
------- ------  -------- ---------- ---------- ---------- ----------  ----------
total        4      0.00       0.00          0          2          6           2

Misses in library cache during parse: 1
Misses in library cache during execute: 1
Optimizer mode: CHOOSE
Parsing user id: SYS   (recursive depth: 1)
Number of plan statistics captured: 2

Rows (1st) Rows (avg) Rows (max)  Row Source Operation
---------- ---------- ----------  ---------------------------------------------------
         0          0          0  LOAD TABLE CONVENTIONAL  IDNSEQ$ (cr=1 pr=0 pw=0 time=125 us)

2つのテーブルを作成したので、Parse, Executeともに2回ずつ実行されています。
また、バインド変数が4つあることがわかります。

次にSQLトレースを直接参照してバインド変数の内容を確認します。

SQLトレースの内容

tkprofの結果から確認した SQLID=cdzktrqhtspgg によって、SQLトレースの中を探します。
先ほど確認したInsert文を含む以下の部分が見つかりました。


PARSING IN CURSOR #140330607060416 len=78 dep=1 uid=0 oct=2 lid=0 tim=118633564085 hv=2711377391
 ad='722200c8' sqlid='cdzktrqhtspgg'
insert into idnseq$(obj#, intcol#, seqobj#, startwith) values (:1, :2, :3, :4)
END OF STMT
PARSE #140330607060416:c=1000,e=220,p=0,cr=0,cu=0,mis=1,r=0,dep=1,og=4,plh=0,tim=118633564084
BINDS #140330607060416:
 Bind#0
  oacdty=02 mxl=22(22) mxlc=00 mal=00 scl=00 pre=00
  oacflg=00 fl2=1000001 frm=00 csi=00 siz=72 off=0
  kxsbbbfp=7fa143f98fb8  bln=22  avl=04  flg=05
  value=91832
 Bind#1
  oacdty=02 mxl=22(22) mxlc=00 mal=00 scl=00 pre=00
  oacflg=00 fl2=1000001 frm=00 csi=00 siz=0 off=24
  kxsbbbfp=7fa143f98fd0  bln=22  avl=02  flg=01
  value=1
 Bind#2
  oacdty=02 mxl=22(22) mxlc=00 mal=00 scl=00 pre=00
  oacflg=00 fl2=1000001 frm=00 csi=00 siz=0 off=48
  kxsbbbfp=7fa143f98fe8  bln=22  avl=04  flg=01
  value=91833
 Bind#3
  oacdty=02 mxl=22(02) mxlc=00 mal=00 scl=00 pre=00
  oacflg=10 fl2=0001 frm=00 csi=00 siz=24 off=0
  kxsbbbfp=7523f68e  bln=22  avl=02  flg=09
  value=1
EXEC #140330607060416:c=0,e=634,p=0,cr=1,cu=3,mis=1,r=1,dep=1,og=4,plh=0,tim=118633564764
STAT #140330607060416 id=1 cnt=0 pid=0 pos=1 obj=0 op='LOAD TABLE CONVENTIONAL  IDNSEQ$ (cr=1 pr=0 pw=0 time=160 us)'
CLOSE #140330607060416:c=0,e=3,dep=1,type=0,tim=118633564815

OBJID=91832obj#に、OBJID=91833seqobj#にそれぞれ代入されていることがわかります。

これにより、テーブルと順序の紐付けができています。

Delete文を探す

次に、テーブルをパージ・オプション付きで削除した際に発行される、Idenitty Column に関するDelete文を探してみましょう。


PARSING IN CURSOR #140330605974448 len=32 dep=1 uid=0 oct=7 lid=0 tim=118957259659 hv=933734747 ad='724c30b8' sqlid='59vjj34vugaav'
delete from obj$ where obj# = :1
END OF STMT
PARSE #140330605974448:c=0,e=556,p=0,cr=0,cu=2,mis=1,r=0,dep=1,og=4,plh=0,tim=118957259659
BINDS #140330605974448:
 Bind#0
  oacdty=02 mxl=22(22) mxlc=00 mal=00 scl=00 pre=00
  oacflg=00 fl2=1000001 frm=00 csi=00 siz=24 off=0
  kxsbbbfp=7fa143d49440  bln=22  avl=04  flg=05
  value=91833
EXEC #140330605974448:c=2000,e=1700,p=0,cr=2,cu=11,mis=1,r=1,dep=1,og=4,plh=225269600,tim=118957261436
STAT #140330605974448 id=1 cnt=0 pid=0 pos=1 obj=0 op='DELETE  OBJ$ (cr=2 pr=0 pw=0 time=410 us)'
STAT #140330605974448 id=2 cnt=1 pid=1 pos=1 obj=36 op='INDEX RANGE SCAN I_OBJ1 (cr=2 pr=0 pw=0 time=37 us cost=2 size=103 card=1)'
CLOSE #140330605974448:c=0,e=2,dep=1,type=3,tim=118957261516
FETCH #140330606193888:c=0,e=10,p=0,cr=1,cu=0,mis=0,r=0,dep=1,og=4,plh=2429035722,tim=118957261705
STAT #140330606193888 id=1 cnt=1 pid=0 pos=1 obj=106 op='TABLE ACCESS BY INDEX ROWID BATCHED IDNSEQ$ (cr=3 pr=0 pw=0 time=13 us cost=0 size=26 card=1)'
STAT #140330606193888 id=2 cnt=1 pid=1 pos=1 obj=107 op='INDEX RANGE SCAN I_IDNSEQ1 (cr=2 pr=0 pw=0 time=14 us cost=0 size=0 card=1)'
CLOSE #140330606193888:c=0,e=12,dep=1,type=1,tim=118957261795
=====================
PARSING IN CURSOR #140330606969696 len=39 dep=1 uid=0 oct=7 lid=0 tim=118957261833 hv=1857173601 ad='74087130' sqlid='7h2gtvxrb4f31'
delete from sys.idnseq$ where obj# = :1
END OF STMT
PARSE #140330606969696:c=0,e=26,p=0,cr=0,cu=0,mis=0,r=0,dep=1,og=4,plh=3651899373,tim=118957261832
BINDS #140330606969696:
 Bind#0
  oacdty=02 mxl=22(22) mxlc=00 mal=00 scl=00 pre=00
  oacflg=00 fl2=1000001 frm=00 csi=00 siz=24 off=0
  kxsbbbfp=7fa1445eea30  bln=22  avl=04  flg=05
  value=91832
EXEC #140330606969696:c=0,e=141,p=0,cr=1,cu=3,mis=0,r=1,dep=1,og=4,plh=3651899373,tim=118957262014
STAT #140330606969696 id=1 cnt=0 pid=0 pos=1 obj=0 op='DELETE  IDNSEQ$ (cr=1 pr=0 pw=0 time=83 us)'
STAT #140330606969696 id=2 cnt=1 pid=1 pos=1 obj=107 op='INDEX RANGE SCAN I_IDNSEQ1 (cr=1 pr=0 pw=0 time=11 us cost=0 size=26 card=1)'
CLOSE #140330606969696:c=0,e=9,dep=1,type=1,tim=118957262089

最初に、OBJID=91833である順序obj$表から削除され、OBJID=91832のエントリがsys.idnseq$表から削除され、Identity Columnの定義が削除されたことがわかります。

オプションはどのように管理されているのか?

USER_TAB_IDENTITY_COLSの定義の中で、decodebitand関数を使った部分があります。(以下のSQL文の4-6行目)
この部分はオプションを管理している部分です。ポイントは sys.obj$表のPROPERTY列にあります。

わかりやすいように、PROPERTY列を並べてどのような値が格納されているかを確認してみました。

SQL> select
  2    o.name TABLE_NAME
  3  , c.name COLUMN_NAME
  4  , decode( bitand(c.property, 137438953472 + 274877906944)
  5          , 137438953472, 'ALWAYS'
  6          , 274877906944, 'BY DEFAULT') GENERATION
  7  , c.property
  8  , so.name SEQUENCE_NAME
  9  from
 10    sys.idnseq$ i
 11  , sys.obj$ o
 12  , sys.col$ c
 13  , sys.seq$ s
 14  , sys.obj$ so
 15  where o.owner# = userenv('SCHEMAID')
 16  and o.obj# = i.obj#
 17  and c.intcol# = i.intcol#
 18  and c.obj# = i.obj#
 19  and s.obj# = i.seqobj#
 20  and so.obj# = i.seqobj#;

TABLE_NAME  COLUMN_NAME  GENERATION      PROPERTY SEQUENCE_NAME
----------- ------------ ---------- ------------- ---------------
T1          C11          ALWAYS      171798691840 ISEQ$$_91838
T2          C21          BY DEFAULT  377957122048 ISEQ$$_91840

以下は、PROPERYT列の十進数を二進数に変換してbitand演算を行ったところを説明したものです。

123456789012345678901234567890123456789
---------+---------+---------+---------
010100000000000000000000000000000000000 171798691840
110000000000000000000000000000000000000 137438953472 + 27487790694
---------------------------------------
010000000000000000000000000000000000000 137438953472 : 'ALWAYS'
 *

101100000000000000000000000000000000000 377957122048
110000000000000000000000000000000000000 137438953472 + 27487790694
---------------------------------------
100000000000000000000000000000000000000 274877906944 : 'BY DEFAULT'
*

この例では39bitのbit列における上位2bit目に1が立っていれば「ALWAYS」、1bit目に1が立っていれば「BY DEFAULT」となっていることがわかります。

データディクショナリ・ビューの定義を確認すると、このようにプロパティ列にビットフラグを立てて属性を管理しているのをよく見かけます。

あまり実業務には役に立たない雑学ですが、Oracleの深いところを理解するには興味深いきっかけになるかもしれません。

次回は、もう少し検証っぽいことをしてみようと思います。

続く

ORAエラーはいくつある?

今週の名言

「人にものを教えることはできない。できることは、相手のなかにすでにある力を見いだすこと、その手助けである。」
ガリレオ・ガリレイ

今回は小ネタで

前回に引き続き「Identity Column」のつもりでしたが、事情により今回は別のトリビア的なネタを紹介します。

アラートログ監視の難しさ

Oracleの運用監視において、アラートログの中の「ORA-」で始まるメッセージを通知するということは当たり前に行われていると思いますが、エラー・メッセージ・マニュアルで「処置: 処置は必要ありません。」のように説明されているアラートが真夜中にエスカレーションされた経験はないでしょうか?

Oracleにある程度詳しい人であれば、経験から「これはとりあえず様子見でよいかな。」というような判断を下すことができるかもしれませんが、24時間待機のオペレータにそこまでの判断を求めることは難しいのではないかと思います。

そこで、「このメッセージは監視対象から外す」というようなルールを決めているところは多いと思います。
私もそのような取り組みに関わったことがあるのですが、システムごとに発生するエラーの傾向が異なるので、どのシステムでも使える普遍的な「監視対象メッセージ一覧」を作るのは現実的にはかなり難しいのではないだろうかという実感を持っています。

ところでOracleエラーっていくつあるの?

いつまで経っても「監視対象メッセージ一覧」を作ることができないことに業を煮やし、Oracleエラーが一体いくつ存在しているのか気になって調べてみたことがあります。
エラー・メッセージ・マニュアルを頭から数えるのはかなり心が折れる作業になりそうなので、以下のPL/SQLスクリプトを作って調べてみました。

Oracle 11.2.0.4の場合

SQL> DECLARE
  2   sql_code NUMBER;
  3   str_pos  NUMBER;
  4   cnt      NUMBER;
  5  BEGIN
  6   str_pos := 0;
  7   cnt := 0;
  8   FOR sql_code IN 1..99999 LOOP
  9    str_pos := INSTR(SQLERRM(-sql_code),'not found;',1,1);
 10    IF str_pos = 0 THEN
 11     cnt := cnt + 1;
 12    END IF;
 13   END LOOP;
 14   DBMS_OUTPUT.PUT_LINE('Nuber of Error Code : ' || cnt);
 15  END;
 16  /
Nuber of Error Code : 54387

PL/SQL procedure successfully completed.

54,387個のメッセージという結果が得られました。

大訂正!!(2015.05.23)

Oracleエラーがこんなに多いはずがないとどうも腑に落ちなかったのですが、とんでもない間違いをしていたことに気付きました。

SQL> BEGIN
  2   DBMS_OUTPUT.PUT_LINE(SQLERRM(-99999));
  3  END;
  4  /
99999: non-ORACLE exception

PL/SQL procedure successfully completed.

この「non-ORACLE exception」をカウントに入れてました。orz
これでは純粋なORA-エラーになりません。

とりあえず修正版

この不要なメッセージも除外するようにコードを書き直して実行してみたのが以下の結果です。

SQL> DECLARE
  2   sql_code NUMBER;
  3   cnt      NUMBER :=0;
  4   err_msg  VARCHAR2(2000);
  5  BEGIN
  6  FOR sql_code IN 1..99999 LOOP
  7    err_msg := SQLERRM(-sql_code);
  8    IF err_msg NOT LIKE '%not found;%' AND err_msg NOT LIKE '%non-ORACLE exception%' THEN
  9     cnt := cnt + 1;
 10    END IF;
 11  END LOOP;
 12  DBMS_OUTPUT.PUT_LINE('Nuber of Error Code : ' || cnt);
 13  END;
 14  /
Nuber of Error Code : 19923

PL/SQL procedure successfully completed.

だいぶ、数が減りました。

実は、まだ無効なメッセージを含んでいる

ところが、以下のように番号は予約されているけれどメッセージが空欄のものが相当あることがわかりました。

SQL> BEGIN
  2   DBMS_OUTPUT.PUT_LINE(SQLERRM(-12428));
  3   DBMS_OUTPUT.PUT_LINE(SQLERRM(-12498));
  4   DBMS_OUTPUT.PUT_LINE(SQLERRM(-12499));
  5  END;
  6  /
ORA-12428:
ORA-12498:
ORA-12499:

PL/SQL procedure successfully completed.

こんなのも一緒にカウントしてたら正確なエラーの数はわかりませんね。

結局、SQLERRM関数を使って調査することは諦めることにします。

従って、上で挙げたエラー数は間違いですので無視してください。お騒がせして申し訳ありません。

その代わりもっとよい方法があるのでそちらを別途紹介することにします。

ORAエラーはいくつある?訂正しました。

12c新機能「Identity Column」の検証②

今週の名言

「一時間の浪費を何とも思わない人は、まだ人生の価値を何も見つけていない。」
チャールズ・ダーウィン

前回のまとめ

12c新機能である「Identity Column」(日本語マニュアルでは「IDENTITY列」)の検証をしています。
前回はこの機能の基本的な動作を確認しました。

テーブル作成時にID列(主キー列)にIdentity Columnの設定を行えば、暗黙的にシーケンス・オブジェクト(順序ジェネレータ、以下順序)が作成されます。

そして、テーブルにレコードをインサートする場合、ID列にどのような値を設定するかを全く意識することがなく(つまりSQL文実行者としては結果的にNullを指定してインサート)ても、レコードを一意に識別できるシーケンス番号が自動的に払い出されインサートが行われます。(サロゲート・キーの生成)

これは、ANSI準拠の他RDBMS(SQL Server、DB2等)からの移行を容易にするものでもあります。

テーブルと順序は互いに独立したオブジェクトだけど

前回、Identity Columnを設定したテーブルを削除(Drop)しても順序は順序として機能することを確認しました。
また、順序の名称も「ISEQ$$_91795」のようにテーブル名とは全く関係のないものになっているので、例えばEMP表の順序であれば「SEQ_EMP」のような名称が自然だと思っていた私にはこの機能は「ちょっと使えないんじゃないか?」という第一印象がありました。

ところが、テーブルは削除しても実体はリサイクルビンといういわゆるゴミ箱で管理されていますが、このリサイクルビンを明示的にパージ(空に)したところ、該当する順序も自動的に削除されていたので、Identity Columnで作成された順序と元テーブルにはこれらを紐付ける何らかの仕組みがあるのではと気付きました。

実は、USER_TAB_IDENTITY_COLSというディクショナリ・ビューでこの関係性を確認することができるのですが、今回はSQLトレースを取得することでどのような内部表にこの情報が格納されているのかを探ってみたいと思います。

1. SQLトレースの準備

それでは、まずSQLトレースを取得する準備をします。トレースファイルはtraceディレクトリ以下に大量に出力されていますので、「IDNCOL」という識別子を付けて該当するファイルが簡単にわかるようにしておきます。
トレースファイルのフルパス名称はV$DIAG_INFOから確認することができます。

SQLトレースは、バインド変数とその内容を確認したいので、レベル4を指定します。

SQL> SHOW USER

ユーザーは"TEST"です。

SQL> ALTER SESSION SET TRACEFILE_IDENTIFIER='IDNCOL';

セッションが変更されました。

SQL> SELECT NAME,VALUE FROM V$DIAG_INFO
  2  WHERE NAME = 'Default Trace File';

NAME                 VALUE
-------------------- ---------------------------------------------------------------------------
Default Trace File   /u01/app/oracle/diag/rdbms/ora12c/ora12c/trace/ora12c_ora_31051_IDNCOL.trc

SQL> ALTER SESSION SET EVENTS '10046 TRACE NAME CONTEXT FOREVER, LEVEL 4';

セッションが変更されました。

2. Identity Column付きテーブルの作成(2種類)

前回ご紹介した要領でテーブルを作成しますが、「ALWAYS」と「BY DEFAULT ON NULL」でそれぞれT1表T2表を作成します。
また、テーブル作成後にそれぞれ主キーを作成します。

SQL> CREATE TABLE t1
  2  (c11 NUMBER       GENERATED ALWAYS AS IDENTITY
  3  ,c12 VARCHAR2(10)
  4  );

表が作成されました。

SQL> ALTER TABLE t1 ADD CONSTRAINT pk_t1 PRIMARY KEY (c11);

表が変更されました。

SQL> CREATE TABLE t2
  2  (c21 NUMBER       GENERATED BY DEFAULT ON NULL AS IDENTITY
  3  ,c22 VARCHAR2(10)
  4  );

表が作成されました。

SQL> ALTER TABLE t2 ADD CONSTRAINT pk_t2 PRIMARY KEY (c21);

表が変更されました。

SQL> SELECT OBJECT_NAME,OBJECT_ID,OBJECT_TYPE
  2  FROM USER_OBJECTS
  3  ORDER BY OBJECT_ID;

OBJECT_NAME   OBJECT_ID OBJECT_TYPE
------------ ---------- -----------------------
T1                91832 TABLE
ISEQ$$_91832      91833 SEQUENCE
PK_T1             91834 INDEX
T2                91835 TABLE
ISEQ$$_91835      91836 SEQUENCE
PK_T2             91837 INDEX

6行が選択されました。

SQL> SELECT SEQUENCE_NAME,INCREMENT_BY,CACHE_SIZE,LAST_NUMBER
  2  FROM USER_SEQUENCES;

SEQUENCE_NAME   INCREMENT_BY CACHE_SIZE LAST_NUMBER
--------------- ------------ ---------- -----------
ISEQ$$_91832               1         20           1
ISEQ$$_91835               1         20           1

USER_OBJECTSを見てわかるように、順序の名称「ISEQ$$_91832」の数字部分は元表のオブジェクトIDです。
そして順序自身のオブジェクトIDは+1した「91833」となっています。
テーブルと順序の作成はCREATE TABLE時1トランザクションで実行されるので、OBJECT_IDは必ず続き番号になるようです。(ALTER TABLE時を除く)

このような仕組みが分かっていればテーブルと順序の関係性はすぐにわかるのですが、これがもっと便利にわかるのが「USER_TAB_IDENTITY_COLS」ビューです。

3. USER_TAB_IDENTITY_COLS

このビューの説明はALL_TAB_IDENTITY_COLSを参照してください。

SQL> SELECT TABLE_NAME,COLUMN_NAME,GENERATION_TYPE,SEQUENCE_NAME,IDENTITY_OPTIONS
  2  FROM USER_TAB_IDENTITY_COLS
  3  ORDER BY TABLE_NAME;

TABLE_NAME  COLUMN_NAME  GENERATION SEQUENCE_NAME   IDENTITY_OPTIONS
----------- ------------ ---------- --------------- ----------------------------------------
T1          C11          ALWAYS     ISEQ$$_91832    START WITH: 1, INCREMENT BY: 1, MAX_VALU
                                                    E: 9999999999999999999999999999, MIN_VAL
                                                    UE: 1, CYCLE_FLAG: N, CACHE_SIZE: 20, OR
                                                    DER_FLAG: N
T2          C21          BY DEFAULT ISEQ$$_91835    START WITH: 1, INCREMENT BY: 1, MAX_VALU
                                                    E: 9999999999999999999999999999, MIN_VAL
                                                    UE: 1, CYCLE_FLAG: N, CACHE_SIZE: 20, OR
                                                    DER_FLAG: N

このビューを確認すれば、TABLE_NAME列とSEQUENCE_NAME列からテーブルと順序の関係性を知ることができます。

GENERATION列からは「ALWAYS」「BY DEFAULT (ON NULL)」のオプションの違いを確認することができます。

さらに、IDENTITY_OPTIONS列によって順序定義の詳細がわかります。

4. テーブルの削除

次にT1表をパージ・オプション付きで、T2表をパージ・オプションを付けないで削除します。

SQL> DROP TABLE t1 PURGE;

表が削除されました。

SQL> DROP TABLE t2;

表が削除されました。

SQL> SELECT SEQUENCE_NAME,INCREMENT_BY,CACHE_SIZE,LAST_NUMBER
  2  FROM USER_SEQUENCES;

SEQUENCE_NAME   INCREMENT_BY CACHE_SIZE LAST_NUMBER
--------------- ------------ ---------- -----------
ISEQ$$_91835               1         20           1

SQL> SELECT TABLE_NAME,COLUMN_NAME,GENERATION_TYPE,SEQUENCE_NAME,IDENTITY_OPTIONS
  2  FROM USER_TAB_IDENTITY_COLS
  3  ORDER BY TABLE_NAME;

TABLE_NAME  COLUMN_NAME  GENERATION SEQUENCE_NAME   IDENTITY_OPTIONS
----------- ------------ ---------- --------------- ----------------------------------------
BIN$Em/lpxe C21          BY DEFAULT ISEQ$$_91835    START WITH: 1, INCREMENT BY: 1, MAX_VALU
feUvgUwoLqM                                         E: 9999999999999999999999999999, MIN_VAL
B1cw==$0                                            UE: 1, CYCLE_FLAG: N, CACHE_SIZE: 20, OR
                                                    DER_FLAG: N

パージ・オプションを付けないで削除した場合、テーブルは「BIN$Em/lpxefeUvgUwoLqMB1cw==$0」のように「BIN$」で始まる名前にRenameされます。
ただし、テーブルのデータを含めた実体はまだ存在していますので、順序との関係性が維持されているわけです。

5. SQLトレースの終了

以下の要領で、SQLトレースを終了します。さらにコマンドプロンプトからtkprofを実行し、トレースファイルを整形しておきます。

SQL> ALTER SESSION SET EVENTS '10046 TRACE NAME CONTEXT OFF';

セッションが変更されました。

-----
$ cd /u01/app/oracle/diag/rdbms/ora12c/ora12c/trace
$ tkprof ora12c_ora_31051_IDNCOL.trc IDNCOL.txt

6. USER_TAB_IDENTITY_COLSの定義を確認する。

SQLトレースの解析は次回にしますが、今回はそのためにUSER_TAB_IDENTITY_COLSの定義を予め確認しておくことにします。

SQL> SELECT TEXT FROM DBA_VIEWS WHERE VIEW_NAME = 'USER_TAB_IDENTITY_COLS';

TEXT
--------------------------------------------------------------------------------
select o.name, c.name,
       decode(bitand(c.property, 137438953472 + 274877906944),
                     137438953472, 'ALWAYS',
                     274877906944, 'BY DEFAULT'),
       so.name,
       'START WITH: '     || i.startwith ||
       ', INCREMENT BY: ' || s.increment$ ||
       ', MAX_VALUE: '    || s.maxvalue ||
       ', MIN_VALUE: '    || s.minvalue ||
       ', CYCLE_FLAG: '   || decode (s.cycle#, 0, 'N', 1, 'Y') ||
       ', CACHE_SIZE: '   || s.cache ||
       ', ORDER_FLAG: '   || decode (s.order$, 0, 'N', 1, 'Y')
from sys.idnseq$ i, sys.obj$ o, sys.col$ c,
     sys.seq$ s, sys.obj$ so
where o.owner# = userenv('SCHEMAID')
and o.obj# = i.obj#
and c.intcol# = i.intcol#
and c.obj# = i.obj#
and s.obj# = i.seqobj#
and so.obj# = i.seqobj#
;

12cから新しくできた「sys.idnseq$」表という実表が鍵を握っているようです。

また、「ALWAYS」「BY DEFAULT (ON NULL)」のオプションはsys.col$.property列に格納された情報から判断することができるようです。

次回はこの辺を詳しく見てみましょう。

続く

12c新機能「Identity Column」の検証①

今週の名言

「自分がわずかなことしか知らないということを知るためには、多くのことを知る必要がある。」
モンテーニュ

Oracle 12c新機能について

今週からOracle 12cの新機能を検証します。

実はOracle 12c 発表されたと思ったらリリース1(R1)はもうターミナル・リリースになるようです。(Database In-Memoryなど大幅に機能拡張:Oracle Database 12cR1の最新パッチセット12.1.0.2がリリースされました

最近のOracle RDBMSは、R1のうちはイノベーター、アーリーアダプターまでが採用する傾向にあって、R2以降本格的にアーリーマジョリティ他に普及していくような都市伝説(?)があるように思います。

私が関わっている案件もほとんどが11gR2で、本番環境で12cに移行するというものをあまり聞いたことはありません。エンドユーザも状況を見ているのではないかという印象を持っています。

従って、12c新機能ネタはまだ新鮮味がある気がしますので、筆者が興味を持った機能を今後積極的に紹介していきたいと思います。

「Identity Column」とは?

12c新機能というと、プラガブル・データベースやDatabase In-Memoryなどが注目されていますが、このブログではどちらかというとあまり陽の当たらない機能で面白そうなものを紹介していこうと思います。

それも、単に機能を紹介するのではなく、実際に使う立場で検証してみたいと思います。

さて「Identity Column」ですが、ANSI準拠のIDENTITY Columnを実装したもので、平たく言うとInsert文における自動生成キー取り出し機能のことです。

マニュアルは以下を参照してください。

Oracle® Database新機能ガイド
12cリリース1 (12.1)
B71327-03
2.1.6.3 IDENTITY列

この機能は上記マニュアルの記述に先行した

  • 2.1.6.1 Oracleの順序に基づく列のデフォルト値
  • 2.1.6.2 明示的なNULL挿入での列のDEFAULT値

の組み合わせで使用する機能であるとも言えます。

「百聞は一見に如かず」とりあえず実際のDDL文から確認していきましょう。

基本動作の確認

検証環境

検証環境はOracle Linux 6.5とOracle12R1です。

SQL> !uname -r
3.8.13-16.2.1.el6uek.x86_64

SQL> select BANNER from v$version;

BANNER
--------------------------------------------------------------------------------
Oracle Database 12c Enterprise Edition Release 12.1.0.2.0 - 64bit Production
PL/SQL Release 12.1.0.2.0 - Production
CORE	12.1.0.2.0	Production
TNS for Linux: Version 12.1.0.2.0 - Production
NLSRTL Version 12.1.0.2.0 - Production

CREATE TABLE文

検証用のTESTユーザはすでに作成済みです。CREATE SEQUENCE権限が付与されていることが必要です。

SQL> show user
ユーザーは"TEST"です。

SQL> create table t1
2 (c1 number GENERATED BY DEFAULT ON NULL AS IDENTITY
3 ,c2 varchar2(10)
4 );

表が作成されました。

GENERATED 〜 AS IDENTITY」がIDENTITY句と呼ばれる記述ですが、「BY DEFAULT ON NULL」はオプションです。これを省略するとデフォルトの「ALWAYS」となります。(先で機能を説明するため、あえてこの設定にします。)

シーケンス確認

IDENTITY句を指定すると、裏で自動的にシーケンス・オブジェクト(順序)が作られます。
USER_SEQUENCEからその状況を確認しておきましょう。

SQL> select SEQUENCE_NAME,INCREMENT_BY,CACHE_SIZE,LAST_NUMBER
  2  ,CYCLE_FLAG,ORDER_FLAG
  3  from user_sequences;

SEQUENCE_NAME             INCREMENT_BY CACHE_SIZE LAST_NUMBER C O
------------------------- ------------ ---------- ----------- - -
ISEQ$$_91795                         1         20           1 N N

ISEQ$$_91795というシステムが命名した順序が作成されているのがわかります。
キャッシュ・サイズはデフォルトの「20」です。

1行インサートする。

それでは、まずは1行インサートしてみましょう。C1列には何も指定せずC2列のみ値を設定してインサートします。
さらに、直後の順序の状況を確認します。

SQL> insert into t1(c2) values ('abc');

1行が作成されました。

SQL> select SEQUENCE_NAME,INCREMENT_BY,CACHE_SIZE,LAST_NUMBER
  2  from user_sequences;

SEQUENCE_NAME             INCREMENT_BY CACHE_SIZE LAST_NUMBER
------------------------- ------------ ---------- -----------
ISEQ$$_91795                         1         20          21

LAST_NUMBERが 1+20(CACHE_SIZE)となっています。つまり最初の生成で1〜20までの数字がキャッシュされ、このインサートでは「1」が払い出されます。(次の生成では21〜40までがキャッシュされる予定です。)

もう1行インサートする。

さらにもう1行インサートします。

SQL> insert into t1(c2) values ('xyz');

1行が作成されました。

SQL> select c1,c2 from t1;

        C1 C2
---------- ----------
         1 abc
         2 xyz

SQL> select SEQUENCE_NAME,INCREMENT_BY,CACHE_SIZE,LAST_NUMBER
  2  from user_sequences;

SEQUENCE_NAME             INCREMENT_BY CACHE_SIZE LAST_NUMBER
------------------------- ------------ ---------- -----------
ISEQ$$_91795                         1         20          21

2行作成されていることがわかります。キャッシュされている番号から払い出されているため順序の内容は変化していません。

一度ロールバックし、再度2行インサートする。

2行インサートした状態でコミットせずにロールバックすると、0行となります。
ここで、先ほど行った2行のインサートを行いC1列にどのような値が入るか確認します。
さらに今度はコミットします。

SQL> rollback;

ロールバックが完了しました。

SQL> select SEQUENCE_NAME,INCREMENT_BY,CACHE_SIZE,LAST_NUMBER
  2  from user_sequences;

SEQUENCE_NAME             INCREMENT_BY CACHE_SIZE LAST_NUMBER
------------------------- ------------ ---------- -----------
ISEQ$$_91795                         1         20          21

SQL> insert into t1(c2) values ('abc');

1行が作成されました。

SQL> insert into t1(c2) values ('xyz');

1行が作成されました。

SQL> select c1,c2 from t1;

        C1 C2
---------- ----------
         3 abc
         4 xyz

SQL> commit;

コミットが完了しました。

テーブルはロールバックされましたが、順序「ISEQ$$_91795」には何の影響も与えません。
キャッシュされている次の番号「3」と「4」が続けて払い出されているのがわかります。

テーブルをドロップして順序の状況を確認する。

SQL> drop table t1;

表が削除されました。

SQL> select SEQUENCE_NAME,INCREMENT_BY,CACHE_SIZE,LAST_NUMBER
  2  from user_sequences;

SEQUENCE_NAME             INCREMENT_BY CACHE_SIZE LAST_NUMBER
------------------------- ------------ ---------- -----------
ISEQ$$_91795                         1         20          21

SQL> select ISEQ$$_91795.nextval from dual;

   NEXTVAL
----------
         5

テーブルを削除しても、順序はそのまま残っていることがわかります。
しかも、キャッシュされているシーケンス番号から次の番号が何の問題もなく払い出せることが確認できます。

リサイクルビンのパージ

テーブルを削除してもリサイクルビンで管理されるだけなので、念のためリサイクルビンを空にして順序の状況を確認します。

SQL> purge recyclebin;

リサイクルビンがパージされました。

SQL> select SEQUENCE_NAME,INCREMENT_BY,CACHE_SIZE,LAST_NUMBER
  2  from user_sequences;

レコードが選択されませんでした。

リサイクルビンを空にして、つまり本当にテーブルを削除すると、テーブル作成時に自動的に作成された順序も削除されるようです。

「BY DEFAULT ON NULL」の意味を確認する。

「IDENTITY Column」の特徴は、値を指定しないで(つまりNullで)インサートしても自動的に連番が設定されることができるというものですが、明示的に値を設定することもできるいうのが「BY DEFAULT ON NULL」オプションです。

結論から言うと、どちらでもよいというのは意味がなくむしろ別の問題の原因となるのでこのオプションを指定すべきではないと考えます。
デフォルトの「ALWAYS」を指定すべきでしょう。

以下に、その理由を説明する検証を行っていきます。

キャッシュサイズを最小にする。

順序のキャッシュサイズをデフォルトの20にしておくと、次に払い出されるシーケンス番号が見えにくくなるので、キャッシュサイズを最小にしてみます。(パフォーマンス上の理由からキャッシュサイズはデフォルトの20より小さい値にすべきではありませんが、検証上あえて小さくします。)

SQL> create table t2
  2  (c1 number       GENERATED BY DEFAULT ON NULL AS IDENTITY (CACHE 1)
  3  ,c2 varchar2(10)
  4  );
create table t2
*
行1でエラーが発生しました。:
ORA-04010: CACHEには1より大きい値を指定する必要があります

おっと!「CACHE 1」という設定は許されていないようです。

それでは「NOCACHE」でやり直します。

SQL> create table t2
  2  (c1 number       GENERATED BY DEFAULT ON NULL AS IDENTITY (NOCACHE)
  3  ,c2 varchar2(10)
  4  );

表が作成されました。

SQL> select SEQUENCE_NAME,INCREMENT_BY,CACHE_SIZE,LAST_NUMBER
  2  from user_sequences;

SEQUENCE_NAME             INCREMENT_BY CACHE_SIZE LAST_NUMBER
------------------------- ------------ ---------- -----------
ISEQ$$_91798                         1          0           1

今度はうまくいきました。

C1列に主キーを設定する。

C1列は本来であればID列であるので主キーを設定します。

SQL> alter table t2 add constraint pk_t2 primary key (c1);

表が変更されました。

C1列に値を指定せずにインサートする

C1列は順序が設定されている列なので、値を指定せずにインサートするのが通常のインサートです。
とりあえず最初のレコードをインサートします。

SQL> insert into t2(c2) values ('abc');

1行が作成されました。

SQL> select SEQUENCE_NAME,INCREMENT_BY,CACHE_SIZE,LAST_NUMBER
  2  from user_sequences;

SEQUENCE_NAME             INCREMENT_BY CACHE_SIZE LAST_NUMBER
------------------------- ------------ ---------- -----------
ISEQ$$_91798                         1          0           2

SQL> select c1,c2 from t2;

        C1 C2
---------- ----------
         1 abc

C1列に値を指定してインサートする。

今度は、明示的にC1列に値を指定してインサートしてみます。

SQL> insert into t2(c1,c2) values (2,'xyz');

1行が作成されました。

SQL> select SEQUENCE_NAME,INCREMENT_BY,CACHE_SIZE,LAST_NUMBER
  2  from user_sequences;

SEQUENCE_NAME             INCREMENT_BY CACHE_SIZE LAST_NUMBER
------------------------- ------------ ---------- -----------
ISEQ$$_91798                         1          0           2

特に問題なくインサートできます。
(ALWAYSの場合はここでエラーとなります。)
また、LAST_NUMBERが「2」のままであることも注目すべきです。つまり次に払い出されるシーケンス番号は「2」です。

今度はC1列に値を指定しないでインサートする。

SQL> insert into t2(c2) values ('xyz');
insert into t2(c2) values ('xyz')
*
行1でエラーが発生しました。:
ORA-00001: 一意制約(TEST.PK_T2)に反しています

SQL> select c1,c2 from t2;

        C1 C2
---------- ----------
         1 abc
         2 xyz

主キーの重複エラーが発生します。
なぜなら、既に「C1=2」となるレコードをインサートしているので、順序が払い出した「2」が重複してしてしまいエラーとなりました。

もう一度C1列に値を指定しないでインサートする。

ORA-00001エラーが発生するとそのレコードはインサートされませんが、もう一度値を指定しないでインサートするレコードはどうなるでしょうか?

SQL> insert into t2(c2) values ('def');

1行が作成されました。

SQL> select c1,c2 from t2;

        C1 C2
---------- ----------
         1 abc
         2 xyz
         3 def

SQL> select SEQUENCE_NAME,INCREMENT_BY,CACHE_SIZE,LAST_NUMBER
  2  from user_sequences;

SEQUENCE_NAME             INCREMENT_BY CACHE_SIZE LAST_NUMBER
------------------------- ------------ ---------- -----------
ISEQ$$_91798                         1          0           4

今度は、次に払い出されるシーケンス番号が「3」となり重複しないので、エラーは発生しません。

結論:IDENTITY列は「GENERATED ALWAYS AS IDENTITY」で設定すべき。

「Identity Column」のメリットあるいは注意点に関する考察は次回以降に回したいと思いますが、今回確認した中での結論はID列に値を指定する選択を許す「BY DEFAULT ON NULL」オプションは使うべきではないということです。
言い換えると「BY DEFAULT ON NULL」とは、値を指定しなければ(Null)順序から番号を払い出し、指定すればその値でインサートするという仕様と考えることもできます。
値を指定するとエラーとなるデフォルトの「ALWAYS」を使うべきだと思います。

create table t1
(c1 number       GENERATED ALWAYS AS IDENTITY  -- (ALWAYS)は省略可
,c2 varchar2(10)
);

続く

「オプティマイザ統計の保留」の検証(その8)

今週の名言

「人は日本の歴史に50ページ書いてもらうより、世界の歴史に1ページ書いてもらうことを心掛けねばならぬ。」
後藤新平

シリーズまとめ

「オプティマイザ統計の保留」について検証してきた今回のシリーズも序章を含めると10回目になりました。

この機能は他にもいろいろ検証して確認したいことはあるのですが、他のテーマも追いかけたいので区切りがよいところでまとめておきたいと思います。

新しい機能にどう向き合うか

私はITというものに対して、ウンチクを語る対象ではなく道具としてビジネスに貢献するものでなければならないという強い思いがあります。

従ってある新機能を目にした時には「これは何の役にたつのだろうか?」とか「あの問題に対してよい解決策になるのではないか?」という視点で確認してみたいという気持ちになります。

Oracle10g以降ルールベース・オプティマイザ(RBO)がサポートされなくなりましたが、クルマがマニュアル・シフトからオートマチック・シフトになったのと同じくらいのインパクトがあったのではないでしょうか。

パラメータ・チューニングやヒント句を駆使して性能改善をバリバリ行ってきたエンジニアにとって、Oracle10gは大きな変換点だったのではないかと思います。
先日もある飲み会で「Oracle9iは結構面白いバージョンだった。」というような意見を一人のベテランエンジニアから聞きました。

Oracle10g以降自動化が進んで、DBAが手を動かして問題解決するという機会は確かに減りましたが、自動化を過信したいわゆる「自動化の落とし穴」という問題も深刻になってきました。

このシリーズは「自動化の落とし穴」にまつわる混乱を「オプティマイザ統計の保留」の検証(序章②)で紹介するところから始めました。

コストベース・オプティマイザ(CBO)というそれまでとは違った仕組みに対して、実行計画が意図せず急変してしまったと言う理由で、統計情報を取ることに消極的であったりヒント句で実行計画をガチガチに固めてしまっていたりする例を未だによく見かけます。

もうすぐOracle12c R2がリリースされるという現在となっては「オプティマイザ統計の保留」はすでに新機能とは呼べなくなっているかもしれませんが、CBOの仕組みをよく理解し、この機能を活用することによって「失敗のない運用」を行うことは十分可能であると思います。

「オプティマイザ統計の保留」の特徴

それではシリーズを振り返ってこの機能の特徴を整理していきます。

1. 保留統計情報は実行計画に影響を与えない

実行計画は公開されたディクショナリ統計情報によって算出されます。従ってプライベート・エリアに格納されディクショナリ統計を更新しない保留統計情報は実行計画に何ら影響を与えることがないというのが一番の特徴です。

一度算出された実行計画を変動させる要因がないということは、実行計画は事実上固定されます。

2. 保留統計情報は履歴を持たない

保留統計は履歴を持ちません。これは以下のような2つの意味を持ちます。

  • 最新の統計情報のみが保持される
  • プライベート・エリア(SYSAUX表領域)を圧迫しない

定期的に統計情報を取得しても、古い統計情報の履歴を削除する必要はありません。

3. セッション単位で保留統計情報を使った実行計画が確認できる

OPTIMIZER_USE_PENDING_STATISTICS初期化パラメータをセッション単位で一時的に「TRUE」に設定し、SQLを実行すると保留統計情報を使用した実行計画が作成されます。

これをExplain PlanやSQL*PlusのAutotrace機能で確認することで、公開統計情報を使用した実行計画と比較することができます。

4. 保留統計情報は手動で公開することができる

もし、保留統計情報を使用した実行計画の方が優れていることを確認できれば、DBMS_STATS.PUBLISH_PENDING_STATSプロシージャを実行することにより任意のタイミングで保留統計情報を公開することができます。

これにより、より優れた実行計画を永続的に使用することができます。

5. 保留統計情報はStandard Editionでも使うことができる

今まで触れてこなかったのですが、この機能はStandard Editionでも使用することができます。

Enterprise EditionではSQL計画管理(SPM:SQL Plan Management)により、統計情報に左右されない安定した実行計画の選択を行うことができます。

SPMのように高度な実行計画の管理はできませんが、SEでも「オプティマイザ統計の保留」によって同じような運用を行うことができます。(手動で運用する分のコストはかかります。)

ベスト・プラクティス

それでは、「オプティマイザ統計の保留」を活用した運用のベスト・プラクティスを考えてみましょう。

もちろん、環境によって個別の要件があるので、柔軟に対応することは重要です。

1. 初期状態では統計情報は「公開」しておく

全く統計情報を取得しない状態で「保留」にすることは可能ですが、統計情報を取得してそれを使用した実行計画が作成されるまでは「公開」のままにしておいた方がよいかもしれません。

2. 実行計画に問題がないことを確認し、テーブル単位で公開属性を「保留」に変更する

公開属性をテーブル単位で「保留」に変更すれば、少なくともそのテーブルに関する実行計画は新たな統計情報によって不安定になることなく固定されます。

ただし、一つのテーブルは多くのSQL文で使用されていますので、あるテーブルの属性変更がどのSQLに関係しているのかを把握するためには、以前このブログで紹介した「V$SQL_PLANでCRUD表モドキを作ってみる②」を参考にして調査をしてもよいかもしれません。

3. 定期的に統計情報を取得して評価する

公開属性が「保留」になっていれば、どんなに統計情報を取得したとしても実行計画が変更されることはありません。従って統計情報を定期的に取得することは継続すべきであると考えます。

これは自動統計情報収集機能にまかせてもよいし、DBAが任意にスケジュールしてもよいかもしれません。

「公開」と「保留」統計情報を比較し、より正しい実行計画が得られることが明らかであれば、その保留統計を都度公開していけばよいのです。

4. 統計情報のロックをうまく使い分ける

揮発性の高いテーブル、つまり一日中に頻繁にTRANCATEが実行されたり、バルク・インサートの対象となっているような、すなわち定期的な統計情報の取得でSELECT時使用される正確な統計情報を収集することができないテーブルに対しては、保留統計情報を使用しても最適な結果が得られないことが考えられます。

そのような場合は、統計情報を削除した後にロックし統計情報がない状態で運用した方がよいかもしれません。

このようなテーブルに対しては、SQL文実行時に動的統計が自動的に収集されます。(ダイナミック・サンプリング)

統計情報の「保留」と「ロック」を必要に応じて使い分けることでより最適な運用を図ることができます。

今回でこのシリーズは終わりにしますが、まだ検証していないことも沢山ありますので、またこのテーマを再開させるかもしれません。

次回は

次回は12cのある新機能に注目して検証していきたいと思います。