はじめに
とある業務システムの開発をしています。
プログラムを書いて、動いた、やったー、で終わればいいですが問題は発生します。
そのうちの一つがパフォーマンスの問題だと思います。
これまで経験した中で、面白いと思ったパフォーマンスの問題を紹介したいと思います。
対象は、JavaとSQL(主にOracle)と一部C++です。
テーブルが多かった
開発しているシステムのテーブル数を数えると3,836個でした。
履歴を格納するテーブルや、一時的にデータを格納するワークテーブルも多いのでそれらを除外しても1,439個でした。結構な数ですね・・・。
これらのテーブルの用途などを覚えておかないと開発のパフォーマンスが上がらない・・・という話しもありますが、それはとりあえず置いておくとして。
一般的にデータベースの設計は正規化しましょうと言われます。
そのため、システムで扱う要素が一つ増えると、管理するためのコードの項目が増えて、分かりやすくするために名称の項目も増えて、それらを正規化して切り出すとコードと名称のテーブルができます。そんな感じでテーブルの数が増えていきます。
そして、検索して画面に表示するというときには名称を表示した方が分かりやすいので、コードと名称のテーブルと結合して名称を取得するようなSQLを書くことになります。というわけで、管理する要素が50個あるテーブルを検索すると、単純に考えると50個のテーブルと結合することになります。
ここで検索を実際に処理してくれるデータベースさんの話しをしますと、SQLを効率よく実行するために事前に色々と考えてくれるという機能があります。非効率なSQLであっても、データベースさんがいい感じに考えて処理をしてくれるという非常に優れたものです。
ただ・・・何事にも限界はあります。
データベースさんが50個の結合を相手に効率の良い方法を色々考えて・・・考える時間だけで1分かかるといった状態が発生するようになりました。いざSQLが実行されるとミリ秒で終わったりするのですけどね(散々待たされたあげく結果は0件だったとか)。
対応方法は結合するテーブルの数を減らすしか無いと思います。
しかし、名称とか取得するのを諦めるわけにもいかないのが悩みどころで・・・。
結局、
・データを名称ごと一度に取得する
のはやめて
・データを取得して
・取得したデータを元に名称を取得しに行く処理を別に実行する
と2段階にする方法を採用しています。
当然処理が2回になる分はオーバーヘッドがあるのですが、データベースさんが無駄に悩むような状況は回避できるようになりました。そのため、パフォーマンスがアップした、あるいはパフォーマンスが安定するようになったという結果が得られました。
Nested Loop と Hash Join と私
前項の通りテーブルの結合が多くなる傾向があるので、結合の仕方によってはパフォーマンスが良かったりよかったり悪かったりします。データベースさんが色々考えてくれるのですが、毎度適切に判断してくれる・・・というわけでもありません。
よく話しにあがるのは結合の方法です。具体的には Nested Loop と Hash Join です。他にも色々ありますが、ここでは割愛させていただきます。
詳しいことは調べていただきたいのですが、簡単に違いを書くと
・Nested Loop は1件ずつレコードを取得する方法です。
・Hash Join は一旦全部データを取得してから必要なデータに絞り込む感じです。
Nested Loop は1件ずつレコードを取得するので、大量に取得する場合は手間がかかります。
Hash Join は絞り込むところはとても効率的ですが、一旦全部データを取得するため、最終的に取得する対象が少ない場合は無駄が多いです。
大体システムは画面で操作します。画面に表示されるデータは精々100件とか、多くても1,000件とかです。100万件とかレコードが入っていることもあるテーブルのデータ量からすると微々たるものです。なので、大体は Nested Loop の方がよいとなりそうです。
パフォーマンスが悪い ⇒ Hash Join してるね ⇒ Nested Loop にしよう
というのはよくある対応方法かなと思います。
なお、Hash Join が必ずしも悪いわけではなく、例えば過去のデータを一通り確認して結果を元に画面に表示するみたいな場合は一旦過去のデータを大量に取得する必要があります。こんなときは Hash Join でOKです。
ただし、システムには画面の操作以外にバッチ処理というのもありまして・・・。
一日ため込んだデータをまとめて処理する、といった話しになるので対象のデータは何万件とかもありえます。そうなると、前述の内容は前提が覆ることになります。
理想を言えば、画面の処理とバッチ処理でプログラムが分かれていることなのですが・・・残念ながらそんなリソースはありません。
そのため、標準機能としては中途半端な状態となりますが
・データベースが適切に判断してくれることをお祈りする。
・お客様の使用状況に合わせて個別にチューニングしてもらう。
といった話しに(残念ながら)落ち着くことになります。
まぁ、私のところにチューニングの依頼がきたら、とりあえず Nested Loop にして早くなったらOK、とかになりそうですね・・・。
処理回数が多い(意図したわけでは無いけど)
開発しているシステムはリッチクライアントなので、画面を操作すると色々気の利いた動作をしてくれます。
例えば、コードを入力すると自動で名称を取得してきて表示してくれます。分かりやすくてGOODです。また、関連するコードを一緒に取得してくれる、というようなこともしてくれます。ある品目を入力したら、いつも購入しているお得意様のコードも一緒に表示してくれる、といった感じですね。
そして、お得意様のコードが設定されると、ついでにお得意様といつも決済している通貨を一緒に表示してくれます。通貨が外貨だった場合は、一緒に為替レートを表示してくれます。・・・といった感じで、関連する内容を数珠つなぎに取得して表示してくれます。
非常に便利です・・・パフォーマンスを気にしなければですが。
取得する、というのはデータベースに問い合わせに行くということです。
ちょっと時間がかかりますが、1回なら気にならない程度です。
しかし、数珠つなぎになって合計10回とか問い合わせることになると・・・かなり時間がかかることもあります。例えば某画面では、項目を入力すると画面が固まったようになります。通信やデータベースの状況にもよりますが、操作を受け付けるようになるまで1分かかることもありました。
対応方法としては数珠つなぎにしないということになります。
数珠つなぎに処理をせず、一括で処理できるように作り変える、とかですね。
終わったと言ったな・・・あれは嘘だ
使っているプログラミング言語は主にJavaなのでメモリの管理とかは基本システムにお任せです。しかし、開発しているシステムに一部C++で書かれているプログラムがあり、こちらはメモリ管理を自前で行っているそうです。ここはそのC++のプログラムでのお話しです。
C++が採用されている理由は処理速度が高速だからです。しかし、世の中には何兆円を売り上げるような巨大企業もあり、当然そのような企業様が扱うデータ量は膨大です。
とある大企業様の案件において、弊社自慢のC++プログラムが膨大なデータ量をどれくらいの時間で処理できるのかまずは確かめようという話しになりました。いわゆるパフォーマンステストというやつですね。
というわけで私が駆り出されることになりました。大企業様が想定するようなボリュームのデータを作って、プログラムを実行してどれくらいの時間がかかるのか確認します。自慢のC++のプログラム VS 超大量データの戦いが始まります。
結果として旗色はかなり悪かったです。余裕で1日とかかかりました。
まぁこんなもんだよな・・・と思いつつ、ログだとかを確認していると、おかしなことに気づきました。処理が終わっているとログには書いてあるのに、処理が返ってきていない?ような状態だったのです。いや早く返ってきてよ、数時間単位で早くなるんだけど・・・。
何だろうなと思いつつパフォーマンステストで収集したデータを調査していると、メモリの使用量の変化が妙だなと思いました。何せ1日かかるような大量データです。これまで見たことが無いような量のメモリを使って処理が行われていました。そして処理が終わったとログが出たあと、実にゆっくりとメモリの使用量が減っていっていました。ちょっとずつちょっとずつメモリを開放していって・・・メモリが解放されるまで待機しているような状態に見えます。
C++のプログラムを開発している方(私の先輩)に聞いてみたところ、メモリを強制的に開放する方法を探し出してくださいまして、とても早く終了するようになりました。
普段Javaくらいしか触らないプログラマーな私には結構新鮮な体験でした。
余談ですが、この案件は失注しました。