SaaS のためのデータベース — Postgres + Drizzle を初心者向けに
SaaS における PostgreSQL と Drizzle ORM の超入門: テーブル/行/リレーション、なぜ Postgres か、TypeScript での型付きスキーマ、クエリ、インデックス、データの安全性、マイグレーション、ローカルと本番の違い。
SaaS のためのデータベース — Postgres + Drizzle を初心者向けに
SaaS を作るなら、ユーザーや注文、投稿などのデータをしまっておく場所(データベース)が必要です。本稿では、定番の PostgreSQL(Postgres)を例に、TypeScript から Drizzle ORM を使って分かりやすく扱う方法を解説します。テーブルと行の超基本、なぜ Postgres が既定として強いか、Drizzle で型付きスキーマを定義/クエリする方法、クエリ/インデックス/データ保護の基礎、ローカル開発と本番の違い、「SQL は必須?」「列を変えたらどうなる?」といった疑問までをカバーします。読み終えるころには、あなたの SaaS における DB の役割を理解し、スキーマを進化させるマイグレーションも自信を持って扱えるようになります。
テーブル/行/リレーション(専門用語なし)
データベースの「テーブル」はスプレッドシートのシートのようなもの。列(カラム)と行(レコード)でできています。
- テーブル: 行と列で構成されたデータのまとまり(例: Users テーブル)。
- 行: 1 レコード。Users なら 1 人分のユーザー情報。
- 列: そのレコードが持つ属性(email, created_at など)。各行は各列に値を持ちます。
- 主キー: 各行を一意に識別するための ID のような列。
- リレーション: テーブル同士のつながり。たとえば Sessions テーブルの user_id が Users の 1 行(ユーザー)を参照する、という具合。ID で別テーブルの行を指す、それが関係データベースの要です。
要するに、テーブルは「ものの種類」を、行は「個々の実体」を、リレーションは「それらのつながり」を表します。Excel を思い浮かべるとシンプルです。
なぜ PostgreSQL が既定として優れているか
SaaS の DB として Postgres は強力な既定値とよく言われます [1]。
- 信頼性: 長年の実績があり、ACID 準拠でトランザクションを安全に扱えます。
- 汎用性と強さ: ユーザー/ブログ/決済など幅広い用途に対応。JSON カラム、全文検索、GIS など高度機能も必要に応じて使えます。
- スケール: スモールスタートから大規模まで。同じ Postgres をチューニング/スケールアップして数百万ユーザーに耐えられます [1]。
- エコシステム: 自前運用もマネージドサービスも選択肢が豊富(Heroku/AWS/GCP/Azure/Supabase/Neon 等)。ツールや情報も潤沢です。
Drizzle ORM 入門:TypeScript で型付きスキーマ
DB と対話するコードは、毎回生 SQL を書く代わりに ORM を使うと安全で生産的です。Drizzle は TypeScript フレンドリーなモダン ORM。Prisma/Sequelize に近い位置づけですが、型安全と SQL への親和性に重点があります。
Drizzle の大きな特長は「スキーマを TypeScript で定義する」こと。テーブル/カラムをコードで記述し、それを単一の真実源としてクエリやマイグレーションに利用します [3]。
- 単一の真実源: スキーマはコード 1 箇所に集約。定義に基づきクエリ整合性を保ち、マイグレーションも生成できます。
- 型安全: スキーマが型になるため、存在しない列を参照すればコンパイルで検出。タイプミスや型不一致を早期に防ぎます。
- 文字列マジック回避: 生 SQL 文字列に比べ、関数/定数経由で構築するため安全性が高いです。
例: ログインセッションを管理する sessions
テーブルを定義してみます。
import { pgTable, varchar, text, timestamp, index, uniqueIndex } from "drizzle-orm/pg-core";
export const sessions = pgTable(
"sessions",
{
id: varchar({ length: 255 }).primaryKey(), // 文字列 ID の主キー
user_id: varchar({ length: 255 }).notNull(), // 所有ユーザーの ID(必須)
token: varchar({ length: 512 }).notNull(), // 一意なセッショントークン(必須)
expires_at: timestamp({ withTimezone: true }).notNull(), // 有効期限
ip_address: varchar({ length: 255 }), // 任意: IP アドレス
user_agent: text(), // 任意: UA 文字列
created_at: timestamp({ withTimezone: true }).notNull().defaultNow(),
updated_at: timestamp({ withTimezone: true }).notNull().defaultNow(),
},
(table) => [
// 制約とインデックス:
uniqueIndex("sessions_token_unique_idx").on(table.token), // トークンの一意性を保証
index("sessions_user_id_idx").on(table.user_id), // user_id での検索を高速化
]
);
ここでやっていることを要約すると:
pgTable("sessions", {...}, (table) => [...])
で Postgres のテーブルを定義します。- 列定義では、
id
は主キー、user_id
/token
は必須、expires_at
は有効期限のタイムスタンプ、created_at/updated_at
は自動でnow()
をデフォルトに付与。 - 第 3 引数では制約/インデックスを定義。
token
の一意制約、user_id
のインデックスなど。
TypeScript 上で定義されるので、タイプミスは編集時点で気づけます。スキーマがコードとして可視化されるため、チームにとっても把握が容易。Drizzle はこの定義をもとにマイグレーションも管理します。
なぜ Next.js/TypeScript と Drizzle が相性良いか? Drizzle は軽量で、巨大なクエリエンジンや複雑なセットアップを要求しません。既存の Postgres 接続(pg
の Pool など)を渡すだけ。エッジ/サーバーレスにも載せやすい直球の SQL トランスパイルです。
Drizzle でのデータ操作(インデックスとデータ安全の基本も)
テーブル定義だけでなく、データを取得/追加/更新/削除する必要があります。Drizzle は型安全な API を提供します。例として「特定ユーザーのクレジット取引履歴を取得」してみます。
SQL なら:
SELECT *
FROM credits
WHERE user_uuid = 'some-user-id'
ORDER BY created_at DESC;
``;
Drizzle の TypeScript では:
```ts
import { db } from "@/db"; // Drizzle の DB インスタンス
import { credits as creditsTable } from "@/db/schema"; // 定義済みの credits テーブル
const userId = "some-user-uuid";
const userCredits = await db.select().from(creditsTable)
.where(creditsTable.user_uuid.eq(userId))
.orderBy(creditsTable.created_at.desc());
読んでの通り、意図に近い表現で、スキーマの型と連動します。結果は credits
の 1 行に対応するオブジェクト配列で、型もコンパイル時に分かります。内部では Drizzle がパラメータ化された SQL を生成し、SQL インジェクション耐性も確保されます。
インデックス/制約/安全性の要点:
- パフォーマンスのためのインデックス:
user_id
で頻繁に検索するならインデックスが効きます。大規模になるほど必須。 - 一意制約: email や token、注文番号などはユニーク制約でデータの整合性を守ります。
- NOT NULL:
.notNull()
は必須列を表し、欠損データの挿入を DB 自体が拒否します。 - データ型と安全性: 型に合わない値は挿入できません。コード側も型で守られます。
- トランザクション: 複数操作を「全部成功 or 全部失敗」にできる安全装置。必要になったら使いましょう。
- バックアップ: 特に本番は定期バックアップを。誤操作から救ってくれます。マネージド Postgres は自動バックアップが便利です。
ローカル開発と本番データベースの違い
ローカル(手元の開発環境)での DB と、本番(実ユーザーが使う環境)での DB では扱いが異なります。
ローカル開発 DB:
- 自分のマシンや Docker で動かす DB。好きに試行錯誤できます(破壊的操作も可)。
.env
などでDATABASE_URL=postgres://...
を設定し、アプリから接続。- チームでも、基本はダミーデータ/テストデータのみを入れます。
- 小規模でも問題なし。ただし本番の巨大データではクエリ体感が変わるので、早いうちからインデックス/クエリ最適化の意識を。
本番 DB:
- 実データの保管庫。安易な実験は厳禁。破損はダウンタイムや恒久的損失に直結します。
- 多くはマネージドサービス(Heroku/AWS/GCP/Azure/Supabase/Neon など)を利用。バックアップ/アップデート/スケールを肩代わり。
- 接続文字列は別管理(サーバーの環境変数など)。ローカルの文字列を本番に使い回さないこと。
- 本番マイグレーション: まずローカル/開発で検証し、デプロイの一環として慎重に適用します。コード側が新列を使う準備が整っているタイミングで実施 [5]。
- 監視/ログ: 規模拡大とともに遅いクエリの監視やログが重要に。
- セキュリティとバックアップ: 強力なパスワード/ネットワーク制限/アップデート/自動バックアップなど、基本を徹底。
まとめ: ローカルと本番を分け、接続先を環境変数で切り替えましょう。ローカルで生成したマイグレーションを、本番にも同じ順序で適用します。誤ってローカルから本番に接続して操作しないよう細心の注意を。
よくある初心者の疑問
Q: Postgres と Drizzle を使うのに SQL の学習は必須ですか?
A: いきなり必須ではありません。ORM の利点は「まずはコードの API だけで進められる」こと。NoSQL 出身でも親しみやすいです。ただし、長期的には SQL の基礎理解が大きな武器になります [4]。遅いクエリの最適化や、ORM の抽象が足りない場面で役立ちます。データモデリング、SELECT/INSERT/UPDATE/DELETE
、WHERE
、JOIN
、集計といった基本だけでも身につける価値があります。
Q: 後から列やテーブルを変えたくなったら?(なぜマイグレーションが必要?)
A: ソフトウェアは変化が常です。users
に age
列を追加したい、username
を handle
にリネームしたい、address
を city
と zipcode
に分割したい——コード上のスキーマを変えたら、DB 側も合わせる必要があります。その橋渡しがマイグレーションです。マイグレーションは「スキーマを旧版→新版へ変更する手順(SQL)」で、Drizzle は多くの場合これを自動生成できます。
手順のイメージ(Drizzle Kit):
- TypeScript のスキーマを編集(列追加/新テーブルなど)。
- 生成コマンドを実行(例
npx drizzle-kit generate
)[5]。現状スキーマとの差分から SQL マイグレーションを作成。リネームっぽい変更は確認を求められることもあります。 - 適用コマンドで DB に反映(例
npx drizzle-kit migrate
)。 - スナップショットが更新され、マイグレーションファイルはリポジトリにコミット(全環境で同じ変更を適用)。
例えば credits
の credits
列を INT
→BIGINT
に変更したい場合、スキーマを integer()
→bigint()
に変えただけでは既存 DB は変わりません。生成/適用で ALTER TABLE
を流し、型を安全に変更します。危険な型変換は警告されるので、段階的移行が必要な場合もあります。開発では気軽にやり直せても、本番ではバックアップ/リバーシブルな変更/入念な検証が重要です。
次の一歩:Drizzle マイグレーションを触ってみる(CTA)
プロジェクトの README に従い、ローカル DB にマイグレーションを生成/適用してみましょう。
npm run db:generate # スキーマ差分からマイグレーションを生成
npm run db:migrate # 生成された SQL を DB に適用
生成されたファイル(drizzle/
や migrations/
配下)を開くと SQL が並びます。TypeScript の定義が SQL にどう写像されるかが分かります。続けて、Drizzle でテストデータを insert→select する小さなスクリプトを書き、動作確認してみましょう。意欲があれば、列を 1 本追加して再度 generate/migrate。差分検出から適用までの流れが体に入ります。
これで、リレーショナルの基本(テーブル/行/リレーション)、Postgres を既定に据える理由、Drizzle による開発体験、変更を安全に運ぶマイグレーションの意義を一通り掴めました。堅実なデータ基盤の上で、SaaS の機能開発を進めていきましょう。ハッピーコーディング!