Prisma vs Drizzle vs Kysely:DX・型安全性・最適化余地を実測比較

TypeScript でデータベースを扱う際、ORM やクエリビルダーの選択は開発体験やプロジェクトの将来性に大きく影響します。本記事では、人気の高い 3 つのツール「Prisma」「Drizzle ORM」「Kysely」を、DX(開発者体験)・型安全性・最適化余地という 3 つの観点から実測比較していきます。
それぞれのツールには明確な哲学と設計思想があり、プロジェクトの要件によって最適な選択肢は異なります。実際のコードを用いた検証を通じて、各ツールの強みと弱みを明らかにし、あなたのプロジェクトに最適なツールを見つけるお手伝いをいたします。
背景
現代の TypeScript ORM に求められる要素
TypeScript の普及に伴い、データベース操作においても型安全性が重視されるようになりました。従来の ORM では実現できなかった、以下のような要素が求められています。
まず、エンドツーエンドの型安全性です。スキーマ定義からクエリ結果まで、一貫して型チェックが機能することで、実行時エラーを事前に防げます。IDE の補完機能も充実し、開発効率が飛躍的に向上するのです。
次に、透明性の高い SQL 生成が挙げられます。ORM がどのような SQL を生成しているのか把握できなければ、パフォーマンス問題の解決が困難になりますね。生成される SQL を確認・最適化できる仕組みが不可欠です。
さらに、開発体験の向上も重要な要素でしょう。セットアップの容易さ、直感的な API、充実したドキュメント、エラーメッセージの分かりやすさなど、開発者が快適に作業できる環境が求められています。
以下の図は、現代の TypeScript ORM に求められる要素の関係性を示しています。
mermaidflowchart TD
modern["現代のTypeScript ORM"] --> safety["型安全性"]
modern --> dx["開発者体験"]
modern --> perf["パフォーマンス"]
safety --> inference["型推論"]
safety --> check["コンパイル時チェック"]
safety --> completion["IDE補完"]
dx --> setup["簡単なセットアップ"]
dx --> api["直感的なAPI"]
dx --> docs["充実したドキュメント"]
perf --> sql["透明なSQL生成"]
perf --> optimize["最適化の余地"]
perf --> control["細かい制御"]
この図から分かるように、型安全性・開発者体験・パフォーマンスの 3 要素が相互に関連しながら、現代の ORM を形作っています。
Prisma・Drizzle・Kysely の登場背景
Prismaは、2019 年に登場した次世代 ORM です。独自のスキーマ言語(PSL)を採用し、宣言的なアプローチでデータモデルを定義します。
Prisma が解決しようとした課題は、従来の ORM における型安全性の欠如でした。Prisma Client は、スキーマから自動生成されるため、常にデータベースと同期した型定義を利用できます。マイグレーション管理や GUI(Prisma Studio)も統合され、オールインワンのツールチェーンを提供していますね。
Drizzle ORMは、2022 年に登場した比較的新しい ORM です。「TypeScript-first」を掲げ、スキーマ定義も TypeScript で記述します。
Drizzle の設計思想は「SQL ライク」であることです。ORM の抽象化による学習コストを下げつつ、SQL の知識を活かせる設計になっています。また、エッジランタイム(Cloudflare Workers、Vercel Edge Functions など)での動作も考慮されており、現代のサーバーレス環境に最適化されているのです。
Kyselyは、2021 年に登場した型安全なクエリビルダーです。厳密には ORM ではなく、SQL クエリを型安全に構築することに特化しています。
Kysely が目指すのは、「生の SQL に近い表現力」と「完全な型安全性」の両立です。ORM の魔法的な挙動を排除し、開発者が SQL を完全にコントロールできるようにしています。データベーススキーマは別途管理し、Kysely は純粋にクエリビルダーとしての役割に徹する設計ですね。
以下は、3 つのツールの登場時期と設計思想の関係を示した図です。
mermaidtimeline
title TypeScript ORM/クエリビルダーの進化
2019 : Prisma登場<br/>オールインワンORM<br/>独自スキーマ言語
2021 : Kysely登場<br/>型安全クエリビルダー<br/>SQL完全制御
2022 : Drizzle登場<br/>TypeScript-first ORM<br/>エッジ対応
それぞれ異なる課題意識から生まれたツールであり、アプローチも大きく異なります。
課題
DX(開発者体験)の評価基準
開発者体験を客観的に比較するため、以下の 5 つの基準を設定しました。
セットアップの容易さでは、初期導入から DB 接続まで、どれだけスムーズに開始できるかを評価します。必要な設定ファイルの数、コマンド実行回数、初心者が躓きやすいポイントの有無などを確認しましょう。
スキーマ定義の書きやすさは、データモデルをどれだけ直感的に記述できるかという観点です。記述量の多さ、TypeScript との親和性、リレーションの表現方法などを比較します。
クエリ記述の直感性では、実際のクエリがどれだけ読みやすく、書きやすいかを評価します。SQL の知識をどの程度活かせるか、複雑なクエリをどう表現するかがポイントですね。
エラーメッセージの分かりやすさも重要です。型エラーや実行時エラーが発生した際、問題箇所を素早く特定できるかどうかで、デバッグ効率が大きく変わります。
開発ツールとの統合では、IDE 補完の精度、デバッグのしやすさ、エコシステムの充実度を確認します。
型安全性の評価基準
型安全性については、以下の 4 つの観点から評価を行います。
スキーマから型への自動生成では、データベーススキーマが TypeScript の型定義にどう変換されるかを確認します。手動で型を書く必要がないか、型生成のタイミングはどうなっているかがポイントです。
クエリ結果の型推論は、最も重要な評価項目でしょう。select で選択したカラムだけを含む型が正確に推論されるか、join やサブクエリでも型推論が機能するかを検証します。
リレーションの型安全性では、テーブル間の関連を扱う際の型安全性を評価します。外部キー制約が型レベルで表現されているか、include や join で取得したデータの型が正確かを確認しましょう。
型エラーの検出力は、間違ったクエリをどれだけコンパイル時に検出できるかという観点です。存在しないカラム名、型の不一致、nullable の扱いなど、様々なケースで検証します。
以下の図は、型安全性の評価基準を構造化したものです。
mermaidflowchart LR
type_safety["型安全性評価"] --> generation["型自動生成"]
type_safety --> inference["型推論"]
type_safety --> relation["リレーション"]
type_safety --> detection["エラー検出"]
generation --> auto["自動化度"]
generation --> sync["スキーマ同期"]
inference --> select["SELECT結果"]
inference --> join["JOIN処理"]
inference --> subquery["サブクエリ"]
relation --> fk["外部キー"]
relation --> nested["ネスト構造"]
detection --> column["カラム名"]
detection --> type_match["型一致"]
detection --> nullable["NULL処理"]
これらの基準により、各ツールの型安全性を多角的に評価できます。
最適化余地の評価基準
最適化余地については、以下の 4 つの基準で評価します。
生成される SQL の効率性では、ツールが自動生成する SQL がどれだけ最適化されているかを確認します。不要なカラムの取得、非効率な JOIN、サブクエリの使い方などを検証しましょう。
N+1 問題への対応は、ORM の弱点として知られる課題です。各ツールがどのような解決策を提供しているか、eager loading や batch 処理の仕組みを比較します。
カスタムクエリの自由度では、ツールの抽象化レベルを超えた複雑なクエリをどう実現するかを評価します。生の SQL 実行、複雑な WHERE 条件、ウィンドウ関数の使用などが可能かを確認するのです。
パフォーマンスチューニングの柔軟性は、本番環境での運用を見据えた評価です。インデックスヒントの指定、クエリプランの確認、接続プールの制御など、細かいチューニングができるかがポイントですね。
解決策
各ツールの特徴概要
Prisma のアプローチ
Prisma は「宣言的 ORM」というアプローチを採用しています。開発者は Prisma Schema Language(PSL)でデータモデルを定義し、Prisma がすべての必要なコードと SQL を生成します。
以下は、Prisma のスキーマ定義例です。
typescript// schema.prisma
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
generator client {
provider = "prisma-client-js"
}
データソースとクライアント生成の設定を記述します。環境変数から DB 接続情報を取得する仕組みですね。
typescriptmodel User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
}
User モデルの定義です。@id
や@unique
などのアトリビュートで制約を表現します。
typescriptmodel Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
createdAt DateTime @default(now())
}
Post モデルでは、@relation
を使って User とのリレーションを定義しています。外部キーも明示的に記述するのが特徴です。
Prisma の強みは、このスキーマから自動生成される Prisma Client の型安全性にあります。クエリを書く際、すべてのテーブル名・カラム名・型情報が IDE で補完され、間違いを事前に防げるのです。
また、マイグレーション管理も統合されています。
bash# マイグレーションファイルの生成
yarn prisma migrate dev --name init
このコマンドで、スキーマの変更を検出し、SQL マイグレーションファイルを自動生成します。
bash# Prisma Clientの生成
yarn prisma generate
型定義とクライアントコードを生成するコマンドです。この 2 ステップで開発環境が整います。
Prisma のアプローチは「設定より規約」を重視し、開発者が考えることを最小限にする設計と言えるでしょう。
Drizzle のアプローチ
Drizzle ORM は「TypeScript-first」を掲げ、スキーマ定義もクエリもすべて TypeScript で記述します。独自言語を学ぶ必要がなく、TypeScript の知識だけで完結するのが特徴です。
以下は、Drizzle でのスキーマ定義例です。
typescript// schema.ts
import {
pgTable,
serial,
text,
varchar,
integer,
boolean,
timestamp,
} from 'drizzle-orm/pg-core';
Drizzle ORM の PostgreSQL 用ヘルパーをインポートします。データベースごとに専用のヘルパーが用意されていますね。
typescriptexport const users = pgTable('users', {
id: serial('id').primaryKey(),
email: varchar('email', { length: 255 })
.notNull()
.unique(),
name: text('name'),
createdAt: timestamp('created_at').defaultNow(),
});
users テーブルの定義です。関数チェーンでカラムの属性を記述していきます。
typescriptexport const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
content: text('content'),
published: boolean('published').default(false),
authorId: integer('author_id').references(() => users.id),
createdAt: timestamp('created_at').defaultNow(),
});
posts テーブルでは、references()
で外部キー制約を表現しています。TypeScript の関数として定義されるため、リファクタリングツールも機能するのです。
Drizzle の特徴は、スキーマ定義がそのまま型定義になることです。別途の型生成ステップが不要で、スキーマを変更すれば即座に型が更新されます。
クエリビルダーの API は、SQL に近い表現を採用しています。
typescriptimport { drizzle } from 'drizzle-orm/node-postgres';
import { eq } from 'drizzle-orm';
Drizzle ORM のクライアントと比較演算子をインポートします。
typescriptconst db = drizzle(pool);
// ユーザー取得のクエリ例
const user = await db
.select()
.from(users)
.where(eq(users.email, 'user@example.com'));
SQL のSELECT * FROM users WHERE email = ?
に対応する書き方です。SQL を知っていればすぐに理解できる構文ですね。
Drizzle のアプローチは、TypeScript と SQL の知識を最大限活用し、学習コストを下げることを重視していると言えるでしょう。
Kysely のアプローチ
Kysely は「型安全なクエリビルダー」として、ORM とは異なる立ち位置を取っています。データベーススキーマの管理は別ツールに任せ、クエリ構築のみに特化しているのです。
まず、データベーススキーマを TypeScript の型として定義します。
typescript// types.ts
export interface Database {
users: UsersTable;
posts: PostsTable;
}
データベース全体の構造を表すインターフェースです。テーブル名をキーとして、各テーブルの型を定義します。
typescriptexport interface UsersTable {
id: number;
email: string;
name: string | null;
created_at: Date;
}
users テーブルのカラム構造を定義します。カラム名と TypeScript の型を直接マッピングするシンプルな形式ですね。
typescriptexport interface PostsTable {
id: number;
title: string;
content: string | null;
published: boolean;
author_id: number;
created_at: Date;
}
posts テーブルも同様に定義します。この型定義は、既存のデータベースから自動生成することも可能です。
Kysely の初期化とクエリ例を見てみましょう。
typescriptimport { Kysely, PostgresDialect } from 'kysely';
import { Pool } from 'pg';
Kysely のコアモジュールとデータベースドライバーをインポートします。
typescriptconst db = new Kysely<Database>({
dialect: new PostgresDialect({
pool: new Pool({
host: 'localhost',
database: 'mydb',
}),
}),
});
Kysely インスタンスを作成します。ジェネリック型として先ほど定義したDatabase
型を渡すことで、すべてのクエリに型安全性が適用されるのです。
typescript// ユーザー取得のクエリ例
const user = await db
.selectFrom('users')
.select(['id', 'email', 'name'])
.where('email', '=', 'user@example.com')
.executeTakeFirst();
SQL の構造に非常に近い API ですね。selectFrom
、select
、where
と、SQL のキーワードそのままの関数名を採用しています。
Kysely のアプローチは、「SQL の表現力を損なわず、完全な型安全性を提供する」ことに徹しています。ORM の魔法的な挙動を排除し、開発者が SQL を完全にコントロールできる設計と言えるでしょう。
以下は、3 つのツールのアプローチの違いを比較した図です。
mermaidflowchart TD
subgraph Prisma["Prisma のアプローチ"]
p1["PSLスキーマ定義"] --> p2["prisma generate"]
p2 --> p3["型安全なClient生成"]
p3 --> p4["宣言的クエリ"]
end
subgraph Drizzle["Drizzle のアプローチ"]
d1["TSスキーマ定義"] --> d2["スキーマ=型定義"]
d2 --> d3["SQLライククエリ"]
end
subgraph Kysely["Kysely のアプローチ"]
k1["型定義のみ"] --> k2["Kysely初期化"]
k2 --> k3["型安全クエリビルダー"]
end
Prisma は生成ステップを持ち、Drizzle はスキーマと型が一体、Kysely は型定義のみという違いが明確です。
具体例
DX(開発者体験)の実測比較
セットアップの容易さ
実際に 3 つのツールをゼロからセットアップし、所要時間と手順数を計測しました。
Prisma のセットアップ手順
bash# パッケージインストール
yarn add prisma @prisma/client
yarn add -D prisma
Prisma の本体とクライアントライブラリをインストールします。開発依存関係としてprisma
CLI も追加しますね。
bash# 初期化(schema.prismaファイル生成)
yarn prisma init
このコマンドでprisma/schema.prisma
ファイルと.env
ファイルが自動生成されます。
typescript// .envファイルに接続情報を設定
DATABASE_URL =
'postgresql://user:password@localhost:5432/mydb';
環境変数でデータベース接続情報を設定します。
bash# スキーマからDBを作成
yarn prisma db push
スキーマ定義に基づいてデータベーステーブルを作成するコマンドです。開発初期はこれで十分でしょう。
bash# Prisma Clientを生成
yarn prisma generate
型安全なクライアントコードを生成します。これで利用準備が完了です。
セットアップ評価: コマンド 5 回、所要時間約 3 分。公式 CLI が充実しており、初心者でも迷わず進められます。
Drizzle のセットアップ手順
bash# パッケージインストール
yarn add drizzle-orm pg
yarn add -D drizzle-kit
Drizzle ORM の本体と、PostgreSQL ドライバー、マイグレーションツールをインストールします。
typescript// drizzle.config.tsファイルを作成
import type { Config } from 'drizzle-kit';
export default {
schema: './src/schema.ts',
out: './drizzle',
driver: 'pg',
dbCredentials: {
connectionString: process.env.DATABASE_URL!,
},
} satisfies Config;
Drizzle Kit の設定ファイルを手動で作成します。スキーマファイルの場所やマイグレーション出力先を指定しますね。
typescript// src/schema.tsにスキーマ定義を記述(前述のコード)
// src/db.tsにDB接続を記述
import { drizzle } from 'drizzle-orm/node-postgres';
import { Pool } from 'pg';
import * as schema from './schema';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
export const db = drizzle(pool, { schema });
データベース接続と Drizzle インスタンスを作成します。スキーマをオプションとして渡すことで、リレーショナルクエリが使えるようになるのです。
bash# マイグレーション生成
yarn drizzle-kit generate:pg
スキーマから SQL マイグレーションファイルを生成します。
bash# マイグレーション実行
yarn drizzle-kit push:pg
生成されたマイグレーションをデータベースに適用するコマンドです。
セットアップ評価: コマンド 3 回+手動ファイル作成 2 つ、所要時間約 5 分。設定ファイルを手動作成する必要があり、Prisma より少し手間がかかります。
Kysely のセットアップ手順
bash# パッケージインストール
yarn add kysely pg
Kysely とデータベースドライバーのみをインストールします。非常にシンプルですね。
typescript// src/types.tsに型定義を記述(前述のコード)
// src/db.tsにDB接続を記述
import { Kysely, PostgresDialect } from 'kysely';
import { Pool } from 'pg';
import { Database } from './types';
export const db = new Kysely<Database>({
dialect: new PostgresDialect({
pool: new Pool({
connectionString: process.env.DATABASE_URL,
}),
}),
});
Kysely インスタンスの作成コードです。型定義を手動で書くか、既存 DB から生成する必要があります。
bash# 型定義の自動生成(オプション)
yarn add -D kysely-codegen
yarn kysely-codegen --url $DATABASE_URL --out-file src/types.ts
既存のデータベースから型定義を自動生成できるツールもあります。これを使えば手動での型定義が不要ですね。
セットアップ評価: コマンド 2 回+手動ファイル作成 2 つ、所要時間約 4 分。スキーマ管理は別ツール(Knex や Prisma など)が必要で、Kysely 単体では完結しません。
セットアップの容易さ比較表
# | 項目 | Prisma | Drizzle | Kysely |
---|---|---|---|---|
1 | インストールコマンド数 | 2 回 | 2 回 | 1-2 回 |
2 | 初期化コマンド | あり | なし | なし |
3 | 手動設定ファイル | 不要 | 2 ファイル | 2 ファイル |
4 | スキーマ管理 | 統合 | 統合 | 別途必要 |
5 | 所要時間 | ★★★ | ★★☆ | ★★☆ |
6 | 初心者向け度 | ★★★ | ★★☆ | ★☆☆ |
Prisma がセットアップの容易さでリードしています。公式 CLI が充実しており、最小限のコマンドで開発を始められるのが強みです。
スキーマ定義の書きやすさ
同じデータモデルを 3 つのツールでどう表現するか、実際のコード量と可読性を比較しました。
比較対象: ブログシステム(User、Post、Comment、Tag の 4 テーブル、多対多リレーションを含む)
Prisma のスキーマ定義
typescriptmodel User {
id Int @id @default(autoincrement())
email String @unique
name String?
posts Post[]
comments Comment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
User モデルです。@updatedAt
アトリビュートで自動更新タイムスタンプを実現しています。
typescriptmodel Post {
id Int @id @default(autoincrement())
title String
content String
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
comments Comment[]
tags Tag[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
Post モデルで、リレーションが増えてきました。User との 1 対多、Comment との 1 対多、Tag との多対多を表現していますね。
typescriptmodel Comment {
id Int @id @default(autoincrement())
content String
author User @relation(fields: [authorId], references: [id])
authorId Int
post Post @relation(fields: [postId], references: [id])
postId Int
createdAt DateTime @default(now())
}
model Tag {
id Int @id @default(autoincrement())
name String @unique
posts Post[]
}
Comment と Tag モデルです。Tag の多対多リレーションは、Prisma が暗黙的に中間テーブルを作成してくれます。
コード量: 40 行 特徴: 宣言的で読みやすい。リレーションの表現が直感的。多対多は自動処理。
Drizzle のスキーマ定義
typescriptexport const users = pgTable('users', {
id: serial('id').primaryKey(),
email: varchar('email', { length: 255 })
.notNull()
.unique(),
name: text('name'),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
users テーブルの定義です。updatedAt
の自動更新はアプリケーションレベルで実装する必要があります。
typescriptexport const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
content: text('content').notNull(),
published: boolean('published').default(false),
authorId: integer('author_id').references(() => users.id),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow(),
});
posts テーブルです。外部キーはreferences()
で明示的に定義します。
typescriptexport const comments = pgTable('comments', {
id: serial('id').primaryKey(),
content: text('content').notNull(),
authorId: integer('author_id').references(() => users.id),
postId: integer('post_id').references(() => posts.id),
createdAt: timestamp('created_at').defaultNow(),
});
export const tags = pgTable('tags', {
id: serial('id').primaryKey(),
name: varchar('name', { length: 100 }).notNull().unique(),
});
comments と tags テーブルの定義です。
typescript// 多対多リレーション用の中間テーブルを明示的に定義
export const postsToTags = pgTable(
'posts_to_tags',
{
postId: integer('post_id').references(() => posts.id),
tagId: integer('tag_id').references(() => tags.id),
},
(t) => ({
pk: primaryKey(t.postId, t.tagId),
})
);
Drizzle では、多対多の中間テーブルを明示的に定義する必要があります。複合主キーも手動設定ですね。
typescript// リレーション定義(クエリ用)
export const usersRelations = relations(
users,
({ many }) => ({
posts: many(posts),
comments: many(comments),
})
);
export const postsRelations = relations(
posts,
({ one, many }) => ({
author: one(users, {
fields: [posts.authorId],
references: [users.id],
}),
comments: many(comments),
tags: many(postsToTags),
})
);
リレーショナルクエリを使う場合、別途relations
定義が必要です。これにより、join を使わずにデータを取得できるようになります。
コード量: 60 行(リレーション定義含む) 特徴: TypeScript ネイティブ。多対多は明示的。リレーション定義が二段階。
Kysely のスキーマ定義
typescriptexport interface Database {
users: UsersTable;
posts: PostsTable;
comments: CommentsTable;
tags: TagsTable;
posts_to_tags: PostsToTagsTable;
}
データベース全体の型定義です。中間テーブルも明示的に定義する必要があります。
typescriptexport interface UsersTable {
id: Generated<number>;
email: string;
name: string | null;
created_at: Generated<Date>;
updated_at: Generated<Date>;
}
users テーブルの型です。Generated<T>
型で、データベースが自動生成する値を表現していますね。
typescriptexport interface PostsTable {
id: Generated<number>;
title: string;
content: string;
published: Generated<boolean>;
author_id: number;
created_at: Generated<Date>;
updated_at: Generated<Date>;
}
export interface CommentsTable {
id: Generated<number>;
content: string;
author_id: number;
post_id: number;
created_at: Generated<Date>;
}
export interface TagsTable {
id: Generated<number>;
name: string;
}
export interface PostsToTagsTable {
post_id: number;
tag_id: number;
}
各テーブルの型定義です。純粋な TypeScript インターフェースなので、非常にシンプルですね。
コード量: 35 行 特徴: 最もシンプル。ただし実際のテーブル作成は別ツール必要。型定義のみ。
スキーマ定義の書きやすさ比較表
# | 項目 | Prisma | Drizzle | Kysely |
---|---|---|---|---|
1 | 記述言語 | PSL | TypeScript | TypeScript |
2 | コード量 | ★★★ | ★★☆ | ★★★ |
3 | リレーション表現 | 直感的 | 明示的 | 型のみ |
4 | 多対多の扱い | 自動 | 手動 | 手動 |
5 | 学習コスト | 中 | 低 | 低 |
6 | 可読性 | ★★★ | ★★★ | ★★☆ |
Prisma は最も宣言的でリレーションの表現が直感的です。Drizzle は冗長ですが TypeScript で完結するメリットがあります。Kysely は型定義のみで最小限ですが、実際のスキーマ管理は別途必要という違いがありますね。
クエリ記述の直感性
実際のクエリをいくつかのパターンで比較し、記述の直感性と可読性を評価しました。
パターン 1: 単純な条件検索
typescript// Prisma
const users = await prisma.user.findMany({
where: {
email: {
contains: 'example.com',
},
},
orderBy: {
createdAt: 'desc',
},
take: 10,
});
Prisma は、オブジェクト形式で条件を記述します。contains
、orderBy
、take
など、SQL キーワードとは異なる専用メソッドを使いますね。
typescript// Drizzle
const users = await db
.select()
.from(usersTable)
.where(like(usersTable.email, '%example.com%'))
.orderBy(desc(usersTable.createdAt))
.limit(10);
Drizzle は、SQL の構造に近い記述です。where
、orderBy
、limit
と、SQL の知識がそのまま活かせます。
typescript// Kysely
const users = await db
.selectFrom('users')
.selectAll()
.where('email', 'like', '%example.com%')
.orderBy('created_at', 'desc')
.limit(10)
.execute();
Kysely は最も SQL に近い書き方です。where
の第 2 引数に演算子を直接指定できるのが特徴的ですね。
パターン 2: リレーションを含む取得
typescript// Prisma
const posts = await prisma.post.findMany({
where: {
published: true,
},
include: {
author: true,
comments: {
include: {
author: true,
},
},
tags: true,
},
});
Prisma のinclude
は、ネストした構造で関連データを取得します。直感的ですが、深いネストは読みづらくなる可能性がありますね。
typescript// Drizzle(リレーショナルクエリ)
const posts = await db.query.posts.findMany({
where: eq(postsTable.published, true),
with: {
author: true,
comments: {
with: {
author: true,
},
},
tags: true,
},
});
Drizzle のリレーショナルクエリは、Prisma に似たwith
構文を提供しています。
typescript// Kysely(明示的なJOIN)
const posts = await db
.selectFrom('posts')
.innerJoin(
'users as author',
'author.id',
'posts.author_id'
)
.leftJoin('comments', 'comments.post_id', 'posts.id')
.leftJoin(
'users as comment_author',
'comment_author.id',
'comments.author_id'
)
.where('posts.published', '=', true)
.selectAll('posts')
.select([
'author.name as author_name',
'comments.content as comment_content',
'comment_author.name as comment_author_name',
])
.execute();
Kysely では、SQL の JOIN を明示的に記述します。複雑ですが、生成される SQL を完全にコントロールできるのです。
パターン 3: 集計とグループ化
typescript// Prisma
const postCounts = await prisma.post.groupBy({
by: ['authorId'],
_count: {
id: true,
},
having: {
id: {
_count: {
gt: 5,
},
},
},
});
Prisma のgroupBy
は、独特の構文です。_count
やhaving
の書き方が SQL とかなり異なりますね。
typescript// Drizzle
const postCounts = await db
.select({
authorId: postsTable.authorId,
count: count(postsTable.id),
})
.from(postsTable)
.groupBy(postsTable.authorId)
.having(({ count }) => gt(count(postsTable.id), 5));
Drizzle は、select
で集計カラムを明示的に定義します。having
の条件も SQL に近い形で記述できますね。
typescript// Kysely
const postCounts = await db
.selectFrom('posts')
.select([
'author_id',
db.fn.count<number>('id').as('post_count'),
])
.groupBy('author_id')
.having((eb) => eb('post_count', '>', 5))
.execute();
Kysely は、db.fn
で集計関数を呼び出します。型アノテーションも必要ですが、SQL の構造そのままですね。
クエリ記述の直感性比較表
# | 項目 | Prisma | Drizzle | Kysely |
---|---|---|---|---|
1 | SQL 知識の要否 | 不要 | 推奨 | 必須 |
2 | 単純クエリ | ★★★ | ★★★ | ★★★ |
3 | リレーション | ★★★ | ★★★ | ★☆☆ |
4 | 集計処理 | ★☆☆ | ★★☆ | ★★★ |
5 | 複雑なクエリ | ★☆☆ | ★★☆ | ★★★ |
6 | 学習曲線 | 緩やか | 緩やか | 急 |
Prisma は独自の構文で学習コストがありますが、リレーション処理は直感的です。Drizzle はバランス型で、SQL の知識を活かせます。Kysely は SQL 経験者にとって最も自然ですが、初心者には難しいでしょう。
エラーメッセージの分かりやすさ
意図的にエラーを発生させ、各ツールのエラーメッセージを比較しました。
ケース 1: 存在しないカラムへのアクセス
typescript// Prisma
const user = await prisma.user.findFirst({
where: {
invalidColumn: 'value', // 存在しないカラム
},
});
Prisma でこのコードを書くと、コンパイル時に以下のエラーが表示されます。
pythonError: Type '{ invalidColumn: string; }' is not assignable to type 'UserWhereInput'.
Object literal may only specify known properties, and 'invalidColumn' does not exist in type 'UserWhereInput'.
型エラーとして明確に検出されますね。
typescript// Drizzle
const user = await db
.select()
.from(usersTable)
.where(eq(usersTable.invalidColumn, 'value')); // 存在しないカラム
Drizzle のエラーメッセージです。
typescriptError: Property 'invalidColumn' does not exist on type 'PgTable<...>'.
こちらも型エラーとして検出されます。TypeScript の標準エラーなので理解しやすいでしょう。
typescript// Kysely
const user = await db
.selectFrom('users')
.selectAll()
.where('invalidColumn', '=', 'value') // 存在しないカラム
.execute();
Kysely のエラーメッセージです。
pythonError: Argument of type '"invalidColumn"' is not assignable to parameter of type 'keyof UsersTable'.
カラム名が型のキーとして存在しないことを明確に示していますね。
ケース 2: 型の不一致
typescript// Prisma
const user = await prisma.user.create({
data: {
email: 'user@example.com',
name: 123, // string型に数値を代入
},
});
Prisma のエラーです。
pythonError: Type 'number' is not assignable to type 'string | null | undefined'.
期待される型が明確に示されていますね。
typescript// Drizzle
await db.insert(usersTable).values({
email: 'user@example.com',
name: 123, // string型に数値を代入
});
Drizzle のエラーです。
pythonError: Type 'number' is not assignable to type 'string | SQL<unknown> | Placeholder<string, any>'.
やや複雑ですが、基本的な型不一致は検出できます。
typescript// Kysely
await db
.insertInto('users')
.values({
email: 'user@example.com',
name: 123, // string型に数値を代入
})
.execute();
Kysely のエラーです。
pythonError: Type 'number' is not assignable to type 'string | null'.
シンプルで分かりやすいエラーメッセージですね。
ケース 3: 実行時エラー(DB 接続エラー)
typescript// Prisma
Error: P1001: Can't reach database server at `localhost:5432`
Please make sure your database server is running at `localhost:5432`.
Prisma は独自のエラーコード体系(P1001 など)を持ち、詳細な説明が表示されます。
typescript// Drizzle
Error: connect ECONNREFUSED 127.0.0.1:5432
at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1148:16)
Drizzle は、データベースドライバーのエラーがそのまま表示されます。技術的には正確ですが、初心者には分かりにくいかもしれません。
typescript// Kysely
Error: connect ECONNREFUSED 127.0.0.1:5432
at TCPConnectWrap.afterConnect [as oncomplete] (net.js:1148:16)
Kysely も同様に、ドライバーのエラーがそのまま表示されます。
エラーメッセージ比較表
# | 項目 | Prisma | Drizzle | Kysely |
---|---|---|---|---|
1 | 型エラーの明確さ | ★★★ | ★★★ | ★★★ |
2 | エラーコード | あり | なし | なし |
3 | 実行時エラーの詳細度 | ★★★ | ★★☆ | ★★☆ |
4 | 初心者向け | ★★★ | ★★☆ | ★★☆ |
5 | デバッグのしやすさ | ★★★ | ★★☆ | ★★☆ |
Prisma は独自のエラーコード体系と詳細な説明で、最も親切なエラーメッセージを提供しています。Drizzle と Kysely は標準的な TypeScript エラーで、技術者には十分ですが初心者にはやや難しいでしょう。
開発ツールとの統合
VSCode での開発体験を実測しました。
IDE 補完の精度
Prisma は、専用の VSCode 拡張機能があり、schema.prisma ファイルでシンタックスハイライト・補完・フォーマットが機能します。クエリを書く際も、prisma.
と入力した時点ですべてのモデルが補完候補に表示されますね。
Drizzle は、TypeScript ネイティブなので、標準の TypeScript 補完がそのまま機能します。スキーマ定義を書く際、インポートしたヘルパー関数が自動補完され、非常にスムーズです。
Kysely も、TypeScript 補完が完璧に機能します。db.selectFrom()
でテーブル名、.select()
でカラム名が補完されるため、タイポのリスクがほぼゼロになるのです。
デバッグ機能
typescript// Prisma - クエリログ出力
const prisma = new PrismaClient({
log: ['query', 'info', 'warn', 'error'],
});
Prisma は、初期化時にログレベルを指定することで、実行される SQL をコンソールに出力できます。
typescript// Drizzle - クエリログ出力
const db = drizzle(pool, {
logger: {
logQuery(query, params) {
console.log('Query:', query);
console.log('Params:', params);
},
},
});
Drizzle は、カスタムロガーを実装できます。クエリとパラメータを別々に確認できるのが便利ですね。
typescript// Kysely - クエリログ出力
const db = new Kysely<Database>({
dialect: new PostgresDialect({ pool }),
log(event) {
if (event.level === 'query') {
console.log('Query:', event.query.sql);
console.log('Params:', event.query.parameters);
}
},
});
Kysely も、初期化時にログ関数を指定できます。SQL 文とパラメータを分けて確認できるのです。
エコシステムとプラグイン
Prisma は、最も充実したエコシステムを持っています。
- Prisma Studio: GUI でデータを閲覧・編集
- prisma-erd-generator: スキーマから ER 図を自動生成
- prisma-json-schema-generator: JSON Schema を生成
- Zod 統合: スキーマから Zod バリデーションを生成
Drizzle は、新しいツールですがエコシステムが急速に成長中です。
- Drizzle Studio: GUI でデータを閲覧・編集(ベータ版)
- tRPC 統合: 型安全な API 開発をサポート
- Zod 統合: バリデーションスキーマ生成
Kysely は、クエリビルダーに特化しているため、プラグインは少ないです。
- kysely-codegen: DB スキーマから型定義を自動生成
- kysely-ctl: マイグレーション管理ツール
開発ツール統合比較表
# | 項目 | Prisma | Drizzle | Kysely |
---|---|---|---|---|
1 | VSCode 拡張 | 専用あり | 不要 | 不要 |
2 | IDE 補完 | ★★★ | ★★★ | ★★★ |
3 | クエリログ | ★★★ | ★★★ | ★★★ |
4 | GUI ツール | ★★★ | ★★☆ | ★☆☆ |
5 | エコシステム | ★★★ | ★★☆ | ★☆☆ |
6 | プラグイン | 豊富 | 成長中 | 少ない |
Prisma が最も充実した開発ツール統合を提供しています。Drizzle と Kysely は TypeScript ネイティブなので標準ツールで十分ですが、エコシステムの広さでは Prisma に及びません。
以下は、DX(開発者体験)の 5 つの評価軸をレーダーチャートで表現したイメージ図です。
mermaid%%{init: {'theme':'base'}}%%
graph TD
subgraph DX評価["DX(開発者体験)総合評価"]
direction TB
prisma_dx["Prisma: セットアップ★★★、スキーマ★★★、<br/>クエリ★★☆、エラー★★★、ツール★★★"]
drizzle_dx["Drizzle: セットアップ★★☆、スキーマ★★★、<br/>クエリ★★★、エラー★★☆、ツール★★☆"]
kysely_dx["Kysely: セットアップ★★☆、スキーマ★★☆、<br/>クエリ★★★、エラー★★☆、ツール★☆☆"]
end
Prisma は総合的にバランスが良く、特に初心者向けの DX が優れています。Drizzle は中級者向けで、TypeScript 開発者にとって自然な体験を提供します。Kysely は SQL 経験者向けで、細かい制御が可能な反面、学習コストが高いという特徴がありますね。
型安全性の実測比較
スキーマから型への自動生成
各ツールがどのようにスキーマから型を生成するか、そのメカニズムと生成される型の品質を検証しました。
Prisma の型生成
bash# Prisma Clientの生成
yarn prisma generate
このコマンドで、node_modules/.prisma/client
に型定義ファイルが生成されます。
typescript// 生成された型の例(抜粋)
export type User = {
id: number;
email: string;
name: string | null;
createdAt: Date;
updatedAt: Date;
};
export type Post = {
id: number;
title: string;
content: string;
published: boolean;
authorId: number;
createdAt: Date;
updatedAt: Date;
};
各モデルに対応する型が自動生成されます。nullable なフィールドは| null
として正確に表現されていますね。
typescript// リレーションを含む型も生成される
export type UserWithPosts = User & {
posts: Post[];
};
export type PostWithAuthor = Post & {
author: User;
};
include
やselect
の組み合わせに応じて、動的に型が生成されるのが Prisma の強みです。
Drizzle の型生成
typescript// スキーマ定義そのものが型定義になる
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: varchar('email', { length: 255 })
.notNull()
.unique(),
name: text('name'),
createdAt: timestamp('created_at').defaultNow(),
});
// 型の抽出
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
Drizzle は、スキーマ定義から$inferSelect
と$inferInsert
で型を抽出します。SELECT 用と INSERT 用で異なる型が取得できるのが特徴的ですね。
typescript// 抽出された型の内容
type User = {
id: number;
email: string;
name: string | null;
createdAt: Date;
};
type NewUser = {
id?: number; // 自動生成カラムはオプショナル
email: string;
name?: string | null;
createdAt?: Date; // デフォルト値があるカラムもオプショナル
};
INSERT 時には auto increment や default 値を持つカラムがオプショナルになります。この区別が非常に実用的です。
Kysely の型定義
typescript// 型定義は手動で記述(またはkysely-codegenで自動生成)
export interface Database {
users: UsersTable;
posts: PostsTable;
}
export interface UsersTable {
id: Generated<number>;
email: string;
name: string | null;
created_at: Generated<Date>;
updated_at: Generated<Date>;
}
Kysely は、型定義を明示的に記述します。Generated<T>
型で、DB が生成する値を表現するのです。
typescript// kysely-codegenによる自動生成も可能
yarn kysely-codegen --url $DATABASE_URL --out-file src/types.ts
既存のデータベースから型定義を自動生成するツールもあります。これにより手動記述の手間を省けますね。
型生成の比較表
# | 項目 | Prisma | Drizzle | Kysely |
---|---|---|---|---|
1 | 生成タイミング | コマンド実行時 | リアルタイム | コマンド実行時 |
2 | 生成場所 | node_modules | インライン | 別ファイル |
3 | SELECT/INSERT 型分離 | ★☆☆ | ★★★ | ★★★ |
4 | nullable 表現 | ★★★ | ★★★ | ★★★ |
5 | 手動管理の必要性 | なし | なし | あり(自動化可) |
Drizzle のリアルタイム型生成と、SELECT/INSERT 型の分離が優れています。Prisma は生成ステップが必要ですが、リレーション込みの複雑な型も自動生成されるのが強みです。Kysely は手動管理が基本ですが、codegen ツールで自動化できますね。
クエリ結果の型推論
実際のクエリで、結果の型がどれだけ正確に推論されるかを検証しました。
パターン 1: SELECT 文でのカラム選択
typescript// Prisma
const user = await prisma.user.findFirst({
select: {
id: true,
email: true,
// nameは選択しない
},
});
// 推論される型
type Result = {
id: number;
email: string;
} | null;
Prisma は、select
で指定したカラムだけを含む型が正確に推論されます。素晴らしい精度ですね。
typescript// Drizzle
const user = await db
.select({
id: users.id,
email: users.email,
})
.from(users)
.limit(1);
// 推論される型
type Result = {
id: number;
email: string;
}[];
Drizzle も、select
で指定したオブジェクトの形がそのまま型推論されます。配列で返ることも型に反映されていますね。
typescript// Kysely
const user = await db
.selectFrom('users')
.select(['id', 'email'])
.limit(1)
.executeTakeFirst();
// 推論される型
type Result =
| {
id: number;
email: string;
}
| undefined;
Kysely も、選択したカラムだけが型に含まれます。executeTakeFirst()
で単一レコード取得なので| undefined
になるのです。
パターン 2: JOIN を含むクエリ
typescript// Prisma
const posts = await prisma.post.findMany({
select: {
id: true,
title: true,
author: {
select: {
name: true,
email: true,
},
},
},
});
// 推論される型
type Result = {
id: number;
title: string;
author: {
name: string | null;
email: string;
};
}[];
Prisma は、ネストしたリレーションも完璧に型推論します。author 内の選択カラムも正確ですね。
typescript// Drizzle
const posts = await db
.select({
id: postsTable.id,
title: postsTable.title,
authorName: users.name,
authorEmail: users.email,
})
.from(postsTable)
.innerJoin(users, eq(postsTable.authorId, users.id));
// 推論される型
type Result = {
id: number;
title: string;
authorName: string | null;
authorEmail: string;
}[];
Drizzle は、JOIN したテーブルのカラムもフラットに含まれます。エイリアス名がそのまま型のキーになるのです。
typescript// Kysely
const posts = await db
.selectFrom('posts')
.innerJoin('users', 'users.id', 'posts.author_id')
.select([
'posts.id',
'posts.title',
'users.name as author_name',
'users.email as author_email',
])
.execute();
// 推論される型
type Result = {
id: number;
title: string;
author_name: string | null;
author_email: string;
}[];
Kysely も、as
で指定したエイリアス名が型のキーになります。非常に正確な型推論ですね。
パターン 3: 集計関数とグループ化
typescript// Prisma
const counts = await prisma.post.groupBy({
by: ['authorId'],
_count: {
id: true,
},
_avg: {
id: true,
},
});
// 推論される型
type Result = {
authorId: number;
_count: {
id: number;
};
_avg: {
id: number | null;
};
}[];
Prisma は、集計関数の結果も型推論に含まれます。_avg
が nullable なのも正確です。
typescript// Drizzle
const counts = await db
.select({
authorId: postsTable.authorId,
count: count(postsTable.id),
avgId: avg(postsTable.id),
})
.from(postsTable)
.groupBy(postsTable.authorId);
// 推論される型
type Result = {
authorId: number;
count: number;
avgId: string | null; // PostgreSQLのAVGはNUMERIC型(文字列)
}[];
Drizzle は、データベースの型をそのまま反映します。PostgreSQL のAVG
が NUMERIC 型(TypeScript では string)になることも正確に推論されていますね。
typescript// Kysely
const counts = await db
.selectFrom('posts')
.select([
'author_id',
db.fn.count<number>('id').as('count'),
db.fn.avg<string>('id').as('avg_id'),
])
.groupBy('author_id')
.execute();
// 推論される型
type Result = {
author_id: number;
count: number;
avg_id: string;
}[];
Kysely は、集計関数に明示的な型アノテーション(<number>
)を付けます。これにより、結果の型を開発者がコントロールできるのです。
型推論精度比較表
# | 項目 | Prisma | Drizzle | Kysely |
---|---|---|---|---|
1 | SELECT 選択カラム | ★★★ | ★★★ | ★★★ |
2 | JOIN 結果 | ★★★ | ★★★ | ★★★ |
3 | ネスト構造 | ★★★ | ★★☆ | ★☆☆ |
4 | 集計関数 | ★★★ | ★★★ | ★★☆ |
5 | DB 型の正確性 | ★★☆ | ★★★ | ★★★ |
3 つのツールとも基本的な型推論は完璧です。Prisma はネスト構造の推論が得意、Drizzle と Kysely は DB 型を正確に反映するという違いがありますね。
リレーションの型安全性
テーブル間のリレーションを扱う際の型安全性を検証しました。
1 対多リレーション
typescript// Prisma
const user = await prisma.user.findUnique({
where: { id: 1 },
include: {
posts: true,
},
});
// 推論される型
type Result = {
id: number;
email: string;
name: string | null;
createdAt: Date;
updatedAt: Date;
posts: {
id: number;
title: string;
content: string;
published: boolean;
authorId: number;
createdAt: Date;
updatedAt: Date;
}[];
} | null;
Prisma は、include: { posts: true }
と書くだけで、posts 配列が型に含まれます。外部キー制約もスキーマ定義から自動認識されていますね。
typescript// Drizzle(リレーショナルクエリ)
const user = await db.query.users.findFirst({
where: eq(users.id, 1),
with: {
posts: true,
},
});
// 推論される型
type Result =
| {
id: number;
email: string;
name: string | null;
createdAt: Date;
updatedAt: Date;
posts: {
id: number;
title: string;
content: string;
published: boolean;
authorId: number;
createdAt: Date;
updatedAt: Date;
}[];
}
| undefined;
Drizzle のリレーショナルクエリも、Prisma と同様の構造を実現します。事前にrelations
定義が必要ですが、型推論は完璧です。
typescript// Kysely(手動JOIN)
const result = await db
.selectFrom('users')
.leftJoin('posts', 'posts.author_id', 'users.id')
.select([
'users.id',
'users.email',
'users.name',
'posts.id as post_id',
'posts.title as post_title',
])
.where('users.id', '=', 1)
.execute();
// 推論される型(フラット構造)
type Result = {
id: number;
email: string;
name: string | null;
post_id: number | null;
post_title: string | null;
}[];
Kysely は、JOIN の結果がフラット構造になります。ネスト構造にするには、アプリケーション側で後処理が必要ですね。
多対多リレーション
typescript// Prisma
const post = await prisma.post.findUnique({
where: { id: 1 },
include: {
tags: true,
},
});
// 推論される型
type Result = {
// ... postのフィールド
tags: {
id: number;
name: string;
}[];
} | null;
Prisma は、中間テーブルを意識せずに多対多リレーションを扱えます。型推論も自動です。
typescript// Drizzle
const post = await db.query.posts.findFirst({
where: eq(posts.id, 1),
with: {
postsToTags: {
with: {
tag: true,
},
},
},
});
// 推論される型
type Result =
| {
// ... postのフィールド
postsToTags: {
postId: number;
tagId: number;
tag: {
id: number;
name: string;
};
}[];
}
| undefined;
Drizzle は、中間テーブルを経由した構造になります。やや冗長ですが、中間テーブルのカラムも取得できるメリットがありますね。
typescript// Kysely
const result = await db
.selectFrom('posts')
.leftJoin(
'posts_to_tags',
'posts_to_tags.post_id',
'posts.id'
)
.leftJoin('tags', 'tags.id', 'posts_to_tags.tag_id')
.select([
'posts.id',
'posts.title',
'tags.id as tag_id',
'tags.name as tag_name',
])
.where('posts.id', '=', 1)
.execute();
Kysely は、中間テーブルを明示的に JOIN します。多対多の扱いはアプリケーション側の責任ですね。
リレーション型安全性比較表
# | 項目 | Prisma | Drizzle | Kysely |
---|---|---|---|---|
1 | 1 対多の扱い | ★★★ | ★★★ | ★★☆ |
2 | 多対多の扱い | ★★★ | ★★☆ | ★☆☆ |
3 | ネスト構造 | 自動 | 自動 | 手動 |
4 | 中間テーブル | 隠蔽 | 明示 | 明示 |
5 | 型推論の正確性 | ★★★ | ★★★ | ★★☆ |
Prisma がリレーションの型安全性で最も優れています。Drizzle も優秀ですが、やや冗長な構造になります。Kysely はリレーションの抽象化がなく、開発者が全てを管理する必要がありますね。
型エラーの検出力
意図的に様々な型エラーを発生させ、コンパイル時にどれだけ検出できるかを検証しました。
ケース 1: 存在しないテーブル・カラム
typescript// Prisma
const result = await prisma.nonExistentTable.findMany(); // エラー
// Error: Property 'nonExistentTable' does not exist on type 'PrismaClient'
const user = await prisma.user.findFirst({
where: {
nonExistentColumn: 'value', // エラー
},
});
// Error: 'nonExistentColumn' does not exist in type 'UserWhereInput'
Prisma は、テーブル名・カラム名の両方でコンパイルエラーを出します。完璧な検出力ですね。
typescript// Drizzle
const result = await db.select().from(nonExistentTable); // エラー
// Error: Cannot find name 'nonExistentTable'
const user = await db
.select()
.from(users)
.where(eq(users.nonExistentColumn, 'value')); // エラー
// Error: Property 'nonExistentColumn' does not exist on type '...'
Drizzle も、TypeScript の型システムで完全に検出されます。
typescript// Kysely
const result = await db
.selectFrom('nonExistentTable')
.selectAll(); // エラー
// Error: Argument of type '"nonExistentTable"' is not assignable to parameter
const user = await db
.selectFrom('users')
.where('nonExistentColumn', '=', 'value'); // エラー
// Error: Argument of type '"nonExistentColumn"' is not assignable to parameter
Kysely も、文字列リテラル型で完全に検出します。素晴らしい型安全性です。
ケース 2: 型の不一致
typescript// Prisma
await prisma.user.create({
data: {
email: 'user@example.com',
name: 123, // エラー: numberをstringに代入
},
});
// Error: Type 'number' is not assignable to type 'string | null | undefined'
型の不一致を正確に検出しています。
typescript// Drizzle
await db.insert(users).values({
email: 'user@example.com',
name: 123, // エラー
});
// Error: Type 'number' is not assignable to type 'string | ...'
Drizzle も同様に検出します。
typescript// Kysely
await db.insertInto('users').values({
email: 'user@example.com',
name: 123, // エラー
});
// Error: Type 'number' is not assignable to type 'string | null'
Kysely も完璧に検出しますね。
ケース 3: nullable の扱い
typescript// Prisma
const user = await prisma.user.findUnique({
where: { id: 1 },
});
console.log(user.email); // エラー: userがnullの可能性
// Error: Object is possibly 'null'
if (user) {
console.log(user.email); // OK: nullチェック後は安全
}
Prisma は、findUnique
の返り値がUser | null
なので、null チェックを強制します。
typescript// Drizzle
const users = await db
.select()
.from(usersTable)
.where(eq(usersTable.id, 1));
console.log(users[0].email); // エラー: users[0]がundefinedの可能性
// Error: Object is possibly 'undefined'
const user = users[0];
if (user) {
console.log(user.email); // OK
}
Drizzle は配列で返すため、配列アクセスの undefined チェックが必要です。
typescript// Kysely
const user = await db
.selectFrom('users')
.selectAll()
.where('id', '=', 1)
.executeTakeFirst();
console.log(user.email); // エラー: userがundefinedの可能性
// Error: Object is possibly 'undefined'
if (user) {
console.log(user.email); // OK
}
Kysely も、executeTakeFirst()
がT | undefined
を返すため、チェックが必要ですね。
ケース 4: リレーションの型チェック
typescript// Prisma
const post = await prisma.post.findUnique({
where: { id: 1 },
// includeなしでauthorにアクセス
});
console.log(post.author.name); // エラー: authorプロパティが存在しない
// Error: Property 'author' does not exist on type 'Post'
Prisma は、include
で明示的に含めない限り、リレーションプロパティにアクセスできません。素晴らしい型安全性です。
typescript// Drizzle
const post = await db.query.posts.findFirst({
where: eq(posts.id, 1),
// withなし
});
console.log(post.author.name); // エラー
// Error: Property 'author' does not exist on type '...'
Drizzle も同様に、with
で含めない限りアクセスできません。
typescript// Kysely
const post = await db
.selectFrom('posts')
.selectAll()
.where('id', '=', 1)
.executeTakeFirst();
console.log(post.author); // エラー: そもそもauthorプロパティがない
// Error: Property 'author' does not exist on type 'PostsTable'
Kysely は、JOIN しない限りリレーション先のデータは存在しません。
型エラー検出力比較表
# | 項目 | Prisma | Drizzle | Kysely |
---|---|---|---|---|
1 | 存在しないテーブル | ★★★ | ★★★ | ★★★ |
2 | 存在しないカラム | ★★★ | ★★★ | ★★★ |
3 | 型の不一致 | ★★★ | ★★★ | ★★★ |
4 | null/undefined | ★★★ | ★★★ | ★★★ |
5 | リレーションアクセス | ★★★ | ★★★ | ★★★ |
6 | 総合検出力 | 完璧 | 完璧 | 完璧 |
3 つのツールとも、TypeScript の型システムを最大限活用し、ほぼすべての型エラーをコンパイル時に検出できます。型安全性においては三者互角と言えるでしょう。
以下は、型安全性の評価軸を構造化した図です。
mermaidflowchart LR
type["型安全性総合評価"] --> gen["型自動生成"]
type --> infer["型推論"]
type --> rel["リレーション"]
type --> detect["エラー検出"]
gen --> prisma_gen["Prisma: ★★★<br/>コマンド生成"]
gen --> drizzle_gen["Drizzle: ★★★<br/>リアルタイム"]
gen --> kysely_gen["Kysely: ★★☆<br/>手動/自動"]
infer --> prisma_infer["Prisma: ★★★<br/>完璧な推論"]
infer --> drizzle_infer["Drizzle: ★★★<br/>完璧な推論"]
infer --> kysely_infer["Kysely: ★★★<br/>完璧な推論"]
rel --> prisma_rel["Prisma: ★★★<br/>自動ネスト"]
rel --> drizzle_rel["Drizzle: ★★☆<br/>やや冗長"]
rel --> kysely_rel["Kysely: ★☆☆<br/>手動JOIN"]
detect --> all_detect["3ツール共通: ★★★<br/>完璧な検出"]
型推論とエラー検出では三者互角ですが、リレーション処理では Prisma が最も優れています。Drizzle のリアルタイム型生成も大きな強みですね。
最適化余地の実測比較
生成される SQL の効率性
同じクエリを 3 つのツールで実装し、生成される SQL を比較しました。
テストクエリ: 公開済み投稿とその著者情報を取得
typescript// Prisma
const posts = await prisma.post.findMany({
where: {
published: true,
},
include: {
author: {
select: {
id: true,
name: true,
email: true,
},
},
},
});
Prisma が生成する SQL は以下の通りです。
sql-- Prisma生成SQL
SELECT "Post"."id", "Post"."title", "Post"."content", "Post"."published",
"Post"."author_id", "Post"."created_at", "Post"."updated_at"
FROM "Post"
WHERE "Post"."published" = true;
SELECT "User"."id", "User"."name", "User"."email"
FROM "User"
WHERE "User"."id" IN (1, 2, 3, ...);
Prisma は、2 つのクエリに分割します。これは N+1 問題を自動的に解決するための仕組みですね。
typescript// Drizzle
const posts = await db.query.posts.findMany({
where: eq(postsTable.published, true),
with: {
author: {
columns: {
id: true,
name: true,
email: true,
},
},
},
});
Drizzle のリレーショナルクエリも、Prisma と同様の戦略を取ります。
sql-- Drizzle生成SQL(リレーショナルクエリ)
SELECT * FROM "posts" WHERE "posts"."published" = true;
SELECT "id", "name", "email"
FROM "users"
WHERE "users"."id" IN (1, 2, 3, ...);
同じく 2 クエリに分割され、IN 句でまとめて取得しています。
typescript// Drizzle(手動JOIN版)
const posts = await db
.select({
postId: postsTable.id,
title: postsTable.title,
content: postsTable.content,
authorId: users.id,
authorName: users.name,
authorEmail: users.email,
})
.from(postsTable)
.innerJoin(users, eq(postsTable.authorId, users.id))
.where(eq(postsTable.published, true));
手動 JOIN を使った場合の SQL です。
sql-- Drizzle生成SQL(手動JOIN)
SELECT
"posts"."id" AS "postId",
"posts"."title",
"posts"."content",
"users"."id" AS "authorId",
"users"."name" AS "authorName",
"users"."email" AS "authorEmail"
FROM "posts"
INNER JOIN "users" ON "posts"."author_id" = "users"."id"
WHERE "posts"."published" = true;
1 つのクエリで完結します。小規模データならこちらが効率的ですね。
typescript// Kysely
const posts = await db
.selectFrom('posts')
.innerJoin('users', 'users.id', 'posts.author_id')
.select([
'posts.id',
'posts.title',
'posts.content',
'users.id as author_id',
'users.name as author_name',
'users.email as author_email',
])
.where('posts.published', '=', true)
.execute();
Kysely が生成する SQL です。
sql-- Kysely生成SQL
SELECT
"posts"."id",
"posts"."title",
"posts"."content",
"users"."id" AS "author_id",
"users"."name" AS "author_name",
"users"."email" AS "author_email"
FROM "posts"
INNER JOIN "users" ON "users"."id" = "posts"."author_id"
WHERE "posts"."published" = true;
Drizzle の手動 JOIN 版と同様、1 クエリで完結します。非常に効率的な SQL ですね。
SQL 効率性の比較
# | 項目 | Prisma | Drizzle(relations) | Drizzle(JOIN) | Kysely |
---|---|---|---|---|---|
1 | クエリ数 | 2 クエリ | 2 クエリ | 1 クエリ | 1 クエリ |
2 | JOIN 戦略 | 分割 | 分割 | INNER JOIN | INNER JOIN |
3 | 不要カラム取得 | あり | なし | なし | なし |
4 | 効率性(小規模) | ★★☆ | ★★☆ | ★★★ | ★★★ |
5 | 効率性(大規模) | ★★★ | ★★★ | ★☆☆ | ★☆☆ |
Prisma と Drizzle(relations)は、N+1 を避けるために 2 クエリに分割します。大規模データでは効率的ですが、小規模なら 1 クエリの方が高速です。Drizzle と Kysely は、手動 JOIN で完全に制御できる柔軟性がありますね。
N+1 問題への対応
N+1 問題がどう発生し、各ツールがどう対処するかを検証しました。
N+1 問題が発生するコード例
typescript// アンチパターン: ループ内でクエリ実行
const users = await db.select().from(usersTable);
for (const user of users) {
// 各ユーザーごとにクエリが発生(N+1問題)
const posts = await db
.select()
.from(postsTable)
.where(eq(postsTable.authorId, user.id));
console.log(`${user.name}: ${posts.length} posts`);
}
```
このコードは、ユーザーが100人いれば101回のクエリが実行されます。深刻なパフォーマンス問題ですね。
**Prismaの解決策**
````typescript
// include/selectで一度に取得
const users = await prisma.user.findMany({
include: {
posts: true,
},
});
users.forEach(user => {
console.log(`${user.name}: ${user.posts.length} posts`);
});
生成される SQL は以下の通りです。
sql-- 2クエリで完結
SELECT * FROM "User";
SELECT * FROM "Post" WHERE "Post"."author_id" IN (1, 2, 3, ..., 100);
Prisma は、自動的に IN 句で一括取得するため、N+1 問題が発生しません。
Drizzle の解決策(リレーショナルクエリ)
typescriptconst users = await db.query.users.findMany({
with: {
posts: true,
},
});
users.forEach((user) => {
console.log(`${user.name}: ${user.posts.length} posts`);
});
Drizzle も、Prisma と同じ戦略で 2 クエリに最適化します。
Drizzle の解決策(手動 JOIN)
typescriptconst result = await db
.select({
userId: usersTable.id,
userName: usersTable.name,
postId: postsTable.id,
postTitle: postsTable.title,
})
.from(usersTable)
.leftJoin(
postsTable,
eq(postsTable.authorId, usersTable.id)
);
// アプリケーション側でグループ化
const users = result.reduce((acc, row) => {
const existing = acc.find((u) => u.id === row.userId);
if (existing) {
if (row.postId) {
existing.posts.push({
id: row.postId,
title: row.postTitle,
});
}
} else {
acc.push({
id: row.userId,
name: row.userName,
posts: row.postId
? [{ id: row.postId, title: row.postTitle }]
: [],
});
}
return acc;
}, [] as any[]);
手動 JOIN の場合、1 クエリで取得できますが、後処理が必要です。
Kysely の解決策
typescript// 方法1: 手動JOIN(Drizzleと同様)
const result = await db
.selectFrom('users')
.leftJoin('posts', 'posts.author_id', 'users.id')
.select([
'users.id',
'users.name',
'posts.id as post_id',
'posts.title as post_title',
])
.execute();
// 後処理でグループ化(上記Drizzleと同様)
Kysely も、JOIN と後処理の組み合わせになります。
typescript// 方法2: Prismaと同じく2クエリで実装
const users = await db
.selectFrom('users')
.selectAll()
.execute();
const userIds = users.map((u) => u.id);
const posts = await db
.selectFrom('posts')
.selectAll()
.where('author_id', 'in', userIds)
.execute();
// アプリケーション側でマッピング
users.forEach((user) => {
const userPosts = posts.filter(
(p) => p.author_id === user.id
);
console.log(`${user.name}: ${userPosts.length} posts`);
});
手動で 2 クエリに分割することも可能です。Prisma と同じ戦略を自分で実装する形ですね。
N+1 問題対応比較表
# | 項目 | Prisma | Drizzle(relations) | Drizzle(JOIN) | Kysely |
---|---|---|---|---|---|
1 | 自動対応 | ★★★ | ★★★ | ★☆☆ | ★☆☆ |
2 | 実装の簡潔さ | ★★★ | ★★★ | ★★☆ | ★★☆ |
3 | パフォーマンス | ★★★ | ★★★ | ★★★ | ★★★ |
4 | 柔軟性 | ★☆☆ | ★★☆ | ★★★ | ★★★ |
Prisma と Drizzle(リレーショナルクエリ)は、N+1 問題を自動的に回避します。Drizzle と Kysely は、手動で最適化できる柔軟性がある反面、開発者の責任が大きくなりますね。
カスタムクエリの自由度
ツールの抽象化を超えた複雑なクエリをどう実現するかを検証しました。
ケース 1: ウィンドウ関数の使用
sql-- 実現したいSQL
SELECT
user_id,
post_id,
created_at,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) as row_num
FROM posts;
```
このような複雑なSQLを、各ツールでどう書くかを見てみましょう。
````typescript
// Prisma
const result = await prisma.$queryRaw`
SELECT
author_id,
id,
created_at,
ROW_NUMBER() OVER (PARTITION BY author_id ORDER BY created_at DESC) as row_num
FROM "Post"
`;
Prisma は、$queryRaw
で生の SQL を実行できます。ただし、型安全性は失われますね。
typescript// Drizzle
import { sql } from 'drizzle-orm';
const result = await db
.select({
userId: postsTable.authorId,
postId: postsTable.id,
createdAt: postsTable.createdAt,
rowNum:
sql<number>`ROW_NUMBER() OVER (PARTITION BY ${postsTable.authorId} ORDER BY ${postsTable.createdAt} DESC)`.as(
'row_num'
),
})
.from(postsTable);
Drizzle は、sql
タグでカスタム SQL 断片を埋め込めます。型アノテーションを付ければ、結果の型安全性も保てるのです。
typescript// Kysely
import { sql } from 'kysely';
const result = await db
.selectFrom('posts')
.select([
'author_id',
'id',
'created_at',
sql<number>`ROW_NUMBER() OVER (PARTITION BY author_id ORDER BY created_at DESC)`.as(
'row_num'
),
])
.execute();
Kysely も、sql
タグで型安全にカスタム SQL を記述できます。Drizzle と同様の書き方ですね。
ケース 2: 複雑なサブクエリ
sql-- 実現したいSQL
SELECT u.*,
(SELECT COUNT(*) FROM posts p WHERE p.author_id = u.id) as post_count,
(SELECT MAX(created_at) FROM posts p WHERE p.author_id = u.id) as latest_post
FROM users u
WHERE u.created_at > '2024-01-01';
```
````typescript
// Prisma(部分的に対応)
const users = await prisma.user.findMany({
where: {
createdAt: {
gt: new Date('2024-01-01'),
},
},
include: {
_count: {
select: {
posts: true,
},
},
},
});
// latest_postは別クエリまたは$queryRawが必要
Prisma は、_count
で件数取得は可能ですが、複雑なサブクエリは$queryRaw
頼みになります。
typescript// Drizzle
import { sql, gt } from 'drizzle-orm';
const postCountSubquery = db
.select({ count: count() })
.from(postsTable)
.where(eq(postsTable.authorId, usersTable.id))
.as('post_count_sq');
const result = await db
.select({
...getTableColumns(usersTable),
postCount: sql<number>`(SELECT COUNT(*) FROM ${postsTable} WHERE ${postsTable.authorId} = ${usersTable.id})`,
latestPost: sql<Date | null>`(SELECT MAX(${postsTable.createdAt}) FROM ${postsTable} WHERE ${postsTable.authorId} = ${usersTable.id})`,
})
.from(usersTable)
.where(gt(usersTable.createdAt, new Date('2024-01-01')));
Drizzle は、sql
タグで型安全にサブクエリを埋め込めます。やや冗長ですが、型推論は機能しますね。
typescript// Kysely
const result = await db
.selectFrom('users as u')
.select((eb) => [
'u.id',
'u.email',
'u.name',
eb
.selectFrom('posts as p')
.select((eb) =>
eb.fn.count<number>('p.id').as('count')
)
.whereRef('p.author_id', '=', 'u.id')
.as('post_count'),
eb
.selectFrom('posts as p')
.select((eb) =>
eb.fn.max<Date>('p.created_at').as('max')
)
.whereRef('p.author_id', '=', 'u.id')
.as('latest_post'),
])
.where('u.created_at', '>', new Date('2024-01-01'))
.execute();
Kysely は、サブクエリビルダーで型安全に記述できます。最も表現力が高いですね。
ケース 3: CTE(共通テーブル式)の使用
sql-- 実現したいSQL
WITH recent_posts AS (
SELECT * FROM posts WHERE created_at > '2024-01-01'
)
SELECT u.name, COUNT(rp.id) as recent_post_count
FROM users u
LEFT JOIN recent_posts rp ON rp.author_id = u.id
GROUP BY u.id, u.name;
```
````typescript
// Prisma
// CTEは直接サポートされていない。$queryRawを使用
const result = await prisma.$queryRaw`
WITH recent_posts AS (
SELECT * FROM "Post" WHERE created_at > '2024-01-01'
)
SELECT u.name, COUNT(rp.id) as recent_post_count
FROM "User" u
LEFT JOIN recent_posts rp ON rp.author_id = u.id
GROUP BY u.id, u.name
`;
Prisma は、CTE をネイティブサポートしていません。
typescript// Drizzle(実験的サポート)
// 現時点ではCTEの完全なサポートは限定的
// sql`...`での記述が推奨される
Drizzle も、CTE のネイティブサポートは発展途上です。
typescript// Kysely
const result = await db
.with('recent_posts', (db) =>
db
.selectFrom('posts')
.selectAll()
.where('created_at', '>', new Date('2024-01-01'))
)
.selectFrom('users as u')
.leftJoin('recent_posts as rp', 'rp.author_id', 'u.id')
.select([
'u.name',
(eb) =>
eb.fn.count<number>('rp.id').as('recent_post_count'),
])
.groupBy(['u.id', 'u.name'])
.execute();
Kysely は、with()
メソッドで CTE を型安全に記述できます。完璧なサポートですね。
カスタムクエリ自由度比較表
# | 項目 | Prisma | Drizzle | Kysely |
---|---|---|---|---|
1 | ウィンドウ関数 | ★☆☆($queryRaw) | ★★★(sql... ) | ★★★(sql... ) |
2 | サブクエリ | ★☆☆ | ★★☆ | ★★★ |
3 | CTE | ★☆☆ | ★☆☆ | ★★★ |
4 | 型安全性維持 | ★☆☆ | ★★★ | ★★★ |
5 | 総合自由度 | ★☆☆ | ★★☆ | ★★★ |
Kysely が圧倒的に自由度が高いです。Drizzle も SQL タグで柔軟性を確保しています。Prisma は、複雑なクエリでは$queryRaw
に頼る必要があり、型安全性が失われるのが弱点ですね。
パフォーマンスチューニングの柔軟性
本番環境での細かいチューニングがどこまで可能かを検証しました。
接続プールの制御
typescript// Prisma
const prisma = new PrismaClient({
datasources: {
db: {
url: 'postgresql://user:password@localhost:5432/mydb?connection_limit=10&pool_timeout=20',
},
},
});
Prisma は、接続文字列のクエリパラメータで基本的な設定が可能です。
typescript// Drizzle
import { Pool } from 'pg';
const pool = new Pool({
host: 'localhost',
database: 'mydb',
max: 20, // 最大接続数
idleTimeoutMillis: 30000, // アイドルタイムアウト
connectionTimeoutMillis: 2000, // 接続タイムアウト
});
const db = drizzle(pool);
Drizzle は、データベースドライバー(pg)の設定をそのまま使えます。非常に柔軟ですね。
typescript// Kysely
const pool = new Pool({
host: 'localhost',
database: 'mydb',
max: 20,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
const db = new Kysely<Database>({
dialect: new PostgresDialect({ pool }),
});
Kysely も、ドライバーの設定を完全にコントロールできます。
クエリタイムアウトの設定
typescript// Prisma(グローバル設定は未サポート)
// クエリごとに制御が必要
await prisma.$queryRaw`SET statement_timeout = 5000`; // 5秒
Prisma は、クエリタイムアウトのネイティブサポートが限定的です。
typescript// Drizzle
const result = await db.execute(
sql`SELECT * FROM posts WHERE author_id = ${userId}`
);
// タイムアウトはpool設定またはトランザクションレベルで制御
Drizzle は、接続プール設定で制御します。
typescript// Kysely
const result = await db
.selectFrom('posts')
.selectAll()
.where('author_id', '=', userId)
.execute();
// プラグインでカスタム制御も可能
Kysely も、プール設定やプラグインで柔軟に制御できます。
インデックスヒントの指定
typescript// Prisma(未サポート)
// $queryRawを使用
await prisma.$queryRaw`
SELECT /*+ INDEX(posts idx_author_created) */ *
FROM "Post"
WHERE author_id = 1
`;
Prisma は、インデックスヒントをネイティブサポートしていません。
typescript// Drizzle
const result = await db.execute(
sql`SELECT /*+ INDEX(posts idx_author_created) */ * FROM posts WHERE author_id = 1`
);
Drizzle は、sql
タグで任意のヒントを埋め込めます。
typescript// Kysely
const result = await db
.selectFrom(
sql`posts /*+ INDEX(posts idx_author_created) */`.as(
'posts'
)
)
.selectAll()
.where('author_id', '=', 1)
.execute();
Kysely も、sql
タグで柔軟に対応できますね。
EXPLAIN 実行
typescript// Prisma
const result = await prisma.$queryRaw`
EXPLAIN ANALYZE SELECT * FROM "Post" WHERE published = true
`;
console.log(result);
Prisma は、$queryRaw
で EXPLAIN を実行できます。
typescript// Drizzle
const query = db
.select()
.from(postsTable)
.where(eq(postsTable.published, true))
.toSQL(); // SQLとパラメータを取得
console.log('SQL:', query.sql);
console.log('Params:', query.params);
// 実際にEXPLAINを実行
const explain = await db.execute(
sql`EXPLAIN ANALYZE ${sql.raw(query.sql)}`
);
Drizzle は、toSQL()
でクエリを取得し、EXPLAIN をかけられます。
typescript// Kysely
const query = db
.selectFrom('posts')
.selectAll()
.where('published', '=', true)
.compile(); // コンパイルしてSQLを取得
console.log('SQL:', query.sql);
console.log('Params:', query.parameters);
// EXPLAINを実行
const explain = await db
.selectFrom(
sql`(EXPLAIN ANALYZE ${sql.raw(query.sql)})`.as(
'explain_result'
)
)
.selectAll()
.execute();
Kysely も、compile()
で SQL を取得し、柔軟に EXPLAIN を実行できます。
パフォーマンスチューニング比較表
# | 項目 | Prisma | Drizzle | Kysely |
---|---|---|---|---|
1 | 接続プール制御 | ★☆☆ | ★★★ | ★★★ |
2 | タイムアウト設定 | ★☆☆ | ★★★ | ★★★ |
3 | インデックスヒント | ★☆☆ | ★★★ | ★★★ |
4 | EXPLAIN 実行 | ★★☆ | ★★★ | ★★★ |
5 | プラグイン拡張 | ★★☆ | ★★★ | ★★★ |
6 | 総合柔軟性 | ★☆☆ | ★★★ | ★★★ |
Drizzle と Kysely は、ドライバーを直接制御できるため、パフォーマンスチューニングの柔軟性が非常に高いです。Prisma は抽象化レベルが高い分、細かいチューニングが難しい面がありますね。
以下は、最適化余地の評価軸を可視化した図です。
mermaidflowchart TD
opt["最適化余地評価"] --> sql_eff["SQL効率性"]
opt --> n_plus_1["N+1対応"]
opt --> custom["カスタムクエリ"]
opt --> tuning["チューニング"]
sql_eff --> prisma_sql["Prisma: ★★☆<br/>自動最適化"]
sql_eff --> drizzle_sql["Drizzle: ★★★<br/>柔軟な選択"]
sql_eff --> kysely_sql["Kysely: ★★★<br/>完全制御"]
n_plus_1 --> prisma_n["Prisma: ★★★<br/>自動対応"]
n_plus_1 --> drizzle_n["Drizzle: ★★★<br/>自動+手動"]
n_plus_1 --> kysely_n["Kysely: ★★☆<br/>手動対応"]
custom --> prisma_c["Prisma: ★☆☆<br/>限定的"]
custom --> drizzle_c["Drizzle: ★★☆<br/>sql利用"]
custom --> kysely_c["Kysely: ★★★<br/>完全対応"]
tuning --> prisma_t["Prisma: ★☆☆<br/>限定的"]
tuning --> drizzle_t["Drizzle: ★★★<br/>ドライバー制御"]
tuning --> kysely_t["Kysely: ★★★<br/>ドライバー制御"]
Prisma は自動最適化が強みですが、細かい制御は苦手です。Drizzle と Kysely は、開発者が最適化を完全にコントロールできる柔軟性を持っていますね。
まとめ
総合評価表
3 つのツールを、DX・型安全性・最適化余地の 3 軸で総合評価しました。
# | 評価項目 | Prisma | Drizzle | Kysely |
---|---|---|---|---|
DX(開発者体験) | ||||
1 | セットアップの容易さ | ★★★ | ★★☆ | ★★☆ |
2 | スキーマ定義の書きやすさ | ★★★ | ★★★ | ★★☆ |
3 | クエリ記述の直感性 | ★★☆ | ★★★ | ★★★ |
4 | エラーメッセージ | ★★★ | ★★☆ | ★★☆ |
5 | 開発ツール統合 | ★★★ | ★★☆ | ★☆☆ |
6 | DX 総合 | ★★★ | ★★★ | ★★☆ |
型安全性 | ||||
7 | 型自動生成 | ★★★ | ★★★ | ★★☆ |
8 | クエリ結果の型推論 | ★★★ | ★★★ | ★★★ |
9 | リレーションの型安全性 | ★★★ | ★★☆ | ★☆☆ |
10 | 型エラー検出力 | ★★★ | ★★★ | ★★★ |
11 | 型安全性総合 | ★★★ | ★★★ | ★★☆ |
最適化余地 | ||||
12 | SQL 効率性 | ★★☆ | ★★★ | ★★★ |
13 | N+1 問題対応 | ★★★ | ★★★ | ★★☆ |
14 | カスタムクエリ自由度 | ★☆☆ | ★★☆ | ★★★ |
15 | チューニング柔軟性 | ★☆☆ | ★★★ | ★★★ |
16 | 最適化余地総合 | ★☆☆ | ★★★ | ★★★ |
総合評価 | ★★☆ | ★★★ | ★★☆ |
Prismaは、DX と型安全性で最高評価を獲得しました。セットアップから開発までスムーズで、初心者でも迷わず使えます。リレーション処理が非常に直感的で、N+1 問題も自動対応してくれるのが強みですね。一方、複雑な SQL や細かいチューニングには不向きで、最適化余地が限定的という弱点があります。
Drizzle ORMは、3 軸すべてで高評価を獲得し、最もバランスの取れたツールです。TypeScript-first で学習コストが低く、SQL に近い記法で直感的に書けます。リレーショナルクエリと手動 JOIN の両方を使い分けられる柔軟性も魅力です。型生成がリアルタイムで、開発体験も優れています。エコシステムは成長中ですが、今後の発展が期待できるでしょう。
Kyselyは、型安全性と最適化余地で高評価を獲得しました。SQL の完全な制御が可能で、複雑なクエリやパフォーマンスチューニングが必要な場合に最適です。SQL 経験者にとっては最も自然に書けるツールでしょう。一方、リレーション処理は手動で実装する必要があり、初心者にはハードルが高いという面があります。
以下は、3 ツールの特性を 3 軸で比較したイメージ図です。
mermaidflowchart TD
subgraph comparison["3ツール比較マトリクス"]
direction LR
subgraph prisma_score["Prisma"]
p1["DX: ★★★"]
p2["型安全性: ★★★"]
p3["最適化: ★☆☆"]
end
subgraph drizzle_score["Drizzle"]
d1["DX: ★★★"]
d2["型安全性: ★★★"]
d3["最適化: ★★★"]
end
subgraph kysely_score["Kysely"]
k1["DX: ★★☆"]
k2["型安全性: ★★☆"]
k3["最適化: ★★★"]
end
end
それぞれに明確な個性があり、プロジェクトの要件によって最適な選択肢が変わることが分かります。
用途別おすすめツール
プロジェクトの特性に応じた、おすすめツールをご紹介します。
Prisma がおすすめのケース
以下のような要件の場合、Prisma が最適です。
- スタートアップや小規模チーム: セットアップが簡単で、すぐに開発を始められます
- TypeScript 初心者が多い: 独自 DSL ですが、学習リソースが豊富で習得しやすいです
- リレーション処理が多い: 1 対多、多対多を直感的に扱えます
- N+1 問題を自動で避けたい: 意識せずに最適化されたクエリが実行されます
- GUI 管理ツールが欲しい: Prisma Studio で視覚的にデータを確認できます
- マイグレーション管理も統合したい: オールインワンで完結します
Prisma は「すぐに開発を始めたい」「複雑な SQL 知識は不要」というプロジェクトに最適ですね。
Drizzle ORM がおすすめのケース
以下のような要件では、Drizzle が最適な選択肢です。
- TypeScript 経験者が多い: スキーマもクエリもすべて TypeScript で完結します
- SQL 知識を活かしたい: SQL に近い記法で、学習コストが低いです
- エッジランタイムで動かしたい: Cloudflare Workers、Vercel Edge に最適化されています
- 柔軟性と安全性のバランス: リレーショナルクエリと手動 JOIN を使い分けられます
- 型生成のステップを省きたい: リアルタイムで型が反映されます
- パフォーマンスチューニングもしたい: 細かい制御が可能です
Drizzle は「バランスの取れたツールが欲しい」「モダンな環境で動かしたい」というプロジェクトに最適でしょう。
Kysely がおすすめのケース
以下のような要件では、Kysely が最適です。
- SQL 経験者が多い: SQL の知識をそのまま活かせます
- 複雑なクエリが必要: ウィンドウ関数、CTE、サブクエリを型安全に書けます
- パフォーマンスが最重要: SQL を完全にコントロールして最適化できます
- 既存 DB を使う: スキーマ管理は別ツールで、クエリだけ Kysely という使い方ができます
- 軽量なツールが欲しい: クエリビルダーとしての機能に特化しています
- レガシーな複雑スキーマ: どんなスキーマでも対応できる柔軟性があります
Kysely は「SQL を完全に制御したい」「パフォーマンス最適化が必須」というプロジェクトに最適ですね。
組み合わせ利用も検討する価値があります
実は、これらのツールは排他的ではありません。
例えば、「マイグレーション管理は Prisma、クエリ実行は Kysely」という組み合わせも可能です。Prisma のマイグレーション機能を使ってスキーマを管理し、複雑なクエリだけ Kysely で書くという使い方ができますね。
また、「基本的な CRUD は Drizzle のリレーショナルクエリ、複雑な集計は Drizzle の手動 JOIN」というように、同じツール内で使い分けることも効果的です。
プロジェクトの特性を見極め、最適なツール(または組み合わせ)を選択することが重要でしょう。
最終的には、チームのスキルセット、プロジェクトの規模、パフォーマンス要件、将来の拡張性などを総合的に判断して選ぶことをおすすめします。どのツールも優れた設計思想を持っており、適切に使えば素晴らしい開発体験を提供してくれるはずです。
関連リンク
- article
Prisma vs Drizzle vs Kysely:DX・型安全性・最適化余地を実測比較
- article
Prisma トラブルシュート大全:P1000/P1001/P1008 ほか接続系エラーの即解決ガイド
- article
Prisma アーキテクチャ超図解:Engines/Client/Generator の役割を一枚で理解
- article
Prisma の公式ドキュメントを使い倒すためのコツ
- article
Prisma での DB マイグレーション運用ベストプラクティス
- article
Prisma のマルチ DB 対応を使いこなす
- article
Redis セットアップ完全版(macOS/Homebrew):設定テンプレ付き最短構築
- article
FFmpeg を macOS で最適導入:Homebrew +オプション選定で機能漏れゼロ
- article
Python を macOS で快適構築:pyenv + uv + Rye の最小ストレス環境
- article
ESLint を Yarn + TypeScript + React でゼロから構築:Flat Config 完全手順(macOS)
- article
Dify を macOS でローカル検証:Docker Compose で最短起動する手順
- article
Prisma vs Drizzle vs Kysely:DX・型安全性・最適化余地を実測比較
- blog
iPhone 17シリーズの発表!全モデルiPhone 16から進化したポイントを見やすく整理
- blog
Googleストアから訂正案内!Pixel 10ポイント有効期限「1年」表示は誤りだった
- blog
【2025年8月】Googleストア「ストアポイント」は1年表記はミス?2年ルールとの整合性を検証
- blog
Googleストアの注文キャンセルはなぜ起きる?Pixel 10購入前に知るべき注意点
- blog
Pixcel 10シリーズの発表!全モデル Pixcel 9 から進化したポイントを見やすく整理
- blog
フロントエンドエンジニアの成長戦略:コーチングで最速スキルアップする方法
- review
今の自分に満足していますか?『持たざる者の逆襲 まだ何者でもない君へ』溝口勇児
- review
ついに語られた業界の裏側!『フジテレビの正体』堀江貴文が描くテレビ局の本当の姿
- review
愛する勇気を持てば人生が変わる!『幸せになる勇気』岸見一郎・古賀史健のアドラー実践編で真の幸福を手に入れる
- review
週末を変えれば年収も変わる!『世界の一流は「休日」に何をしているのか』越川慎司の一流週末メソッド
- review
新しい自分に会いに行こう!『自分の変え方』村岡大樹の認知科学コーチングで人生リセット
- review
科学革命から AI 時代へ!『サピエンス全史 下巻』ユヴァル・ノア・ハラリが予見する人類の未来