Skip to main content

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 pg or mysql2 queries
  • 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.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.