Drizzle vs Kysely in 2026: SQL-First ORMs Compared
·PkgPulse Team
TL;DR
Drizzle if you need schema definition + migrations; Kysely if you want a pure query builder over an existing schema. Drizzle (~2M weekly downloads) is a full ORM — define schema in TypeScript, generate migrations, and query. Kysely (~800K downloads) is a type-safe query builder — it generates SQL types from an existing database but doesn't manage migrations. Both produce excellent TypeScript-typed SQL.
Key Takeaways
- Drizzle: ~2M weekly downloads — Kysely: ~800K (npm, March 2026)
- Drizzle = ORM + migration tool — Kysely = query builder only
- Both are SQL-first — no hidden N+1 queries, no magic
- Kysely has Introspect support — generate types from existing database
- Drizzle is growing faster — newer project but rapidly gaining adoption
What Each Covers
Drizzle:
✓ Schema definition (TypeScript)
✓ Migration generation (drizzle-kit)
✓ Type-safe queries
✓ Relations (not joins — explicit relation definitions)
✓ Edge runtime support (Cloudflare Workers, Bun, Deno)
✓ Multiple databases (PostgreSQL, MySQL, SQLite)
Kysely:
✓ Type-safe queries
✓ Database introspection (kysely-codegen)
✗ Schema definition (use migrations separately)
✗ Migration management (use db-migrate, Flyway, or write raw SQL)
✓ Plugin system (dialects for different databases)
✓ PostgreSQL, MySQL, SQLite
Schema Definition (Drizzle Only)
// Drizzle — define schema in TypeScript
import { pgTable, serial, text, integer, timestamp, boolean } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: text('email').notNull().unique(),
name: text('name').notNull(),
role: text('role', { enum: ['admin', 'user', 'moderator'] }).default('user'),
createdAt: timestamp('created_at').defaultNow(),
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
content: text('content'),
authorId: integer('author_id').references(() => users.id),
published: boolean('published').default(false),
});
// Then run: npx drizzle-kit generate → creates migration files
// npx drizzle-kit push → applies to database
// Kysely — types come FROM the database, not to it
// Use kysely-codegen to introspect:
// npx kysely-codegen --url postgres://localhost/mydb
// Generated types (from introspection):
interface Database {
users: UsersTable;
posts: PostsTable;
}
interface UsersTable {
id: Generated<number>;
email: string;
name: string;
created_at: Generated<Date>;
}
Query API
// Drizzle — relational-style queries
import { db } from './db';
import { users, posts } from './schema';
import { eq, desc, and, like } from 'drizzle-orm';
// Select with WHERE
const activeAdmins = await db
.select()
.from(users)
.where(and(eq(users.role, 'admin'), like(users.email, '%@company.com')))
.orderBy(desc(users.createdAt))
.limit(10);
// Join
const postsWithAuthors = await db
.select({
postTitle: posts.title,
authorName: users.name,
})
.from(posts)
.leftJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.published, true));
// Insert
const [newUser] = await db.insert(users).values({
email: 'alice@example.com',
name: 'Alice',
}).returning();
// Kysely — SQL-like query builder
import { db } from './db';
// Select with WHERE
const activeAdmins = await db
.selectFrom('users')
.selectAll()
.where('role', '=', 'admin')
.where('email', 'like', '%@company.com')
.orderBy('created_at', 'desc')
.limit(10)
.execute();
// Join
const postsWithAuthors = await db
.selectFrom('posts')
.innerJoin('users', 'users.id', 'posts.author_id')
.select(['posts.title as postTitle', 'users.name as authorName'])
.where('posts.published', '=', true)
.execute();
// Insert
const newUser = await db
.insertInto('users')
.values({ email: 'alice@example.com', name: 'Alice' })
.returningAll()
.executeTakeFirst();
Raw SQL Escape Hatches
// Drizzle — sql template tag
import { sql } from 'drizzle-orm';
const result = await db.select({
count: sql<number>`count(*)::int`,
}).from(users);
// Full raw query
const rows = await db.execute(sql`
SELECT users.*, count(posts.id) as post_count
FROM users
LEFT JOIN posts ON posts.author_id = users.id
GROUP BY users.id
`);
// Kysely — sql template tag
import { sql } from 'kysely';
const result = await db
.selectFrom('users')
.select([
'users.id',
sql<number>`count(posts.id)::int`.as('post_count'),
])
.leftJoin('posts', 'posts.author_id', 'users.id')
.groupBy('users.id')
.execute();
When to Choose
Choose Drizzle when:
- Starting a new project (schema definition is a major feature)
- You want TypeScript-first migrations
- Using edge runtimes (Cloudflare Workers, Vercel Edge)
- You want the full "define schema → generate migration → query" workflow
- Team prefers code-first database management
Choose Kysely when:
- Existing database with established schema and migrations
- You want a drop-in type-safe replacement for raw
pgormysql2queries - You use Flyway, Liquibase, or SQL migration files (separate from ORM)
- Maximum SQL flexibility with type safety on top
Compare Drizzle and Kysely package health on PkgPulse.
See the live comparison
View drizzle vs. kysely on PkgPulse →