MySQL 5.5.19 にて ON DUPLICATE KEY UPDATE 使用時に LAST_INSERT_ID を呼ぶとプライマリキーの値によって不可思議な現象が起こる話
例によって同僚に、ON DUPLICATEを使ってる時の、LAST_INSERT_IDの挙動がなんかおかしいという話を受け調査を開始した。
複数の同僚に手伝ってもらいながら検証した結果次のような現象が起こることが分かったため、メモしておく。
ON DUPLICATE でプライマリキーに「文字」を利用する場合、LAST_INSERT_IDの値が 0 に変化し予期せぬ更新がかかったり、更新が失敗する問題がある。
これは、LAST_INSERT_ID が数値 しか扱えないことが原因と思われる。
次に検証手順を記す。
とりあえず、MySQLのバージョンは 5.5.19
■一つ目のパターン「数値」の場合
このパターンは想定通りに動くので、特に問題は起こらない
1.まずは簡単なテーブルを作成する
mysql> CREATE TABLE IF NOT EXISTS fff ( -> id VARCHAR(64), -> last_update DATETIME, -> PRIMARY KEY (id) -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8; Query OK, 0 rows affected (0.03 sec)
2.プライマリキーに 「数値」をいれて、普通にINSERTする ・・・まあ普通に通る
mysql> INSERT INTO fff (id, last_update) VALUES ('9', NOW()); Query OK, 1 row affected (0.04 sec) mysql> SELECT * FROM fff\G *************************** 1. row *************************** id: 9 last_update: 2013-01-11 22:11:45 1 row in set (0.00 sec)
3. プライマリキーに 「数値」をいれて、普通にON DUPLICATEする ・・・これも問題なく通る
正しくインサーとされ、LAST_INSERT_IDも正しい値が取れる。
mysql> INSERT INTO fff (id, last_update) VALUES ('9', NOW()) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id), last_update = NOW(); Query OK, 2 rows affected (0.02 sec) mysql> SELECT * FROM fff\G *************************** 1. row *************************** id: 9 last_update: 2013-01-11 22:11:51 1 row in set (0.00 sec) mysql> SELECT LAST_INSERT_ID(); +------------------+ | LAST_INSERT_ID() | +------------------+ | 9 | +------------------+ 1 row in set (0.00 sec)
4. 最後にお掃除する
mysql> DROP TABLES fff; Query OK, 0 rows affected (0.01 sec)
■二つ目のパターン「文字」の場合
このパターンは想定通りに動かないので、特に注意を要する
1.まずは簡単なテーブルを作成する
mysql> CREATE TABLE IF NOT EXISTS fff ( -> id VARCHAR(64), -> last_update DATETIME, -> PRIMARY KEY (id) -> ) ENGINE=InnoDB DEFAULT CHARSET=utf8; Query OK, 0 rows affected (0.03 sec)
2.プライマリキーに 「文字」をいれて、普通にINSERTする ・・・まあ普通に通る
mysql> INSERT INTO fff (id, last_update) VALUES ('X', NOW()); Query OK, 1 row affected (0.01 sec) mysql> SELECT * FROM fff\G *************************** 1. row *************************** id: X last_update: 2013-01-11 22:12:29 1 row in set (0.00 sec)
3. プライマリキーに 「文字」をいれて、普通にON DUPLICATEする ・・・これはWARNINGSは出るものの問題なく通るように見える
・・・が、SELECTの結果をみるとid が X から 0 になぜか変換されている。
LAST_INSERT_IDもX ではなく 0 が取れる。
この時点ですでにおかしいが、さらに続ける。
mysql> INSERT INTO fff (id, last_update) VALUES ('X', NOW()) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id), last_update = NOW(); Query OK, 2 rows affected, 1 warning (0.01 sec) mysql> SHOW WARNINGS; +---------+------+----------------------------------------+ | Level | Code | Message | +---------+------+----------------------------------------+ | Warning | 1292 | Truncated incorrect INTEGER value: 'X' | +---------+------+----------------------------------------+ 1 row in set (0.00 sec) mysql> SELECT * FROM fff\G *************************** 1. row *************************** id: 0 last_update: 2013-01-11 22:12:38 1 row in set (0.02 sec) mysql> SELECT LAST_INSERT_ID(); +------------------+ | LAST_INSERT_ID() | +------------------+ | 0 | +------------------+ 1 row in set (0.00 sec)
4. 再度プライマリキーに 「文字」をいれて、普通にON DUPLICATEする ・・・今度はWARNINGSも出ず問題なく通る
SELECT結果も正しく、Xの行が追加されている
LAST_INSERT_IDはやはり X ではなく 0 が取れる。
mysql> INSERT INTO fff (id, last_update) VALUES ('X', NOW()) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id), last_update = NOW(); Query OK, 1 row affected (0.02 sec) mysql> SELECT * FROM fff\G *************************** 1. row *************************** id: 0 last_update: 2013-01-11 22:38:33 *************************** 2. row *************************** id: X last_update: 2013-01-11 22:39:18 2 rows in set (0.00 sec) mysql> SELECT LAST_INSERT_ID(); +------------------+ | LAST_INSERT_ID() | +------------------+ | 0 | +------------------+ 1 row in set (0.00 sec)
5. さらに再度プライマリキーに 「文字」をいれて、普通にON DUPLICATEする ・・・今度はERRORになてインサートできない
わかる人にはもうわかると思うが、これは id = 'X' ではなく、既に存在する別の id = 0 の行とおなじプライマリキーで id = 'X' の行をUPDATE 仕様とした結果、プライマリキーの競合が起こり更新できい状態が発生している。
mysql> INSERT INTO fff (id, last_update) VALUES ('X', NOW()) ON DUPLICATE KEY UPDATE id = LAST_INSERT_ID(id), last_update = NOW(); ERROR 1062 (23000): Duplicate entry '0' for key 'PRIMARY'
6. 最後にお掃除する
mysql> DROP TABLES fff; Query OK, 0 rows affected (0.01 sec)
■まとめ
このように、LAST_INSERT_ID が数値 しか扱えないず、LAST_INSERT_ID('文字') とした場合には 0 が返る仕様?によりON DUPLICATEとLAST_INSERT_IDを併用する際にプライマリキーに 「文字」をいれた場合、不可思議な現象が発生することが有るようです。
PHPのAPCの apc.include_once_override 設定時にいくつかの条件を満たした場合に include_once 等が失敗する現象
↓の続き
誤報 APC の apc.include_once_override で動的な include_once 等が失敗する問題
http://d.hatena.ne.jp/narusase/20121030
どうも同僚の調査によると、動的なinclude_onceが原因ではないようだ
・・・というわけで、極限まで単純化したサンプルコードを書いてテストしてみた。
ファイルその1. /home/htdocs/test.php
<?php function include_test($x = array()) { include_once('hogehoge.php'); } echo '1'."\n"; include_test(); echo '2'."\n"; include_test(); echo 'end-'."\n";
ファイルその2. /home/lib/hogehoge.php
<?php class hogehoge { } or <?php const hogehoge = 0;
ざっくりとわかっている条件を列挙する
- 1.二つのファイルにわかれている必要がある(include_onceするので当然だが)
- さらに読みこまれるファイルがカレントディレクトリにある場合は起こらない(パス周辺の条件がある?)
- 2.include_onceする対象は相対パス指定でないといけない(フルパス指定では起こらない)
- なお、当然だが、パスは通っている必要がある
- 3.include_onceするされる相対はclass or constを記述する(functionでは起こらない)
- const のばあい 下記のNoticeになるがFATALではないので動きはする
- Notice: Constant hogehoge already defined in /home/lib/hogehoge.php on line 2
- const のばあい 下記のNoticeになるがFATALではないので動きはする
- 4.関数内でinclude_onceしないと起こらない
- 5.関数内には必ず引数が必要で array() で初期化されている(引数が無い、初期化の値がNULLだとおこらない)
- また、その引数にはコールする際いかなる値も渡してはならない
つまり、5. の条件は不明だがそもそも関数内でinclude_onceされており、さらにパスがini_setなどによるパスの変化によって別ファイルが指定される可能性がある場合に起こっているようだ。
結論としては、発生条件は良く分からないが、下記の条件のいずれかを守れば「回避できそう」ということが分かりました。
1.関数内でinclude_once or require_once を呼ばない。
2.カレントディレクトリ以外のファイルの include_once or require_once はフルパスで指定する
3.どうしても関数内でinclude_once or require_once を呼ぶ場合、引数の初期化を行わないか、必ずarray()以外(NULL推奨)で初期化する
誤報APC の apc.include_once_override で動的な include_once 等が失敗する問題
とあるよくわからないエラーを調査したときPEARのMail.php の@ 指定を解除したところ下記のようなエラーが発生したのでちょいと調べて見た。
結果としては先に設定したAPCの apc.include_once_override = 1 の設定が悪さをしていた事が分かったので一応メモっておく
PHP Fatal error: include() [<a href='function.include'>function.include</a>]: Cannot redeclare class mail_smtp in /省略/Mail.php on line 52
ネット上をいろいろ探してみるといくつかのサイトで引っかかる既知の問題のようだが、PEARのMail.phpの下記のようなコードが駄目らしい。
static function &factory($driver, $params = array()) { $driver = strtolower($driver); @include_once 'Mail/' . $driver . '.php'; ...省略... }
具体的にはこのfactory関数は include_once で関数の引数として渡される文字列からinclude対象のファイル名を作っているのだが、include_once_override はこのような include_once の書き方に対応できないらしい。
まだきちんと原因を追ったのではないがおそらく、APCのinclude_once_overrideの利用時はinclude_onceはincludeと同じような動きをするようになってしまっており二重定義をして起こられている物と思われる。
また、これは動的なinclude_onceだけで起こっているようなので、静的にinclude_onceに続く文字列をキーにしてincludeするかしないかを決定していると考えると現象に説明がつく。(ほんと推測ではなくはソースを当たるべき)
とりあえずは、include_once_overrideを無効化して対処したが、上記のコードはおそらく、下記のようにすることで回避できると思われる。
static function &factory($driver, $params = array()) { $driver = strtolower($driver); switch ($driver) { case 'mail': include_once 'Mail/mail.php'; break; case 'sendmail': include_once 'Mail/sendmail.php'; break; case 'smtp': include_once 'Mail/smtp.php'; break; default: include_once 'Mail/' . $driver . '.php'; // include_once_override 非利用時用の保険 break; } ...省略... }
追記:
いろいろ調べたてサンプルコードを書いたところこちらの原因は誤報でした
動的に変数を使わずinclude_onceしてもこのエラーは起こるようです
もう少し調べたのち、改めてメモ書きます
PHPのAPCの利用によるメモリ使用量の削減について
きたーーーメモリ使用量減った!!
以前から悩んでいたPHPのメモリ使用量が高い問題だがようやく対処方法が分かった
php.iniに下記の設定を追加することで、とあるプログラムでは14MB食ってたのが4MBまで減少した(1プロセス当り)
apc.include_once_override = 1
apc.lazy_classes = 1
apc.lazy_functions = 1
この設定は関数やクラスの遅延読み込み的なことをする設定のようなので、実際には走らないコードを多く含むものについてはそこそこ効くと思われます。
なお、include_once_override の設定はコードの書き方によってはトラブルになることが多いとのことなので、使うならしばらくテスト環境などで寝かせておいてテストした方が良いかもしれません。
また、lazy_classes、lazy_functions は遅延読み込みだとすると性質上、ほんの少しだろうがパフォーマンスの劣化の副作用が伴う可能性があります。
PHPの /** から始まるコメントが、require_once実行時に無駄にメモリを食う問題
知り合いから教えてもらったのだが、PHPでは /** から始まるコメントが、require_once実行時に無駄にメモリを食う問題があるらしい・・・ということで調査してみた
まずは、確認用のコードとして test.php を書く
<?php echo ' peek usage'."\n"; echo 'pre = '.memory_get_peak_usage().' / '.memory_get_usage()."\n"; $x = '0123456789'; for ($i = 0; $i != 12; $i++) { $x = $x . $x; // 適当にメモリを使ってrequire_once 時に新たにメモリを確保する必要があるようにしておく } $before = memory_get_peak_usage(); echo 'before = '.$before.' / '.memory_get_usage()."\n"; require_once('hoge.php'); $after = memory_get_peak_usage(); echo 'after = '.$after.' / '.memory_get_usage()."\n"; echo 'after - before = '.($after - $before)."\n\n"; echo 'strlen = '.strlen($x)."\n";
<?php class hoge { /** * aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa * aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa * aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa * aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa * aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa */ public function hogehoge0() { return '01234567890123456789012345678901234567890123456789'; } public function hogehoge1() { return '01234567890123456789012345678901234567890123456789'; } public function hogehoge2() { return '01234567890123456789012345678901234567890123456789'; } }
・・・で、test.phpを実行
[user@host ~]$ php -v PHP 5.3.10 (cli) (built: Mar 13 2012 10:54:48) Copyright (c) 1997-2012 The PHP Group Zend Engine v2.3.0, Copyright (c) 1998-2012 Zend Technologies with Xdebug v2.1.4, Copyright (c) 2002-2012, by Derick Rethans [user@host ~]$ php test.php peek usage pre = 670656 / 639032 before = 703808 / 680448 after = 706328 / 685792 after - before = 2520 strlen = 40960 strlen = 40960
require_once の前後で2520バイトピークのメモリ量が増えている
次に、コメントを /** から /* に変更して再度実行してみる
[user@host ~]$ php test.php peek usage pre = 670656 / 639032 before = 703808 / 680448 after = 705832 / 685296 after - before = 2024 strlen = 40960
require_once の前後で2024バイトピークのメモリ量が増えている
次は、 // コメントに変えてみる
[user@host ~]$ php test.php peek usage pre = 670656 / 639032 before = 703808 / 680448 after = 705832 / 685296 after - before = 2024 strlen = 40960
こちらも、require_once の前後で2024バイトピークのメモリ量が増えている
結果として /** 形式の場合、 /* や // の形式に比べて 496バイトほど無駄にメモリを使っている様子が見て取れ、たしかに/** から始まるコメントが、require_once実行時に無駄にメモリを食う問題が存在するようである。
ちなみに、/** の場合に、コメント自体の長さを増やすとどうなるのかさらに追試してみたところ、一文字増やすごとに増えるわけではないが、文字数が多ければ多いほど無駄にメモリを食っている様が見て取れた
・・・以上検証終わり
たぶん、require_once するときにプリプロセッサで // と /* のコメントは無視しているのに、なぜか /** は無視されないようになっている気がする
なお、requireや、include_once、includeでも起こるかどうかは未検証
・・・PHPのソースを負ったわけではないのであくまで「気がする」
・・・だれかバグか仕様かだけでも調査してくれる人いないかなー
追記(2012-08-16 22:25):
ブックマークに id:iakio さんよりコメントが有りました
下記の通り、PHPのリフレクションでドキュメントコメントを取得するものがあるそうで、 /** のコメントはその戻り値として用いられるため、この現象は仕様ということで良いようです。
http://jp.php.net/manual/ja/reflectionclass.getdoccomment.php
仕様ということですので、一応 タイトルの「問題」には取り消し線を入れておきます。