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

Oracleアーキテクチャをどのように理解するか

私がDBAになったきっかけ

私は20代に某官庁で大型汎用計算機の仕事に携わった経験があったのですが、その後の異動で30代半ばまでITとは全く別の世界で生きてきました。

元々コンピュータは自分に合っているという意識があったので、民間で本格的にITに携わってみたいと転職をしました。

最初はそれまでの所属官庁と関係の深いあるSIerに入社し、そこで初めてOracleデータベースに触りました。汎用計算機時代にやっと出始めた初期のRDBMSに少し携わる機会はあったのですが、COBOLから従来型のファイルシステムの代わりにRDBMSを利用するようなシステムの運用を行っており、民間で見たOracle7 はそれとは全くの別物でした。

その民間会社で恐る恐るOracleの経験をスタートし、約2年後に当時Oracle Master保有率の日本一を争っているような会社に転職しました。

その間はずっとアプリ開発者としてのキャリアを積んでいましたが、Unixも知らなければサーバに触ることも殆ど無く、GUIの開発ツールでPL/SQLのコードをひたすら書く毎日でした。

最初はテーブルのリレーションや実行計画に関する知識もなく、結合の仕方を間違えて発生した重複行を「DISTINCT」で無理矢理1行にまとめるようなおかしなことをやっていました。今思えば赤面モノです。

資格取得に熱心な会社でしたので、転職1年後くらいにOracle Master Plutinum for Oracle 8の資格を取得しました。その後しばらくしてあるネットワークに強い運用アウトソーシング会社からDBA常駐の依頼を受けDBA人生がスタートしました。

仕組みがわかると楽しい

アプリケーションの開発経験はそこそこ積んでいましたが運用経験は皆無です。今思えば無謀な挑戦でしたが、初日にたまたまSQLチューニングを頼まれる機会があり、何とかその場で解決できたので、専門家として少しは認めてもらえました。

ただし、Unix系の経験は殆どなし、サーバやネットワークに関しても同様でしたので、最初の同僚には「思わずめまいがした。」と言われました。

でも、Oracleのスペシャリストはほとんど居なかったので、それなりに期待もされて、ある日上司から社内セミナーでOracleについて話して欲しいと頼まれました。

引き受けてみたものの、何をどう話せばよいのか皆目わからず、日本オラクル社のサイトを探していたら「Oracleアーキテクチャ」の資料がアップされているのを見つけました。

Oracleについて人前で初めて話すというプレッシャーは相当なものでしたが、下手なことはできないとまずはその資料を徹底的に読みこなすことから始めました。

REDOやUNDO(当時はRBSでしたが)、あるいはリスナー経由の接続など、(資格を取っていたにも関わらず)それまで何となくしか理解できていなかったことが改めて明確に理解でき、実際に試すことが出来る環境があったので実地の経験も積んで、初めてOracleが動く仕組みを体系的に理解出来ました。

社内Oracleセミナーは、おかげさまで大変盛況で、半日×2回の講座を希望する人が多すぎてさらに追加の講座を実施するほどでした。

インターネット創世時代から活躍されているネットワーク・エンジニアの方も参加されたのですが、データベースというのはその会社では真空状態のようで、まだまだ未熟とは言えその日から社内的にもスペシャリストとして認知してもらえたように思います。

新人は何を学ぶか?

その後新入社員研修講師や新人向け勉強会のオブザーバをする機会が沢山ありました。未経験者がどのように興味を持ってOracleアーキテクチャを学べばよいのかということについて私なりのイメージが固まってきたので以下にまとめます。

  1. UPDATE文実行の裏でどのような仕組みが動いているかを考える。SELECT、INSERT、DELETEはUPDATEがわかれば理解できる。
  2. 単純なブロック更新の仕組みから発展させ、セッションの確立から、SQL文の解析等どんどん深堀りする。
  3. ユーザ管理のバックアップを理解することでリカバリを考慮した仕組みを理解する。
  4. 自由に再起動が出来る自分専用の検証環境を準備し、参考書等のスクリプト等を実地に確かめる。
  5. プロセスをKillしたり、ファイルを壊してみたり異常状態とそこからのリカバリを確かめる。
  6. 実行計画、統計情報の読み方を覚えて、どのようにSQL文が実行されるかを考える。
  7. OracleもOSの上で動いているものだという意識でOS、ハードウェアについても知識を広げる。

1番目のUPDATE文に関する内部処理の流れをスラスラ何も見ずにホワイトボードに描くことができれば、DBAとしての基礎が確実に備わっていると思います。

いったん筋が通った理解ができれば、そこにどんどん肉付けをして自信を深めることができます。

こういう私もまだ知らないことが沢山あることを自覚していますが、少なくとも初めて「わかった!」と思った時の感動を新人の方々にも味わってもらいたいと心から思います。

OracleマニュアルはEvernoteに取り込んで使おう

HTMLだったら何でも取り込める

またしても、Evernoteネタです。

それぐらいEvernoteは仕事の必需品です。

以前、iPadでOracleマニュアルを持ち出す という記事を書いたことがあります。Oracleマニュアルは私にとって商売道具であり愛読書でもあります。

