71 人が閲覧(直近 30 日) Python

psycopg2とpsycopg3の違い|16の非互換とpsycopg[binary]の選び方・移行手順

psycopg2とpsycopg3の違い|16の非互換とpsycopg[binary]の選び方・移行手順

psycopg3への移行でつまずく箇所はほぼ決まっています。パッケージ名がpsycopgに変わったこと、パラメータの埋め込みがクライアント側からサーバー側へ移ったこと、そしてwith文が接続そのものを閉じるようになったこと。この3つはそれぞれ違う壊れ方をします。パッケージ名はModuleNotFoundError、パラメータ束縛はSETやDDLでの実行時エラー、with文は2周目に閉じた接続をつかむ形で表面化します。ここでは公式が「Differences from psycopg2」として挙げる16項目を移行で踏む順に並べ直し、psycopg2の非同期モードでできること・できないこと、SQLAlchemy 2.1で変わった既定ドライバ、一括INSERTの計測値までを、2026年10月時点のpsycopg 3.3.6とpsycopg2 2.9.13で確かめた結果として整理します。

まとめ:移行前に押さえる6点

  • パッケージ名はpsycopg。PyPIのpsycopg3は配布ファイルを1件も持たない予約枠なので、pip install psycopg3は必ず失敗する。psycopg2-binaryの置き換え先はpsycopg[binary]、psycopg2の置き換え先はpsycopg[c]。
  • パラメータはサーバー側で束縛される。そのためSET・NOTIFYとDDLでは%sが使えず、IN %sにタプルを渡す書き方も通らない。= ANY(%s)への書き換えかClientCursorへの切り替えで対処する。
  • with connは接続ごと閉じる。psycopg2ではトランザクションだけがコミットされ接続は生きていた。トランザクションを区切りたいだけならconn.transaction()を使う。
  • Python 3.9では両方とも最新版が入らない。psycopg 3.3とpsycopg2 2.9.13がそろってPython 3.10以上を要求するため、3.9の環境では警告もなくpsycopg 3.2.13・psycopg2-binary 2.9.12まで巻き戻る。
  • SQLAlchemy 2.1からpostgresql://の既定はpsycopg 3。psycopg2しか入れていない環境でSQLAlchemyを2.1へ上げるとModuleNotFoundError: No module named 'psycopg'で起動しなくなる。
  • psycopg2は終息していない。公式READMEは「広く使われており現在も保守されているが、新機能が入る予定はない」と明記している。移行の判断軸は「psycopg3にしかない機能が要るか」で、最大の理由はasyncio。psycopg2にもasync_=1の非同期モードはあるが、asyncioのイベントループには載らない。

以下、インストール、16の非互換、ORM経由での指定、接続プール、非同期、psycopg3で新設された機能の順に見ていきます。

psycopg3のインストール|正しいパッケージ名とbinary・c・純Pythonの選び分け

pip install psycopg3が失敗する理由とpsycopg[binary]への読み替え

psycopg3のディストリビューション名はpsycopg3ではなくpsycopgです。公式のインストール手順にも「Fun fact: there is no psycopg3 package, only psycopg!」という注記が入っています。PyPIにはpsycopg3という名前のプロジェクト自体は存在しますが、バージョンは0.1、classifierは「Development Status :: 1 – Planning」で、配布ファイルが1件も登録されていません。したがってpip install psycopg3を実行すると次のように落ちます。

$ pip install psycopg3
ERROR: Could not find a version that satisfies the requirement psycopg3 (from versions: none)
ERROR: No matching distribution found for psycopg3

正しい導入コマンドは次のとおりです。pip自体が古いとバイナリホイールを解決できないため、公式も先にpipの更新を指示しています(要件はpip 20.3以降)。

pip install --upgrade pip
pip install "psycopg[binary]"

binary・c・純Pythonの3択と選定基準

psycopgのextrasはc・binary・pool・test・dev・docsの6種類で、このうちインストール方式を決めるのがbinaryとcです。公式ドキュメントが示す3方式の違いは次のとおりです。

方式 コマンド 速度 libpqの事前導入 ビルドツール psycopg2での対応物
バイナリ pip install “psycopg[binary]” 速い 不要 不要 psycopg2-binary
ローカルビルド pip install “psycopg[c]” 速い 必要 必要 psycopg2
純Python pip install psycopg 遅い 必要 不要 該当なし

公式は大多数のユーザーにbinaryを推奨し(表の見出しが「Binary installation (recommended for most users)」)、本番サイトに限ってcを推奨しています(「This is the preferred way to install Psycopg for a production site.」)。理由は、システムのlibpqやlibsslに動的リンクされるため、OS側のセキュリティ更新がそのままpsycopgにも効くからです。バイナリ版はlibpqを同梱するので、OSを更新してもpsycopg内部のライブラリは古いままになります。

ただしcにはCコンパイラ、Pythonの開発ヘッダ(python3-devなど)、libpq-dev、PATH上のpg_configという4つの前提が要ります。公式は拡張のビルドエラーを自力で読めることも条件に挙げており、CIやコンテナでビルドが詰まるならpsycopg[binary]へ切り替えるのが早道です。もう一つ注意が要るのがApple Siliconで、psycopg 3.1.20以降、ARM64 macOS向けのバイナリパッケージはmacOS 14.0未満に対して提供されなくなりました。13以前のmacOSではOSを上げるか、ローカルビルドか純Pythonに切り替えます。PyPyはバイナリ版とローカルビルドがサポート外で、純Pythonのみです。

