Python | DB・SQL:接続プール

Python
スポンサーリンク

接続プールって何?まずはイメージから

接続プール(コネクションプール)は、
「データベースへの接続を、使い回すための“待機列(プール)”」です。

DB への接続は、実はかなり重い処理です。
毎回「接続を開く → クエリする → 接続を閉じる」をやっていると、
接続を開くコストだけでアプリがどんどん遅くなります。

そこで、

あらかじめ何本か接続を作っておく
使うときはプールから 1 本借りる
使い終わったら閉じずにプールに返す

という仕組みが「接続プール」です。
「毎回新しく作る」のではなく「作っておいたものを再利用する」ことで、
パフォーマンスと安定性を両方上げるための仕組みだと思ってください。


なぜ接続を毎回作ると遅くなるのか

DB 接続は「重いネットワーク処理」

例えば、PostgreSQL や MySQL に接続するとき、内部ではこんなことが起きています。

サーバーとの TCP 接続を張る
認証(ユーザー名・パスワード・権限チェック)を行う
セッションの初期化(設定・トランザクションモードなど)を行う

これらは全部「そこそこ重い処理」です。
1 回だけなら気になりませんが、
Web アプリでリクエストごとに毎回新規接続していたら、
「接続を張るだけの時間」が積み重なって、レスポンスがどんどん重くなります。

「接続を開く・閉じる」を繰り返すコードのイメージ

例えば、こんなコードを想像してください。

import psycopg2

def get_user(user_id: int):
    conn = psycopg2.connect("postgres://...")
    cur = conn.cursor()
    cur.execute("SELECT id, name FROM users WHERE id = %s", (user_id,))
    row = cur.fetchone()
    cur.close()
    conn.close()
    return row
Python

この関数を 1 秒間に何十回・何百回も呼ぶと、
毎回 connect() が走り、そのたびに接続確立のコストがかかります。

「クエリ自体は軽いのに、接続のせいで遅い」
という、もったいない状態になりがちです。


接続プールの基本的な考え方

「接続を再利用する」という発想

接続プールは、発想をこう変えます。

必要になるたびに新しく接続を作る
→ あらかじめ接続をいくつか作っておいて、使い回す

イメージとしては、

プールの中に「接続オブジェクト」が何本か並んでいる
アプリが「接続が欲しい」と言うと、プールから 1 本貸し出される
使い終わったら「close」ではなく「プールに返却」される
別のリクエストが来たら、その返却済み接続をまた使う

という流れです。

これにより、

接続確立のコストを何度も払わなくて済む
同時に使える接続数を制御できる(DB に負荷をかけすぎない)

というメリットが得られます。


SQLAlchemy での接続プールのイメージ

Engine が接続プールを持っている

SQLAlchemy では、create_engine を呼んだときに、
裏で「接続プール」が自動的に用意されます。

from sqlalchemy import create_engine

engine = create_engine(
    "postgresql+psycopg2://user:password@localhost:5432/appdb",
    echo=True,
)
Python

この engine が、接続プールを内蔵しています。
Session や engine.connect() を通してクエリを投げるとき、
実際にはこのプールから接続が貸し出され、使い終わるとプールに戻されます。

つまり、SQLAlchemy を普通に使っているだけで、
「接続プールあり」の世界にすでに乗っている、ということです。

プールのサイズや挙動を指定する

接続プールの設定は、create_engine の引数で変えられます。

engine = create_engine(
    "postgresql+psycopg2://user:password@localhost:5432/appdb",
    pool_size=5,
    max_overflow=10,
    pool_timeout=30,
    pool_recycle=1800,
)
Python

ここでよく使うのは次のようなイメージです。

pool_size
プールに常に保持しておく接続の本数(例:5 本)

max_overflow
pool_size を超えて、一時的に増やしてよい接続の上限(例:+10 本)

pool_timeout
プールから接続を借りようとして空きがないとき、何秒待つか

pool_recycle
接続を何秒ごとに作り直すか(長時間使いっぱなしによる問題を避けるため)

