rm /blog

IT系技術職のおっさんがIT技術とかライブとか日常とか雑多に語るブログです。* 本ブログに書かれている内容は個人の意見・感想であり、特定の組織に属するものではありません。/All opinions are my own.*

orとunionとテスト環境に関する小話

ふと思い出したので書いてみる。 昔のPG修正の失敗談である。

 

(1)「集計対象フラグ」="1"のデータのみ集計対象とする集計処理がある

(2)「集計対象フラグ」は店舗別にマスタに設定されている

(3)集計処理は、「当日分テーブル」を全件取得して「累積テーブル」に合算する

(4)「当日分テーブル」と「累積テーブル」は同じテーブルレイアウトになっている 「当日分テーブル」と「累積テーブル」のレイアウトイメージは以下のような感じである

店舗コード 実績数 集計対象フラグ
0001 10 0
0003 35 1
0004 2 1

「当日分テーブル」は夜間処理開始前にTRUNCATEして全件クリアな状態にし、 発生した実績明細データをもとにまず「当日分テーブル」を作成する。 それをもとに「累積テーブル」に値を合算させていくイメージになる。 ただ、業務仕様上、集計しておくべき範囲は店舗で切り分けを行うことになっており、 それは店舗マスタ(的なもの)に保持する「集計対象フラグ」に依存する。 この「集計対象フラグ」は専用のマスタメンテナンス画面で更新(変更)することができる。


非常に簡単にいうと↑のような仕組みを構築したのだが、運用後に考慮漏れを発見した。 1回の集計処理において、同一店舗で「集計対象フラグ」が混在することを意識していなかったのである。

ようするに”集計対象の店舗は昨日も今日も明日も集計対象だ”という (今にして思うと)意味不明の前提が処理のロジックに組み込まれていた。

このため、たとえば 「12/17まで集計対象ではなかったが、12/18から集計対象にした」 という店舗に関しては、12/18夜間処理において 当日分テーブル.集計対象フラグ="1"(12/18の実績から) 累積テーブル.集計対象フラグ="0" (12/17までの実績から) という状態が生まれるので、「集計対象フラグ」が"0"と"1"で混在する形になる。 集計処理では、こういったデータを処理対象外にしてしまっていたので、 「その日に集計対象にした」という店舗の実績値がその日の夜間処理では抜け落ちてしまっていた。

※実体験におけるこのケースでは、「集計処理」というのが”当日発生した実績値をもとに1から再集計” という処理の特性をもっていたため、 ↑の例でいうと12/18夜間処理では不正になるが 翌12/19夜間処理でその店舗で実績が発生すると再集計されてきれいになる。加えてこの「集計対象フラグ」の変更タイミングが業務上土曜日に行われることが通例になっていて、集計値が不正になるのが日曜日ONLY(休日なのであまり見ない)ということが多く、発覚が遅れた。 この問題に対処するため、処理対象のデータ取得条件を以下のように見直した。

■修正前(フラグ混在ケースに対応していない)

select 当日分テーブル.店舗コード
      ,当日分テーブル.実績数
      ,累積テーブル.実績数
from 当日分テーブル
    ,累積テーブル
where 当日分テーブル.集計対象フラグ='1'
  and 累積テーブル.集計対象フラグ='1'
  and 当日分テーブル.店舗コード=累積テーブル.店舗コード

■修正後(フラグ混在ケースに対応していない)

select 当日分テーブル.店舗コード
      ,当日分テーブル.実績数
      ,累積テーブル.実績数
from 当日分テーブル
    ,累積テーブル
where (当日分テーブル.集計対象フラグ='1'
   or  累積テーブル.集計対象フラグ='1')
  and 当日分テーブル.店舗コード=累積テーブル.店舗コード

要するに条件をandからorに変更したのである。これにより集計不正の問題は解消されたが、 一方で処理がむちゃくちゃ遅くなって実用に耐えなくなった。


条件をandからorに変更したことで、実行計画が変わってしまったのが理由である。 hintの実装やINDEXの新規構築等も考えたが、結果的に↑の実装を以下のように変更することで (「修正前(バグってた時期)」に比べれば多少遅くなったが)解決に至った。 ■修正後(フラグ混在ケースに対応していない)


select 当日分テーブル.店舗コード
      ,当日分テーブル.実績数
      ,当日分テーブル.実績数
from 当日分テーブル
where 当日分テーブル.集計対象フラグ='1'
union all
select 累積テーブル.店舗コード
      ,累積テーブル.実績数
from 累積テーブル
where 累積テーブル.集計対象フラグ='1'

orで結合していた箇所を分離し、 必要な分をそれぞれ別個に取得してunionで加算するように変更した。 この後、↑を副問い合わせにした内部集計をいったんはさむものの、 これによって処理のパフォーマンスは(orに比べれば)大幅に向上した。


これを通して何が言いたいかとうわけでもない。 思い出したので書いてみただけ、というところが強い。 ただ、思い返すに、orを使った条件指定はこの処理以外にも存在するのだが、 ここで挙げた事例ほど処理性能が問題にならなかったのは、 他のor処理の処理対象とする件数が少ないためだろう。

この事例では「累積テーブル」のほうは10億件に近いレコードを保持しているので 処理性能の問題が顕著に出てきてしまった。 一方で、「事前に(テストの段階で)わからなかったのか?」というのは 例え当事者であってすら疑問に思うものである。 だが気付かなかった。 いろいろ理由はあるけども、 テスト環境に本番と同レベルのデータ状況(10億件)を再現することが テスト環境のテーブルスペース的に不可能だったというのが一つの言い訳として挙げられる。

テスト環境というのは、ほぼすべてのシステムにおいて必ず存在しているものと思うが、 それらが稼働中の本番環境と同スペックのマシンかといわれると、多くの場合そうではない気がする。 たとえばOracleにおいては、CPUコア数等はライセンス費用に直結するため、「テスト環境」という環境にそこまで厳格なスペックを求めないのがふつうであると思う。 今回の事例もそうだったし、 というか開発上の経緯から既存のテスト環境内に無理やりインスタンス作って 場所の一部を間借りするようなまねをして用意したテスト環境だったので、 そんな余裕などあるわけがなかった。 割り当てられたテーブルスペースももはや私の個人PCのほうが容量が大きいくらいだったので、 正直最低限の検証しかできない環境だったというのは事実としてある。 また、上述した「開発上の経緯」においては、 顧客予算の都合でのテスト環境削減が開発着手前の決定事項にあったこともあり、 (もう少しお金出してくれたなら間借りするようなコスイ真似してテスト環境こしらえなくても 専用のテスト環境用意できたのにね…という意地悪な主張) そのあたりも踏まえると当時 「うん遅くなったね、でも仕方ないよね性能検証できる環境ないんだもん(^^)」 という気持ちがあったことも正直、否定できない。

しかしながら、事前に性能面での問題がキャッチアップできなかったのは 個人的にも課題としてとらえておく必要があると思ったし、 本番環境のデータの分布を真似て 小規模な「当日分テーブル」「累積テーブル」を用意すれば 小規模なりに”性能劣化”の状態を再現することはできたので、 これ以降は気にしておく事項の一つとして、本件のことを忘れずにいる。