対応バージョンの範囲とPython 3.9で起きる巻き戻り

psycopg 3.3系の対応範囲は、Pythonが3.10から3.15(3.15対応は2026年9月18日の3.3.6で追加)、PyPyが3.10から3.11、PostgreSQLが10から18です。見落としやすいのがPythonの下限で、公式は「Python 3.8 and 3.9 are supported before Psycopg 3.3」と書いています。psycopg2も2.9.13のリリースノートに「Drop support for Python 3.9.」とあり、2026年9月9日公開の2.9.13からrequires_pythonが3.10以上になりました。どちらも3.9でpip install自体は成功するので、入る版だけが黙って古くなります。

実行環境 psycopg psycopg_pool psycopg2-binary Cursor.results()
Python 3.14.6 3.3.6 3.3.3 2.9.13 あり
Python 3.9.6 3.2.13 3.2.8 2.9.12 なし

上の値は、同じコマンドpip install "psycopg[binary,pool]" psycopg2-binaryを2つのPythonのvenvで実行し、pip listで読んだ結果です(2026年10月13日)。エラーも警告も出ないため、「ドキュメントどおりに書いたのにcur.results()が無い」という形で初めて気づくことになります。macOSの/usr/bin/python3はAppleの開発ツールに依存し、3.9系などの古い版の場合があるため、手元で検証用のvenvを作るときにこの巻き戻りを踏みがちです。稼働環境のPythonバージョンは、psycopgのバージョンとセットで確認してください。

python -c "import sys, psycopg, psycopg_pool; print(sys.version.split()[0], psycopg.__version__, psycopg_pool.__version__)"

PostgreSQL側は10から18が対象ですが、公式がCIで回しているのはサポート期間中のリリースだけで、期限切れのバージョンはベストエフォート扱いです。稼働中のPostgreSQLがどの世代なのかはPostgreSQLのバージョン確認方法で先に確定させておくと手戻りが減ります。

psycopg2とpsycopg3の非互換16項目|移行で実際に踏む順

公式ドキュメントの「Differences from psycopg2」は、末尾の「What’s new in Psycopg 3」(新機能へのリンク集)を除くと16項目を挙げています。全部を暗記する必要はなく、移行作業で先に踏むのは上から数個です。次の表は公式の16項目を移行で踏む順に並べ直したもので、影響範囲が広い項目は下位ケースごとに行を割り、公式で1項目のカーソルサブクラスは置き換え先が3通りあるため3行に展開しています。行数と公式の項目数は一致しません。例外名はpsycopg 3.3.6とPostgreSQL 18.6で実行して確かめたものです。

項目 psycopg2 psycopg3
パッケージ名 psycopg2 / psycopg2-binary psycopg[c] / psycopg[binary]
パラメータ束縛 クライアント側 サーバー側(拡張問い合わせプロトコル)
SET・NOTIFYの%s 使える SyntaxError
DDLの%s 使える UndefinedParameter
拡張問い合わせプロトコル 不使用 既定で使用(PgBouncer管理コンソール等では不可)
パラメータ付きの複文 使える SyntaxError(分割かsql.SQL)
複数結果セット 最後の1つだけ results()で全件
IN %s にタプル 使える = ANY(%s) にリスト
IS %s 使える IS NOT DISTINCT FROM %s
辞書で受け取る RealDictCursor row_factory=dict_row
名前付きタプル NamedTupleCursor row_factory=namedtuple_row
位置と名前の両対応 DictCursor 相当物なし
型適応 旧adaptation 全面刷新
可変長関数のキャスト 推論できる IndeterminateDatatype(明示キャスト)
autocommit時のread_only 効く 効かない(GUCで指定)
COPY copy_from / copy_expert copy() 1本
with conn トランザクションを閉じる 接続を閉じる
callproc() あり 廃止(CALL / SELECT)
client_encoding 属性あり 廃止(ConnectionInfo.encoding)
infinity日付 date.maxへ写像 オーバーフロー

この表で「使える/使えない」が変わる行は、いずれもサーバーサイドバインディングという1つの設計変更から派生しています。そこから見ます。

サーバーサイドバインディングによるSET・NOTIFY・DDLでの%s不可

psycopg2はSQL文字列とパラメータをクライアント側で結合してからサーバーへ送っていました。psycopg3はクエリとパラメータを分けて送り、PostgreSQL側で束縛します。値がSQL本文と別に送られるので、値の中身でSQLの構造が変わることはありません。ただしSQL本文そのものを文字列連結で組み立てれば、インジェクションは従来どおり起こり得ます。そしてPostgreSQLがパラメータを受け付けない構文では、この束縛方式が使えません。SELECTとINSERT・UPDATE・DELETEは問題ありませんが、SETやNOTIFY、それにDDL全般が該当します。落ちたときの例外は同じではありません。SETとNOTIFYはSyntaxError、CREATE TABLEなどのDDLはUndefinedParameterになるので、エラー名でSQLの種類を絞り込めます。

