Level.9 システムの改良

システムを改良し、速度の向上を測定せよ。
WWWベースは速度上有利であることはありえない。しかし、実際には、WWWベース以外でシステムが作られることは、
もはや、ありえない。改良点はどこにあるのだろうか? データベース設計自体を工夫することが考えられる。
同時にアップデートされるテーブルは少ない方が良い。 データベースサーバに、なにかのオプションがないだろうか?
インデックスとかキャッシュとか... プログラムで工夫できないか? 複数のアップデートをまとめるとか。


- contents -

1. 改良したシステムおよびインタフェース

中村電機オンラインショップ(改)
(Oracleの使用期間の都合上、閲覧は2006/01/15頃まで)

2.検索結果の分散表示

2-1. ページング

Level6で作成した閲覧システムでは、検索結果を同ページに全て出力する。
そのため、該当件数が膨大な数になってしまうと全てを表示するのに時間が掛かり、計算機への負荷も大きくなる。
ページ分散前
そこで、検索結果を指定した件数で分割し、1ページにおける検索結果の表示数を減らす手法(ページング)をとることにした。
ページ分散後
ページングは大手検索サイトである「Yahoo!」や「Google」でも使用されている。
各ページはそれぞれに相互リンクを持ち、ユーザが自由に行き来できる。

2-2. 仮想テーブルの利用

検索結果を分割・表示する処理では
  1. 入力したキーワードに該当する行を指定したテーブルから抽出する。
  2. 抽出された複数(または単数)行から指定範囲行を出力する。
    (ex. 出力件数が10件、3ページ目の検索結果を見る場合)
    [(3-1)*10+1, 3*10] = [21, 30]の範囲の行を出力する。
を繰り返し行うことになる。(下図参照)
仮想テーブル使用前
2-1で示したページングにより生成される、
相互リンクでのページ移動の場合、「商品名」や「値段」などの条件は同じである。
したがって、ページ移動の時までDBにキーワードを送信するのは不要である。
そこで、今回はビューと言われる機能を使用する。

ビューとは、既に定義されている実テーブルから作成される仮想的なテーブルのことである。
ビューの中にはデータは存在せず、実テーブルに対するアクセスの形を定義する。
仮想テーブル使用後
まず、入力されたキーワードに該当する行を全て抽出する。
抽出された行にしたがってビューを生成するため、キーワードによるselectは
今後新しいキーワードを入力しない限り不要となる。
その後、作成されたビューから指定範囲内の行を抽出する。
これにより、繰り返し行うデータ抽出の際のSQL文の短縮に繋がる。

3. テーブルの結合

前述のLevelで、複数のテーブルにおけるselect文の結果の結合を「UNION」によって行っていた。
UNION演算子は、2つ以上のクエリの出力を和結合、すなわち、クエリの結果を結合し、そこから重複する行を削除する。

今回扱うデータは通信販売での商品情報であり、複数のテーブル(ジャンル)において
商品名やメーカー、特に商品IDなどが重複することは考えられない。
そこで、UNION演算子の代わりに「UNION ALL」を使用する。
「UNION ALL」はUNIONと違って重複する行の削除を、つまり重複チェックを行わない。

そこで、実際に「UNION」と「UNION ALL」での違いを計測する。
条件は
union.php
[oracle@pw008 j03056]$ php union.php // UNION
01:  2.76349秒
02:  2.70194秒
03:  2.89512秒
04:  3.32322秒
05:  2.85082秒
06:  2.85565秒
07:  2.95929秒
08:  2.78938秒
09:  2.86383秒
10:  2.70990秒
sum: 28.71264秒
       
[oracle@pw008 j03056]$ php union.php // UNION ALL
01:  0.22327秒
02:  0.21694秒
03:  0.21755秒
04:  0.21746秒
05:  0.22283秒
06:  0.22368秒
07:  0.21811秒
08:  0.21747秒
09:  0.21932秒
10:  0.22271秒
sum: 2.19934秒
       

結果からわかるように、かなりの時間短縮になった。
検索条件が詳細に設定されていればそれほど差は見られないが、
データの重複がほとんどない場合に限り、「全検索」といった大規模な検索では非常に有効である。

4. プログラム

4-1. メインプログラム

nakaden.php

//--------------------
// [$flag]
// 1: 検索ページ初期アクセス状態
// 2: 新しいキーワードによる検索結果
// 3: リンクによる検索結果のページ移動
// 4: 商品を購入した
//--------------------
$flag = 1;


〜 html(head)部省略〜

<center><h2>中村電機</h2></center>

<center>

<?
if ($flag == 4) {
     zaiko_updata($_POST['buy_id']);
     echo $_POST['buy_item']."をお買い上げ真にあざーす!!&;t;br>\n";
}
?>


〜 form部省略 〜


// DBへの接続
$conn = start_connect();

if ($flag == 1) {
     exit_script("");
} else if ($flag == 2) {
     // 入力したキーワードの該当行を抽出するSQL文の生成
     $sql = make_sql($_POST['cate'],     $_POST['highlows'],
		     $_POST['item_word'],   $_POST['maker_word'],
		     $_POST['serial_word'], $_POST['price']);
     if ($debug) {
	  echo $sql."<br>\n";
     }
     // 仮想テーブルの生成
     $all_ros = create_view($conn, $sql, $view_tbl);
     if($all_ros == NULL) {
	  exit_script("fail create_view()<br>\n");
     }
     $_SESSION['all_ros'] = $all_ros;
     $st = 1;
} else {
     $st = ($page-1) * $max_ros + 1;
}

〜省略〜