今では紙でのOracleマニュアルを見ることはなくなりましたが、PDF、HTML、最近ではePub形式のマニュアルを無料で入手できるということは、紙マニュアルの時代を知るものとして隔世の感があります。

いろいろなフォーマットの中で知りたい場所がある程度わかっているものはやはりHTML形式に軍配が上がるのではないかと思いますが、Evernote はHTMLとの相性が抜群です。

Evernote Web Clipper

私は主にGoogle Chromeをメインのブラウザとして使っているのですが、ChromeにしろFierfoxにしろ「Evernote Web Clipper」という拡張機能(エクステンション)が提供されています。(インストール方法に関してはリンク先を参照して下さい。)

Evernote Web Clipper

Evernote Web Clipperをインストールするとブラウザに象のアイコンが表示されます。

拡張機能

クリップの仕方

クリップの実際

 

私はマニュアルのクリップを以下の要領で行っています。

  1. 取り込みたいマニュアルを表示させます。この場合ローカルディスクに保存したHTMLファイルではなくOTNサイトのマニュアルをインターネット経由で表示させます。(例は、Oracle Databaseリファレンス 11gR2 の V$SEGSTAT_NAMEの箇所です。)
  2. 取り込みたい箇所をドラッグして選択状態にします。
  3. Evernote Web Clipperのアイコンをクリックすると、上の写真のように「選択範囲」にマークがついた状態でダイアログが開きます。そのまま緑の「保存」ボタンをクリックするとEvernoteの新しいノートが作成されます。ノートのタイトルはTitleタグに記述されたものが自動的に指定されます。保存先はデフォルトノートブックですが、このタイミングで任意のノートブックを指定することも可能です。
  4. 作成されたノートは下の写真のようになります。保存先ノートブックを変更したり、必要なタグ付けをします。例では「Oracle:リファレンス」と「V$ビュー」というタグを付けています。
  5. マニュアルの記述だけではわかりにくい場合は、実際のSQL文を実行した結果を貼り付けたり、字の色を変えたりして自分だけの情報としてまとめます。

ノート

Evernoteにまとめておくと(Evernote Web Clipperがインストールしてあれば)、普通にGoogle検索をすると右側に自分のEvernote内の検索結果も同時に表示してくれます。まさに「自分だけのGoogle」です!

膨大なマニュアルの中から必要な部分を抜き出し、自分だけのコメントを付けて保存しておくと、記憶が定着するのに非常に有効です。

必要な情報をサッと取り出せる、こういうインフラを日頃から整えておけば仕事にきっと役立ちます。お試し下さい!

Google検索

NULLを排除した設計①

なぜNullを排除しなければならないのか?

テーブルを設計する際、Nullについてはあまり考慮されていないように思います。主キーは当然としてもむやみにNot Null制約を定義することは、テストデータの作り難さなどの理由でむしろ避けられているかもしれません。

ただし、緻密なテーブル設計を目的とした場合、Not Null制約によってNullデータを排除することは非常に重要です。

Nullには2つの意味がある

例えば

update CUSTOMERS set REMARKS = null where CUST_ID = 25000;

というDMLであたかも「Null」という値を設定できているような印象を持ってしまいますが、それではなぜ

select * from CUSTOMERS where REMARKS = null;

ではなく、

select * from CUSTOMERS where REMARKS is null;

というSQL文にしなければならないのでしょうか?プログラマのためのSQL 第4版 ジョー・セルコ著 には次のような一節があります。

「2値論理は、ONとOFFの2つしか状態を持たないバイナリコンピュータや多くの数学分野と極めて相性がいい。しかしSQLは3値論理を採用している。すなわち、TRUE、FALSE、UNKNOWNである。UNKNOWN という値は、比較述語や他の述語の戻り値として生じるものだが、れっきとした真理値であり、データがないことの目印であるNULLとは異なる。これが、SQLにおいて(x=NULL)ではなく(x IS [NOT] NULL) と書かねばならない理由である。NULL は値でも変数でもないので、値や変数にしか適用できない「=」という述語は適用できないのだ。」(P.266)

つまり、SQLでは「状態UNKNOWN」としてのNullと、「データがないことの目印」であるNullが混在しているということが誤解を生みやすくなっています。

Nullを許すことの問題点

以下のSQLを見てみましょう。

SQL> select 1/null from dual;

    1/NULL
----------
(Null)

変数の中にNullが紛れ込むと、どんなほとんどの場合(もちろん例外もありますが)関数の結果がNullになります。

その他、外部キーの参照先にNullデータがあると参照整合性制約の定義としては問題なくても値が取得できない問題が発生します。

プログラマのためのSQL 第4版 ジョー・セルコ著 には次のような記述もあります。