# 以下は独立した例。NG例は例外で停止するため、対処例とは分けて実行する。
# 同じ非autocommit接続でNG例を試した後は、conn.rollback()してから次の例を実行する。
# NG: SET / NOTIFY / DDL ではサーバーサイドバインディングが効かない
conn.execute("SET TimeZone TO %s", ["UTC"])
# psycopg.errors.SyntaxError: syntax error at or near "$1"

# DDL は投げられる例外の種類が違う
conn.execute("CREATE TABLE foo (id int DEFAULT %s)", [42])
# psycopg.errors.UndefinedParameter: there is no parameter $1

# 対処1: 同じことをする関数へ置き換える(SET なら set_config、NOTIFY なら pg_notify)
conn.execute("SELECT set_config('TimeZone', %s, false)", ["UTC"])
conn.execute("SELECT pg_notify(%s, %s)", ["chan", "42"])

# 対処2: sql.SQL でクライアント側に組み立てる
from psycopg import sql
cur.execute(sql.SQL("CREATE TABLE {} (id int DEFAULT {})").format(sql.Identifier("foo"), 42))

# 対処3: ClientCursor を使い psycopg2 と同じ束縛方法に戻す
cur = psycopg.ClientCursor(conn)
cur.execute("CREATE TABLE foo (id int DEFAULT %s)", [42])

対処3のClientCursorは、Connection.cursor_factoryに設定すれば以降のcursor()が既定で返すようになります。移行の初手としてこれを入れると、psycopg2時代のSQLをほぼそのまま動かせます。ただしClientCursorはバイナリ形式のパラメータ・結果とプリペアドステートメントに対応しません。これらが必要ならサーバー側束縛へ移行し、既存SQLとの互換性を優先する場合はClientCursorの継続利用も選択肢になります。PgBouncerの管理コンソールのように拡張問い合わせプロトコル自体が通らない相手では、逆にClientCursorが正解になります。

同じ理由で、パラメータを渡すときにセミコロン区切りの複文を1回のexecute()に入れられません(cannot insert multiple commands into a prepared statementで落ちます)。パラメータを使わない複文は通常実行できますが、バイナリ形式で結果を取得する場合やパイプラインモードでは実行できません。なおCREATE DATABASEのようにトランザクション外でしか実行できない文が複文に混ざると、autocommitを有効にしていてもエラーになります。この制約はpsycopg2でも同じです。

IN %sとIS %sの書き換え先|= ANY()とIS NOT DISTINCT FROM

psycopg2で頻出だった2つの書き方が通らなくなります。どちらも書き換え先はpsycopg2でも動くので、両対応のコードを書く場合はpsycopg3側の形に統一しておくと安全です。

# psycopg2 では書けた形(psycopg3 では SyntaxError)
conn.execute("SELECT * FROM foo WHERE id IN %s", [(10, 20, 30)])
conn.execute("SELECT * FROM foo WHERE field IS %s", [None])

# 以下はpsycopg3のコード。SQLとパラメータはpsycopg2でも共通だが、実行にはconn.cursor().execute()を使う
conn.execute("SELECT * FROM foo WHERE id = ANY(%s)", [[10, 20, 30]])
conn.execute("SELECT * FROM foo WHERE field IS NOT DISTINCT FROM %s", [None])

= ANY()への置き換えには副次的な利点が付いてきます。INは空のリストを受け付けませんが、= ANY()は空配列をそのまま渡せる。候補が0件になり得る検索条件なら、psycopg2のままでも= ANY()に寄せておくと分岐が1本減ります。注意が要るのはIS NOT DISTINCT FROMのほう。IS %sの置き換えがIS NOT DISTINCT FROM %s、IS NOT %sの置き換えがIS DISTINCT FROM %sで、NOTの位置が逆転します。

もう1つ、可変長引数の関数で型が決まらずエラーになることがあります。json_build_array(%s, %s)のような呼び出しでIndeterminateDatatypeが出た場合は、%s::textのように明示的なキャストを足せば解決します。

with connectionの意味変更とtransaction()での置き換え

psycopg2のwith conn:はトランザクションのコミット・ロールバックだけを行い、接続は開いたままでした。psycopg3ではwithブロックを抜けた時点で接続そのものが閉じます(抜けた直後のconn.closedはTrue)。ファイルオブジェクトなど他のPythonリソースと同じ挙動に揃えたという変更で、psycopg2の感覚のまま「ループの中でwithブロックを回す」コードを持ち込むと、2周目で接続済みのはずの接続が閉じています。

import psycopg

# with を抜けると「接続」が閉じる(psycopg2 はトランザクションだけ閉じた)
with psycopg.connect("postgresql://user:pass@localhost:5432/appdb") as conn:
    with conn.cursor() as cur:
        cur.execute("SELECT id, name FROM users WHERE id = %s", (10,))
        print(cur.fetchone())

