Knex vs Kysely ― SQL クエリビルダー徹底比較
1. 結論
TypeScript プロジェクトで型安全性を最優先にするなら Kysely、既存の大規模エコシステムや豊富なドキュメントを活かしたいなら Knex を選ぶべきです。 新規プロジェクトで TypeScript を採用しているなら Kysely が第一候補になりますが、マイグレーション機能やシーダーなど「バッテリー同梱」の利便性を重視する場合は Knex が依然として有力な選択肢です。
2. 比較表
| 観点 | Knex | Kysely |
|---|---|---|
| GitHub Stars(2025年時点) | ≈ 19,500+ | ≈ 11,500+ |
| 初回リリース | 2013年 | 2022年 |
| TypeScript 対応 | @types/knex(型定義は後付け) | コアが TypeScript で書かれており、完全な型推論 |
| 型安全性 | △ クエリ結果は基本 any | ◎ SELECT のカラムまで型推論される |
| バンドルサイズ(unpacked) | ≈ 860 KB | ≈ 470 KB |
| 依存パッケージ数 | 多い(tarn, colorette 等) | 最小限 |
| 対応 DB | PostgreSQL, MySQL, SQLite3, MSSQL, CockroachDB, Oracle (community) | PostgreSQL, MySQL, SQLite, MSSQL |
| マイグレーション | ◎ 組み込み(CLI 付き) | ○ 組み込み(CLI は別途 or 自作) |
| シーダー | ◎ 組み込み | × なし |
| トランザクション | ◎ | ◎ |
| Raw SQL | ◎ knex.raw() | ◎ sql テンプレートタグ |
| プラグイン / 拡張 | 豊富(Objection.js, Bookshelf 等の ORM 基盤) | プラグインシステムあり(camelCase 変換等) |
| 学習コスト | 低〜中(ドキュメント・記事が豊富) | 中(型システムの理解が必要) |
| コミュニティ規模 | 大きい・成熟 | 急成長中 |
| メンテナンス状況 | 安定(更新頻度はやや低下傾向) | 活発 |
3. それぞれの強み
Knex の強み
バッテリー同梱の充実度
Knex は「クエリビルダー」にとどまらず、マイグレーション CLI・シーダー・コネクションプーリング(tarn.js) をすべて内包しています。npx knex migrate:make 一発でマイグレーションファイルが生成でき、追加ツールの選定に悩む必要がありません。
圧倒的なエコシステム
10 年以上の歴史があり、Objection.js や Bookshelf.js といった ORM の基盤として採用されてきました。Stack Overflow や Qiita・Zenn の日本語記事も豊富で、トラブルシューティングの情報に困ることはほぼありません。
幅広い DB サポート
CockroachDB や Oracle(コミュニティドライバ)まで対応しており、エンタープライズ環境での採用実績も多いです。
Kysely の強み
圧倒的な型安全性
Kysely 最大の差別化ポイントは エンドツーエンドの型推論 です。テーブル定義の型を一度書けば、SELECT・INSERT・UPDATE・DELETE のすべてでカラム名の補完と型チェックが効きます。存在しないカラムを指定するとコンパイルエラーになるため、ランタイムエラーを大幅に削減 できます。
軽量かつゼロ依存に近い設計
コアパッケージの依存はほぼゼロで、バンドルサイズも Knex の約半分です。サーバーレス環境(AWS Lambda、Cloudflare Workers 等)でのコールドスタートにも有利です。
モダンな API 設計
TypeScript ファーストで設計されているため、IDE の補完体験が非常に優れています。sql テンプレートリテラルによる Raw SQL も型安全に扱えます。
4. コード例で比較
以下では、同じ PostgreSQL のテーブルに対して 基本的な CRUD 操作 を行うコードを比較します。
テーブル定義(共通の前提)
CREATE TABLE users (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
4-1. セットアップ
Knex
// knex の場合、型定義は手動で付与するか any で受ける
import Knex from "knex";
const db = Knex({
client: "pg",
connection: {
host: "localhost",
port: 5432,
user: "admin",
password: "password",
database: "myapp",
},
});
// 型を付けたい場合は interface を定義してジェネリクスで渡す
interface UserRow {
id: number;
name: string;
email: string;
created_at: Date;
}
Kysely
import { Kysely, PostgresDialect, Generated } from "kysely";
import { Pool } from "pg";
// DB スキーマ全体を型として定義する
interface Database {
users: UsersTable;
}
interface UsersTable {
id: Generated<number>;
name: string;
email: string;
created_at: Generated<Date>;
}
const db = new Kysely<Database>({
dialect: new PostgresDialect({
pool: new Pool({
host: "localhost",
port: 5432,
user: "admin",
password: "password",
database: "myapp",
}),
}),
});
ポイント: Kysely では
Database型を定義することで、以降のすべてのクエリにカラム名・型の補完が効きます。Generated<T>は INSERT 時に省略可能なカラムを表します。
4-2. SELECT(条件付き取得)
Knex
// 戻り値は any[] になりがち(ジェネリクスで補う)
const users = await db<UserRow>("users")
.select("id", "name", "email")
.where("name", "like", "%田中%")
.orderBy("created_at", "desc")
.limit(10);
// users: UserRow[] — ただし select で絞ったカラムだけという型にはならない
// users[0].created_at にアクセスしても型エラーにならない(実際は undefined)
Kysely
const users = await db
.selectFrom("users")
.select(["id", "name", "email"])
.where("name", "like", "%田中%")
.orderBy("created_at", "desc")
.limit(10)
.execute();
// users: { id: number; name: string; email: string }[]
// users[0].created_at にアクセスすると → コンパイルエラー ✅
ポイント: Kysely は
selectで指定したカラムだけを持つ型を自動推論します。Knex のジェネリクスでは SELECT で絞ったカラムまでは追跡できません。
4-3. INSERT
Knex
const [inserted] = await db<UserRow>("users")
.insert({
name: "山田太郎",
email: "yamada@example.com",
})
.returning("*");
// inserted: UserRow(returning を使わないと id のみ)
Kysely
const inserted = await db
.insertInto("users")
.values({
name: "山田太郎",
email: "yamada@example.com",
})
.returningAll()
.executeTakeFirstOrThrow();
// inserted: { id: number; name: string; email: string; created_at: Date }
// "namee" のようなタイポ → コンパイルエラー ✅
4-4. UPDATE
Knex
const updatedCount: number = await db<UserRow>("users")
.where("id", 1)
.update({ name: "山田次郎" });
Kysely
const result = await db
.updateTable("users")
.set({ name: "山田次郎" })
.where("id", "=", 1)
.executeTakeFirst();
console.log(result.numUpdatedRows); // bigint
4-5. DELETE
Knex
const deletedCount: number = await db<UserRow>("users")
.where("id", 1)
.delete();
Kysely
const result = await db
.deleteFrom("users")
.where("id", "=", 1)
.executeTakeFirst();
console.log(result.numDeletedRows); // bigint
4-6. JOIN を含む複雑なクエリ
Knex
interface OrderWithUser {
orderId: number;
userName: string;
total: number;
}
const rows = await db<OrderWithUser>("orders as o")
.join("users as u", "u.id", "o.user_id")
.select("o.id as orderId", "u.name as userName", "o.total")
.where("o.total", ">", 10000)
.orderBy("o.total", "desc");
// rows: OrderWithUser[] — ただし型は自分で定義する必要がある
Kysely
// Database 型に orders テーブルも定義済みとする
const rows = await db
.selectFrom("orders as o")
.innerJoin("users as u", "u.id", "o.user_id")
.select(["o.id as orderId", "u.name as userName", "o.total"])
.where("o.total", ">", 10000)
.orderBy("o.total", "desc")
.execute();
// rows: { orderId: number; userName: string; total: number }[]
// 型は自動推論される ✅
4-7. トランザクション
Knex
await db.transaction(async (trx) => {
const [user] = await trx<UserRow>("users")
.insert({ name: "佐藤花子", email: "sato@example.com" })
.returning("*");
await trx("orders").insert({
user_id: user.id,
total: 5000,
});
});
Kysely
await db.transaction().execute(async (trx) => {
const user = await trx
.insertInto("users")
.values({ name: "佐藤花子", email: "sato@example.com" })
.returningAll()
.executeTakeFirstOrThrow();
await trx
.insertInto("orders")
.values({
user_id: user.id, // user.id は number 型として推論される
total: 5000,
})
.execute();
});
4-8. マイグレーション
Knex
# CLI が組み込み
npx knex migrate:make create_users_table
npx knex migrate:latest
npx knex seed:make initial_users
npx knex seed:run
// migrations/20250101000000_create_users_table.ts
import { Knex } from "knex";
export async function up(knex: Knex): Promise<void> {
await knex.schema.createTable("users", (table) => {
table.increments("id").primary();
table.string("name", 255).notNullable();
table.string("email", 255).notNullable().unique();
table.timestamp("created_at").notNullable().defaultTo(knex.fn.now());
});
}
export async function down(knex: Knex): Promise<void> {
await knex.schema.dropTable("users");
}
Kysely
// migrations/2025-01-01T00-00-00-create-users.ts
import { Kysely, sql } from "kysely";
export async function up(db: Kysely<unknown>): Promise<void> {
await db.schema
.createTable("users")
.addColumn("id", "serial", (col) => col.primaryKey())
.addColumn("name", "varchar(255)", (col) => col.notNull())
.addColumn("email", "varchar(255)", (col) => col.notNull().unique())
.addColumn("created_at", "timestamp", (col) =>
col.notNull().defaultTo(sql`now()`)
)
.execute();
}
export async function down(db: Kysely<unknown>): Promise<void> {
await db.schema.dropTable("users").execute();
}
// マイグレーション実行は自分でランナーを書く(または kysely-ctl を利用)
import { promises as fs } from "fs";
import path from "path";
import { FileMigrationProvider, Migrator } from "kysely";
const migrator = new Migrator({
db,
provider: new FileMigrationProvider({
fs,
path,
migrationFolder: path.join(__dirname, "migrations"),
}),
});
const { results, error } = await migrator.migrateToLatest();
ポイント: Knex は CLI 一発でマイグレーション・シードを管理できます。