#08 データベースの高速化

コネクションプールとまとめ

シリーズの最終回は、クエリのパフォーマンスとは少し異なる視点——接続管理を取り上げます。どれだけクエリを最適化しても、接続確立のオーバーヘッドが大きければパフォーマンスは頭打ちになります。 コネクションプール? データベース接続を使い回す仕組み。接続確立には時間がかかるため、あらかじめ接続を作っておき、リクエスト毎に貸し出す。接続数の上限管理にも役立つ。 を正しく設計することで、スループットを大幅に向上できます。

コネクションプールの仕組み アプリサーバー (ワーカースレッド) Worker 1 Worker 2 Worker 3 Worker 4 待機中... 接続要求 (空き待ち) コネクションプール 接続 #1 【使用中】 接続 #2 【使用中】 接続 #3 【使用中】 接続 #4 【アイドル】 接続 #5 【アイドル】 max_pool_size = 10 返却 TCP接続 (事前確立済み) MySQL Server Thread #1 ←接続#1 Thread #2 ←接続#2 Thread #3 ←接続#3 常時接続維持 プールなし(毎回接続)のコスト TCP 3ウェイハンドシェイク: 〜1ms MySQL認証: 〜2ms 合計: クエリより接続コストが高い場合も プールあり(再利用)のメリット 接続コスト: ほぼゼロ(確立済み) スループット: 大幅向上 DBサーバーの最大接続数も節約
図1: コネクションプールの仕組み

データベース接続のコスト

MySQL へ接続するたびに以下の処理が行われます。

  1. TCPの3ウェイハンドシェイク
  2. MySQL の認証プロトコル(ユーザー名・パスワードの検証)
  3. セッションの初期化(文字セット、タイムゾーン等の設定)

この処理は数ミリ秒〜数十ミリ秒かかります。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)を設定して古い接続を検出・再確立する仕組みを入れましょう。


シリーズ全体のまとめ

パフォーマンスチューニング全体マップ MySQL 本体DBサーバー ① 計測・発見 スロークエリログ パフォーマンススキーマ ⑥ 接続管理 コネクションプール タイムアウト設定 ② 実行計画分析 EXPLAIN / EXPLAIN ANALYZE type / rows / Extra 確認 ③ インデックス設計 B-tree / 複合インデックス カバリングインデックス ④ クエリ最適化 N+1解消 / JOIN活用 SELECT * 排除 / Keyset ⑤ キャッシュ戦略 Redis / Cache-Aside TTL設計 / 無効化戦略 チューニングの基本順序 ①計測 → ②分析 → ③インデックス → ④クエリ最適化 → ⑤キャッシュ → ⑥接続管理
図2: パフォーマンスチューニングの全体マップ

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 で合計影響時間の大きいクエリを特定している
  • EXPLAINtype: ALL のクエリがないか確認している

インデックス

  • WHEREJOIN ONORDER BY で使うカラムにインデックスがある
  • カラムに関数を適用した WHERE 条件がない
  • 型変換が発生する比較がない
  • 複合インデックスの先頭列を省略した条件がない

クエリ品質

  • SELECT * を使っていない
  • ループ内でクエリを発行していない(N+1がない)
  • 相関サブクエリをJOINに書き直している
  • OFFSET ページネーションをKeyset方式に変更している

アーキテクチャ

  • 変更頻度の低いデータにキャッシュを適用している
  • キャッシュの無効化タイミングが正しく設計されている
  • コネクションプールのサイズが適切に設定されている
  • wait_timeout とプールのkeepaliveが整合している

このチェックリストを定期的に見直すことで、パフォーマンスの問題を早期に発見・解消できます。クエリのパフォーマンスはシステムの成長とともに変化します。定期的な計測と改善を習慣にしてください。