Alembic 入門:PostgreSQL × Alembic でマイグレーションを書いてみる
こんにちは 人材育成室 育成メンバーチームで 研修中の はすと です。
前回の記事 Alembic 入門:migrations/ の中身とリビジョンの仕組みを理解する で、参加しているプロジェクトの migrations/ を覗きながら Alembic の基本を整理しました。今回はその続編として、実際に自分でマイグレーションを書いて動かしてみます。
プロジェクトと同じ環境で動かしたかったので、DB は PostgreSQL を Docker で立ち上げて使います。プロジェクトでも採用されている op.execute() で生 SQL を書くパターンで進めます。
やること
4つのリビジョンを順に作って、最後に1つ戻します。
usersテーブルを作る(id / name / email)ageカラムを追加する(NOT NULL・既存データへの対応)emailにユニークインデックスを後から追加するstatusカラムを追加する- 最後に1つ
downgradeする
各リビジョンを適用するたびに alembic current と alembic history で状態を確認します。
1. 作業ディレクトリと PostgreSQL を準備する
作業用のディレクトリを作って、Docker で PostgreSQL を立ち上げます。
mkdir alembic-pg-tutorial && cd alembic-pg-tutorial
docker-compose.yml を書きます。
services:
db:
image: postgres:16
environment:
POSTGRES_USER: tutorial
POSTGRES_PASSWORD: tutorial
POSTGRES_DB: tutorial
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
pgdata:
docker compose up -d
localhost:5432 で PostgreSQL が起動します。
2. Python 環境を作る
python -m venv .venv
source .venv/bin/activate
pip install sqlalchemy alembic psycopg2-binary
プロジェクトの requirements.txt と同じ3つを入れています。
3. Alembic を初期化する
alembic init migrations