「優秀なSQLプログラマならば、NULLに対処するにあたり、まずはDDLにおいてこれを排除することに全力を傾けるだろう。すべてのテーブルのすべての列に原則としてNOT NULL 制約を付与し、正当な理由がある場合にだけNULLの使用を許可する。間違っても、こういう努力をしないまま、クエリで実装依存の変換関数に頼ればいいなどと考えないでほしい。 NULLは、SQL を知らない人々を混乱させるのだ。 加えて、NULLを使うのはコストの高い行為でもある。というのも、通常、NULLはそれが現れる列の行に余計なピットフラグを持つことで実装されている。これは前述のとおり、NULLが列自身の値ではないためだ。これによって、ストレージ要件や、インデックスおよび検索条件において本来なら必要なかったことを考慮する必要がある。」(P.272)

セルコ曰く「NULLを排除した設計」とはどういうものでしょうか?

この部分は重要だと思うので、簡単な例を挙げて説明します。

顧客テーブルを例にして

どこにでもあるような顧客テーブル(CUSTOMERS)を考えてみます。

テーブル構造は以下の様な簡単なもので、50,000件のテストデータが格納されています。

顧客テーブル

性別カラムには必ず「M」または「F」の値が格納されていてそれぞれ25,000件ずつになるようにデータを作成しています。

SQL> select SEX,count(*) from CUSTOMERS group by SEX;

SEX    COUNT(*)
---- ----------
M         25000
F         25000

備考欄を追加する

ここで、顧客毎に異なる特記事項を記録する必要が生じたことを考えてみましょう。特記事項は長くなりそうなのでCLOB型で記録する必要があるものとします。

さあ、どのように変更したらよいでしょうか?

1. 単純にカラムを追加する(Nullを排除しない設計)

それでは、よくあるパターンとしてカラム追加で対応する方法を考えてみます。

元のテーブルを丸ごとコピーしたCUSTOMERS_1テーブルに、CLOB型のREMARKSカラムを追加してみましょう。

顧客テーブル1

 

SQL> desc CUSTOMERS_1
 名前             NULL?    型
 ---------------- -------- ------------------
 CUST_ID          NOT NULL NUMBER(9)
 CUST_LNAME       NOT NULL VARCHAR2(30)
 CUST_FNAME       NOT NULL VARCHAR2(30)
 SEX              NOT NULL VARCHAR2(4)

SQL> alter table CUSTOMERS_1 add (REMARKS clob);

表が変更されました。

SQL> desc CUSTOMERS_1
 名前             NULL?    型
 ---------------- -------- -------------------
 CUST_ID          NOT NULL NUMBER(9)
 CUST_LNAME       NOT NULL VARCHAR2(30)
 CUST_FNAME       NOT NULL VARCHAR2(30)
 SEX              NOT NULL VARCHAR2(4)
 REMARKS                   CLOB

他のカラムはNot Null制約が付与されていますが、追加したばかりのREMARKSカラムはNullデータを許しています。

備考欄にデータを設定する

カラムを追加しただけではデータは「Null」なので、次のUpdate文でデータを設定します。


SQL> update CUSTOMERS_1 set REMARKS = ‘0' where mod(CUST_ID,5)  = 0;

10000行が更新されました。

SQL> update CUSTOMERS_1 set REMARKS = ‘1' where mod(CUST_ID,10) = 1;

5000行が更新されました。

SQL> update CUSTOMERS_1 set REMARKS = '2' where mod(CUST_ID,20) = 2;

2500行が更新されました。

SQL> update CUSTOMERS_1 set REMARKS = '3' where mod(CUST_ID,50) = 3;

1000行が更新されました。

SQL> commit;

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

SQL> select
  2   nvl(DBMS_LOB.SUBSTR(REMARKS, 1000, 1),'Null') REMARKS
  3  ,count(*)
  4  from
  5   CUSTOMERS_1
  6  group by
  7   DBMS_LOB.SUBSTR(REMARKS, 1000, 1)
  8  order by
  9   1;

REMARKS           COUNT(*)
--------------- ----------
0                    10000
1                     5000
2                     2500
3                     1000
Null                 31500

2. Nullを排除した設計

ジョー・セルコの言う「DDLにおいてこれを排除する」とは以下の様に備考欄を別テーブルに分割した構造です。

顧客テーブル2

REMARKSカラムにはNot Null制約を定義し、Nullデータを入力できないようにします。

DDLは以下のようになります。(ここでは便宜上CUSTOMER_1表を元にデータと一緒に作成しています。)

SQL> create table CUST_2_REMARKS as select CUST_ID,REMARKS from CUSTOMERS_1 where REMARKS is not null;

表が作成されました。

SQL> alter table CUST_2_REMARKS add constraint FK_CUST_ID_2 FOREIGN KEY (CUST_ID) REFERENCES CUSTOMERS_2 (CUST_ID);

表が変更されました。

SQL> alter table CUST_2_REMARKS modify REMARKS not null;

表が変更されました。

SQL> desc CUST_2_REMARKS
 名前              NULL?    型
 ----------------- -------- -------------
 CUST_ID           NOT NULL NUMBER(9)
 REMARKS           NOT NULL CLOB

パッと見た感じ、2つのテーブルをジョインしなければならないしパフォーマンス的には不利なような気もしますが、次回は両者のパフォーマンスの違いを見てみましょう。

(続く)