トランザクションだけを区切りたい場合はConnection.transaction()を使います。こちらはネストしたトランザクションにも対応していて、内側のブロックはセーブポイントとして扱われます。ただし、先行するクエリで暗黙のトランザクションが始まっていると、このブロックはセーブポイントだけを管理し、終了時にも外側のトランザクションをコミットしません。psycopg2のwith conn:から置き換える際は、ブロック開始前のトランザクション状態も確認します。

with psycopg.connect(DSN) as conn:
    with conn.transaction():
        conn.execute("UPDATE accounts SET balance = balance - %s WHERE id = %s", (100, 1))
        conn.execute("UPDATE accounts SET balance = balance + %s WHERE id = %s", (100, 2))
    # ここで COMMIT。接続が閉じるのは外側の with を抜けたとき

autocommitを有効にした接続では、read_onlyなどのトランザクション特性属性が効きません。効くのは非autocommit接続の暗黙トランザクションと、transaction()ブロックが作るトランザクションだけです。autocommitのまま読み取り専用にしたいときはSET default_transaction_read_only TO trueを実行します。

RealDictCursor・NamedTupleCursorからrow_factoryへの対応表

psycopg2では取得形式をカーソルのサブクラスで切り替えていました。psycopg3ではカーソルではなく行ファクトリを差し替えます。psycopg.rowsが提供するファクトリはtuple_row・dict_row・namedtuple_row・class_row・args_row・kwargs_row・scalar_rowの7種類です。

from psycopg.rows import dict_row, namedtuple_row

# 接続全体の既定を辞書にする(psycopg2 の RealDictCursor 相当)
conn = psycopg.connect(DSN, row_factory=dict_row)

# カーソル単位でも指定できる(NamedTupleCursor 相当)
cur = conn.cursor(row_factory=namedtuple_row)

移行時に問題になるのはDictCursorです。列番号でも列名でも引ける行を返すこの挙動には、psycopg3に相当物がありません。row[0]とrow["name"]を同じコード内で混在させている箇所は、どちらかに統一する書き換えが必要になります。ここが実質的に一番手のかかる移行作業です。

複数結果セットの取得|psycopg 3.3で追加されたCursor.results()

psycopg2は複文を実行しても最後の文の結果しか返しませんでしたが、psycopg3では全部の結果にアクセスできます。psycopg 3.3ではCursor.results()が追加され、それ以前はnextset()を使います。

cur.execute("SELECT 1; SELECT 2")
for _ in cur.results():
    print(cur.fetchone())
# (1,)
# (2,)

COPY・callproc()・client_encodingの変更点

psycopg2のcopy_from()やcopy_expert()はファイルライクなオブジェクトを前提にしており、プログラムが動的に生成したデータを流し込むのが面倒でした。psycopg3ではCursor.copy()の1本にまとまり、返ってきたオブジェクトに対してブロック単位でも行単位でも読み書きできます。ファイル依存がなくなったことで、非同期のCOPYも書けるようになりました。

with conn.cursor() as cur:
    with cur.copy("COPY users (id, name) FROM STDIN") as copy:
        for row in rows:
            copy.write_row(row)

cursor.callproc()は実装されていません。PostgreSQLの位置指定パラメータやプロシージャ、集合を返す関数を扱えない中途半端な仕様だったためで、置き換え先は素直にSELECT function_name(...)かCALL procedure_name(...)をexecute()に渡す形です。

client_encoding属性も廃止されました。psycopg3はデータベースのクライアントエンコーディングを見て自動でデコードするため、読むだけならConnectionInfo.encodingを参照します。接続時に指定したいときは接続パラメータのclient_encodingを使います。

最後に日付です。PostgreSQLはPythonのdatetimeより広い範囲を扱え、無限大を表すinfinityという値も持ちます。psycopg2はこれをdate.maxに写像していましたが、infinityと9999-12-31が同じPython値になり区別できない問題がありました。psycopg3は9999年より後の日付を一律オーバーフローとして扱います。infinityを入れた列を読んでいるコードは、ここで例外に変わります。

psycopg 3.3系での接続とクエリ実行の基本形

psycopg.connectとwith文を使った接続の書き方

接続はモジュール直下のpsycopg.connect()で作ります。DSNはpostgresql:で始まる接続URIでも、host=... dbname=...形式のキーワード文字列でも、キーワード引数でも渡せます。接続情報をソースに直書きしないという原則は変わらないので、環境変数か設定ファイルから読み込んでください。libpqが解釈するPGHOST・PGUSER・PGPASSWORDなどの環境変数はpsycopg3でもそのまま効きます。

import os
import psycopg

conn = psycopg.connect(
    host=os.environ["PGHOST"],
    dbname=os.environ["PGDATABASE"],
    user=os.environ["PGUSER"],
    password=os.environ["PGPASSWORD"],
)

conn.execute()を直接呼ぶとカーソルを作って返してくれるので、1文だけ流すときはcursor()を経由する必要がありません。取得系のメソッドはfetchone()・fetchmany(size)・fetchall()でpsycopg2と同じです。psycopg 3.3からはカーソルが単なるiterableではなくiteratorになったため、next(cur)という書き方も使えます。

パラメータ渡しとsql.SQLによる識別子の組み立て

