src/content/docs/joins.mdx
import CodeTabs from '@mdx/CodeTabs.astro'; import CodeTab from '@mdx/CodeTab.astro'; import Section from '@mdx/Section.astro';
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.
Drizzle ORM has APIs for INNER JOIN [LATERAL], FULL JOIN, LEFT JOIN [LATERAL], RIGHT JOIN, CROSS JOIN [LATERAL].
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),
})
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.
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!
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: <CodeTabs items={["index.ts", "schema.ts"]}> <CodeTab>
import { user } from "./schema";
const parent = alias(user, "parent");
const result = db
.select()
.from(user)
.leftJoin(parent, eq(parent.id, user.parentId));
select ... from "user" left join "user" "parent" on "parent"."id" = "user"."parent_id"
// result type
const result: {
user: {
id: number;
name: string;
parentId: number;
};
parent: {
id: number;
name: string;
parentId: number;
} | null;
}[];
export const user = pgTable("user", {
id: integer("id").primaryKey({ autoIncrement: true }),
name: text("name").notNull(),
parentId: integer("parent_id").notNull().references((): AnyPgColumn => user.id)
});
Drizzle ORM delivers name-mapped results from the driver without changing the structure.
You're free to operate with results the way you want, here's an example of mapping many-one relational data:
type User = typeof users.$inferSelect;
type Pet = typeof pets.$inferSelect;
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[];
}>;
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();
const result = db.select().from(cities).leftJoin(users, eq(cities.id, users.cityId)).all();
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();