V$SQL_PLANでCRUD表モドキを作ってみる(番外編)

ピボットテーブルの縦横を入れ替える

テーブルの数とSQL文の数はたいていの場合後者の方が多いので、ピボットテーブルの横軸(列ラベル)にテーブル名(OBJECT_NAME)、縦軸(行ラベル)にSQL_IDを指定するのが自然だと思うのですが、列ラベルの高さは一番長いテーブル名の長さに合わせるため無駄に高くなってしまいます。

見やすさを追求するためには、下図のように列ラベルと行ラベルをドラッグ&ドロップで入れ替えてしまうというのも一つの工夫です。

PIVOT2

SQL_IDは13バイト全部を表示させる必要はありませんし、テーブル名が縦に並んでいる方が圧倒的に見やすいのではないでしょうか?

必要に応じてどちらを列ラベルに指定した方がよいかを考えて指定しましょう。

DBA_HIST_SQL*は上位◯個のSQL文しか格納しない

前回の投稿で本番業務への影響をできるだけ小さくするため、V$SQL_*の代わりにDBA_HIST_SQL_*を使った方がよいと書いたのですが、一つ注意点があります。

V$SQL_*の情報はすべてDBA_HIST_SQL*に保存されるのではなく、上位SQL(Top SQL)のみがフィルタリングされて格納されます。(これはDBMS_WORKLOAD_REPOSITORY.MODIFY_SNAPSHOT_SETTINGSで指定する「TOPNSQL」で変更することが可能です。デフォルト「30」)

AWRはパフォーマンス問題を解決するための情報を取得するので、問題となりそうな一部のSQLの情報しか保持しません。従ってDBA_HIST_SQL_*によって得られる情報は網羅性に欠けるということを念頭に置いて下さい。

V$SQL_PLANでCRUD表モドキを作ってみる③

ここからはExcelで

前回のクエリーの結果をMicrosoft Excelのピボットテーブルを利用して集計してみましょう。(Excel2010での方法を説明します。)
今回はある架空アプリケーションにおける実績データを対象とします。

一番簡単なのはSQL*Plusの実行結果を以下のようにExcelのシートにコピー&ペーストするやり方です。

Excelにコピー&ペースト

 データをスペースで区切る

データ>区切り位置 で「区切り位置指定ウィザード」を呼び出し、「スペースによって右または左に揃えられた固定長フィールドのデータ」を選択して、データを区切ります。

区切り位置指定ウィザード

ピボットテーブルで集計

データを区切ったら、A〜C列(CRUD, SQL_ID, OBJECT_NAME)を選択した状態から、挿入>ピボットテーブル で「ピボットテーブルの作成(ウィザード)」を呼び出します。配置する場所が「新規ワークシート」となっていることを確認し、「OK」をクリックします。

ピボットテーブルウィザード

右側にピボットテーブルのフィールドリストを指定する画面が出てくるので、リストのそれぞれを下のボックスにドラッグして指定します。

  • レポートフィルタ: CRUD
  • 列ラベル: OBJECT_NAME
  • 行ラベル: SQL_ID
  • 値: CRUD(データの個数となっていることを確認)

ピボットテーブル設定

これで完成ですが、見やすくするためにテーブル名を90°回転させて列幅を揃える等の整形をします。

CRUD表を使ってみる

作成したCRUD表(モドキですが)は縦にSQL(SQL_ID)、横にテーブル名が並んだ表で、それぞれの交差部分に「1」が立っていれば、そのSQL文で該当するテーブルを使っていることを意味します。

まず、下図のB1カラム(レポートフィルタ)で「R」を選択し、SELECT文を見てみましょう。

1つのSELECT文で参照するテーブル数

例えば、SQL_ID = 2p8c09m8rupuk のSQL文は「ABCD_BHA」「NIC」「STU3」という3つのテーブルをアクセスしています。一番右端の総計欄を見るとSQL文でアクセスしているテーブル数の合計がわかります。(表を横に見ます。)

R1

この合計数が多いSQLはジョインしているテーブルの数が多い可能性が高いため、注目する必要があるものです。

あるテーブルを使用しているSQL文とその数

逆にあるテーブルから関連するSQL文とその数を確認するには、表を縦に見ます。

R2

上図の例では、「EFGH_DAY_OUT」というテーブルは一番下の総計欄が「34」になっています。これは34種類の異なるSQL文で使用されていることを示しています。

この数が多いということは

  1. 多くのSQL文でアクセスされるホットなテーブルである。
  2. バインド変数を使わないために類似SQL文が多数存在している。

という2つの可能性を示しています。

いずれにせよ、パフォーマンス・チューニングで注目すべきポイントになりますので、AWRレポート等他の情報と突き合わせて問題の特定を行うようにしましょう。

また、このあるテーブルに関係するSQL文の一覧を得ることは、テーブル定義の変更で影響を与える対象を把握するのにも役立ちます。

SQLテキストと実行計画を確認する

一覧ではSQL文を表示させていません。SQL_IDが特定できたら以下のSQL文を実行してSQLテキストと実行計画を確認します。

