MySQLでINSERTが遅い
おそい!おそい!おそい!
ノートパソコンにDockerでコンテナを立ててろくでもないプログラムを開発しているのですが、データを格納するためにデータベースを使うことが多いです。
でもですね、MySQLのINSERT
がすごくおそいよ…。とんでもなく遅いです。
遅くなる原因はいろいろあると思うのですが、素人なりにちょっとした検証をしてみました。
タイトルから結論はお分かりになるかと思いますが、もしかしたらなんかの参考になるかもしれません。
ぜひ暇つぶしにどうぞ。
現状の確認
なにが遅いって言われてもよくわからないでしょうから、実際に遅さを晒してみたいと思います。
テスト環境は以下。
- MySQL稼働環境
- Docker公式MySQLコンテナ(5.7.12)
- Docker公式PHPコンテナ(7.0.6-apache)
- コンテナにメモリ上限・CPU上限は設定していません
- Dockerホスト
- OS:GUN/LINUX Debian8
- CPU: Pentium B980(泣)
- メモリ:6GB
- HDD:250GB
以下のようなコードを書いていろいろ動かしてみようと思います。
ランダムで生成した文字列と数列をプリペアードステートメントでINSERT
する単純なコードです。簡単な経過時間タイマーを挟んでINSERT
の経過時間を測れるようにしています。
少ない量のINSERT
ではどんな感じになるかです。100件からテストぉ~。
インサートに3.9636571407318秒かかりました
100件で4秒かかる…遅いよね?これ。100件でこれだから先が思いやられるぜ。
では1000件いってみよう。
インサートに41.069952011108秒かかりました
ああ…。大体10倍じゃないか…。なんならちょっとおまけ付いているし。なんかもう10000件も結果は目に見えているな…。次は400秒ですか?すでにカップラーメン待機時間 を優に越してますよ?
では10000件です…。
インサートに429.5260488987秒かかりました
まじかよ。400秒超えちゃっているよ。件数が増えるごとにマシマシじゃないかい。こんなに時間掛かるの?ウル(ry…。
さてやりたくもないですが、一応100000件をやっておきましょう…。間違いなく一時間以上かかるよね…。
インサートに4290.681718111秒かかりました
10000件のまんま10倍じゃんか。もう少し増えるのかと思ったけど。
さて一応各件数を10回ずつやってみて平均を出してみました。100000件10回やったら10時間以上かかりましたよ…。
ただどの件数もほとんど毎回、秒数が変わらずブレることがなかったです。そういう意味では安定していますね。
改めて表にしてみました。
INSERT件数 | 経過時間(10回平均) | 100件時経過時間を基準にした場合のINSERT1件毎コスト |
---|---|---|
100件 | 3.98秒 | 100% |
1000件 | 41.0秒 | 103% |
10000件 | 429.6秒 | 107% |
100000件 | 4290.6秒 | 108% |
こうしてみると件数が増えると、少しずつINSERT
1件あたりのコストが増加する傾向があるようですね。
これって件数が増すごとに効率が悪くなるということですよね。メモリの使用量とかに関係あるのかな?
時間が掛かる理由を考察
素人の邪推ですので無能考察ですが、こんなに遅くては考察せずにいられません。
まずlog
を確認してみたいと思います。
実はmy.cnf
に以下の記述をいれてlog
をとっていました。
general_log = 1
general_log_file = /var/lib/mysql/general.log
これは「一般クエリーログ」というもので、MySQLが内部的に処理したSQL
を記録してくれるlog
です。
MySQLには別にスロークエリログという、遅い(重い?)クエリを記録するlog
もありますが、今回は1件1件のINSERT
が遅いわけではないので、用途としてはあまり役に立たなかったので…。
一般クエリーログの中身
さて一般クエリーログには実行したクエリがズラッと並びます。
2016-04-29T03:54:48.397347Z 120 Connect root@172.17.0.3 on testdb using TCP/IP
2016-04-29T03:54:48.397806Z 120 Query CREATE TABLE IF NOT EXISTS testdb.inserttest (
id int NOT NULL AUTO_INCREMENT PRIMARY KEY,
cipher text COLLATE utf8mb4_general_ci NOT NULL,
numbers int NOT NULL
) ENGINE=InnoDB
2016-04-29T03:54:48.398189Z 120 Query TRUNCATE TABLE testdb.inserttest
2016-04-29T03:54:48.589870Z 120 Query INSERT INTO testdb.inserttest (cipher, numbers) VALUE ('ubkxvhzz', 49968446)
2016-04-29T03:54:48.623271Z 120 Query INSERT INTO testdb.inserttest (cipher, numbers) VALUE ('wuqlbxwy', 35468813)
2016-04-29T03:54:48.656612Z 120 Query INSERT INTO testdb.inserttest (cipher, numbers) VALUE ('mxsnftyh', 71472222)
2016-04-29T03:54:48.733247Z 120 Query INSERT INTO testdb.inserttest (cipher, numbers) VALUE ('wdgdyqgm', 26972414)
2016-04-29T03:54:48.801567Z 120 Query INSERT INTO testdb.inserttest (cipher, numbers) VALUE ('jgiubvxg', 46285974)
2016-04-29T03:54:48.835137Z 120 Query INSERT INTO testdb.inserttest (cipher, numbers) VALUE ('wgrcquak', 87428660)
2016-04-29T03:54:48.868555Z 120 Query INSERT INTO testdb.inserttest (cipher, numbers) VALUE ('irtqsxwt', 33006945)
2016-04-29T03:54:48.902010Z 120 Query INSERT INTO testdb.inserttest (cipher, numbers) VALUE ('esudinex', 53510753)
2016-04-29T03:54:48.935468Z 120 Query INSERT INTO testdb.inserttest (cipher, numbers) VALUE ('gyahveex', 79988357)
2016-04-29T03:54:48.969347Z 120 Query INSERT INTO testdb.inserttest (cipher, numbers) VALUE ('sxumycqx', 16392670)
2016-04-29T03:54:49.002476Z 120 Query INSERT INTO testdb.inserttest (cipher, numbers) VALUE ('nvtabgun', 50932736)
2016-04-29T03:54:49.036079Z 120 Query INSERT INTO testdb.inserttest (cipher, numbers) VALUE ('wkrdiimq', 68907608)
2016-04-29T03:54:49.069494Z 120 Query INSERT INTO testdb.inserttest (cipher, numbers) VALUE ('vyfumwml', 75924991)
2016-04-29T03:54:49.103361Z 120 Query INSERT INTO testdb.inserttest (cipher, numbers) VALUE ('kacigmxh', 32306552)
2016-04-29T03:54:49.136339Z 120 Query INSERT INTO testdb.inserttest (cipher, numbers) VALUE ('udxdobbd', 21351223)
2016-04-29T03:54:49.174059Z 120 Query INSERT INTO testdb.inserttest (cipher, numbers) VALUE ('urdxpgtb', 63325929)
2016-04-29T03:54:49.214546Z 120 Query INSERT INTO testdb.inserttest (cipher, numbers) VALUE ('sqjgdgrh', 51020142)
2016-04-29T03:54:49.248166Z 120 Query INSERT INTO testdb.inserttest (cipher, numbers) VALUE ('awckumft', 95404717)
2016-04-29T03:54:49.281490Z 120 Query INSERT INTO testdb.inserttest (cipher, numbers) VALUE ('orokhsfw', 74060032)
2016-04-29T03:54:49.314998Z 120 Query INSERT INTO testdb.inserttest (cipher, numbers) VALUE ('gzcritfy', 71167886)
2016-04-29T03:54:49.348426Z 120 Query INSERT INTO testdb.inserttest (cipher, numbers) VALUE ('dtosxtti', 43605821)
2016-04-29T03:54:49.381899Z 120 Query INSERT INTO testdb.inserttest (cipher, numbers) VALUE ('wcgjrdgn', 52963458)
2016-04-29T03:54:49.415379Z 120 Query INSERT INTO testdb.inserttest (cipher, numbers) VALUE ('fuhtxdfw', 95560976)
2016-04-29T03:54:49.448864Z 120 Query INSERT INTO testdb.inserttest (cipher, numbers) VALUE ('adrwtlqi', 52981588)
2016-04-29T03:54:49.482252Z 120 Query INSERT INTO testdb.inserttest (cipher, numbers) VALUE ('badzlypr', 97461706)
軽く説明ですが、クエリ実行時間、コマンドID(例では120っての)実行したquery
の順で並んでいます。
例では最初にConnect
があって、これはPDO
の
$pdo = new PDO($dns, $usr, $pas);
に相当する箇所です。その次にテーブル作成のSQL
とTRUNCATE
のSQL
が実行されています。これは今回の経過時間とは関係が無い部分です。
その次からがINSERT
がズバッと続いています。
INSERT INTO testdb.inserttest (cipher, numbers) VALUE ('badzlypr', 97461706)
for
文でぶん回したquery
がぞくぞくと実行されています。これが今回の経過時間で計っている部分です。
データを検証
さてそれでは集計しやすく量もそれなりな1000件INSERT
時のlog
を元に少し詳しく見てみます。
まず1件あたりのINSERT
のコストの集計です。
最大値 | 最小値 | 平均値 | 中央値 |
---|---|---|---|
0.5697106481秒 | 0.0278553241秒 | 0.0480243067秒 | 0.0391620370秒 |
最大値とんでもないことになっていますな…。逆に最小値は平均や中央値と比べるとそんなに小さくない感じです。
どうやら@MINOのシステムでは0.027秒/クエリ
というのが速さの限界値なのかもしれません。
さて1000件のデータをのせるのは縦に長くなりすぎるので、グラフにて。
@MINOはもっと平坦なグラフを予想していたのですが、随分特徴のあるグラフになっています。
スパイクの存在
まず1つめとしてスパイクになっている部分が結構ある点です。
最大値を見ていただければわかる通り、えらい時間がかかっているINSERT
があります。そしてそれは後半につれて多くなっている様に見えます。
INSERT
の量が増えるとこのようなスパイクの発生も増えるということのようで、件数が増えることにコストが増加することが視覚的にもわかった気がします。
矩形波様の推移
2つめですが、このグラフは矩形波にとても似ています。
@MINO趣味でシンセサイザーを弾いたりするのですが、矩形波は凸凹
と規則正しい波形で、ポーっというような感じの音がなりますよ。木管楽器の波形は矩形波に近いとか言われています。関係ないですけど。
この矩形波パターンがなぜ起きるのかはわかりませんでした。たまたま@MINOの環境でこういう結果になっただけなのかもしれませんし、こういった挙動が普通なのかもしれません。
非常に規則的で視覚的にはスパイクとは別に存在している特徴のように思えます。ディスクの書き込みとかもこういった感じの速度分布になるようなグラフを見たことがあるような無いような…。
低い部分平均 | 高い部分平均 |
---|---|
0.0398~0.041 | 0.05 |
別環境での検証
さて@MINOはの腐ったパソコンだけでは検証にならんので、レンタルサーバでもやってみます。Conohaクラウドを使ってみました。
- MySQL稼働環境
- Docker公式MySQLコンテナ(5.7.12)
- Docker公式PHPコンテナ(7.0.6-apache)
- コンテナにメモリ上限・CPU上限は設定していません
- Dockerホスト
- Conoha
- CPU2コア
- メモリ1GB
- SSD50GB
コードは先ほどと同じです(接続情報以外は)
とりあえず100件
0.092894077301025秒かかりました
ん?ものすごく早いぞ?
1000件では
0.93638491630554秒かかりました
あう…。速度ぜんぜんちゃうやんけ…。1000件でも1秒掛かってないぞ…。@MINOの腐れノートパソコンでは1000件が40秒ですから、40倍以上の速度ですな。
10000件では
9.6642940044403秒かかりました
速度差は量が増えても変わらないですな。
100000件では
100.56400489807秒かかりました
@MINOの腐れノートパソコンでは1時間以上かかっていたのに…。
件数毎の結果をまとめました。
INSERT件数 | 経過時間(10回平均) | 100件時経過時間を基準にした場合のINSERT1件毎コスト |
---|---|---|
100件 | 0.0928秒 | 100% |
1000件 | 0.9363秒 | 100.7% |
10000件 | 9.6642秒 | 104% |
100000件 | 100.5640秒 | 108% |
件数が増える毎に1件毎INSERT
のコストが増えるのはさっきと同じのようです。
ただ@MINOのノートパソコンと比べると雲泥の差ですね。
データを検証
さっきと同じように1000件INSERT
時のlogを検証してみます。
1件あたりのINSERT
のコストの集計です。
最大値 | 最小値 | 平均値 | 中央値 |
---|---|---|---|
50.0462963015647ナノ秒 | 5.05787037252681ナノ秒 | 10.8325609481811ナノ秒 | 10.9490740918261秒 |
先ほどでは単位が「秒」でしたが、今回は「ナノ秒」ですのでご注意を。平均で10.8ナノ秒でINSERT
を処理しているようです。
さてグラフです。
スパイクの量が少ない
見るからにスパイクの量が少ないですね。グラフ自体はギザギザしていますが、パッと見のスパイクは3~4個くらいです。
また先ほど@MINOのノートと違い、後半にスパイクが多くなるという傾向も見られません。
かなり速度を保っている印象です。
あんまり特徴がない
先ほどのグラフより特徴的ではないですね。さっきのはかなり特徴的に矩形波様になっていましたので、ちょっと期待していた感じとは違いました。
なんとなく視覚的には周期があるようなないような、矩形波様にみえなくもないですが…。
気のせいですか。
ただ@MINOのノートの時の矩形波様のグラフはあくまで@MINOのノートパソコン上での特徴のようですね。
あの矩形波がこちらでも顕著にでたらおもしろかったのですが(なにが?)…
ありきたりな結論
おそらくなんですが、INSERT
の速さはストレージの性能が大きく影響しているように感じます。
顕著に違うのってそこなんですよね。
@MINOの腐れノートパソコンのストレージは旧来の5400rpmのHDD、一方ConohaのデフォルトストレージはSSDです。
普通HDDと比べてSSDは書き込みで数倍~数十倍の速さが出るものです。それがそのまま速度差として出ていてもおかしくはないですよね。
今回の検証は元も子もないかもしれませんが「SSDが早い!!」という当たり前の結果なのかもしれません。
もちろん他にも速くなっている要素があると思います。ただメモリはConohaで1GB、@MINOのノートでは6GB積んでいます。ノートパソコンでは他のアプリもいくつか動いていますが、少くともConohaの方が使えるメモリが多いというわけではありません。
CPUに関してはちょっとベンチマークの仕方がわからなかったので比べていませんが、まさか40倍の差があるとは思えません。(あるの?)
やっぱりストレージの性能差なんだろうな~と一人で納得しています。
まとめ
いやしかし40倍の速度差はかなりビビりました。
今回はMySQLの設定やSQL
の書き方でチューニングを行い速度を得るような内容にならなかったので、がっかりした方もおられるかもしれませんが、検証したように、システムが違うとこれほどに差が出る場合があるということは@MINOには収穫でした。
プログラミングのテスト実行環境もクラウドに移すかと考えている次第です。
だってクラウド借りるの月1000円くらいだもの。それで40倍だもの。
そもそも物理的な速度には抗えないので、まずはハードウエアが最適なのか、次にそのハードを最大限使い切るという心構えが必要ですね。
次回(になるかわかりませんが)はSQL
の書き方で速度を確保する方法を試してみたいと思います。