Documentation
Joins

Joins [SQL]

Join clause in SQL is used to combine 2 or more tables, based on related columns between them. Drizzle ORM joins syntax is a balance between the SQL-likeness and type safety.

Join types

Drizzle ORM has APIs for INNER JOIN, FULL JOIN, LEFT JOIN and RIGHT JOIN Lets have a quick look at examples based on below table schemas:

export const users = pgTable('users', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
});
 
export const pets = pgTable('pets', {
  id: serial('id').primaryKey(),
  name: text('name').notNull(),
  ownerId: integer('owner_id').notNull().references(() => users.id),
})

Left Join

const result = await db.select().from(users).leftJoin(pets, eq(users.id, pets.ownerId))
select ... from "users" left join "pets" on "users"."id" = "pets"."owner_id"
// result type
const result: {
    user: {
        id: number;
        name: string;
    };
    pets: {
        id: number;
        name: string;
        ownerId: number;
    } | null;
}[];

Right Join

const result = await db.select().from(users).rightJoin(pets, eq(users.id, pets.ownerId))
select ... from "users" right join "pets" on "users"."id" = "pets"."owner_id"
// result type
const result: {
    user: {
        id: number;
        name: string;
    } | null;
    pets: {
        id: number;
        name: string;
        ownerId: number;
    };
}[];

Inner Join

const result = await db.select().from(users).innerJoin(pets, eq(users.id, pets.ownerId))
select ... from "users" inner join "pets" on "users"."id" = "pets"."owner_id"
// result type
const result: {
    user: {
        id: number;
        name: string;
    };
    pets: {
        id: number;
        name: string;
        ownerId: number;
    };
}[];

Full Join

const result = await db.select().from(users).fullJoin(pets, eq(users.id, pets.ownerId))
select ... from "users" full join "pets" on "users"."id" = "pets"."owner_id"
// result type
const result: {
    user: {
        id: number;
        name: string;
    } | null;
    pets: {
        id: number;
        name: string;
        ownerId: number;
    } | null;
}[];

Partial select

If you need to select a particular subset of fields or to have a flat response type - Drizzle ORM supports joins with partial select and will automatically infer return type based on .select({ ... }) structure

await db.select({
  userId: users.id,
  petId: pets.is,
}).from(user).leftJoin(pets, eq(users.id, pets.ownerId))
select "users"."id", "pets","id" from "users" left join "pets" on "users"."id" = "pets"."owner_id"
// result type
const result: {
  userId: number;
  petId: number | null;
}[];

You might've noticed that petId can be null now, it's because we're left joining and there can be users without a pet.

It's very important to keep in mind when using sql operator for partial selection fields and aggregations when needed, you should to use sql<type | null> for proper result type inference, that one is on you!

const result = await db.select({
  userId: users.id,
  petId: pets.id,
  petName1: sql`upper(${pets.name})`,
  petName2: sql<string | null>`upper(${pets.name})`,
  //˄we should explicitly tell 'string | null' in type, since we're left joining that field
}).from(user).leftJoin(pets, eq(users.id, pets.ownerId))
select "users"."id", "pets"."id", upper("pets"."name")... from "users" left join "pets" on "users"."id" = "pets"."owner_id"
// result type
const result: {
  userId: number;
  petId: number | null;
  petName1: unknown;
  petName2: string | null;
}[];

To avoid plethora of nullable fields when joining tables with lots of columns - we can utilise our nested select object syntax Our smart type inference will make whole object nullable instead of making all table fields nullable!

await db.select({
  userId: users.id,
  userName: users.name,
  pet: {
    id: pets.id,
    name: pets.name,
    upperName: sql<string>`upper(${pets.name})`
  }
}).from(user).fullJoin(pets, eq(users.id, pets.ownerId))
select ... from "users" full join "pets" on "users"."id" = "pets"."owner_id"
// result type
const result: {
    userId: number | null;
    userName: string | null;
    pet: {
        id: number;
        name: string;
        upperName: string;
    } | null;
}[];

Aliases & Selfjoins

Drizzle ORM supports table aliases which comes really handy when you need to do selfjoins Lets say you need to fetch users with their parents

index.ts
schema.ts
import { user } from "./schema";
 
const parent = alias(user, "parent")
const result = db
  .select()
  .from(user)
  .leftJoin(parent, eq(parent.id, user.id));
select ... from "user" left join "user" "parent" on "parent"."id" = "user"."id"
// result type
const result: {
    user: {
        id: number;
        name: string;
        parentId: number;
    };
    parent: {
        id: number;
        name: string;
        parentId: number;
    } | null;
}[];

Aggregating results

Drizzle ORM delivers name-mapped results from the driver without changing the structure. You're free to operate with results the way you want and here's an example of mapping many-one relational data

type User = InferModel<typeof users>;
type Pet = InferModel<typeof pets>;
 
const rows = db.select({
    user: users,
    pet: pets,
  }).from(users).leftJoin(pets, eq(users.id, pets.ownerId)).all();
 
const result = rows.reduce<Record<number, { user: User; pets: Pet[] }>>(
  (acc, row) => {
    const user = row.user;
    const pet = row.pet;
 
    if (!acc[user.id]) {
      acc[user.id] = { user, pets: [] };
    }
 
    if (pet) {
      acc[user.id].pets.push(pet);
    }
 
    return acc;
  },
  {}
);
 
// result type
const result: Record<number, {
    user: User;
    pets: Pet[];
}>;

Many-to-one example

import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
import { drizzle } from 'drizzle-orm/better-sqlite3';
 
const cities = sqliteTable('cities', {
  id: integer('id').primaryKey(),
  name: text('name'),
});
 
const users = sqliteTable('users', {
  id: integer('id').primaryKey(),
  name: text('name'),
  cityId: integer('city_id').references(() => cities.id)
});
 
const db = drizzle(sqlite);
 
const result = db.select().from(cities).leftJoin(users, eq(cities2.id, users2.cityId)).all();

Many-to-many example

const users = sqliteTable('users', {
  id: integer('id').primaryKey(),
  name: text('name'),
});
 
const chatGroups = sqliteTable('chat_groups', {
  id: integer('id').primaryKey(),
  name: text('name'),
});
 
const usersToChatGroups = sqliteTable('usersToChatGroups', {
  userId: integer('user_id').notNull().references(() => users.id),
  groupId: integer('group_id').notNull().references(() => chatGroups.id),
});
 
 
// querying user group with id 1 and all the participants(users)
db.select()
  .from(usersToChatGroups)
  .leftJoin(users, eq(usersToChatGroups.userId, users.id))
  .leftJoin(chatGroups, eq(usersToChatGroups.groupId, chatGroups.id))
  .where(eq(chatGroups.id, 1))
  .all();