set pages 1000
set lines 140
select plan_table_output from table(DBMS_XPLAN.DISPLAY_CURSOR('<SQL_ID>'));

 

応用編

インデックス – SQL

V$SQL_PLANを使えば、テーブルとSQLの関係だけでなく、インデックスとSQLの関係を分析することもできます。

例えば、あるインデックスの定義を変更しようとする場合、1つのSQLだけに注目してしまうと他のSQLに影響があることに気づかず新たな問題を引き起こしてしまうかもしれません。

そのような場合、インデックスとSQLの相関表が役に立ちます。

DBA_HIST_*を使う

V$SQL_PLANもX$SQLAREAもメモリ(SGA)上にキャッシュされている情報です。従って、共有プールの使用率が高い場合等にはどんどんキャッシュから追い出されてしまい欲しい情報が得られない場合があります。

もし、AWRが使える環境であれば

  • V$SQL_PLAN → DBA_HIST_SQL_PLAN
  • V$SQLAREA → DBA_HIST_SQLTEXT

に置き換えれば、AWR(SQL)レポート保存期間以内のデータが保証されますので、事後解析でも有用なデータが得られます。

念のためAWR版の情報取得SQLを以下に紹介します。

select
 decode(CRUD_TYPE_ID,2,'C'
                    ,3,'R'
                    ,6,'U'
                    ,7,'D') CRUD
,SQL_ID
,OBJECT_NAME
,OPTIONS
from
(
select distinct
 SQL_ID
,OBJECT_NAME
,decode(operation,'TABLE ACCESS',3
                 ,'UPDATE',6
                 ,'DELETE',7) CRUD_TYPE_ID
,OPTIONS
from
 DBA_HIST_SQL_PLAN --V$SQL_PLAN
where OPERATION in ('TABLE ACCESS','UPDATE','DELETE')
and   OBJECT_OWNER ='xxxx'
union all
select
 SQL_ID
,ltrim(
  replace(
   upper(
    substr(
     regexp_substr(DBMS_LOB.SUBSTR(SQL_TEXT, 1000, 1),'INTO[[:space:]]+[^[:space:]\(]+',1,1,'i')
        ,6)
         )
    ,'"')
      ) OBJECT_NAME
,COMMAND_TYPE CRUD_TYPE_ID
,null OPTIONS
from
 DBA_HIST_SQLTEXT --V$SQLAREA
where COMMAND_TYPE = 2
) I
,DBA_TABLES D
where I.OBJECT_NAME = D.TABLE_NAME
and   D.OWNER       = 'xxxx'
order by
 CRUD_TYPE_ID
,SQL_ID
,OBJECT_NAME
;

最後に注意事項

V$SQL_PLAN等は負荷の高い状況ではアクセスしないことをお勧めします。DBAが手動で負荷状況を見ながら注意深く実行して下さい。

従って、スクリプトを組んで定期的に自動実行するような運用は行わない方が望ましいと思います。

V$SQL_PLANでCRUD表モドキを作ってみる②

UPDATEとDELETE

UPDATE文とDELETE文の実行計画については以下の例のとおりです。

-----------------------------------------------------------------------------
| Id  | Operation          | Name   | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------
|   0 | UPDATE STATEMENT   |        |     1 |     8 |     1   (0)| 00:00:01 |
|   1 |  UPDATE            | EMP    |       |       |            |          |
|*  2 |   INDEX UNIQUE SCAN| PK_EMP |     1 |     8 |     0   (0)| 00:00:01 |
-----------------------------------------------------------------------------
-----------------------------------------------------------------------------
| Id  | Operation          | Name   | Rows  | Bytes | Cost (%CPU)| Time     |
-----------------------------------------------------------------------------
|   0 | DELETE STATEMENT   |        |     1 |    26 |     1   (0)| 00:00:01 |
|   1 |  DELETE            | EMP    |       |       |            |          |
|*  2 |   INDEX UNIQUE SCAN| PK_EMP |     1 |    26 |     1   (0)| 00:00:01 |
-----------------------------------------------------------------------------

Operation=UPDATE or DELETE となる Name列が示すものが、対象のテーブル名です。

SELECTと同様にV$SQL_PLANから情報を取得するクエリーは以下のとおりです。

select distinct
 SQL_ID
,OBJECT_NAME
,decode(operation,'UPDATE',6
                 ,'DELETE',7) CRUD_TYPE_ID
from V$SQL_PLAN
where OPERATION in ('UPDATE','DELETE')
and   OBJECT_OWNER = 'xxxx'
order by
 OBJECT_NAME
,CRUD_TYPE_ID
,SQL_ID
;

SELECTも一緒に取得するように書きなおしたSQL文の実行例は以下のとおりです。

