Drizzle ORM vs Kysely: Chọn công cụ query TypeScript nào trong 2026?
Drizzle thắng về developer experience và quy trình full-stack. Kysely thắng về độ chính xác kiểu dữ liệu tại compile time. Đây là cách chọn.
Bởi Ethan
2.037 từ · 11 phút đọc
Chọn Drizzle nếu bạn đang bắt đầu một dự án TypeScript mới và muốn có schema-first development, tự động sinh SQL migration, cùng các adapter sẵn dùng ngay cho môi trường edge. Chọn Kysely nếu query của bạn thực sự phức tạp — subquery lồng nhau nhiều tầng, CTE kết hợp aggregation, JSONB path operations — và bạn cần type system bắt lỗi query không hợp lệ trước khi chúng lên production.
Bài viết này dành cho ai
Developer TypeScript đang bắt đầu project Postgres hoặc SQLite mới trong 2026, hoặc đang chuyển khỏi Prisma. Nếu bạn đã dùng một trong hai công cụ này được sáu tháng và query đang chạy ổn, cứ tiếp tục — không đáng migration chỉ vì bài viết này.
Chúng tôi đã thử gì
Chúng tôi chạy cả hai công cụ trên một schema Postgres 16 gồm ba bảng: users, posts, và comments, với đầy đủ các dạng query từ CRUD đến CTE cho đến JSONB aggregation. Host là Neon serverless Postgres — cả hai công cụ đều có tài liệu hỗ trợ chính thức.
| Package | Phiên bản ổn định | RC | Phát hành |
|---|---|---|---|
drizzle-orm | 0.45.2 | v1.0.0-rc.3 | rc.3 phát hành 2026-05-18 |
drizzle-kit | bundled | — | — |
kysely | 0.29.2 | — | 2026-05-16 |
Mức độ phổ biến tính đến 2026-05-19:
| Chỉ số | Drizzle | Kysely |
|---|---|---|
| Lượt tải npm mỗi tuần | 9.695.559 | 6.010.148 |
| GitHub stars | 34.459 | 13.825 |
| Issues đang mở | 1.784 | 155 |
Khoảng cách về số issue một phần do lượng người dùng lớn hơn, một phần do tính năng phức tạp hơn — đáng lưu ý, nhưng không cần lo ngại thái quá.
Định nghĩa schema
Drizzle là schema-first. Bạn định nghĩa bảng bằng TypeScript, chạy drizzle-kit generate, và nó tạo ra các file SQL migration:
import { pgTable, serial, varchar, integer } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: varchar('name', { length: 256 }),
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
authorId: integer('author_id').references(() => users.id),
title: varchar('title', { length: 512 }),
});
Kysely làm ngược lại. Không có schema file — bạn viết TypeScript interface mô tả database, rồi trỏ Kysely vào đó:
interface Database {
users: UsersTable;
posts: PostsTable;
}
export const db = new Kysely<Database>({
dialect: new NeonDialect({ connectionString: process.env.DATABASE_URL! }),
});
Với project mới, cách tiếp cận của Drizzle đưa bạn đến productive nhanh hơn. Với project cũ — database đã có sẵn, không muốn viết lại schema bằng TypeScript — Kysely cho phép thêm type safety mà không cần đụng vào schema.
Xây dựng query
Cả hai đều dùng fluent builder API. Drizzle import các entity từ table; Kysely dùng tham chiếu kiểu string.
Drizzle:
const result = await db
.select({ name: users.name, postCount: count(posts.id) })
.from(users)
.leftJoin(posts, eq(posts.authorId, users.id))
.groupBy(users.id)
.where(gt(users.id, 100));
Kysely:
const result = await db
.selectFrom('users as u')
.select(['u.name', db.fn.count<number>('posts.id').as('postCount')])
.leftJoin('posts', 'posts.author_id', 'u.id')
.groupBy('u.id')
.where('u.id', '>', 100)
.execute();
Với CRUD thông thường và join đơn giản, cả hai đều tốt. Sự khác biệt thể hiện rõ khi query trở nên phức tạp.
Query phức tạp: CTE, JSONB, kết quả lồng nhau
Đây là nơi hai công cụ phân hóa rõ ràng.
CTE
Drizzle hỗ trợ CTE nhưng type inference có thể sai khi dùng template tag sql bên trong CTE mà không có alias rõ ràng. Kiểu dữ liệu của field trở thành DrizzleTypeError và không thể tham chiếu downstream:
const sq = db.$with('post_counts').as(
db.select({ userId: posts.authorId, cnt: sql<number>`count(*)` })
.from(posts).groupBy(posts.authorId)
);
const result = await db.with(sq).select().from(sq);
Kysely truyền kiểu dữ liệu chính xác qua CTE mà không cần annotation thủ công:
const result = await db
.with('post_counts', (db) =>
db.selectFrom('posts')
.select(['author_id as userId', db.fn.countAll<number>().as('cnt')])
.groupBy('author_id')
)
.selectFrom('post_counts')
.selectAll()
.execute();
Kết quả lồng nhau và JSONB
Kysely có sẵn các helper jsonArrayFrom và jsonObjectFrom trả về mảng lồng nhau với kiểu dữ liệu chính xác trong một query duy nhất:
import { jsonArrayFrom } from 'kysely/helpers/postgres';
const result = await db
.selectFrom('person')
.select((eb) => [
'id',
jsonArrayFrom(
eb.selectFrom('pet')
.select(['pet.id as pet_id', 'pet.name'])
.whereRef('pet.owner_id', '=', 'person.id')
.orderBy('pet.name')
).as('pets')
])
.execute();
// Type: { id: string; pets: { pet_id: string; name: string }[] }[]
SQL-like API của Drizzle trả về kiểu dữ liệu phẳng. Để có kết quả lồng nhau, bạn phải dùng Relational API (RQB), vốn cần khai báo with: đặt cùng schema:
const result = await db.query.users.findMany({
with: { posts: true },
});
// Type: { id: number; name: string; posts: { id: number; title: string }[] }[]
Cách dùng RQB hoạt động tốt khi schema được map đầy đủ. Với các JSONB path operations (PostgreSQL ->>, #>>, @>), cả hai công cụ đều buộc bạn dùng template tag sql — các JSONB column type của Drizzle và helper của Kysely đều không phủ hết hết các operator.
Type safety ở cấp query
Đây là điểm khác biệt cốt lõi về triết lý. Kysely kiểm tra cấu trúc query tại compile time. API dựa trên callback của nó theo dõi table và column nào đang trong scope của mỗi subquery — tham chiếu đến column ngoài scope là lỗi TypeScript trước khi chạy bất cứ thứ gì.
Drizzle kiểm tra kết quả query. Các kiểu dữ liệu dẫn xuất từ schema đảm bảo kết quả SELECT được type đúng, nhưng builder ít nghiêm ngặt hơn về những gì có thể diễn đạt. Aggregation tùy chỉnh phức tạp cần annotation sql<Type> rõ ràng và không có runtime validation.
Một nhóm ghi nhận điều này (dev.to, 2025): Drizzle không bắt được tham chiếu column không hợp lệ trong query lồng nhau sâu, trong khi callback scoping của Kysely bắt lỗi ngay tại compile time. Với app nặng CRUD, sự khác biệt này hiếm khi quan trọng. Với analytics dashboard, data pipeline, hay bất cứ thứ gì có 5+ lần join — nó quan trọng rất nhiều.
Quản lý migration
Drizzle thắng rõ ràng ở điểm này.
drizzle-kit là bộ công cụ migration đầy đủ đi kèm Drizzle:
# Phát hiện thay đổi schema, tạo file migration .sql
npx drizzle-kit generate
# Áp dụng các migration còn chờ
npx drizzle-kit migrate
# Prototype nhanh: push schema trực tiếp, bỏ qua file
npx drizzle-kit push
drizzle-kit pull reverse-engineer database hiện có thành TypeScript schema — hữu ích cho team đang chuyển từ Prisma hoặc raw SQL. Drizzle Studio (npx drizzle-kit studio) cung cấp giao diện trình duyệt để duyệt và sửa dữ liệu.
Kysely có class Migrator. Migration là các file TypeScript thuần mà bạn phải tự viết:
export async function up(db: Kysely<any>): Promise<void> {
await db.schema
.createTable('users')
.addColumn('id', 'serial', (col) => col.primaryKey())
.addColumn('name', 'varchar(256)')
.execute();
}
export async function down(db: Kysely<any>): Promise<void> {
await db.schema.dropTable('users').execute();
}
Không có tính năng tự sinh migration. Bạn viết tay từng migration. Các công cụ cộng đồng như kysely-migration-cli hoặc ley có thể hỗ trợ workflow, nhưng mặc định vẫn là thủ công. Điều đó ổn nếu bạn muốn kiểm soát hoàn toàn và có kỷ luật. Với hầu hết team, migration tự sinh của Drizzle tiết kiệm thời gian thực sự.
Edge runtime
Drizzle được thiết kế rõ ràng cho môi trường edge. Adapter chính thức cho Cloudflare Workers (D1, Neon HTTP), Vercel Edge, Deno Deploy, Bun, Supabase Edge Functions, và nhiều hơn nữa. Các adapter này do team Drizzle duy trì và theo dõi khi driver upstream thay đổi.
Kysely tiếp cận các target tương tự qua các dialect cộng đồng: kysely-d1 cho Cloudflare D1, kysely-neon cho Neon, kysely-libsql cho Turso. Các dialect này hoạt động tốt nhưng do cộng đồng duy trì — đôi khi có độ trễ khi driver upstream thay đổi.
Với team nhắm đến Cloudflare Workers hoặc Vercel Edge với Neon làm database, setup của Drizzle rất gọn:
// src/db.ts
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql);
Kysely với Neon cần dialect cộng đồng:
import { Kysely } from 'kysely';
import { NeonDialect } from 'kysely-neon';
export const db = new Kysely<Database>({
dialect: new NeonDialect({ connectionString: process.env.DATABASE_URL! }),
});
Cả hai đều chạy được. Drizzle ít dependency hơn một bước.
Debug
Drizzle Studio là lợi thế thực sự khi debug — giao diện web nội bộ để duyệt và sửa dữ liệu, khởi động bằng một lệnh duy nhất. Drizzle v1.0.0-rc.1 (2026-04-30) bổ sung JIT mappers tùy chọn với mức giảm 25–30% độ trễ row-mapping theo công bố của họ (release notes).
Thông báo lỗi TypeScript của Drizzle với query type phức tạp đã cải thiện từ v1.0 nhưng vẫn có thể xuất hiện giá trị DrizzleTypeError khó hiểu khi bạn thiếu alias.
Lỗi của Kysely thường sạch hơn tại compile time. Với query rất phức tạp, đôi khi bạn có thể gặp giới hạn “Type instantiation is excessively deep” của TypeScript — Kysely có tài liệu cách xử lý trong phần recipes. 155 issue đang mở của Kysely so với 1.784 của Drizzle phản ánh phạm vi lỗi nhỏ hơn và scope đơn giản hơn, không chỉ đơn thuần là ít người dùng hơn.
Drizzle vs Kysely: Kết luận
Chọn Drizzle cho project mới. Schema-first development, migration tự sinh, Drizzle Studio, adapter edge chính thức — đây là lựa chọn mà hầu hết team TypeScript nên dùng trong 2026. Nhanh đến productive hơn Prisma với ít abstraction overhead hơn, và v0.45.2 đã ổn định cho production. RC v1.0 đang định hình tốt, nhưng đừng chờ nếu bạn đang bắt đầu ngay hôm nay.
Chọn Kysely khi query của bạn vượt ra ngoài CRUD thông thường. Subquery lồng nhau sâu, CTE với aggregation, dynamic column selection, hoặc JSONB path operations — type validation tại compile time của Kysely bắt lỗi mà Drizzle bỏ qua. Cũng là lựa chọn đúng nếu bạn đang chuyển codebase Knex.js cũ (API shape tương tự) hoặc nếu database đã tồn tại và bạn không muốn viết lại schema bằng TypeScript DSL.
Cả hai công cụ đều xuất ra raw SQL. Không cái nào thêm overhead của query engine. Lựa chọn nằm ở dev loop và đánh đổi về type safety, không phải hiệu năng.
Chúng tôi đã thử nghiệm cả hai trên Neon serverless Postgres — setup cho cả hai đều được tài liệu tốt và hoạt động ổn định trên môi trường edge. Nếu bạn vẫn đang cân nhắc giữa nhiều công cụ truy cập dữ liệu TypeScript, Best TypeScript ORM năm 2026 so sánh Prisma, Drizzle và các lựa chọn khác.
Lưu ý
- Phiên bản đã thử: drizzle-orm 0.45.2, drizzle-orm v1.0.0-rc.3, kysely 0.29.2, Postgres 16, Neon serverless host.
- Không đề cập MySQL dialect. Drizzle có MySQL adapter; Kysely có MySQL dialect cộng đồng. Cả hai đều chạy, nhưng các tính năng đặc thù MySQL (ví dụ JSON_TABLE) có mức hỗ trợ khác nhau.
- Drizzle’s Relations API (RQB) là một abstraction riêng phủ trên SQL-like API. Nếu bạn dùng nó hoàn toàn, một số lưu ý về CTE và JSONB ở trên không áp dụng — nhưng bạn từ bỏ khả năng kiểm soát ở cấp SQL.
- Neon là affiliate partner (xem disclosure ở trên). Điều đó không ảnh hưởng kết luận — Drizzle vẫn thắng phần migration dù bạn dùng Postgres host nào.
- Drizzle v1.0 vẫn đang RC. v0.45.2 ổn định cho production. Kiểm tra trang releases của Drizzle trước khi nâng cấp.
Tài liệu tham khảo
- Drizzle ORM docs overview
- Drizzle migrations guide
- Drizzle PostgreSQL column types
- Drizzle SQL escape hatch
- Drizzle Studio
- Drizzle relations / RQB
- Drizzle GitHub — releases, issues
- Kysely docs intro
- Kysely migrations
- Kysely nested arrays example
- Kysely GitHub
- Marmelab: Kysely vs Drizzle (Jun 2025)
- Why we switched from Drizzle to Kysely (dev.to, 2025)
- PkgPulse: Drizzle v1 vs Prisma 6 vs Kysely (2026)
- npm download stats: drizzle-orm, kysely — tuần 2026-05-13