Asial Blog

Recruit! Asialで一緒に働きませんか?

MySQLのストアドプロシージャと生PHPによるパフォーマンス比較

カテゴリ :
バックエンド(プログラミング)
タグ :
Tech
PHP
JMeter
皆さん、こんばんは。笹亀です。

7月もあっという間に10日間が過ぎて、夏真っ盛りになってきました。
自分も夏対策でアイス眠というマットレスを購入して夏を乗り越えようと思っております。

さて、本日はストアドプロシージャについて検証をしてみたいと思います。
ストアドプロシージャとは、一連のSQL文や処理に名前をつけて保存したものです。
PHPの関数と同じでSQLを関数みたいにしたものがストアドプロシージャになります。
MySQLではストアドプロシージャはMySQLでは5.0から利用が可能です。
今回はMySQLのストアドプロシージャの使い方だけではなく、
生PHPで記載したときとのパフォーマンスを比較してみたいと思います。

MySQLのストアドプロシージャを呼び出して処理をするPHPプログラムとストアドプロシージャで作成したものと同じSQLを実行するPHPプログラムを作成します。
上記2つのプログラムを前回のブログで紹介したJMeterで負荷をかけて、パフォーマンス値を検証してみたいと思います。

まずはテスト用のデータ保存用のテーブルを作成します。
  1. CREATE TABLE `t_test_procedure` (
  2.   `id` int(20) NOT NULL AUTO_INCREMENT,
  3.   `body` text,
  4.   `created_at` datetime,
  5.   `updated_at` datetime,
  6.   PRIMARY KEY (`id`)
  7. ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

次にMySQL用の簡単なストアドプロシージャを作成します。
 ※IDをSELECTしてそのIDのデータがあればUPDATE、なければINSERTする
  1. delimiter $$
  2. DROP PROCEDURE IF EXISTS TEST_PROCEDURE$$
  3. -- usage : call TEST_PROCEDURE('1', 'てきすとデータほげほげ', @code);
  4. -- usage : call TEST_PROCEDURE('2', 'てきすとデータほげほげ', @code);
  5.   /*
  6.    * テスト用のプロシジャープログラム
  7.     IDでSELECTして同じものがあれば更新するプロシジャー
  8.    */
  9.   CREATE PROCEDURE TEST_PROCEDURE (
  10.     IN  v_id       BIGINT,    -- ID
  11.     IN  v_text     TEXT,     -- テストテキスト
  12.     OUT code        TEXT     -- 結果コード  
  13.   ) 
  14. ESC:BEGIN
  15.     DECLARE  cnt       BIGINT;
  16.     DECLARE EXIT HANDLER FOR SQLWARNING, SQLEXCEPTION, NOT FOUND
  17.       BEGIN
  18.         SET code    = '-99';
  19.       END;   
  20.   
  21.   /* データ既に登録されているか確認 */
  22.   SELECT COUNT(id) INTO cnt FROM t_test_procedure
  23.       WHERE id = v_id;
  24.   /* 登録されていない場合はINSERTそれ以外はUPDATE */
  25.   IF cnt = 0 THEN
  26.     INSERT INTO t_test_procedure VALUES (v_id, v_text, now(), now());
  27.   ELSE
  28.     UPDATE t_test_procedure SET body = v_text, updated_at = now() WHERE id = v_id;
  29.   END IF;
  30.   
  31.   SET code = '0'; -- 正常コードを返す
  32. END;
  33. $$
  34. delimiter ;

次にプロシージャを実行するPHPプログラムとプロシージャを展開したPHPプログラムを準備します。
■プロシージャ版
  1. <?php
  2. $dsn = 'mysql:dbname=test;host=localhost;unix_socket=/tmp/mysql.sock';
  3. $user = 'root';
  4. $password = 'pass';
  5.  
  6. try{
  7.     $dbh = new PDO($dsn, $user, $password);
  8. }catch (PDOException $e){
  9.     print('Error:'.$e->getMessage());
  10.     die();
  11. }
  12.  
  13. $stamp = microtime();
  14. list($msec, $sec) = explode(" ", $stamp);
  15.  
  16. $text ='TESTTEST String BODY DATE = ';
  17. $text .= date('Y/m/d H:i:s.') . (float)$msec;
  18.  
  19. $dbh->query("START TRANSACTION");
  20. try {
  21.   $sql = 'call TEST_PROCEDURE(?,?,@res)';
  22.   $sth = $dbh->prepare($sql);
  23.   $sth->execute(array($_GET['id'], $text));
  24. } catch(Exception $e) {
  25.   $dbh->query("ROLLBACK");
  26.   exit;
  27. }
  28.  
  29. $dbh->query("COMMIT");
  30.  
  31. //$sth = $dbh->prepare("SELECT @res");
  32. //$sth->execute();
  33. //$result = $sth->fetchAll();
  34.  
  35. var_dump($result[0][0]);
  36.  
  37. $dbh = null;

■展開版
  1. <?php
  2. $dsn = 'mysql:dbname=test;host=localhost;unix_socket=/tmp/mysql.sock';
  3. $user = 'root';
  4. $password = 'pass';
  5.  
  6. try{
  7.     $dbh = new PDO($dsn, $user, $password);
  8. }catch (PDOException $e){
  9.     print('Error:'.$e->getMessage());
  10.     die();
  11. }
  12.  
  13. $stamp = microtime();
  14. list($msec, $sec) = explode(" ", $stamp);
  15.  
  16. $text ='TESTTEST String BODY DATE = ';
  17. $text .= date('Y/m/d H:i:s.') . (float)$msec;
  18.  
  19. $dbh->query("START TRANSACTION");
  20. try {
  21.   $sth = $dbh->prepare("SELECT count(id) FROM t_test_procedure WHERE id = ?");
  22.   $sth->execute(array($_GET['id']));
  23.   $result = $sth->fetchAll();
  24.  
  25.   if ($result[0][0] == 0) {
  26.     $sql = 'INSERT INTO t_test_procedure VALUES(?, ?, now(), now())';
  27.     $sth = $dbh->prepare($sql);
  28.     $sth->execute(array($_GET['id'], $text));
  29.   } else {
  30.     $sql = 'UPDATE t_test_procedure SET body=?, updated_at=now() WHERE id=?';
  31.     $sth = $dbh->prepare($sql);
  32.     $sth->execute(array($text, $_GET['id']));
  33.   }
  34. } catch(Exception $e) {
  35.   $dbh->query("ROLLBACK");
  36.   exit;
  37. }
  38.  
  39. $dbh->query("COMMIT");
  40.  
  41. $dbh = null;

上記2つのプログラムを実行するそれぞれのテストケースをJMeterで作成します。JMeterについては前回ブログに使い方などを記載しておりますので、参考にしてください。
 ※1〜20,000までのIDを発行して登録して1〜20,000までを発行しおわった後に更新するテストケースを作成します(。

同時アクセス100で200回処理(計:20,000アクセス)をさせました結果を表示します。
注目する箇所は「Throughput」部分です。

プロシージャ版の負荷テスト結果

Throughput: 248.5/sec(合計値

展開版の負荷テスト結果

Throughput: 231.0/sec(合計値

予想ではプロシージャの方が処理が遅いと思っていましたが、展開版の方が時間が処理能力がよくありませんでした。
以前のプロジェクトで複雑な処理をしたプロシージャがあり、負荷テストをした際にそのプロシージャの実行に時間がかかっておりました。対応として今回の検証のようにPHPで展開したものを新たに用意して両方の負荷テストしました。その際にはPHPで展開して実装した方が2倍くらいの処理能力が向上しました。その経緯があったので展開版の方が処理能力がいいと思っておりました。

今回の検証でシンプルな処理をまとめてするのであればプロシージャを利用してもさほど処理能力は変わらないとがわかりました!ただ、プロシージャに頼り過ぎて複雑にSQL文と処理を組み込んでしまうとパフォーマンス低下につながりますので、うまく使い分けをして利用することをお薦め致します^^