SQL> select distinct
   2   SQL_ID
   3  ,OBJECT_NAME
   4  ,decode(operation,'TABLE ACCESS',3
   5                   ,'UPDATE',6
   6                   ,'DELETE',7) CRUD_TYPE_ID
   7  ,OPTIONS
   8  from
   9   V$SQL_PLAN
  10  where OPERATION in ('TABLE ACCESS','UPDATE','DELETE')
  11  and   OBJECT_OWNER = 'SCOTT'
  12  order by
  13   OBJECT_NAME
  14  ,CRUD_TYPE_ID
  15  ,SQL_ID;

 SQL_ID        OBJECT_NAME  CRUD_TYPE_ID OPTIONS
 ------------- ------------ ------------ --------
 a2dk8bdn0ujx7 EMP                     3 FULL
 2r07krus3ums3 EMP                     6
 7hr1wxxb8hkqc EMP                     7 

INSERTは難しい

INSERT文も同じような要領で情報を取れると思ったのですが、なかなかやっかいです。

以下はINSERT文の実行計画です。

---------------------------------------------------------------------------------
| Id  | Operation                | Name | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------
|   0 | INSERT STATEMENT         |      |     1 |    38 |     1   (0)| 00:00:01 |
|   1 |  LOAD TABLE CONVENTIONAL | EMP  |       |       |            |          |
---------------------------------------------------------------------------------

Operation=”LOAD TABLE CONVENTIONAL” となるName列を持ってくればよさそうな気がします。
しかし、いろいろなINSERT文を見ていて次のような実行計画に出くわしました。

--------------------------------------------------------------------------------------------------------------
| Id  | Operation                | Name                      | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
--------------------------------------------------------------------------------------------------------------
|   0 | INSERT STATEMENT         |                           |       |       |       | 47068 (100)|          |
|   1 |  LOAD TABLE CONVENTIONAL |                           |       |       |       |            |          |
|*  2 |   HASH JOIN              |                           | 22794 |    44M|    44M| 47068   (1)| 00:09:25 |
|*  3 |    TABLE ACCESS FULL     | Txxxxxxxxxxxxxxxxxxxxxxxx | 22794 |    44M|       |  1922   (1)| 00:00:24 |
|   4 |    TABLE ACCESS FULL     | Mxxxxxxxxxxx              |  4861K|   148M|       | 32769   (1)| 00:06:34 |
--------------------------------------------------------------------------------------------------------------

これは実際の実行計画のテーブル名をマスクしたものですが、INSERT対象のテーブル名はどこにも出てきていないことがおわかりでしょうか?

つまり、実行計画からINSERT先のテーブルを特定することはできないのです。

正規表現を使う

そこでV$SQLAREAを使って情報を取得することにしました。SQL_TEXT列にSQLテキストの先頭1,000バイトが入っているので、そこから文字列操作でテーブル名を取り出す作戦です。

SUBSTR関数等いろいろ試してみたのですが、最終的に10g以降で使えるようになった正規表現機能で抜き出すことにしました。
以下が情報取得用のクエリーです。

select
 SQL_ID
,ltrim(
  replace(
   upper(
    substr(
     regexp_substr(SQL_TEXT,'INTO[[:space:]]+[^[:space:]\(]+',1,1,'i')
        ,6)
         )
    ,'"')
      ) OBJECT_NAME
,COMMAND_TYPE CRUD_TYPE_ID
,null OPTIONS
from V$SQLAREA
where COMMAND_TYPE = 2
order by
 SQL_ID
,OBJECT_NAME
;

解説:

  • 7行目がもっとも肝になる部分です。「INTO [テーブル名]」となる文字列をregexp_substr関数で抜き出します。
  • substr関数で抜き出した文字列の6バイト目以降をテーブル名として取り出します。(6行目)
  • upper関数でテーブル名を大文字に統一します。(5行目)
  • たまにテーブル名がダブルクォートで囲まれている場合があるのでreplace関数で取り除きます。これが最終的なOBJECT_NAMEとなります(4行目)

以下は実行例です。
V$SQLAREAにはテーブルの所有者を特定できる情報がないので、上記クエリーの結果とDBA_TABLESをジョインしてOWNER列で絞り込む条件を加えています。

SQL> select distinct
  2   SQL_ID
  3  ,OBJECT_NAME
  4  ,CRUD_TYPE_ID
  5  ,OPTIONS
  6  from
  7  (
  8  select
  9   SQL_ID
 10  ,ltrim(
 11    replace(
 12     upper(
 13      substr(
 14       regexp_substr(SQL_TEXT,'INTO[[:space:]]+[^[:space:]\(]+',1,1,'i')
 15          ,6)
 16           )
 17      ,'"')
 18        ) OBJECT_NAME
 19  ,COMMAND_TYPE CRUD_TYPE_ID
 20  ,null OPTIONS
 21  from
 22   V$SQLAREA
 23  where COMMAND_TYPE = 2
 24  ) I
 25  ,DBA_TABLES D
 26  where I.OBJECT_NAME = D.TABLE_NAME
 27  and   D.OWNER       = 'SCOTT'
 28  order by
 29   SQL_ID
 30  ,OBJECT_NAME
 31  ;

SQL_ID        OBJECT_NAME                    CRUD_TYPE_ID OPTIONS
------------- ------------------------------ ------------ --------
7071f28x901k7 EMP                                       2
gjvsdk24ufadk EMP                                       2

