どうしても計算フィールドを使いたい! だけど……

Webアプリケーションを実装していくにあたり、場面によってはどうしても計算フィールドを使用しなければならない箇所が出てくる。

計算式がすべて1つの同じテーブルにて完結する場合は、索引を作成する計算フィールドや数字フィールド+入力値の自動化「計算値(「フィールドに既存の値が存在する場合は置き換えない」チェックを外す)」で代用が効く。しかしほかのテーブルのフィールドを計算に使用すると、トリッキーなことをしない限り外部レコードが更新されても計算したいレコードの更新がおこなわれないため、この方法は使えない。

リレーションシップフィールドを使用した計算フィールドは、索引を設定できない。この状態で検索の対象にした場合は、レコード数10,000件のテーブルにおいて索引を設定したフィールドと動作速度を比較しておよそ100倍から400倍の差が出ている。よほどのことがない限り、リレーションシップフィールドを使用した索引非保存の計算フィールドに対して検索をしないほうが良いのは明白だ。

条件がそろった場合は、スクリプトを使った検索の方が速い?

一定の条件がそろった場合、検索前にスクリプトを実行させる(-script.prefind)方法を取ることでパフォーマンスが改善できる場合がある。その条件は次の2つ。

  • リレーション先のテーブルに登録されているレコード数が少ないとき
  • 計算式を使いたいフィールドは、検索処理ごとに全レコードを横断して計算する必要がない(他テーブルの値を参照して、必要なレコードだけを絞り込みたい場合)

フラグ管理をしたい計算フィールドが10,000レコードを超える大規模のテーブル(A)にあっても、その計算式で使用されているリレーションシップフィールドはレコード数が10から500件程度のテーブル(B)にある場合。すこし特殊な場面だが、この場合「テーブルAの検索前にテーブルBからテーブルAに計算を実施する」ことで処理量を減らすことが可能だ。

フィールド・検索処理の前提は次のとおり。

  • フラグとして計算式を利用したい、結果として0または1を返す
  • 計算フィールドの代わりに、全索引を設定する数字フィールドを使用
  • 検索したい値は1

FileMakerスクリプトの処理手順はおおまかに次のとおり。

  1. テーブルAのレイアウト上で全レコード表示、数値フィールドを「0」で全置換
  2. テーブルBのレイアウト上で、テーブルAの計算に必要なレコードだけを検索して絞り込み
  3. テーブルBからテーブルAの数値フィールドに対して「1」で全置換

リレーションシップフィールドをもとに全レコードを計算させるのではなく、あらかじめリレーションシップ先のテーブルで計算に必要なレコードだけを絞り込み、計算結果を必要なレコードだけに書き込むという手順だ。

実際に試す

ここで使用したFileMakerファイルの情報は次のとおり。

テーブル「scriptTest」

  • 計算フィールドを使用したいテーブル。10,000レコードを格納
  • 比較用に「st_flg_1」計算フィールドと、FileMakerスクリプト計算用の「st_flg_2」数字フィールドを用意
  • 計算式は If ( scriptTest_sub::sts_flg = 1 ; 1 ; 0 )

テーブル「scriptTest_sub」

  • scriptTestの計算フィールドで使用するリレーションシップフィールドがあるテーブル。100レコードを格納
  • sts_randomにrandom関数の実行結果を代入
  • sts_flgには sts_random が 0.5 以上の場合に1、それ以外の場合は0
  • 1レコードがscriptTestテーブルのn件目からn*100件目にリレーション

計算フィールドを使用してフラグ管理をしたいテーブル「scriptTest」のフィールド情報。st_flg_1は計算式でリレーションシップフィールドを使用しているため、索引を設定できない非保存の計算フィールドとなる

リレーションシップフィールドがあるテーブル「scriptTest_sub」のフィールド情報。sts_flgはsts_randomの内容によって1か0を返す。親-子-孫の構成もあるため、ここではあえて非保存の計算フィールドとしている

スクリプト「calcTo_st_flg_2」。「st_flg_1」計算式を別のアプローチで実装、必要な計算だけをおこない必要なレコードだけに戻す

レイアウト「scriptTest」。Webアプリケーション上で「st_flg_1が"1"、検索」と「スクリプト"calcTo_st_flg_2"を実行後、st_flg_2が"1"、検索」のパフォーマンスを計測する

レイアウト「scriptTest_sub」。「calcTo_st_flg_2」スクリプトで使用するだけのレイアウトだ

実際にst_flg_1とst_flg_2フィールドを使用して検索するPHPコードを書き、YSlowで実行速度を計測する。

PHPファイル - find_st_flg_1.php

<?php

include_once('./fx/FX.php');
include_once('./fx/server_data.php');

$data = new FX($serverIP, $webCompanionPort, $dataSourceType, $scheme);
$data->SetDBData($databaseFileName,'scriptTest', 50);
$data->SetDBUserPass($webUN,$webPW);
$data->SetCharacterEncoding('utf8');
$data->SetDataParamsEncoding('utf8');
$data->AddDBParam('st_flg_1' , 1);
$dataSet = $data->FMFind();
echo 'done. ErrorCode: ' . $dataSet['errorCode'];
?>

PHPファイル - find_st_flg_2.php

<?php

include_once('./fx/FX.php');
include_once('./fx/server_data.php');

$data = new FX($serverIP, $webCompanionPort, $dataSourceType, $scheme);
$data->SetDBData($databaseFileName,'scriptTest', 50);
$data->SetDBUserPass($webUN,$webPW);
$data->SetCharacterEncoding('utf8');
$data->SetDataParamsEncoding('utf8');
$data->AddDBParam('-script.prefind' , 'calcTo_st_flg_2');
$data->AddDBParam('st_flg_2' , 1);
$dataSet = $data->FMFind();
echo 'done. ErrorCode: ' . $dataSet['errorCode'];
?>

YSlowの計測結果とまとめは次のとおり。

find_st_flg_1.phpにアクセス。1回目に10,000レコードの計算がおこなわれ、かなりの処理時間がかかっていることがわかる。2回目はデータベース側のキャッシュが効いたため高速に動作

find_st_flg_2.phpにアクセス。1回目、2回目ともにキャッシュの効いたパフォーマンスにはかなわないものの、そこそこのパフォーマンスを出せている

  find_st_flg_1.php find_st_flg_2.php
1回目 14.455 [s] 7.213 [s]
2回目 2.032 [ms] 7.348 [ms]

非保存の計算フィールドの検索は、リレーションシップ先のテーブルにまったく変更がない場合はデータベース側のキャッシュが効くので、2回目以降の検索速度は圧倒的に早くなる。しかしたった1箇所でも変更があった場合は再計算がおこなわれてしまうため、1回目と同様かなりの処理時間がかかってしまう。比較してFileMakerスクリプトを使用した場合はキャッシュを使用したスピードにはかなわないものの、安定してそこそこのパフォーマンスを出せている。

レコード数が多ければ多いほど、計算式内でリレーションシップフィールドを使用する数が多ければ多いほど計算フィールドの検索は処理に時間がかかるようになる。FileMakerスクリプトで2回全置換をおこなうため、この実装方式は使いどころは限られてしまうが覚えておいて損はない高速化テクニックだ。いろいろな応用の仕方があるので、計算フィールドのパフォーマンス劣化の壁に当たった場合はぜひ試してみてほしい。