通常使うプレースホルダは%s(位置指定)と%(name)s(名前指定)で、これはpsycopg2から変わりません。変わったのは前述のとおり束縛される場所です。パラメータとして渡せるのは文字列・数値・日付・NULLなどの値で、テーブル名や列名などの識別子は渡せません。プレースホルダ自体をシングルクォートで囲む必要はありません。

識別子を動的に組み立てる必要があるときはpsycopg.sqlモジュールを使います。sql.Identifierは識別子として引用され、通常のPython値はリテラルへ変換されます。ただし、sql.SQLなどのComposableオブジェクトは各オブジェクトの規則で展開され、SQL断片の内容が自動でエスケープされるわけではありません。

from psycopg import sql

query = sql.SQL("CREATE TABLE {} (id int DEFAULT {})").format(
    sql.Identifier("foo"), 42
)
print(query.as_string())
# CREATE TABLE "foo" (id int DEFAULT 42)

文字列連結やf-stringでSQLを組み立てるのはインジェクションの入口。sql.SQLなら、サーバーへ送る前の文字列をas_string()で確認できるぶんデバッグも楽です。

SQLAlchemyとDjangoで使うドライバの指定|2.1で変わった既定値

ORM経由でPostgreSQLに繋いでいる場合、psycopg2とpsycopg3のどちらが使われるかはORM側の既定で決まります。ここが2026年9月に変わりました。SQLAlchemy 2.1の移行ガイドは「Default PostgreSQL driver changed to psycopg (psycopg 3)」として、ドライバ名を省いたpostgresql://形式のURLがpsycopgを使うようになったと説明しています。2.1.0のPyPI公開日は2026年9月24日です。

影響が出るのは、URLをpostgresql://と書き、psycopg2だけを入れている環境です。SQLAlchemy 2.0.54とpsycopg2-binaryの組み合わせではpsycopg2が選ばれていたのに対し、同じURLのままSQLAlchemyを2.1.4へ上げると、create_engine()の時点で次のエラーになりました。

import sqlalchemy as sa

# SQLAlchemy 2.1.4 + psycopg2-binary のみの環境
sa.create_engine("postgresql://user:pass@localhost/appdb")
# ModuleNotFoundError: No module named 'psycopg'

# ドライバを URL で明示すれば 2.0 でも 2.1 でも同じ挙動になる
sa.create_engine("postgresql+psycopg://user:pass@localhost/appdb")   # psycopg 3
sa.create_engine("postgresql+psycopg2://user:pass@localhost/appdb")  # psycopg2

依存の更新で壊れるのを避けるなら、URLにドライバ名を書くのが確実です。SQLAlchemy自身もpostgresql+psycopg://の明示を「preferred」としています。pip install "sqlalchemy[postgresql]"で入るドライバも2.1からpsycopg 3に変わりました。

DjangoはENGINEにdjango.db.backends.postgresqlを書くだけで、どちらのドライバを使うかは自動で決まります。Django 6.1.2のdjango/db/backends/postgresql/base.pyはpsycopgを先にimportし、見つからないときだけpsycopg2を読みます。両方が入っている環境では、設定を変えなくてもpsycopg 3が使われます(Django 6.1.2でis_psycopg3がTrueになることを確認)。psycopg 3対応はDjango 4.2で入り、6.1の下限はpsycopg 3.1.12以上またはpsycopg2 2.9.9以上です。DATABASES定義や接続プールの設定はDjangoとPostgreSQLの接続設定、SQLAlchemy側の基本はSQLAlchemyとは?Python ORMの基本と使い方で扱っています。

接続プールpsycopg_poolの既定値と設定の勘所

psycopg2の接続プールはpsycopg2.poolとして本体に同梱され、SimpleConnectionPoolとThreadedConnectionPoolが用意されていました。psycopg3ではプールがpsycopg-poolという別パッケージに分離されています。本体とはバージョン番号の進み方も別で、2026年10月時点でpsycopgが3.3.6、psycopg-poolが3.3.3です。pip install "psycopg[pool]"と書けば依存として一緒に入ります。3.3.3は同期プールのワーカーが24時間タスクが無いと終了してしまう不具合の修正版なので、夜間に処理が途切れるバッチを抱えているなら上げておく価値があります。

from psycopg_pool import ConnectionPool

pool = ConnectionPool(DSN, min_size=4, max_size=20, open=False)
with pool:
    pool.wait()
    with pool.connection() as conn:
        conn.execute("SELECT 1")

ConnectionPoolの8引数と既定値

ConnectionPoolの主な引数の既定値は次のとおりです。数値はpsycopg_pool 3.3.3のコンストラクタシグネチャから読んだものです。

引数 既定値 意味
min_size 4 常時保持する接続数
max_size None 上限(Noneならmin_sizeで固定)
timeout 30.0秒 接続待ちの上限
max_waiting 0 待機キューの上限(0は無制限)
max_lifetime 3600.0秒 1接続の寿命
max_idle 600.0秒 min_size超過分を閉じるまでの遊休時間
reconnect_timeout 300.0秒 再接続を諦めるまでの時間
num_workers 3 接続準備のバックグラウンドワーカー数