Web アプリでは、「同時に何リクエストくらい来るか」「DB がどれくらい耐えられるか」を考えながら、
このあたりの値を調整していきます。


Web アプリでの接続プールの使われ方イメージ

「1 リクエスト=1 セッション」+「セッションの裏に接続プール」

FastAPI や Flask などで SQLAlchemy を使うとき、
よくあるパターンはこうです。

Engine(接続プール付き)をアプリ全体で 1 個作る
リクエストごとに Session を 1 個作る
Session は内部で Engine の接続プールから接続を借りる
リクエスト処理が終わったら Session を閉じる(接続はプールに返る)

コードのイメージはこんな感じです。

from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker

engine = create_engine("postgresql+psycopg2://...", pool_size=5, max_overflow=10)
SessionLocal = sessionmaker(bind=engine)

def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()
Python

ここで db.close() と書いていますが、
実際には「接続を完全に破棄」ではなく「プールに返却」です。

つまり、

アプリ全体で接続プールを共有しつつ
各リクエストは「セッション」という単位で DB と会話する

という二段構造になっています。


接続プールがないとどうなるか(極端な例)

毎回新規接続する Web アプリを想像してみる

もし接続プールを使わず、
リクエストごとに毎回 connect() して close() していたらどうなるか。

1 秒間に 100 リクエスト来る
そのたびに DB への新規接続が 100 回発生する
DB サーバーは接続確立と認証でヘトヘトになる
アプリ側も接続待ちでレスポンスが遅くなる

さらに、DB 側には「同時接続数の上限」があります。
PostgreSQL なら max_connections、MySQL なら max_connections などです。

接続プールなしで無制限に connect() しまくると、
この上限にぶつかって「これ以上接続できません」というエラーになります。

接続プールは、

接続の再利用で速くする
同時接続数を制御して DB を守る

という 2 つの役割を同時に果たしてくれます。


接続プールで意識しておきたいポイント

「プールを増やせばいい」は罠

「遅いなら pool_size を増やせばいいんでしょ?」
と考えたくなりますが、これは半分正解で半分罠です。

プールを増やすと、

同時に処理できるリクエスト数は増える
でも DB にかかる負荷も増える

というトレードオフがあります。

DB サーバーの CPU・メモリ・ディスク I/O が耐えられる範囲を超えてしまうと、
逆に全体が遅くなったり、タイムアウトが増えたりします。

だから、接続プールのサイズは、

アプリ側の同時リクエスト数
DB サーバーのスペック
他のサービスとの兼ね合い

を見ながら、少しずつ調整していくのが現実的です。

「プールを使っている前提」でコードを書く

SQLAlchemy を使うときは、
基本的に「Engine はアプリ全体で 1 個」「Session は短命」という設計にします。

毎回 create_engine していると、
そのたびに新しい接続プールができてしまい、
プールの意味が薄れてしまいます。

よくあるアンチパターンはこれです。

def get_user(user_id: int):
    engine = create_engine("postgresql+psycopg2://...")
    SessionLocal = sessionmaker(bind=engine)
    session = SessionLocal()
    user = session.query(User).get(user_id)
    session.close()
Python

create_engine を関数の中で呼んでしまうと、
呼ぶたびに新しいプールが作られます。

正しくは、engineSessionLocal はモジュールレベル(アプリ起動時)で 1 回だけ作り、
リクエストごとに SessionLocal() でセッションだけを作る、という形にします。


まとめ(接続プールは「DB 接続を賢く共有するための土台」)

接続プールを初心者目線で整理すると、こうなります。

DB への接続は重いので、毎回新しく作るのではなく、「あらかじめ作っておいた接続をプールして再利用する」仕組みが接続プール。
SQLAlchemy の create_engine は、デフォルトで接続プールを持っていて、Session や engine.connect() はそのプールから接続を借りて使う。
Web アプリでは「Engine(プール)はアプリ全体で 1 個」「リクエストごとに Session を作って、終わったら閉じてプールに返す」という設計が基本。
プールサイズを増やせばいいわけではなく、DB の限界や他サービスとのバランスを見ながら調整する必要がある。

タイトルとURLをコピーしました