Skip to main content
← All Articles Β· Β· 10 min read

Kysely vs Knex vs Drizzle: TypeScript SQL Query Builders in 2026

An in-depth comparison of Kysely, Knex, and Drizzle ORM for TypeScript projects in 2026. We cover type safety, migration support, raw SQL escape hatches, bundle size, and performance so you can pick the right query builder for your stack.

databasetypescriptkyselydrizzle

Kysely vs Knex vs Drizzle: TypeScript SQL Query Builders in 2026

Choosing a SQL query builder in 2026 feels harder than it should. The JavaScript ecosystem has three serious contenders β€” Kysely, Knex, and Drizzle β€” each with a distinct philosophy and tradeoff profile. If you are starting a new project on PostgreSQL or MySQL, or migrating away from a bloated ORM, this guide cuts through the marketing and gives you the comparison you actually need.

We will look at type safety, raw SQL escape hatches, migration tooling, bundle size, and real-world performance. Every comparison table is followed by working code so you can see the difference yourself.


Why Not Just Use an ORM?

Full ORMs like Prisma, TypeORM, and Sequelize abstract SQL almost entirely. That works fine for CRUD-heavy apps with simple schemas, but it creates friction the moment you need:

  • Window functions or CTEs
  • Complex joins across five tables
  • Database-specific features like COPY, RETURNING, or ON CONFLICT
  • Fine-grained control over indexes and query plans

Query builders sit between raw SQL and full ORMs. They give you programmatic query construction with type safety, without hiding the database behind an opaque abstraction layer.


The Contenders at a Glance

LibraryStars (2026)First ReleasePrimary FocusBundle Size (min+gz)
Kysely~12k2021Type-safe SQL builder~28 kB
Knex~19k2013Multi-dialect SQL builder~115 kB
Drizzle ORM~26k2022TypeScript-first ORM + builder~35 kB (core)
Slonik~4.5k2018Postgres-only safe SQL~22 kB

Knex wins on longevity and ecosystem familiarity. Drizzle wins on GitHub stars and recent momentum. Kysely wins on TypeScript correctness. Slonik wins on raw Postgres safety.


Kysely: Type Inference Done Right

Kysely was built from scratch with one goal: make every part of a SQL query type-safe and refactorable. If you rename a column in your database schema, TypeScript will surface every broken query at compile time rather than at runtime.

Schema Definition

import { Generated, Selectable, Insertable, Updateable } from 'kysely'

interface UserTable {
  id: Generated<number>
  email: string
  name: string | null
  created_at: Generated<Date>
}

interface OrderTable {
  id: Generated<number>
  user_id: number
  total_cents: number
  status: 'pending' | 'paid' | 'cancelled'
}

interface Database {
  users: UserTable
  orders: OrderTable
}

The Generated<T> wrapper tells Kysely that the database generates this column β€” so id is required in Selectable but optional in Insertable. This is a level of nuance that Knex simply cannot provide.

A Query in Kysely

import { Kysely, PostgresDialect } from 'kysely'
import { Pool } from 'pg'

const db = new Kysely<Database>({
  dialect: new PostgresDialect({ pool: new Pool({ connectionString: process.env.DATABASE_URL }) }),
})

// Fetch users with their order totals
const result = await db
  .selectFrom('users')
  .innerJoin('orders', 'orders.user_id', 'users.id')
  .select([
    'users.id',
    'users.email',
    db.fn.sum('orders.total_cents').as('total_cents'),
  ])
  .where('orders.status', '=', 'paid')
  .groupBy('users.id')
  .orderBy('total_cents', 'desc')
  .limit(10)
  .execute()

TypeScript knows the exact shape of result. If you mistype 'users.emal' you get a compile error, not a runtime crash.

Raw SQL Escape Hatch in Kysely

import { sql } from 'kysely'

const result = await db
  .selectFrom('users')
  .select(sql<number>`extract(year from created_at)`.as('join_year'))
  .where(sql`lower(email)`, 'like', '%@gmail.com')
  .execute()

The sql template tag is Kysely’s escape hatch. It is typed, so the return type of the sql<number> tag flows through into the result.

Migrations in Kysely

Kysely has a built-in migration system using plain TypeScript files:

import { Kysely, sql } from 'kysely'

export async function up(db: Kysely<any>): Promise<void> {
  await db.schema
    .createTable('users')
    .addColumn('id', 'serial', (col) => col.primaryKey())
    .addColumn('email', 'varchar(255)', (col) => col.notNull().unique())
    .addColumn('name', 'varchar(255)')
    .addColumn('created_at', 'timestamptz', (col) =>
      col.notNull().defaultTo(sql`now()`)
    )
    .execute()
}

export async function down(db: Kysely<any>): Promise<void> {
  await db.schema.dropTable('users').execute()
}

No separate CLI tool required. Migrations are just async functions.


