コネクションプールとまとめ
シリーズの最終回は、クエリのパフォーマンスとは少し異なる視点——接続管理を取り上げます。どれだけクエリを最適化しても、接続確立のオーバーヘッドが大きければパフォーマンスは頭打ちになります。 コネクションプール? データベース接続を使い回す仕組み。接続確立には時間がかかるため、あらかじめ接続を作っておき、リクエスト毎に貸し出す。接続数の上限管理にも役立つ。 を正しく設計することで、スループットを大幅に向上できます。
データベース接続のコスト
MySQL へ接続するたびに以下の処理が行われます。
- TCPの3ウェイハンドシェイク
- MySQL の認証プロトコル(ユーザー名・パスワードの検証)
- セッションの初期化(文字セット、タイムゾーン等の設定)
この処理は数ミリ秒〜数十ミリ秒かかります。1リクエストごとに接続と切断を繰り返すと、クエリ本体より接続コストのほうが高くつくケースもあります。
// NG: リクエストのたびに接続・切断を繰り返す
function handleRequest(): void {
$pdo = new PDO('mysql:host=db;dbname=app', 'user', 'pass');
$result = $pdo->query("SELECT ...");
$pdo = null; // 切断
}
コネクションプールの仕組み
コネクションプール? データベース接続を使い回す仕組み。接続確立には時間がかかるため、あらかじめ接続を作っておき、リクエスト毎に貸し出す。接続数の上限管理にも役立つ。 は、あらかじめ一定数の接続を確立してプールに保持し、アプリケーションからのリクエストに対して接続を貸し出す仕組みです。
アプリ → プールに接続を要求
プール → アイドル接続があればそれを貸し出す
なければ新規接続を確立(max_pool_size に達していれば待機)
クエリ実行
アプリ → 接続をプールに返却(閉じずに再利用)
接続の確立コストは初回のみで、以降は再利用されます。
PHP/Laravel での設定例
// config/database.php
'mysql' => [
'driver' => 'mysql',
'host' => env('DB_HOST', '127.0.0.1'),
'database' => env('DB_DATABASE', 'forge'),
// ...
'options' => [
PDO::ATTR_PERSISTENT => true, // 接続の永続化
],
],
Laravel では PgBouncer(PostgreSQL向け)や ProxySQL(MySQL向け)などの外部コネクションプーラーと組み合わせることも一般的です。
Node.js(mysql2)での設定例
const pool = mysql.createPool({
host: 'localhost',
user: 'root',
database: 'app',
connectionLimit: 10, // 最大接続数
waitForConnections: true, // 接続枯渇時に待機するか
queueLimit: 0, // 待機キューの上限(0=無制限)
});
// プールから接続を取得してクエリ実行
pool.query('SELECT ...', (err, results) => {
// 完了後に自動的にプールに返却される
});
プールサイズの設計
「プールサイズは大きいほど良い」は誤りです。MySQL はスレッドモデルで動作するため、接続数が増えるほどスレッドの切り替えコストが増加し、ある閾値を超えるとスループットが下がります。
HikariCP の公式推奨式(Java向けだが考え方は普遍的)
connections = ((core_count * 2) + effective_spindle_count)
core_count:CPUのコア数effective_spindle_count:ディスクのスピンドル数(SSDなら1)
4コアCPU + SSDの場合:(4 * 2) + 1 = 9 前後が目安です。
実際には負荷テストで計測しながら調整します。まずは小さめから始めて増やすのが安全です。
接続タイムアウトの設定
MySQL 側の設定
-- アイドル接続の生存時間(デフォルト8時間)
SHOW VARIABLES LIKE 'wait_timeout';
SET GLOBAL wait_timeout = 600; -- 10分
-- インタラクティブ接続のタイムアウト
SET GLOBAL interactive_timeout = 600;
アプリケーション側の設定
// Laravel の場合
'mysql' => [
// ...
'options' => [
PDO::ATTR_TIMEOUT => 5, // 接続タイムアウト 5秒
],
// 接続が切れていた場合のリトライ設定
'reconnect' => true,
],
wait_timeout より長くプールに保持していると「MySQL server has gone away」エラーが発生します。プールの keepalive や接続確認クエリ(SELECT 1)を設定して古い接続を検出・再確立する仕組みを入れましょう。
シリーズ全体のまとめ
8回にわたって学んできた内容を振り返ります。
ep01:計測からはじめる
- スロークエリ? 実行に時間がかかるSQLクエリのこと。MySQLのスロークエリログで記録・分析できる。ボトルネックの特定に不可欠で、`long_query_time` で閾値を設定する。 ログで遅いクエリを特定する
- pt-query-digest で影響度の大きいクエリに優先順位をつける
- 「計測→分析→改善→再計測」のサイクルを回す
ep02:EXPLAINで見える化する
- EXPLAIN? MySQLがSQLをどのように実行するかの「実行計画」を表示するコマンド。インデックスが使われているか、何行スキャンしているかなどを確認でき、チューニングの出発点になる。 の
type列は最重要(ALLを撲滅する) rowsの数が大きいほど改善余地があるUsing filesort/Using temporaryは要注意サイン
ep03:B-treeインデックスの基礎
- B-treeインデックス? MySQLのデフォルトのインデックス構造。バランス木(Balanced Tree)で値を管理し、等値検索・範囲検索・ORDER BY の高速化に有効。ほとんどのユースケースでこれを使う。 は O(log n) の検索を実現する
- InnoDB のクラスタインデックスとセカンダリインデックスの違いを理解する
- カバリングインデックス? クエリで必要な全カラムがインデックスに含まれていて、テーブル本体を参照せずに結果を返せる状態。EXPLAINのExtraに`Using index`と表示される。非常に高速。 が
Using indexで最速
ep04:インデックスが効かないパターン
- カラムへの関数適用・型変換でインデックスが無効化される
LIKE '%前方%'はフルスキャン- 複合インデックス? 複数のカラムを組み合わせて作成するインデックス。`(last_name, first_name)` のように定義すると、先頭列から順に検索する場合に効果的。列の順序が重要。 は最左プレフィックスルール
ep05:N+1問題の根絶
- N+1問題? メインクエリで N 件取得した後、ループ内で N 回追加クエリを実行してしまうアンチパターン。100件の注文を取得して各注文のユーザーを1件ずつ取得すると101クエリになる。JOINかサブクエリで解消する。 はクエリ本数の問題
- JOIN? 複数のテーブルを結合して一つの結果セットにする操作。INNER JOIN(両方に存在する行のみ)・LEFT JOIN(左テーブルの全行 + 一致した右テーブル)などがある。 またはIN句で一括取得する
- ORMのEager Loadを活用する
ep06:クエリチューニング
SELECT *を避け必要な列だけ取得する- 相関サブクエリをJOINに書き直す
- OFFSETページネーションをKeyset方式に変える
ep07:キャッシュ戦略
- クエリキャッシュ? 同じSELECT文の結果をメモリに保存して再利用する仕組み。まったく同じクエリが繰り返し来る場合に高速化できる。アプリ層(Redis等)でのキャッシュが一般的。 (廃止)の代わりにアプリケーション層でキャッシュする
- Cache-Aside・Read-Through・Write-Throughを使い分ける
- TTLはデータの更新頻度に合わせて設計する
ep08:コネクションプール
- 接続確立コストを コネクションプール? データベース接続を使い回す仕組み。接続確立には時間がかかるため、あらかじめ接続を作っておき、リクエスト毎に貸し出す。接続数の上限管理にも役立つ。 で吸収する
- プールサイズは大きすぎず、CPUコア数ベースで設計する
パフォーマンスチューニングチェックリスト
計測・診断
- スロークエリログを有効化し、
long_query_time = 1で計測している - pt-query-digest で合計影響時間の大きいクエリを特定している
-
EXPLAINでtype: ALLのクエリがないか確認している
インデックス
-
WHERE・JOIN ON・ORDER BYで使うカラムにインデックスがある - カラムに関数を適用した WHERE 条件がない
- 型変換が発生する比較がない
- 複合インデックスの先頭列を省略した条件がない
クエリ品質
-
SELECT *を使っていない - ループ内でクエリを発行していない(N+1がない)
- 相関サブクエリをJOINに書き直している
- OFFSET ページネーションをKeyset方式に変更している
アーキテクチャ
- 変更頻度の低いデータにキャッシュを適用している
- キャッシュの無効化タイミングが正しく設計されている
- コネクションプールのサイズが適切に設定されている
-
wait_timeoutとプールのkeepaliveが整合している
このチェックリストを定期的に見直すことで、パフォーマンスの問題を早期に発見・解消できます。クエリのパフォーマンスはシステムの成長とともに変化します。定期的な計測と改善を習慣にしてください。