---
title: "psycopg2とpsycopg3の違い｜16の非互換とpsycopg[binary]の選び方・移行手順"
url: "https://www.issoh.co.jp/tech/details/8225/"
published: 2025-08-15
updated: 2026-10-13
categories: ["Python"]
publisher: "株式会社一創"
---

# 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`です。[公式のインストール手順](https://www.psycopg.org/psycopg3/docs/basic/install.html)にも「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のリリースノート](https://github.com/psycopg/psycopg2/blob/master/NEWS)に「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のバージョン確認方法](/tech/details/16965/)で先に確定させておくと手戻りが減ります。

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

[公式ドキュメントの「Differences from psycopg2」](https://www.psycopg.org/psycopg3/docs/basic/from%5Fpg2.html)は、末尾の「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の移行ガイド](https://github.com/sqlalchemy/sqlalchemy/blob/main/doc/build/changelog/migration%5F21.rst)は「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の接続設定](/tech/details/17105/)、SQLAlchemy側の基本は[SQLAlchemyとは？Python ORMの基本と使い方](/tech/details/7912/)で扱っています。

## 接続プール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

プールを開くタイミングにも変更が入っています。[公式のプール解説](https://www.psycopg.org/psycopg3/docs/advanced/pool.html)は、コンストラクタで開く現在の既定について「将来のリリースで既定が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`で実行前に落ちます。[公式のモジュール解説](https://www.psycopg.org/docs/module.html)も`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)
```

このモードには制約が多くあります。[公式の非同期サポートの節](https://www.psycopg.org/docs/advanced.html)によると、接続は常に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警告を避けるためです。新規に書くコードは最初からこの形にしておけば、既定が変わっても書き直しが要りません。非同期化そのものの向き不向きは[非同期処理とは？同期処理との違いから実装方式まで](/tech/details/13590/)で整理しています。FastAPIのプロジェクト構成に組み込む場合は[FastAPIのベストプラクティス](/tech/details/4347/)のディレクトリ設計が参考になります。

## 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](https://github.com/psycopg/psycopg2)にある注記は「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移行やドライバ更新をまとめて計画したい場合は、[データベース設計・移行支援](/service/system/database/)でも相談を受け付けています。

## よくある質問

### 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のメーリングリスト](https://www.postgresql.org/message-id/36cffb61-3912-915c-4933-3bcd9cac063a%40dndg.it)で説明しており、社内で別に作られていた、接続を異常に開き続ける不出来なドライバを皮肉って「psychopg」と名付けるつもりが、打ち間違えて「psycopg」になり、そのまま定着したとのことです。名前の由来からは「サイコピージー」という読みが考えられますが、これは発音についての推測であり、作者の投稿で公式な読み方が指定されているわけではありません。

## 関連記事

- [DjangoとPostgreSQLの接続設定｜DATABASES定義と接続プール・固有フィールドの判断](/tech/details/17105/)
- [PostgreSQLのバージョン確認方法｜サーバ・クライアント別コマンドとEOL判定・更新計画](/tech/details/16965/)
- [SQLAlchemyとは？Python ORMの基本と使い方を入門から実践まで解説](/tech/details/7912/)
- [FastAPIのベストプラクティス｜SQLAlchemyでのディレクトリ構成・モデル設計・テスト](/tech/details/4347/)
- [非同期処理とは？同期処理との違いから実装方式まで実装者目線で解説](/tech/details/13590/)

---

出典: [psycopg2とpsycopg3の違い｜16の非互換とpsycopg\[binary\]の選び方・移行手順](<https://www.issoh.co.jp/tech/details/8225/>)（株式会社一創）