max_sizeがNoneのままだとプールはmin_size本で固定されます。スパイクを吸収したいなら明示的に指定してください。timeoutの30秒はWebリクエストの応答時間としては長すぎることが多く、ここを縮めておかないと「DBが詰まったときにアプリ側のワーカーが全部塞がる」形の障害になります。

pool.wait()による起動時のヘルスチェック

設定ミスに早く気づくにはpool.wait()が有効です。プールを開いただけでは接続の失敗が表面化せず、最初のリクエストがPoolTimeoutで落ちて初めて発覚します。wait()はmin_size本の接続が確立するまでブロックし、時間内に揃わなければ例外を投げるので、起動時のヘルスチェックとして機能します。

open引数の扱いと非同期プールのdeprecation

プールを開くタイミングにも変更が入っています。公式のプール解説は、コンストラクタで開く現在の既定について「将来のリリースで既定がFalseに変わる可能性がある」として、開いた状態を前提にするならopen=Trueを明示するよう求めています。上のコード例でopen=Falseを渡してwith pool:で開いているのはこのためです。非同期プールはさらに踏み込んでいて、コンストラクタで開くのはdeprecated扱い。AsyncConnectionPoolではawait pool.open()を呼ぶか、async withで開いてください。

psycopg2とpsycopg3の非同期の違い|async_=1とasyncioでの実測

「psycopg2では非同期処理ができない」と書かれることがありますが、正確ではありません。psycopg2にも2.2から非同期モードがあります。ただしそれはasyncioとは別物で、asyncioのコードから素直に使える形にはなっていません。ここが移行判断の分かれ目になるので、実際に動かして確かめました(Python 3.14.6、psycopg2-binary 2.9.13、psycopg 3.3.6、PostgreSQL 18.6)。

psycopg2の非同期モード|async_=1とpoll()の書き方と制約

psycopg2の非同期接続はconnect()にasync_=1を渡して作ります。古い資料に残っているasync=1は、asyncがPythonの予約語になった3.7以降ではSyntaxError: invalid syntaxで実行前に落ちます。公式のモジュール解説もasync_を「asyncがキーワードになったPython向けの別名」と説明しています。接続後は、呼び出し側がpoll()でソケットの状態を見ながら待つ必要があります。

import select
import psycopg2
import psycopg2.extensions as ext

def wait(conn):
    while True:
        state = conn.poll()
        if state == ext.POLL_OK:
            break
        elif state == ext.POLL_WRITE:
            select.select([], [conn.fileno()], [])
        elif state == ext.POLL_READ:
            select.select([conn.fileno()], [], [])

aconn = psycopg2.connect(DSN, async_=1)   # async=1 は Python 3.7 以降 SyntaxError
wait(aconn)
acur = aconn.cursor()
acur.execute("SELECT pg_sleep(0.2), 42")
wait(acur.connection)
print(acur.fetchone())   # ('', 42)

このモードには制約が多くあります。公式の非同期サポートの節によると、接続は常にautocommitで変更できず、1つの非同期接続で2つのカーソルが並行してクエリを走らせることもできません。executemany()・名前付きカーソル・ラージオブジェクト・set_client_encoding()が使えず、COPYも非対応です。実際に呼ぶと次の例外になりました。

非同期接続での操作 結果(psycopg2 2.9.13)
conn.autocommit True(変更不可)
conn.commit() ProgrammingError
conn.cursor(“srv”)(名前付き) ProgrammingError
conn.set_session(readonly=True) ProgrammingError

トランザクションが要るならBEGINとCOMMITをexecute()で自分で送ることになります。geventやeventletで使う場合は、psycopg2.extensions.set_wait_callback()で待機用のコールバックを登録し、通常の同期コードのまま協調的に待たせる方式が別にあり、そのコールバック集がpsycogreenです。ただしpsycogreenの最新版は2020年2月公開の1.0.2で、asyncio向けの橋渡し役だったaiopgも2022年10月の1.4.0を最後に更新が止まっています。

asyncioからpsycopg2を呼んだときのブロック実測

FastAPIなどのasyncioのコードの中でpsycopg2を同期的に呼ぶと、DBの応答を待つ間イベントループ全体が止まります。SELECT pg_sleep(0.5)を投げるコルーチンを5本asyncio.gather()で同時に走らせ、各処理で別々の接続を作って全体の経過時間を測りました。同じ接続を共有した場合、psycopg3の非同期APIでもクエリ実行は直列化されます。

呼び方 5本同時の経過時間 イベントループ
psycopg2をコルーチン内で直接呼ぶ 2.59秒 止まる(直列化)
psycopg2をasyncio.to_thread()で呼ぶ 0.52秒 止まらない
psycopg3のAsyncConnection 0.52秒 止まらない

直接呼ぶと0.5秒×5本がそのまま足し算になり、その間ほかのリクエストも処理されません。asyncio.to_thread()でスレッドに逃がせば並行にはなりますが、同時実行数ぶんスレッドと接続を消費します。既存のpsycopg2資産を抱えたままASGIへ移る間のつなぎとしては使えるものの、恒久的な構成にはしにくい。asyncioで書くならpsycopg3の非同期APIに移るのが筋です。

AsyncConnectionとAsyncConnectionPoolの書き方

