knex vs kysely 徹底比較

knex の詳細kysely の詳細
AI生成コンテンツ

この記事はAIによって生成されました。内容の正確性は保証されません。最新の情報は公式ドキュメントをご確認ください。

Knex vs Kysely ― SQL クエリビルダー徹底比較

1. 結論

TypeScript プロジェクトで型安全性を最優先にするなら Kysely、既存の大規模エコシステムや豊富なドキュメントを活かしたいなら Knex を選ぶべきです。 新規プロジェクトで TypeScript を採用しているなら Kysely が第一候補になりますが、マイグレーション機能やシーダーなど「バッテリー同梱」の利便性を重視する場合は Knex が依然として有力な選択肢です。


2. 比較表

観点KnexKysely
GitHub Stars(2025年時点)≈ 19,500+≈ 11,500+
初回リリース2013年2022年
TypeScript 対応@types/knex(型定義は後付け)コアが TypeScript で書かれており、完全な型推論
型安全性△ クエリ結果は基本 any◎ SELECT のカラムまで型推論される
バンドルサイズ(unpacked)≈ 860 KB≈ 470 KB
依存パッケージ数多い(tarn, colorette 等)最小限
対応 DBPostgreSQL, MySQL, SQLite3, MSSQL, CockroachDB, Oracle (community)PostgreSQL, MySQL, SQLite, MSSQL
マイグレーション◎ 組み込み(CLI 付き)○ 組み込み(CLI は別途 or 自作)
シーダー◎ 組み込み× なし
トランザクション
Raw SQLknex.raw()sql テンプレートタグ
プラグイン / 拡張豊富(Objection.js, Bookshelf 等の ORM 基盤)プラグインシステムあり(camelCase 変換等)
学習コスト低〜中(ドキュメント・記事が豊富)中(型システムの理解が必要)
コミュニティ規模大きい・成熟急成長中
メンテナンス状況安定(更新頻度はやや低下傾向)活発

3. それぞれの強み

Knex の強み

バッテリー同梱の充実度

Knex は「クエリビルダー」にとどまらず、マイグレーション CLI・シーダー・コネクションプーリング(tarn.js) をすべて内包しています。npx knex migrate:make 一発でマイグレーションファイルが生成でき、追加ツールの選定に悩む必要がありません。

圧倒的なエコシステム

10 年以上の歴史があり、Objection.jsBookshelf.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 一発でマイグレーション・シードを管理できます。