前回の記事で見た構成と同じディレクトリ(alembic.ini、migrations/env.py、migrations/versions/ 等)が作られます。
4. 接続先を PostgreSQL に書き換える
alembic.ini の sqlalchemy.url を PostgreSQL に向けます。
sqlalchemy.url = postgresql+psycopg2://tutorial:tutorial@localhost:5432/tutorial
これで Alembic が Docker 上の PostgreSQL に繋がるようになります。
5. 最初のリビジョン: users テーブルを作る
alembic revision -m "create users table"
migrations/versions/ の中に {ハッシュID}_create_users_table.py というファイルが作られます。ファイル名の先頭にハッシュID(例: 5e82ab61eaf0)が付くのは、alembic.ini のデフォルト設定(file_template = %%(rev)s_%%(slug)s)によるものです(公式ドキュメント)。プロジェクトによっては file_template をカスタマイズして日時や連番形式のファイル名にしていることもあります。
生成直後のスケルトンはこんな内容です。
"""create users table
Revision ID: 5e82ab61eaf0
Revises:
Create Date: 2026-05-02 08:37:18.836780
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '5e82ab61eaf0'
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
pass
def downgrade() -> None:
"""Downgrade schema."""
pass
upgrade() と downgrade() の pass を書き換えます。不要な import sqlalchemy as sa も削除し、プロジェクトに合わせて op.execute() で生 SQL を書く形にします。
"""create users table
Revision ID: 5e82ab61eaf0
Revises:
Create Date: 2026-05-02 08:37:18.836780
"""
from alembic import op
revision: str = '5e82ab61eaf0'
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
op.execute("""
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL
)
""")
def downgrade() -> None:
op.execute("DROP TABLE IF EXISTS users")
alembic upgrade head で適用します。
$ alembic upgrade head
INFO [alembic.runtime.migration] Running upgrade -> ..., create users table
psql で確認します。
$ docker compose exec db psql -U tutorial -d tutorial -c "\d users"
Table "public.users"
Column | Type | Nullable | Default
--------+-------------------------+----------+-----------------------------------
id | integer | not null | nextval('users_id_seq'::regclass)
name | character varying(50) | not null |
email | character varying(100) | not null |
Indexes:
"users_pkey" PRIMARY KEY, btree (id)
users_pkey は id SERIAL PRIMARY KEY を宣言したことで PostgreSQL が自動的に作成した B-tree インデックスです。PRIMARY KEY 制約はインデックスによって内部的に強制されるため、明示的に作らなくても最初から存在します。
合わせて、Alembic の管理用テーブル alembic_version も自動で作られています。ここに「現在どのリビジョンまで適用したか」が記録されています。
$ docker compose exec db psql -U tutorial -d tutorial -c "TABLE alembic_version"
version_num
-------------
5e82ab61eaf0
(1 row)
6. 2つ目のリビジョン: age カラムを追加する(NOT NULL・既存データへの対応)
まず users テーブルにテストデータを入れておきます。
$ docker compose exec db psql -U tutorial -d tutorial \
-c "INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com'), ('Bob', 'bob@example.com')"
INSERT 0 2
次のリビジョンを作ります。
alembic revision -m "add age to users"
age を NOT NULL で追加したい場合、こう書きたくなります。
def upgrade() -> None:
op.execute("ALTER TABLE users ADD COLUMN age INTEGER NOT NULL")
しかしこれを alembic upgrade head で適用すると失敗します。
$ alembic upgrade head
...
sqlalchemy.exc.InternalError: (psycopg2.errors.NotNullViolation) column "age" of relation "users" contains null values
既存の行に age の値がないため、NOT NULL 制約を満たせないからです。DEFAULT を同時に指定すると解決できます。既存行が初期値で埋まり NOT NULL を満たせます。追加後に DROP DEFAULT でデフォルト値だけ外します。
def upgrade() -> None:
# DEFAULT を付けて追加(既存行は 0 で埋まる)
op.execute("ALTER TABLE users ADD COLUMN age INTEGER NOT NULL DEFAULT 0")
# 永続的なデフォルト値が不要なら DROP DEFAULT する
op.execute("ALTER TABLE users ALTER COLUMN age DROP DEFAULT")
def downgrade() -> None:
op.execute("ALTER TABLE users DROP COLUMN age")
$ alembic upgrade head
INFO [alembic.runtime.migration] Running upgrade 5e82ab61eaf0 -> ..., add age to users
$ docker compose exec db psql -U tutorial -d tutorial -c "SELECT * FROM users"
id | name | email | age
----+-------+----------------------+-----
1 | Alice | alice@example.com | 0
2 | Bob | bob@example.com | 0
(2 rows)
既存の行が 0 で埋まった状態で age カラムが追加されています。
alembic history で2つ目のリビジョンが積まれたことを確認できます。
$ alembic history
5e82ab61eaf0 -> ... (head), add age to users
<base> -> 5e82ab61eaf0, create users table
7. 3つ目のリビジョン: email にユニークインデックスを追加する
「あとからカラムに制約やインデックスを足す」のは実プロジェクトでもよく出るパターンです。
alembic revision -m "add unique index to users email"
def upgrade() -> None:
op.execute("CREATE UNIQUE INDEX ix_users_email_unique ON users (email)")
def downgrade() -> None:
op.execute("DROP INDEX IF EXISTS ix_users_email_unique")
$ alembic upgrade head
確認すると、email にユニークインデックスが追加されています。
$ docker compose exec db psql -U tutorial -d tutorial -c "\d users"
... Indexes:
"users_pkey" PRIMARY KEY, btree (id)
"ix_users_email_unique" UNIQUE, btree (email)
3つのリビジョンが積まれたこのタイミングで alembic current と alembic history を確認してみます。
$ alembic current
INFO [alembic.runtime.migration] Context impl PostgresqlImpl.
INFO [alembic.runtime.migration] Will assume transactional DDL.
... (head)
(head) は「最新のリビジョンまで適用済み」を意味します。
$ alembic history
... -> ... (head), add unique index to users email
... -> ..., add age to users
<base> -> 5e82ab61eaf0, create users table
「今どこにいるか」「何が積まれているか」が一目でわかります。リビジョンが増えてきたときに状況を把握するための定番コマンドです。
8. 4つ目のリビジョン: status カラムを追加する
status カラムを足します。型には PostgreSQL の Enum 型を使います。
alembic revision -m "add status to users"
def upgrade() -> None:
op.execute("CREATE TYPE user_status AS ENUM ('active', 'inactive', 'banned')")
op.execute("ALTER TABLE users ADD COLUMN status user_status NOT NULL DEFAULT 'active'")
def downgrade() -> None:
op.execute("ALTER TABLE users DROP COLUMN status")
op.execute("DROP TYPE user_status")
$ alembic upgrade head
status カラムが Enum 型で追加されたことを確認します。
$ docker compose exec db psql -U tutorial -d tutorial -c "\d users"
...
status | user_status | not null | 'active'::user_status
\dT で Enum 型自体も登録されていることを確認します。
\dT とは
psql のメタコマンドで、データベースに登録されているデータ型(Types)を一覧表示します。CREATE TYPE で作ったカスタム型が正しく登録されているかをここで確認できます。
$ docker compose exec db psql -U tutorial -d tutorial -c "\dT user_status"
List of data types
Schema | Name | Description
--------+-------------+-------------
public | user_status |
ポイントは、upgrade() でカラム追加と Enum 型作成の両方を書いている点です。downgrade() 側でも、カラム削除と Enum 型削除の両方を順序を意識して書く必要があります(カラムが残ったまま型を消そうとするとエラーになるため、先にカラム → 後で型)。
9. downgrade で1つ戻す
最後に、status カラムを追加したマイグレーションを取り消してみます。
$ alembic downgrade -1
status カラムも、user_status 型もきれいに消えます。
$ docker compose exec db psql -U tutorial -d tutorial -c "\d users"
... (status カラムなし)
$ docker compose exec db psql -U tutorial -d tutorial -c "\dT user_status"
Did not find any relation named "user_status".
upgrade() と逆向きの downgrade() を自分で書いておくことで、PostgreSQL 固有の Enum 型もちゃんと巻き戻せます。逆に downgrade() を書き忘れると、alembic downgrade が動かないだけでなく、本番で「やっぱり戻したい」となったときに手動 SQL を叩く羽目になります。
10. 後片付け
DB をまっさらにしたい場合は、Docker のボリュームごと消せばよいです。
docker compose down -v
補足: ファイル名を時系列で並べる
デフォルトではリビジョンファイル名がハッシュIDになるため、ls でファイルを見ても作成順がわかりません。
5e82ab61eaf0_create_users_table.py
e17198a8a58b_add_age_to_users.py
...
方法①: file_template で日時を含める
alembic.ini の file_template を変更すると、ファイル名に日時を含められます。デフォルトではコメントアウトされている行を有効にします。
# alembic.ini
file_template = %%(year)d%%(month).2d%%(day).2d%%(hour).2d%%(minute).2d_%%(slug)s
この設定で alembic revision を実行すると 202605020837_create_users_table.py のようなファイル名になり、時系列順で並ぶようになります。
方法②: --rev-id で連番を明示指定する
--rev-id オプションでリビジョンIDを直接指定できます。
alembic revision --rev-id 0001 -m "create users table"
alembic revision --rev-id 0002 -m "add age to users"
ファイル名が 0001_create_users_table.py、0002_add_age_to_users.py となり、alembic history の表示も 0001、0002 と読みやすくなります。チームで作業する場合は番号の衝突に注意が必要です。
まとめ
自分の手で動かしてみて、Alembic の全体像がつかめました。
alembic upgrade/alembic downgradeでDBのバージョンを自由に切り替えられる- カラムの追加・変更・削除もリビジョンとして積み重ねて管理できる
alembic current/alembic historyでいつでも「今どこにいるか」を確認できる
新しいカラムの追加やテーブルの変更が必要なときに、自分でリビジョンファイルを書いて対応できるようになりました。
同じように Alembic を触り始めた方の参考になれば嬉しいです。