psycopg3はAsyncConnection・AsyncCursor・AsyncConnectionPoolを持ち、同期版とほぼ同じ形のコードをasync/await付きで書けます。追加パッケージは不要です(プールはpsycopg[pool])。

import asyncio
from psycopg_pool import AsyncConnectionPool

async def main():
    async with AsyncConnectionPool(DSN, open=False) as pool:
        async with pool.connection() as conn:
            cur = await conn.execute("SELECT id FROM users WHERE id = %s", (10,))
            print(await cur.fetchone())

asyncio.run(main())

open=Falseを渡してasync withで開いているのは、前節のdeprecation警告を避けるためです。新規に書くコードは最初からこの形にしておけば、既定が変わっても書き直しが要りません。非同期化そのものの向き不向きは非同期処理とは?同期処理との違いから実装方式までで整理しています。FastAPIのプロジェクト構成に組み込む場合はFastAPIのベストプラクティスのディレクトリ設計が参考になります。

psycopg3で新規に入った機能|プリペアド・パイプライン・COPY

prepare_threshold=5による自動プリペアドステートメント

psycopg3は同じクエリが繰り返されると自動でプリペアドステートメントに切り替えます。閾値はConnection.connect()のprepare_threshold引数で、既定値は5です。公式の定義は「prepare_threshold回を超えて実行されたあとに自動でprepareされる」で、同一接続で同じSQLを5回実行したあと、6回目の実行時にプロトコル上でステートメントを準備し、その実行から準備済みステートメントを使います。SQL文のPREPAREを発行するわけではなく、実行計画の再利用はPostgreSQLの汎用計画・カスタム計画の選択にも依存します。

この機能は接続単位で効くため、短命な接続では閾値に達しにくくなります。ただし、1リクエスト内で同じクエリを繰り返す場合や、プールを使わず長時間接続を保持する場合にも利用できます。逆に、動的にSQL文字列を組み立てて毎回異なるクエリを投げる設計だと、プリペアド化されないまま管理コストだけが乗る形になります。無効化したいときはprepare_thresholdにNoneを渡してください。なおPgBouncerのようなミドルウェアを挟む場合、psycopg 3.2以降はPgBouncer 1.22以降かつmax_prepared_statementsが0より大きいことに加え、クライアント側のlibpq 17以降が必要です。古いlibpqを使う場合は、Connection.prepared_max = Noneとして準備済みステートメントの解放を無効にする回避策があります。

一括INSERTの実測|executemany・execute_values・COPYの速度差

パイプラインモードは、複数のクエリの結果を待たずにまとめてサーバーへ送る仕組みで、Connection.pipeline()をコンテキストマネージャとして使います。psycopg 3.1からはexecutemany()が内部でこのモードを使うようになりました(libpq 14以降)。psycopg2のexecutemany()は1行ごとに往復するため、ここで差が付きます。

どの程度の差かを、3列(int・text・float8)×10万行のINSERTで測りました。PostgreSQL 18.6を同一マシンのUnixソケットで接続し(Intel Core i9-9880HのmacOS、Python 3.14.6)、各方式を3回ずつ実行した中央値です。計測には接続、テーブルの削除・作成、投入処理、コミットを含みます。投入先には索引・制約を設けず、共通の行データ生成は計測外、psycopg2のCOPY用StringIO生成は計測内です。

方式 10万行の所要時間
psycopg2 executemany() 15.64秒
psycopg2 extras.execute_values()(page_size=1000) 2.09秒
psycopg2 copy_expert()(StringIO経由) 0.32秒
psycopg3 executemany() 5.95秒(3回で4.53〜7.22秒)
psycopg3 copy() + write_row() 0.39秒

読み取れることは3つあります。第一に、psycopg2から移しただけでexecutemany()は約2.6倍速くなりました。第二に、psycopg2でもexecute_values()を使っていれば、psycopg3のexecutemany()より速い。速度だけを理由に移行する必要はないということです。第三に、件数が多いならどちらのドライバでもCOPYが桁違いに速く、psycopg2のcopy_expert()(3回とも0.32〜0.33秒)とpsycopg3のcopy()(同0.38〜0.40秒)の差は毎回0.07秒前後で、10万行の投入全体から見れば小さな差です。COPYでのpsycopg3の利点は速さではなく、ファイルを経由せずwrite_row()でPythonのタプルをそのまま流せる書きやすさにあります。今回はネットワークを挟まない構成なので、往復1回あたりの遅延が大きいクラウドのDBでは、1行ごとに往復するpsycopg2のexecutemany()ほど不利が大きくなります。

psycopg2を使い続けてよい場面と移行すべき場面の線引き

psycopg2が使えなくなるという情報が出回っていますが、一次情報はそう言っていません。psycopg2のREADMEにある注記は「The psycopg2 package is still widely used and actively maintained, but it is not expected to receive new features.(psycopg2パッケージは今も広く使われ、活発に保守されているが、新機能が入る予定はない)」です。直近のリリースは2026年9月9日の2.9.13で、Python 3.15への対応と不正なbytea入力の解析修正が入っています。EOLが宣言されているわけではありません。