//--------------------------------
// SQL文の実行
// $_SESSION['all_ros'] : 仮想テーブル全体の行数
// $ros     : 返されたテーブルの行数 ($max_rowsと同値)
// $res     : テーブルの連想配列
//--------------------------------
list($ros,$res) = select_range_data($conn, $view_tbl, $st, $st+$max_ros-1);

if ($ros == NULL) {
     print_r($ros);
     exit_script("fail select_range_data()");
}

$start = ($page-1)*$max_ros+1;
$end   = $start + $max_ros-1;

for ($i = 0; $i < $ros; $i++) {


〜商品表示前半省略〜


  // 在庫チェック
  $zaiko = get_zaiko($conn, $tmp[1]);
  if ($zaiko == NULL) {
       exit_script("fail get_zaiko\n");
  } else if ($zaiko > 1) {
       $msg = "在庫有り";
       if (1) {
	    $msg .= " $zaiko";
       }
  } else {
       $msg = "在庫なし";
  }
}


// 同キーワードによる検索結果の他ページへのリンク
for ($i=1; $i*$max_ros < $max_ros+$_SESSION['all_ros']; $i++) {
  if ($i == $page) {
    $next .= " $i ";
  } else {
    $next .= " <a href=\"$PHP_SELF?page=$i\">$i</a> ";
  }
}
echo "$next<br>";
end_connect($conn);
?>
</div>
</body>
</html>
  
ページングやビューの作成の他に といった処理を追加した。

SQLプログラム

sql.php

function make_sql($cate,$highlows,$item,$maker,$serial,$price) {

〜省略〜

     if ($table_all) {
	  for ($i=1;$i<count($category);$i++) {
	       $tmp = $tmp.str_replace('TB',$category[$i],$sql);
	       if($i%(count($category)-1)!=0){
	            // UNION ALL に変更
		    $tmp = $tmp." UNION ALL ";
	       }
	  }
	  $sql = $tmp;
     }
     return $sql;
}

//------------------------------
// $sqlによって抽出された行を
// 仮想テーブルとして生成する
//
// arg
//   $con      : 接続id
//   $sql      : SQL文
//   $view_tbl : 仮想テーブル名
//
// return 
//   $view_tblの行数
//------------------------------
function create_view($con, $sql, $view_tbl) {
  $sql  = "create or replace view $view_tbl as ".$sql;
  $stmt = oci_parse($con,$sql);

  if (!oci_execute($stmt,OCI_DEFAULT)) {
       echo "error create_view\n";
       oci_free_statement($stmt);
       return NULL;
  }
  oci_free_statement($stmt);

  /* $view_tblの行数を計算する */
  $stmt = oci_parse($con, "select count(*) from $view_tbl");
  if(!oci_execute($stmt,OCI_DEFAULT)){
       echo "error create_view_2\n";
       oci_free_statement($stmt);
       return NULL;
  }
  if (!($all_ros = oci_fetch_row($stmt))) {
       echo "error create_view3\n";
       oci_free_statement($stmt);
       return NULL;
  }
  oci_free_statement($stmt);
  return $all_ros[0];
}

/*
 * 指定したテーブルから指定した件数のデータを返す
 */
function select_range_data($con, $tbl, $start, $end)
{
     $sql  = "select * from (";
     $sql .= "select rownum as seq_no,$tbl.* from $tbl) ";
     $sql .= "where seq_no between $start and $end";

     $stmt = oci_parse($con,$sql);
     if(!oci_execute($stmt,OCI_DEFAULT)){
	  echo "error select_range_data\n";
	  oci_free_statement($stmt);
	  return array(NULL, NULL);
     }

     $ros = oci_fetch_all($stmt,$res);

     oci_free_statement($stmt);
     return array($ros,$res);
}


/*
 * 商品ID が $id の在庫を取得する。
 */
function get_zaiko($con, $id)
{
     $sql = "select item_stock from stock where item_id = '".$id."'";
     $stmt = oci_parse($con,$sql);

     if (!oci_execute($stmt,OCI_DEFAULT)) {
	  echo "error get_zaiko\n";
	  oci_free_statement($stmt);
	  return NULL;
     }

     if (!($zaiko = oci_fetch_row($stmt))) {
	  echo "error get_zaiko2\n";
	  oci_free_statement($stmt);
	  return NULL;
     }

     oci_free_statement($stmt);
     return $zaiko[0];
}

/*
 * 在庫の更新(在庫数-1)
 */
function zaiko_updata($ID)
{
     $sql  = "lock table stock in exclusive mode nowait";
     $con  = start_connect();
     $stmt = oci_parse($con,$sql);

     $sql  = "update stock set ITEM_STOCK = ITEM_STOCK -1 where ITEM_ID = $ID";

     $stmt=oci_parse($con,$sql);
     if (!oci_execute($stmt,OCI_DEFAULT)) {
	  oci_rollback($con);
	  return NULL;
     } else {
	  oci_commit($con);
     }

     oci_free_statement($stmt);
     oci_close($con);
}
  
ビューの作成、及びテーブルの範囲指定のSQL構文は以下の通りである。

ビューの作成
    SQL> create or replace view ビュー名 as select ....;
  
「or replace」により、同じ名前のviewがあれば上書きする。

範囲指定行の抽出
  SQL> select * from (
     > select rownum as seq_no,テーブル名.* from テーブル名 where ...)
     > where seq_no between START and END;
  
まずは副問い合わせで、条件に合う行に seq_no という擬似列を追加する。seq_no には昇順で整数が入る。
その後、where句の between によってSTARTからENDまでの行を抽出する。

実行結果

Oracleの使用期限が過ぎたとき用

初期アクセス状態(jpg)
検索結果(jpg)
他ページへのリンク(jpg)
該当件数多過時の処理(jpg)
購入ボタン押下時(jpg)


以上

Level8 Index Level10