UNIONでまとめてみる

最後に、INSERT/SELECT/UPDATE/DELETE の結果をまとめてみましょう。24行目の「union all」に注目して下さい。

補足すると、CRUD_TYPE_IDをdecode関数で「C」「R」「U」「D」に変換しています。(4行目)
なぜ、このような面倒くさいことをやっているのかというと、最初から「C」「R」「U」「D」という文字列にすると、この順番にソートしてくれないのでCRUD_TYPE_IDという数値を使っているのです。(細かいですが私のこだわりです。)

それから、SQL*Plusコマンドを使って、CRUD別のSQL数も集計してみました。(1-2行目)

SQL> break on crud skip page
SQL> compute count of sql_id on crud
SQL> select
  2   decode(CRUD_TYPE_ID,2,'C'
  3                      ,3,'R'
  4                      ,6,'U'
  5                      ,7,'D') CRUD
  6  ,OBJECT_NAME
  7  ,SQL_ID
  8  ,OPTIONS
  9  from
 10  (
 11  select distinct
 12   SQL_ID
 13  ,OBJECT_NAME
 14  ,decode(operation,'TABLE ACCESS',3
 15                   ,'UPDATE',6
 16                   ,'DELETE',7) CRUD_TYPE_ID
 17  ,OPTIONS
 18  from
 19   V$SQL_PLAN
 20  where OPERATION in ('TABLE ACCESS','UPDATE','DELETE')
 21  and   OBJECT_OWNER = 'SCOTT'
 22  union all
 23  select
 24   SQL_ID
 25  ,ltrim(
 26    replace(
 27     upper(
 28      substr(
 29       regexp_substr(SQL_TEXT,'INTO[[:space:]]+[^[:space:]\(]+',1,1,'i')
 30          ,6)
 31           )
 32      ,'"')
 33        ) OBJECT_NAME
 34  ,COMMAND_TYPE CRUD_TYPE_ID
 35  ,null OPTIONS
 36  from
 37   V$SQLAREA
 38  where COMMAND_TYPE = 2
 39  ) I
 40  ,DBA_TABLES D
 41  where I.OBJECT_NAME = D.TABLE_NAME
 42  and   D.OWNER       = 'SCOTT'
 43  order by
 44   CRUD_TYPE_ID
 45  ,OBJECT_NAME
 46  ,SQL_ID
 47  ;

C OBJECT_NAME          SQL_ID        OPTIONS
- -------------------- ------------- ---------------
C EMP                  7071f28x901k7
  EMP                  gjvsdk24ufadk
*                      -------------
c                                  2

C OBJECT_NAME          SQL_ID        OPTIONS
- -------------------- ------------- ---------------
R EMP                  7071f28x901k7 BY INDEX ROWID
  EMP                  a2dk8bdn0ujx7 FULL
  EMP                  b84685tx0a4u0 BY INDEX ROWID
  EMP                  f93ucvwf6d1vh FULL
*                      -------------
c                                  4

C OBJECT_NAME          SQL_ID        OPTIONS
- -------------------- ------------- ---------------
U EMP                  2r07krus3ums3
*                      -------------
c                                  1

C OBJECT_NAME          SQL_ID        OPTIONS
- -------------------- ------------- ---------------
D EMP                  7hr1wxxb8hkqc
*                      -------------
c                                  1

どのテーブル(OBJECT_NAME)がどのSQL(SQL_ID)によってINSERT/SELECT/UPDATE/DELETEの対象になっているかが、この一覧からわかります。
(OPTIONSは単なるおまけですのでなくても構いません)

この例ではSCOTT.EMP表しか出てきていませんが、実際は非常に沢山の結果が返ってくるはずです。

次回は、MS Excelのピボットテーブルを使って CRUD表らしく加工してみます。

(続く)

V$SQL_PLANでCRUD表モドキを作ってみる①

パフォーマンス問題の犯人探し?

情報システムに問題(特にパフォーマンス問題)が発生した場合、運用を担っているインフラ・チームが真っ先にやり玉に挙げられることが多いようです。

そんな時のアプリ・チームの主張は

  • 自分たちはアプリケーションに一切手を加えていないので悪くない。悪いはずがない。
  • インフラ・チームが何かやらかしたのではないか?(過去に痛い経験があればなおさら)
  • 隠しパラメータみたいな秘密のスイッチがあるらしいから、それをうまく使って切り抜けてくれ!
  • インフラには高い金をつぎ込んでいるのだから何とかしろ!

というようなものが多いのではないでしょうか?

しかも、大抵の場合アプリ・チームの方が人数が多いので、多勢に無勢いつの間にか問題はインフラ・チームのせいになってしまっている、というようなことはよくあります。

最近は、新卒でインフラ・チームに配属されるというケースも多くなっていますが、ベテラン開発者から運用者に転向しているような技術者は総じてスキルのレベルが高いので(好意的に解釈して)重要な問題の解決を期待されているということの裏返しなのかもしれません。