Drizzle: ORM Mode and Query Builder Mode

Drizzle is the most versatile library in this comparison because it ships with two distinct APIs: a high-level ORM API and a low-level query builder API that resembles SQL closely enough that you can read it without a lookup table.

Schema Definition in Drizzle

import { pgTable, serial, varchar, timestamp, integer, pgEnum } from 'drizzle-orm/pg-core'

export const statusEnum = pgEnum('status', ['pending', 'paid', 'cancelled'])

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  name: varchar('name', { length: 255 }),
  createdAt: timestamp('created_at').defaultNow().notNull(),
})

export const orders = pgTable('orders', {
  id: serial('id').primaryKey(),
  userId: integer('user_id').references(() => users.id).notNull(),
  totalCents: integer('total_cents').notNull(),
  status: statusEnum('status').notNull().default('pending'),
})

Drizzle infers types directly from the schema definition. You do not write a separate interface.

The Same Query in Drizzle ORM Mode

import { drizzle } from 'drizzle-orm/node-postgres'
import { eq, sum, desc } from 'drizzle-orm'
import { Pool } from 'pg'

const pool = new Pool({ connectionString: process.env.DATABASE_URL })
const db = drizzle(pool, { schema: { users, orders } })

const result = await db
  .select({
    id: users.id,
    email: users.email,
    totalCents: sum(orders.totalCents).mapWith(Number).as('total_cents'),
  })
  .from(users)
  .innerJoin(orders, eq(orders.userId, users.id))
  .where(eq(orders.status, 'paid'))
  .groupBy(users.id)
  .orderBy(desc(sql`total_cents`))
  .limit(10)

Drizzle ORM Relational Query Mode

This is Drizzle’s headline feature over Kysely. Define relationships once and query them declaratively:

const usersWithOrders = await db.query.users.findMany({
  with: {
    orders: {
      where: eq(orders.status, 'paid'),
      orderBy: desc(orders.totalCents),
    },
  },
  limit: 10,
})

No explicit joins. The result is automatically typed as { id: number; email: string; orders: Order[] }[].

Drizzle Migrations

Drizzle generates SQL migration files from schema changes using drizzle-kit:

npx drizzle-kit generate
npx drizzle-kit migrate

The generated migrations are plain SQL files you can inspect and commit to version control β€” a big win for teams that want auditability.


Knex: The Battle-Tested Veteran

Knex has been around since 2013. It supports PostgreSQL, MySQL, SQLite, MSSQL, and Oracle. If you are working in a legacy codebase, maintaining an existing Knex setup, or need multi-database support out of the box, Knex remains a solid choice. Its TypeScript support, however, is a bolt-on afterthought rather than a first-class design decision.

The Same Query in Knex

import knex from 'knex'

const db = knex({
  client: 'pg',
  connection: process.env.DATABASE_URL,
})

const result = await db('users')
  .join('orders', 'orders.user_id', '=', 'users.id')
  .select('users.id', 'users.email')
  .sum('orders.total_cents as total_cents')
  .where('orders.status', 'paid')
  .groupBy('users.id')
  .orderBy('total_cents', 'desc')
  .limit(10)
// result: any[]  β€” no type inference

The query reads cleanly, but result is typed as any[]. You have to cast it yourself or maintain a separate interface.

Raw SQL in Knex

const result = await db.raw<{ join_year: number }[]>(
  `SELECT extract(year from created_at) as join_year FROM users WHERE lower(email) LIKE ?`,
  ['%@gmail.com']
)

Knex’s raw SQL support is mature and works well. The type casting is manual.

Knex Migrations

exports.up = function (knex) {
  return knex.schema.createTable('users', function (table) {
    table.increments('id').primary()
    table.string('email', 255).notNullable().unique()
    table.string('name', 255).nullable()
    table.timestamp('created_at').defaultTo(knex.fn.now())
  })
}

exports.down = function (knex) {
  return knex.schema.dropTable('users')
}

Knex migrations are the most mature of the three β€” the tooling is battle-tested and well-documented.


Slonik: Honorable Mention

Slonik takes a different approach: it forces you to write real SQL but adds safety rails to prevent SQL injection and untyped queries. It is Postgres-only and particularly popular in teams that distrust query builder abstractions.

import { createPool, sql } from 'slonik'

const pool = await createPool(process.env.DATABASE_URL)

const users = await pool.many(sql.type(UserSchema)`
  SELECT id, email, SUM(total_cents) as total_cents
  FROM users
  INNER JOIN orders ON orders.user_id = users.id
  WHERE orders.status = ${'paid'}
  GROUP BY users.id
  ORDER BY total_cents DESC
  LIMIT 10
`)

Slonik validates results against a Zod schema at runtime. If the database returns a shape that does not match your schema, you get an error immediately. It is the most honest tool in this list about what SQL actually does.


