---
url: /compare/bigal-vs-kysely.md
description: >-
  Compare BigAl and Kysely for PostgreSQL - ORM vs query builder, relationships,
  migrations, JSONB, pgvector, row locks, CTEs, and runtimes.
---

# BigAl vs Kysely

Kysely is a type-safe SQL query builder, not an ORM. You describe tables as TypeScript interfaces and write queries
that mirror SQL. BigAl is a PostgreSQL ORM with decorator models, relationships, and `.populate()`. Choose Kysely for
full SQL control across databases, including CTEs. Choose BigAl for repository-style CRUD with relationships on
Postgres and less SQL to write.

This page compares BigAl 16 with Kysely 0.29, the current stable release.

## At a glance

| Criterion         | BigAl                                                                      | Kysely 0.29                                                                        |
| ----------------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- |
| Kind              | ORM with repositories and relationships                                    | SQL query builder; no entities or relationships                                    |
| Databases         | PostgreSQL only                                                            | PostgreSQL, MySQL, SQLite, SQL Server, PGlite; more via community dialects         |
| Query style       | Fluent builder: `find().where({...}).sort().limit()`                       | SQL-shaped: `selectFrom('products').where('price', '>=', 100)`                     |
| Schema definition | Classes with `@table` and `@column` decorators                             | A `Database` interface, hand-written or generated by kysely-codegen                |
| Migrations        | None; BigAl issues no DDL, so pair it with a migration tool                | `Migrator` runs hand-written `up` and `down` files; no diff generation             |
| Relationships     | `model`, `collection`, `through`; loaded with `.populate()`                | Joins, or `jsonArrayFrom` and `jsonObjectFrom` to nest rows as JSON                |
| JSONB             | Property paths (`->`, `->>`) and `@>` containment in `.where()`            | Typed paths: `eb.ref('col', '->>').key('theme')`                                   |
| DISTINCT ON       | `.distinctOn([...])`                                                       | `.distinctOn(...)`                                                                 |
| ON CONFLICT       | `create()` option: `onConflict` with `ignore` or `merge`                   | `.onConflict((oc) => oc.column('sku').doUpdateSet(...))` or `.doNothing()`         |
| pgvector          | `vector` columns; `nearestTo` sorting and distance filters                 | `sql` template, or helpers from the `pgvector/kysely` package                      |
| Row locks         | `.lock()`: update, no key update, share, key share; `nowait`, `skipLocked` | `.forUpdate()`, `.forShare()`, and key variants; `.skipLocked()`, `.noWait()`      |
| CTEs              | Not supported; use raw SQL                                                 | `.with()` and `.withRecursive()`                                                   |
| Type safety       | From class properties; `.select()` and `.populate()` narrow results        | Flows from the `Database` interface; selections and aliases shape results          |
| Runtime deps      | Zero; add `postgres-pool`, `pg`, or `@neondatabase/serverless`             | Zero; add a driver such as `pg`                                                    |
| Runtimes          | Node.js 22.11+, Bun, Deno 2; edge runtimes untested                        | Node.js 22+, Deno, Bun, and other JavaScript runtimes                              |
| Transactions      | `transaction()` with isolation level and lock, statement, idle timeouts    | `db.transaction().execute()` with isolation level; `startTransaction()` savepoints |
| Raw SQL           | `pool.query()`, or `query()` on the transaction scope                      | `sql` tagged template, composable inside builder queries                           |

## When to choose BigAl

* You want relationships defined once on the model and loaded with [`.populate()`](/guide/querying#populate), instead
  of writing joins or JSON aggregation per query.
* You want CRUD methods such as `create()`, `update()`, and `destroy()` that return typed records.
* You want [vector search](/guide/querying#vector-distance-queries) and
  [JSONB filters](/guide/querying#jsonb-querying) without extra helper packages.
* You want [read replica routing](/reference/configuration#read-replicas) and `beforeCreate` and `beforeUpdate` hooks
  built in.

## When to choose Kysely

* You want every query to read like the SQL it runs, including CTEs, window functions with `.over()`, and complex
  joins.
* You need MySQL, SQLite, or SQL Server, or you want one query style across them.
* You prefer database types generated from the live schema over hand-written model classes.
* You want built-in migration running and savepoints.

## Migrating from Kysely

BigAl and Kysely can share one `pg` pool, so you can move one table at a time. Keep Kysely, or `pool.query()`, for
CTEs and other queries BigAl does not model. See [BigAl vs Raw SQL](/advanced/bigal-vs-raw-sql) for that split.

### Models

::: code-group

```ts [Kysely]
import type { Generated } from 'kysely';

interface Database {
  products: {
    id: Generated<number>;
    name: string;
    sku: string;
    price_cents: number;
    store_id: number;
  };
  stores: { id: Generated<number>; name: string };
}
```

```ts [BigAl]
import { column, Entity, primaryColumn, table } from 'bigal';
import type { Store } from './Store';

@table({ name: 'products' })
export class Product extends Entity {
  @primaryColumn({ type: 'integer' })
  public id!: number;

  @column({ type: 'string', required: true })
  public name!: string;

  @column({ type: 'string', required: true })
  public sku!: string;

  @column({ type: 'integer', required: true, name: 'price_cents' })
  public priceCents!: number;

  @column({ model: () => 'Store', name: 'store_id' })
  public store!: number | Store;
}
```

:::

### Queries and upserts

::: code-group

```ts [Kysely]
const rows = await db
  .selectFrom('products')
  .innerJoin('stores', 'stores.id', 'products.store_id')
  .select(['products.id', 'products.name', 'stores.name as store_name'])
  .where('products.price_cents', '>=', 1000)
  .where('products.name', 'ilike', '%widget%')
  .orderBy('products.name')
  .limit(10)
  .execute();

await db
  .insertInto('products')
  .values({ sku: 'WDG-001', name: 'Widget', price_cents: 999, store_id: 1 })
  .onConflict((oc) => oc.column('sku').doUpdateSet((eb) => ({ price_cents: eb.ref('excluded.price_cents') })))
  .execute();
```

```ts [BigAl]
const products = await productRepository
  .find()
  .where({ priceCents: { '>=': 1000 }, name: { contains: 'widget' } })
  .sort('name asc')
  .limit(10)
  .populate('store', { select: ['name'] });

await productRepository.create({ sku: 'WDG-001', name: 'Widget', priceCents: 999, store: 1 }, { onConflict: { action: 'merge', targets: ['sku'], merge: ['priceCents'] } });
```

:::

Last reviewed: September 2026