ただし、インフラ・チームの名誉のために補足しますが、私はパフォーマンス問題の8〜9割近くはSQLつまりアプリケーションの問題のような印象を持っています。(なぜアプリケーションに手を加えていないのに問題が発生するのかは改めて触れたいと思います。)

DBAは問題解決の要

DBAは多くの場合インフラ・チームの一員として数えられていますが、データベースはミドルウェアと言いながらOSから見れば一種のアプリケーションであるので、DBAがアプリ・チームとの仲介役としてうまく機能できれば問題解決に対して非常に強い組織になります。

専任のDBAを置いている組織は少ないかもしれません。サーバ管理者が兼任していたりしてDBAの職務が曖昧になっている場合、インフラ・チームとアプリ・チームの溝が深くなってしまうことはよくあります。

DBA自らがアプリケーションを理解する

問題が発生してからアクションを起こすのでは遅すぎます。DBAは常日頃からアプリケーションに対して関心を持つ必要があります。理想的にはアプリ・チームの責任としてDBAが理解できるドキュメントを整備するのがベストですが、現実的にはなかなか難しいことが多いかと思います。

従って、DBA側から能動的に情報を収集することが必要です。DBAから見た最も身近なアプリケーションはSQL文です。Oracleでは実行されているSQL文に関するさまざまな情報をV$SQL*ビュー(Oracle11gR2であれば30種)から得ることができます。

V$SQL_PLAN

それではV$SQL_PLANを使用してSQL文とテーブルの相関表を作ってみましょう。あるSQL文がどのテーブルを使用しているか、あるいはあるテーブルはどのようなSQL文によって使用されているかを一覧にしたものです。これはDB設計におけるCRUD表に近いものですが、実際に動いているSQL文の情報を使って作るというのがポイントです。

 実行計画のおさらい(SELECT)

以下の簡単な実行計画の例で考えてみます。

-----------------------------------------------------------------------------------
| Id  | Operation                     |  Name        | Rows  | Bytes | Cost (%CPU)|
-----------------------------------------------------------------------------------
|   0 | SELECT STATEMENT              |              |     3 |   189 |    10  (10)|
|   1 |  NESTED LOOPS                 |              |     3 |   189 |    10  (10)|
|   2 |   NESTED LOOPS                |              |     3 |   141 |     7  (15)|
|*  3 |    TABLE ACCESS FULL          | EMPLOYEES    |     3 |    60 |     4  (25)|
|   4 |    TABLE ACCESS BY INDEX ROWID| JOBS         |    19 |   513 |     2  (50)|
|*  5 |     INDEX UNIQUE SCAN         | JOB_ID_PK    |     1 |       |            |
|   6 |   TABLE ACCESS BY INDEX ROWID | DEPARTMENTS  |    27 |   432 |     2  (50)|
|*  7 |    INDEX UNIQUE SCAN          | DEPT_ID_PK   |     1 |       |            |
-----------------------------------------------------------------------------------

まず中程の「Name」列は、その実行計画でアクセス(SELECT)されるテーブルあるいはインデックスとなります。テーブルなのかインデックスなのか名前から判断することもできますが確実なのは左隣りの「Operation」列が

  • TABLE ACCESS  で始まっているものが「テーブル」
  • INDEX で始まっているものが「インデックス」

です。

V$SQL_PLANからSELECT対象のテーブル一覧を取得する

V$SQL_PLANから情報を取得するクエリーを考えてみます。

select distinct
 SQL_ID
,OBJECT_NAME
,3 CRUD_TYPE_ID
,OPTIONS
from
 V$SQL_PLAN
where OPERATION like 'TABLE%'
and   OBJECT_OWNER = 'xxxx'
order by
 OBJECT_NAME
,CRUD_TYPE_ID
,SQL_ID;

8行目のWHERE条件でテーブルアクセスのオペレーションのみを取り出しています。
9行目ではテーブルの所有者で絞り込みをしています。

4行目で何をしているのかというと、V$SQL_PLANにはない V$SQL.COMMAND_TYPE に相当する情報をここで付加しています。
SELECTであれば「3」になります。この理由は追って説明します。

以下は実行例です。

SQL> select distinct
  2   SQL_ID
  3  ,OBJECT_NAME
  4  ,3 CRUD_TYPE_ID
  5  ,OPTIONS
  6  from
  7   V$SQL_PLAN
  8  where OPERATION like 'TABLE%'
  9  and   OBJECT_OWNER = 'SCOTT'
 10  order by
 11   OBJECT_NAME
 12  ,CRUD_TYPE_ID
 13  ,SQL_ID;

SQL_ID        OBJECT_NAME  CRUD_TYPE_ID OPTIONS
------------- ------------ ------------ --------
0cc0hqvr7kpgx BONUS                   3 FULL
30hdyzpfrzgbf BONUS                   3 FULL
3154rqzb8xudy DEPT                    3 FULL
678kpdu03jc8x DEPT                    3 FULL
1srhq04p4x0zz EMP                     3 FULL
a2dk8bdn0ujx7 EMP                     3 FULL
25tuw6725a9h7 SALGRADE                3 FULL
30p00zk91247c SALGRADE                3 FULL

8行が選択されました。

(続く)