Skip to Content
🚀 Getting StartedFirst Query

Last Updated: 3/20/2026


First Query

Once you’ve installed Kysely and defined your database types, you’re ready to write your first query. This guide walks through a complete SELECT query from database instantiation to fetching typed results.

Setting Up the Database Instance

First, create a Kysely instance with your database configuration:

import { Kysely, PostgresDialect } from 'kysely' import { Pool } from 'pg' interface Database { person: { id: number first_name: string last_name: string | null age: number } } const db = new Kysely<Database>({ dialect: new PostgresDialect({ pool: new Pool({ host: 'localhost', database: 'my_database', }) }) })

The Kysely<Database> type parameter tells TypeScript which tables and columns are available. Every query method will validate against this type.

Building a SELECT Query

The selectFrom method starts a SELECT query. It takes a table name and returns a SelectQueryBuilder:

const query = db.selectFrom('person')

At this point, TypeScript knows you’re querying the person table, so it will only allow you to reference columns from that table.

Selecting Columns

Use the select method to specify which columns to retrieve:

const query = db .selectFrom('person') .select('first_name')

The return type is now { first_name: string }[]. Kysely inferred the type from your Database interface.

You can select multiple columns by passing an array:

const query = db .selectFrom('person') .select(['id', 'first_name', 'last_name'])

The return type is { id: number; first_name: string; last_name: string | null }[].

To select all columns, use selectAll():

const query = db .selectFrom('person') .selectAll()

This returns all columns from the person table with their correct types.

Adding a WHERE Clause

The where method filters rows. It takes three arguments: a column reference, a comparison operator, and a value:

const query = db .selectFrom('person') .select(['id', 'first_name']) .where('age', '>=', 18)

TypeScript validates that:

  • 'age' is a column in the person table
  • 18 is assignable to the age column’s type (number)

You can chain multiple where calls to add AND conditions:

const query = db .selectFrom('person') .select(['id', 'first_name']) .where('age', '>=', 18) .where('last_name', 'is not', null)

For OR conditions, use the expression builder callback:

const query = db .selectFrom('person') .select(['id', 'first_name']) .where((eb) => eb.or([ eb('first_name', '=', 'Jennifer'), eb('first_name', '=', 'Arnold') ]))

The eb (expression builder) parameter provides methods for building complex expressions. See src/query-builder/select-query-builder.ts for the full where method signature and examples.

Ordering Results

The orderBy method sorts the result set:

const query = db .selectFrom('person') .select(['id', 'first_name', 'age']) .orderBy('age', 'desc')

You can order by multiple columns:

const query = db .selectFrom('person') .select(['id', 'first_name', 'age']) .orderBy('last_name', 'asc') .orderBy('first_name', 'asc')

Limiting Results

The limit method restricts the number of rows returned:

const query = db .selectFrom('person') .select(['id', 'first_name']) .where('age', '>=', 18) .orderBy('age', 'desc') .limit(10)

This selects the 10 oldest adults. You can combine limit with offset for pagination:

const query = db .selectFrom('person') .select(['id', 'first_name']) .orderBy('id') .limit(10) .offset(20)

This skips the first 20 rows and returns the next 10.

Executing the Query

Query builders are immutable — they don’t execute until you call an execution method. The execute method runs the query and returns a promise of an array:

const people = await db .selectFrom('person') .select(['id', 'first_name', 'age']) .where('age', '>=', 18) .orderBy('age', 'desc') .limit(10) .execute() // people: Array<{ id: number; first_name: string; age: number }> console.log(people[0].first_name)

The execute method compiles the query to SQL, sends it to the database, and returns the results. The return type is inferred from the select calls.

executeTakeFirst

If you expect at most one row, use executeTakeFirst:

const person = await db .selectFrom('person') .select(['id', 'first_name']) .where('id', '=', 1) .executeTakeFirst() // person: { id: number; first_name: string } | undefined if (person) { console.log(person.first_name) }

This returns the first row or undefined if no rows match.

executeTakeFirstOrThrow

If you expect exactly one row and want to throw an error if none is found, use executeTakeFirstOrThrow:

const person = await db .selectFrom('person') .select(['id', 'first_name']) .where('id', '=', 1) .executeTakeFirstOrThrow() // person: { id: number; first_name: string } console.log(person.first_name)

This throws a NoResultError if no row is found. You can provide a custom error constructor:

class PersonNotFoundError extends Error { constructor() { super('Person not found') } } const person = await db .selectFrom('person') .select(['id', 'first_name']) .where('id', '=', 1) .executeTakeFirstOrThrow(PersonNotFoundError)

Complete Example

Here’s a complete example that ties everything together:

import { Kysely, PostgresDialect } from 'kysely' import { Pool } from 'pg' interface Database { person: { id: number first_name: string last_name: string | null age: number } } const db = new Kysely<Database>({ dialect: new PostgresDialect({ pool: new Pool({ host: 'localhost', database: 'my_database', }) }) }) async function findAdults() { const adults = await db .selectFrom('person') .select(['id', 'first_name', 'last_name', 'age']) .where('age', '>=', 18) .orderBy('age', 'desc') .limit(10) .execute() for (const person of adults) { console.log(`${person.first_name} ${person.last_name}: ${person.age} years old`) } } findAdults()

How Type Inference Works

Kysely’s type inference is powered by TypeScript’s template literal types and conditional types. When you write:

.select(['id', 'first_name'])

Kysely parses the string literals 'id' and 'first_name' at the type level, looks them up in your Database interface, and builds a result type { id: number; first_name: string }.

This works even with aliased columns:

const result = await db .selectFrom('person') .select(['id', 'first_name as name']) .executeTakeFirst() // result: { id: number; name: string } | undefined console.log(result?.name)

Kysely parsed 'first_name as name', extracted the alias 'name', and added it to the result type. The implementation is in src/parser/select-parser.ts.

Query Builder Immutability

Every query builder method returns a new instance. This means you can safely reuse query fragments:

const baseQuery = db .selectFrom('person') .select(['id', 'first_name']) const adults = await baseQuery.where('age', '>=', 18).execute() const minors = await baseQuery.where('age', '<', 18).execute()

Both queries are independent. Calling .where() on baseQuery doesn’t modify it — it returns a new builder with the where clause added.

This immutability is implemented in the SelectQueryBuilder class (see src/query-builder/select-query-builder.ts). Every method creates a new instance with a cloned query node tree.

What’s Next

  • CRUD Operations: Learn INSERT, UPDATE, and DELETE queries — including upserts, returning clauses, and typed result objects.
  • Expressions: Go deeper with the expression builder for complex conditions, subqueries, and computed columns.