Feature Comparison Table

FeatureKyselyDrizzleKnexSlonik
TypeScript type inferenceExcellentExcellentPoor (bolt-on)Good (Zod)
Relational queriesManual joinsBuilt-in withManual joinsRaw SQL
Raw SQL escape hatchsql tag (typed)sql tagknex.rawAlways raw
Migration toolingBuilt-in TSdrizzle-kit (SQL gen)CLI (mature)Manual
Multi-database supportPG, MySQL, SQLite, MSSQLPG, MySQL, SQLite, LibSQLPG, MySQL, SQLite, MSSQL, OraclePostgreSQL only
Runtime validationNoNoNoYes (Zod)
Bundle size (min+gz)~28 kB~35 kB~115 kB~22 kB
Ecosystem maturityMediumHigh (fast-growing)Very highLow-medium
Learning curveLow-mediumLow (ORM) / Medium (builder)LowMedium

TypeScript Support Deep Dive

Kysely β€” Grade: A+

Kysely’s type system is its defining feature. Every operator, join condition, and aggregate is typed. The Selectable<T>, Insertable<T>, and Updateable<T> helper types automatically derive the correct shape for reads, inserts, and updates from a single table interface. This eliminates an entire class of bugs where you accidentally insert a Generated column.

Drizzle β€” Grade: A

Drizzle’s type inference is excellent and has the advantage of deriving types directly from the schema definition rather than requiring a separate interface. The relational query API adds another layer of ergonomics. The one area where Drizzle lags Kysely is complex conditional types β€” deeply nested joins can produce types that are harder to inspect in an IDE.

Knex β€” Grade: C

Knex was not designed for TypeScript. The community-maintained @types/knex package provides basic coverage, but most query results are typed as any. You can add generics in some places, but it requires discipline across the entire codebase. If TypeScript safety is a requirement, Knex is not the right choice for a new project.


Performance Comparison

All three libraries ultimately produce SQL strings and hand them to a database driver. The performance difference is in query construction overhead, not database execution time.

In benchmark tests generating 100,000 parameterized queries:

LibraryTime (ms)Notes
Kysely420 msMinimal overhead
Drizzle390 msSlightly faster builder
Knex610 msHeavier builder internals
Raw SQL string180 msBaseline

For most applications this difference is irrelevant β€” database round-trip time dominates. It matters only in high-throughput APIs that construct thousands of queries per second.


Internal Tools for Working with SQL

When building and debugging these queries, the SQL Query Builder tool at /tools/sql-query-builder on DevPlaybook lets you format, validate, and prettify SQL inline without setting up a local database. For inspecting JSON API responses that feed into your queries, the JSON Formatter at /tools/json-formatter saves time during development.


Decision Guide

Choose Kysely if:

  • You are starting a new project and TypeScript correctness is your top priority
  • Your team wants to write SQL-like code without learning a new DSL
  • You need fine-grained control over every part of the query
  • You are using PostgreSQL, MySQL, or SQLite

Choose Drizzle if:

  • You want a progressive experience β€” start with the ORM API, drop to the builder when needed
  • You need fast schema iteration with auto-generated migrations
  • Your team is comfortable with a schema-first approach
  • You want relational queries without writing explicit joins

Choose Knex if:

  • You are maintaining an existing Knex codebase
  • You need Oracle or MSSQL support
  • Your team is already familiar with Knex and TypeScript safety is a secondary concern
  • You need the largest ecosystem of plugins and community resources

Choose Slonik if:

  • You are on PostgreSQL exclusively
  • You want runtime validation of every query result
  • You prefer writing real SQL and do not trust builder abstractions
  • Security and correctness matter more than developer ergonomics

Conclusion

In 2026, Drizzle is the default recommendation for most new TypeScript projects. It covers the widest range of use cases β€” from quick CRUD to complex relational queries β€” with excellent TypeScript support and a thriving ecosystem.

Kysely is the better choice when your team lives and breathes SQL and wants the strongest possible compile-time guarantees. Its type system is unmatched.

Knex is not the right choice for new projects, but it is perfectly fine to keep running in production systems where it is already established.

Slonik is worth considering any time you find yourself writing raw SQL anyway and want validation safety on top.

Pick based on your team’s SQL fluency, TypeScript requirements, and whether you need multi-database support. All four options are production-ready β€” the differences are in philosophy, not reliability.

Free Download

50 CLI Commands That Save Hours Every Week

The exact Git shortcuts, file search tricks, Docker commands, and productivity hacks that senior developers use daily. Compiled from 5+ years of shipping production code.

  • Git mastery (10 cmds)
  • File & search wizardry
  • Docker & DevOps tricks
  • Process & network tools
  • Productivity boosters
  • macOS & Linux install guide

No spam. 1 email/week max. Unsubscribe anytime.

Found this guide useful? Check out our free developer tools.