SQLAlchemy(エスキューエル・アルケミー)は、PythonからリレーショナルデータベースをSQLの表現力を保ったまま扱うためのライブラリです。SQL文を組み立てる「Core」と、Pythonのクラスをテーブルに対応づける「ORM」の2層でできており、ORMを使う場面でも、SQLに近い書き方が必要な場面でも、同じライブラリで済みます。
2026年9月24日に2.1.0が公開され、現行は2.1.5(10月16日)です。2.1ではPython 3.11以上が必須になり、PostgreSQLの既定ドライバやSessionの自動フラッシュの挙動も変わりました。この記事では、2.1.5と2.0.55をPython 3.13・SQLite 3.53.1で実際に動かした結果をもとに、基本の使い方と2.0からの違いを解説します。
まとめ:SQLAlchemyの要点と2.1で変わった点
- SQLAlchemyはPythonのSQLツールキット兼ORM。読み方は「エスキューエル・アルケミー」(SQLを「シークェル」と読む人は「シークェル・アルケミー」)。公式サイトとFAQに発音を定めた記述は無い
- 現行は2.1系(2.1.5)。2.0系(2.0.55)は重大なバグ修正だけを受け付ける保守版で、Python 3.7〜3.10の新規環境では
pip install sqlalchemyが互換性のある2.0系を選ぶ - 基本の部品は、接続を管理するEngine、トランザクションの単位になるSession、テーブルを表すモデルの3つ
- 書き方は
DeclarativeBase・Mapped[]・mapped_column()・select()が標準。session.query()も2.1で動くが新規には使わない - ORMのオブジェクトを取り出すなら
session.scalars()、複数列を行で受け取るならsession.execute() - 2.1へ上げると、
text()の実行前にも未保存の変更が書き込まれる。postgresql://はpsycopg(3)を探し、asyncioにはsqlalchemy[asyncio]の導入が要る
SQLAlchemyとは:読み方とCore・ORMの二層構造
公式サイトは、SQLAlchemyを「Python SQL toolkit and Object Relational Mapper」と説明しています。作者はMichael Bayerで、公式のリリース履歴では最初の0.1系が2006年2月14日に出ています。ライセンスはMITで、PostgreSQL・MySQL・MariaDB・SQLite・Oracle・SQL Serverに標準で対応します。
読み方は「エスキューエル・アルケミー」
綴りは「SQL」と英単語の「alchemy(錬金術)」が並んだ形で、SQLをアルファベット読みすれば「エスキューエル・アルケミー」です。SQLを「シークェル」と読む人は「シークェル・アルケミー」と呼びます。公式サイト・FAQ・作者のブログを当たりましたが、発音を指定した記述は見つかりませんでした。本記事では「エスキューエル・アルケミー」と表記します。カタカナ混じりの「SQLアルケミー」や、略称の「SQLA」と書かれることもあります。
CoreとORMの違い
Coreは、テーブルと列をPythonのオブジェクトで表し、SELECTやINSERTを関数の組み合わせで組み立てる層です。ORMはその上に載り、1行を1つのPythonオブジェクトとして扱います。ORMを使っていても、集計や一括更新ではCoreの書き方をそのまま混ぜられます。
| 項目 | Core | ORM |
|---|---|---|
| 扱う単位 | Table・Column・行(Row) | モデルクラスのインスタンス |
| 実行の入口 | Connection.execute() | Session.execute()・scalars() |
| 変更の追跡 | しない | Sessionが追跡して書き込む |
| 向く処理 | 集計・一括更新・バッチ | 画面単位のCRUD・関連の読み込み |
ORMという仕組みそのものの利点と欠点は、言語をまたいでORMとは?O/Rマッパーの仕組み・メリットとデメリットをSQL比較と実行結果で解説で整理しています。
SQLAlchemy 2.1と2.0の違いと選定基準
公式サイトでは2.1が「Current Release Series」、2.0が「Maintenance Release」と表示されています。保守版は重大なバグ修正だけを受け付ける扱いで、2.0.55も2.1.5と同じ10月16日に出ました。PyPIとWhat’s New in SQLAlchemy 2.1から、入門者に影響する差分を抜き出すと次のとおりです。
| 項目 | 2.0.55 | 2.1.5 |
|---|---|---|
| 位置づけ | 保守版 | 現行版 |
| Python | 3.7以上 | 3.11以上 |
| greenlet | 一部CPU環境で自動導入 | [asyncio] 指定時のみ |
| postgresql:// の既定 | psycopg2 | psycopg(3) |
| oracle:// の既定 | cx_Oracle | oracledb |
| text() 実行前のautoflush | しない | する |
新規開発は2.1を選びます。Python 3.7〜3.10から上げられない環境や、Python 3.7以上で使っているライブラリが2.1に未対応の場合は、pip install "sqlalchemy<2.1" で2.0系に固定します。Python 3.10の仮想環境で版を指定せずに pip install sqlalchemy を実行すると、エラーにならず2.0.55が入りました。手元で2.1のつもりで書いた記事のコードが動かないときは、まず python -c "import sqlalchemy; print(sqlalchemy.__version__)" で版を確かめてください。
SQLAlchemyのインストールとDBドライバの追加
本体はpipで入ります。2.1.5で必須の依存は typing-extensions だけで、DBに接続するドライバは別に入れます。
python -m venv .venv
source .venv/bin/activate
pip install "sqlalchemy==2.1.5"
python -c "import sqlalchemy; print(sqlalchemy.__version__)"
# 2.1.5
SQLiteはPython標準の sqlite3 で動くので追加は不要です。ほかのDBは、ドライバを直接入れるか、SQLAlchemyのextras(角括弧の指定)で一緒に入れます。
| DB | extrasの例 | 入るドライバ | URLの先頭 |
|---|---|---|---|
| PostgreSQL | sqlalchemy[postgresql] | psycopg | postgresql+psycopg:// |
| MySQL | sqlalchemy[pymysql] | PyMySQL | mysql+pymysql:// |
| Oracle | sqlalchemy[oracle] | oracledb | oracle+oracledb:// |
| SQL Server | sqlalchemy[mssql] | pyodbc | mssql+pyodbc:// |
psycopgとpsycopg2の違いや、どちらを入れるべきかはpsycopg2とpsycopg3の違い|16の非互換とpsycopg[binary]の選び方・移行手順で比較しています。
asyncio利用時の追加依存とインストール指定
2.1から、非同期拡張が使うgreenletは自動では入りません。pip install sqlalchemy aiosqlite だけの環境で create_async_engine("sqlite+aiosqlite:///:memory:") から接続すると、次のエラーで止まりました。
ImportError: The SQLAlchemy asyncio module requires that the Python 'greenlet' library is installed. In order to ensure this dependency is available, use the 'sqlalchemy[asyncio]' install target: 'pip install sqlalchemy[asyncio]'
公式のインストールFAQのとおり pip install "sqlalchemy[asyncio]" を実行するとgreenlet 3.5.6が入り、同じコードが通りました。FastAPIなどで AsyncSession を使うプロジェクトは、requirementsの記述を sqlalchemy から sqlalchemy[asyncio] に書き換えておきます。
Engine・Session・モデルの役割と書き方
SQLAlchemyのコードは、Engineを作る、モデルを定義する、Sessionを開いて操作する、の3段で組みます。
create_engineと接続URL(database URI)
Engineは接続プールを持ち、DBへの接続を管理するオブジェクトです。アプリで1つ作り、使い回します。接続先は 方言+ドライバ://ユーザー:パスワード@ホスト:ポート/DB名 の形のURLで渡します。
from sqlalchemy import create_engine
engine = create_engine("sqlite:///app.db", echo=True)
# PostgreSQL: "postgresql+psycopg://app:secret@localhost:5432/shop"
# MySQL: "mysql+pymysql://app:secret@localhost:3306/shop"
echo=True にすると、発行されたSQLがログに出ます。ドライバ部分を省いた postgresql:// は、2.1.5ではpsycopgを、2.0.55ではpsycopg2を読み込もうとします。片方しか入っていない環境では ModuleNotFoundError: No module named 'psycopg' のように落ちるので、URLにはドライバまで書いておくと版をまたいでも動きます。
パスワードに @ や / が入る場合は、文字列のURLに直接書かないでください。app:p@[email protected] を渡すと、パスワードは p、ホストは [email protected] と解釈されました。URL.create() なら未エスケープの値を部品ごとに渡せます。URLオブジェクトをEngineに渡す場合は文字列の解析を回避でき、文字列に変換する際には必要な記号がエスケープされます。
from sqlalchemy import create_engine
from sqlalchemy.engine import URL
url = URL.create(
"postgresql+psycopg",
username="app",
password="p@ss/word",
host="db.example.com",
port=5432,
database="shop",
)
print(url.render_as_string(hide_password=False))
# postgresql+psycopg://app:p%40ss%[email protected]:5432/shop
engine = create_engine(url)
sessionmakerでSessionLocalを作る理由
Sessionはオブジェクトの追加・変更を追跡し、flush時にSQLを発行します。flushは問い合わせ前にも自動で起こり、commit() は残りの変更をflushしてトランザクションを確定します。毎回 Session(engine) と書く代わりに、設定を固めた工場を sessionmaker で1つ作り、そこからSessionを取り出すのが一般的です。SessionLocal は変数名の一例で、名前に決まりはありません。
from sqlalchemy.orm import sessionmaker
SessionLocal = sessionmaker(engine)
with SessionLocal() as session: # 終わったら close
...
session.commit()
with SessionLocal.begin() as session: # 抜けるときに commit、例外なら rollback
...
リクエストごとにSessionを開いて閉じる設計や、FastAPIの依存性注入との組み合わせはFastAPIのベストプラクティス|ディレクトリ構成・SQLAlchemy 2.1の設計・テストを実測で解説で扱っています。
DeclarativeBaseとMappedによるモデル定義
モデルは DeclarativeBase を継承した基底クラスから作ります。列の型やnullableを明示していない場合は、Mapped[] の型注釈から列の型とNULL可否が推論され、Mapped[str] はNOT NULL、Mapped[str | None] はNULL可の列になります。下のモデルで create_all() を実行すると、usersテーブルのnameは VARCHAR(50) のNOT NULL、emailはNULL可で作られました。
from sqlalchemy import ForeignKey, String
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(50))
email: Mapped[str | None]
posts: Mapped[list["Post"]] = relationship(back_populates="author")
class Post(Base):
__tablename__ = "posts"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(100))
user_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
author: Mapped[User] = relationship(back_populates="posts")
Base.metadata.create_all(engine)
古い記事に出てくる declarative_base() は2.1でも残っていますが、sqlalchemy.ext.declarative から読み込んだ関数を呼び出すと MovedIn20Warning が出ます。新しく書くなら上の DeclarativeBase を使います。create_all() は既存テーブルの列を変更しないので、運用中のスキーマ変更はAlembic(アレンビック)とは?SQLAlchemyのマイグレーションの使い方とautogenerateの落とし穴【1.20対応】で管理します。
CRUDの書き方:add_all・select・更新・削除
上のモデルとSQLite用Engineを使い、追加・取得・更新・削除と集計を通しで実行するコードです。出力例は空のapp.dbで1回だけ実行した場合のもので、再実行時は新しい検証用DBファイルを指定してください。2.1.5と2.0.55のどちらでも同じ出力になりました。
from sqlalchemy import func, select
from sqlalchemy.orm import selectinload
with SessionLocal.begin() as session:
taro = User(name="taro", email="[email protected]")
hanako = User(name="hanako")
taro.posts = [Post(title="はじめての投稿"), Post(title="2本目")]
session.add_all([taro, hanako]) # INSERT
with SessionLocal() as session:
users = session.scalars(select(User).order_by(User.id)).all()
print([u.name for u in users]) # SELECT
hanako = session.scalars(select(User).where(User.name == "hanako")).one()
hanako.email = "[email protected]" # UPDATE
session.commit()
post = session.scalars(select(Post).where(Post.title == "2本目")).one()
session.delete(post) # DELETE
session.commit()
stmt = (
select(User.name, func.count(Post.id).label("post_count"))
.outerjoin(User.posts)
.group_by(User.id)
.order_by(User.id)
)
print(session.execute(stmt).all()) # JOIN + GROUP BY
for u in session.scalars(select(User).options(selectinload(User.posts))):
print(u.name, u.email, [p.title for p in u.posts])
['taro', 'hanako']
[('taro', 1), ('hanako', 0)]
taro [email protected] ['はじめての投稿']
hanako [email protected] []
更新はオブジェクトの属性を書き換えて commit() するだけで、UPDATE文はSessionが作ります。relationship で関連づけた投稿は、taro.posts に入れておけば user_id を自分で埋めなくても保存されます。
session.executeとsession.scalarsの違い
Session.execute() は行(Row)の集まりを返し、Session.scalars() は各行の先頭の要素だけを返します。usersに2行入れて4通りを実行した結果です。
| 呼び出し | 結果 |
|---|---|
| execute(select(User)).first() | (<User object>,) |
| scalars(select(User)).first() | Userオブジェクト |
| execute(select(User.id, User.name)).all() | [(1, ‘taro’), (2, ‘hanako’)] |
| scalars(select(User.id, User.name)).all() | [1, 2] |
execute(select(User)) はResultを返し、そこから取り出す各行はUserを1要素として持つRowなので、row.name では取れず row.User.name か row[0].name と書くことになります。モデルを1種類取り出すなら scalars()、複数列や複数モデルを並べて取るなら execute() と覚えれば足ります。最後の例のように、複数列に scalars() を使うと2列目以降は黙って捨てられます。
1件だけ取る場合、主キーなら session.get(User, 1)、条件つきなら scalars(...).one() か one_or_none() を使います。該当が無いとき、one() は NoResultFound を送出し、one_or_none() は None を返しました。
selectinloadによるリレーションのN+1対策
relationship() の既定では、user.posts に初めて触れた時点で投稿を取りに行きます。ユーザー20人の投稿をループで数えたところ、発行されたSQLはユーザー一覧の1本と各ユーザーの20本で計21本でした。.options(selectinload(User.posts)) を付けると、投稿をIN句でまとめて読むため2本に減りました。一覧画面のように親を複数件取ってから子に触れる処理では、最初から selectinload を付けておきます。
2.0から2.1へ上げると挙動が変わる箇所
2.0の書き方は2.1でもほぼそのまま動きますが、エラーにならずに結果だけが変わる箇所があります。2.0.55と2.1.5で同じコードを動かして差が出た3か所を示します。
text()実行前へのautoflush対象の拡大
Sessionは、問い合わせの直前に未保存の変更をDBへ送る(autoflush)設定が既定です。2.0ではこの動作がORMの文に限られていましたが、2.1では実行のたびに無条件で行うよう変わりました。
import sqlalchemy
from sqlalchemy import create_engine, text
from sqlalchemy.orm import Session
# 上で定義したBaseとUserを使い、空の検証専用DBを作る
check_engine = create_engine("sqlite://")
Base.metadata.create_all(check_engine)
with Session(check_engine) as session:
session.add(User(name="taro"))
count = session.execute(text("SELECT count(*) FROM users")).scalar_one()
print(sqlalchemy.__version__, count)
# 2.0.55 0
# 2.1.5 1
同じコードで件数が0から1に変わりました。add() の直後に生SQLで「まだ保存されていないこと」を前提に件数や重複を確かめていた処理は、2.1で結果が変わります。逆に、2.0で text() の前に session.flush() を手で書いていたコードは、そのままでも害はありません。
ドライバを省いたURLとasyncioの依存
postgresql:// と oracle:// は、2.1でそれぞれpsycopg(3)とoracledbを既定に切り替えました。2.0.55では No module named 'psycopg2'、2.1.5では No module named 'psycopg' と、探しに行くモジュールが変わることを実行で確かめています。psycopg2だけを入れた本番環境で版を上げると、接続の時点で落ちます。asyncioを使うプロジェクトでgreenletが入らなくなる点は、インストールの節で示したとおりです。
Select.filter_by()の探索範囲拡大と同名属性の衝突
2.1の select().filter_by() は、最後にJOINしたテーブルだけでなく、FROM句の全テーブルから属性名を探すようになりました。差が出やすいのは、どのテーブルにもある id で絞る場合です。
stmt = select(User).join(Post).filter_by(id=1)
# 2.0.55: WHERE posts.id = :id_1 (投稿のidで絞られる)
# 2.1.5 : AmbiguousColumnError: Attribute name "id" is ambiguous; it exists in multiple FROM clause entities (Mapper[User(users)], posts). Use filter() with explicit column references instead of filter_by().
2.0ではエラーにならず、ユーザーのidのつもりで書いた条件が投稿のidに掛かっていました。2.1では文を組み立てた時点で例外になるので、版を上げたときに初めて見つかる不具合です。JOINを含むクエリでは filter_by() を使わず、where(User.id == 1) のように列を明示します。
SQLAlchemyでつまずきやすいエラーと対処
| エラー・警告 | 起きる場面 | 対処 |
|---|---|---|
| DetachedInstanceError | Sessionを閉じた後に属性へ触れた | Session内で読み切る |
| NoResultFound | one() で該当0件 | one_or_none() を使う |
| MultipleResultsFound | one() で該当2件以上 | 一意性を確認。先頭1件でよい場合のみfirst() |
| ModuleNotFoundError: psycopg | URLが指定するpsycopgが未導入 | 導入するか導入済みドライバを指定 |
| ImportError(greenlet) | 2.1でasyncioを使った | sqlalchemy[asyncio] を入れる |
| MovedIn20Warning | 旧名前空間のdeclarative_base()を呼んだ | DeclarativeBase に移る |
DetachedInstanceError は、Sessionから切り離されたオブジェクトが未読み込みの値を取りに行ったときに起きます。既定では commit() の時点で読み込んだ属性が失効するため、commit() してSessionを閉じた後に user.name を読むとこのエラーになります。Sessionの基本の解説にあるとおり sessionmaker(engine, expire_on_commit=False) で失効を止めれば user.name は読めますが、まだ読み込んでいない user.posts は同じエラーになりました。関連が必要なら、Sessionを閉じる前に selectinload で読んでおきます。
SQLAlchemyを選ぶ場面と選ばない場面
SQLAlchemyが合うのは、FlaskやFastAPIのようにDB層を自分で選ぶフレームワークで、複数のDBに対応したいときや、ORMとCoreを混ぜて集計まで書きたいときです。データベースの設計や既存DBからの移行もあわせて任せたい場合は、データベース設計・移行支援のようなサービスに相談する選択肢もあります。
一方、次の場面では別の手段を選びます。
- Djangoで作るなら、Django ORMを使う。管理画面・マイグレーション・フォームがDjango ORM前提で組まれており、SQLAlchemyに替えると付属機能の多くを捨てることになる(FlaskとDjangoの違いはDjangoとFlaskの違い|内蔵範囲・非同期・案件規模から選ぶ判断基準)
- FastAPIでPydanticのモデルとテーブル定義を1つにまとめたいなら、SQLAlchemyの上に作られたSQLModelを検討する(SQLModelとは?SQLAlchemyとPydanticを統合したPython製ORMの特徴と使い方)
- SQLiteに数本のSQLを流すだけのスクリプトなら、標準の
sqlite3で足りる。モデル定義とSessionの管理がコードの大半を占めてしまう
よくある質問
SQLAlchemyとDjango ORMは何が違いますか?
Django ORMはDjangoに組み込まれ、Djangoのモデル・管理画面・マイグレーションと一体で動きます。SQLAlchemyはフレームワークに依存しない単体のライブラリで、Coreを使えばSQLに近い細かな制御もできます。Djangoを使うならDjango ORM、それ以外のPythonアプリならSQLAlchemyが基本の選択です。
1.x系のsession.query()は2.1でも動きますか?
動きます。2.1.5で session.query(User).filter(User.id == 1).first() を実行したところ、警告も出ずに結果が返りました。公式は session.query() を旧来のAPI(legacy)と位置づけており、新しく書くコードは select() と session.scalars() に揃えるのが無難です。
SQLAlchemyとSQLModelはどう使い分けますか?
SQLModelは内部でSQLAlchemyを使い、1つのクラスをテーブル定義とPydanticモデルの両方に使えるようにしたライブラリです。FastAPIで単純なCRUDを書くなら記述が減ります。複雑なリレーションや集計、SQLAlchemyの新しい機能が要るなら、SQLAlchemyを直接使う方が詰まりにくくなります。
テーブル定義の変更(マイグレーション)はどうしますか?
Base.metadata.create_all() は存在しないテーブルを作るだけで、既存テーブルへの列追加や型変更はしません。運用中のスキーマ変更には、同じ作者が開発するAlembicを使います。モデルとDBの差分から変更スクリプトを自動生成し、適用と巻き戻しを管理できます。
FastAPIなどの非同期処理で使えますか?
使えます。sqlalchemy[asyncio] を入れ、create_async_engine() と AsyncSession を使います。ドライバもasyncpgやaiosqliteのような非同期対応のものを選びます。なお、同期のSessionを async def の中で使うと、DBを待つ間イベントループが止まります。