したがって判断軸は「サポートが切れるか」ではなく「psycopg3の新機能が要るか」です。次のいずれかに当てはまるなら移行する価値があります。

  • asyncioで非同期にDBへアクセスしたい。psycopg2側はasync_=1の低水準APIと更新の止まったaiopgしかなく、これが移行の最大の理由になる。
  • 動的に生成したデータをCOPYで流し込みたい。psycopg2のファイルベースAPIでは中間ファイルかバッファのラッパーが要る。
  • mypyなどで静的型検査を通したい。psycopg3は型ヒント付きで設計されている。
  • SQLAlchemy 2.1以降へ上げる予定がある。既定ドライバに合わせておけば、URLの書き換えと二重管理が要らない。

逆に、同期のWebアプリケーションでSELECTとINSERTを素直に投げているだけなら、移行を急ぐ理由はありません。とくにDictCursorを広く使っている既存コードは、置き換え先が存在しないぶん書き換え量が読みにくく、費用対効果が悪くなります。新規プロジェクトはpsycopg3から始め、既存システムはPython本体のバージョンアップやフレームワークの更新と同じタイミングでまとめて移行する、という切り分けが現実的です。前者はpsycopg2のREADME自身が「Psycopg 3 is the evolution of psycopg2 and is where new features are being developed: if you are starting a new project you should probably start from 3!」と勧めているとおりです。

両方を同時に扱う期間は、psycopg2でも動く書き方(= ANY()、IS NOT DISTINCT FROM、明示的なconn.commit()とconn.close())に先に寄せておくと、実際の切り替え時の差分が小さくなります。既存システムのPostgreSQL移行やドライバ更新をまとめて計画したい場合は、データベース設計・移行支援でも相談を受け付けています。

よくある質問

pip install psycopg3 でインストールできますか?

できません。PyPIに配布ファイルがないため、No matching distribution found for psycopg3で失敗します。正しくはpip install "psycopg[binary]"です。公式のインストール手順にも「there is no psycopg3 package, only psycopg」と明記されています。

psycopg[binary] と psycopg2-binary はどう違いますか?

役割は同じで、Cライブラリを同梱した事前ビルド版という位置づけです。違いはパッケージ構成にあります。psycopg2ではpsycopg2とpsycopg2-binaryが同じPythonパッケージをインストールするため、公式の言い方で「依存関係の管理が複雑になり、両方を入れると問題が起きる」状態でした。psycopg3ではインターフェースのpsycopgが本体で、[binary]や[c]は高速化用のextrasとして分かれています。psycopg[binary]と書いたときに実際に入るディストリビューションの名前はpsycopg-binaryで、版は本体と同じ3.3.6です。

psycopg2 はいつまで使えますか?

終了日は公表されていません。psycopg2のREADMEは「広く使われており活発に保守されているが、新機能が入る予定はない」と述べており、2026年9月9日には2.9.13がリリースされています。バグ修正と新しいPythonへの追随は続く一方、新機能はpsycopg3側にのみ入ります。ただし2.9.13でPython 3.9のサポートが外れたので、3.9のまま使い続ける環境には2.9.12より新しい修正が届きません。

psycopg2 で async/await は使えますか?

直接は使えません。psycopg2の非同期モードはconnect(DSN, async_=1)で作り、poll()でソケットを見ながら待つ低水準のAPIで、asyncioのコルーチンとしては動きません。asyncioの中から使うならasyncio.to_thread()でスレッドに逃がすのが現実的な回避策で、新しく書くならpsycopg3のAsyncConnectionを使います。async=1と書くとPython 3.7以降はSyntaxErrorです。

psycopg3 の接続プールはどのパッケージにありますか?

psycopg-poolという別パッケージです。psycopg2ではpsycopg2.poolとして本体に含まれていましたが、psycopg3では分離され、バージョンも独立して進みます(2026年10月時点で本体3.3.6に対しプールは3.3.3)。pip install "psycopg[pool]"でまとめて導入でき、from psycopg_pool import ConnectionPoolで読み込みます。

psycopg の読み方と名前の由来は?

公式ドキュメントやFAQに読み方の定めは見当たりません。名前の由来は作者のFederico Di Gregorio氏がPostgreSQLのメーリングリストで説明しており、社内で別に作られていた、接続を異常に開き続ける不出来なドライバを皮肉って「psychopg」と名付けるつもりが、打ち間違えて「psycopg」になり、そのまま定着したとのことです。名前の由来からは「サイコピージー」という読みが考えられますが、これは発音についての推測であり、作者の投稿で公式な読み方が指定されているわけではありません。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.10.29 テックブログ ソニー生命の不正アクセス:38.5万件の対象確認と窓口、停止中の手続き
  2. 2026.10.31 テックブログ 松屋の不正アクセス:237万件の対象確認と窓口、漏れたパスワードの守り方
  3. 2026.10.31 テックブログ 無印良品の情報漏洩:MUJIアプリ最大2,726万件の対象確認となりすましメール対策
  4. 2026.10.31 テックブログ osinaの情報漏洩:約23万人の対象確認とSNS連携の解除、分析ツールへ渡すデータの絞り方
  5. 2026.05.18 テックブログ マネーフォワードで連携できない・失敗するときの対処法|エラー別の原因と再連携のやり方

RELATED POSTS 関連記事

目次