Joins [SQL]

SQL์˜ Join ์ ˆ์€ ํ…Œ์ด๋ธ” ๊ฐ„์˜ ๊ด€๋ จ ์ปฌ๋Ÿผ์„ ๊ธฐ๋ฐ˜์œผ๋กœ 2๊ฐœ ์ด์ƒ์˜ ํ…Œ์ด๋ธ”์„ ๊ฒฐํ•ฉํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. Drizzle ORM์˜ join ๊ตฌ๋ฌธ์€ SQL ์œ ์‚ฌ์„ฑ๊ณผ ํƒ€์ž… ์•ˆ์ •์„ฑ ์‚ฌ์ด์˜ ๊ท ํ˜•์„ ๋งž์ถฅ๋‹ˆ๋‹ค.

Join types

Drizzle ORM์€ INNER JOIN [LATERAL], FULL JOIN, LEFT JOIN [LATERAL], RIGHT JOIN, CROSS JOIN [LATERAL]์„ ์œ„ํ•œ API๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ์•„๋ž˜ ํ…Œ์ด๋ธ” ์Šคํ‚ค๋งˆ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์˜ˆ์ œ๋ฅผ ๋น ๋ฅด๊ฒŒ ์‚ดํŽด๋ณด๊ฒ ์Šต๋‹ˆ๋‹ค:

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;
}[];

Left Join Lateral

const subquery = db.select().from(pets).where(gte(users.age, 16)).as('userPets')
const result = await db.select().from(users).leftJoinLateral(subquery, sql`true`)
select ... from "users" left join lateral (select ... from "pets" where "users"."age" >= 16) "userPets" on true
// result type
const result: {
    user: {
        id: number;
        name: string;
    };
    userPets: {
        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;
    };
}[];

Inner Join Lateral

const subquery = db.select().from(pets).where(gte(users.age, 16)).as('userPets')
const result = await db.select().from(users).innerJoinLateral(subquery, sql`true`)
select ... from "users" inner join lateral (select ... from "pets" where "users"."age" >= 16) "userPets" on true
// result type
const result: {
    user: {
        id: number;
        name: string;
    };
    userPets: {
        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;
}[];

Cross Join

const result = await db.select().from(users).crossJoin(pets)
select ... from "users" cross join "pets"
// result type
const result: {
    user: {
        id: number;
        name: string;
    };
    pets: {
        id: number;
        name: string;
        ownerId: number;
    };
}[];

Cross Join Lateral

const subquery = db.select().from(pets).where(gte(users.age, 16)).as('userPets')
const result = await db.select().from(users).crossJoinLateral(subquery)
select ... from "users" cross join lateral (select ... from "pets" where "users"."age" >= 16) "userPets"
// result type
const result: {
    user: {
        id: number;
        name: string;
    };
    userPets: {
        id: number;
        name: string;
        ownerId: number;
    };
}[];

Partial select

ํŠน์ • ํ•„๋“œ ํ•˜์œ„ ์ง‘ํ•ฉ์„ ์„ ํƒํ•˜๊ฑฐ๋‚˜ ํ”Œ๋žซ ์‘๋‹ต ํƒ€์ž…์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ, Drizzle ORM์€ ๋ถ€๋ถ„ select๋ฅผ ์ง€์›ํ•˜๋Š” join์„ ์ œ๊ณตํ•˜๋ฉฐ .select({ ... }) ๊ตฌ์กฐ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ๋ฐ˜ํ™˜ ํƒ€์ž…์„ ์ž๋™์œผ๋กœ ์ถ”๋ก ํ•ฉ๋‹ˆ๋‹ค.

await db.select({
  userId: users.id,
  petId: pets.id,
}).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;
}[];

์ด์ œ petId๊ฐ€ null์ผ ์ˆ˜ ์žˆ์Œ์„ ์•Œ ์ˆ˜ ์žˆ๋Š”๋ฐ, ์ด๋Š” left join์„ ์‚ฌ์šฉํ•˜๊ณ  ์žˆ๊ณ  pet์ด ์—†๋Š” ์‚ฌ์šฉ์ž๊ฐ€ ์žˆ์„ ์ˆ˜ ์žˆ๊ธฐ ๋•Œ๋ฌธ์ž…๋‹ˆ๋‹ค.

๋ถ€๋ถ„ ์„ ํƒ ํ•„๋“œ ๋ฐ ์ง‘๊ณ„์— sql ์—ฐ์‚ฐ์ž๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ ๋งค์šฐ ์ค‘์š”ํ•œ ์ ์€, ์ ์ ˆํ•œ ๊ฒฐ๊ณผ ํƒ€์ž… ์ถ”๋ก ์„ ์œ„ํ•ด sql<type | null>์„ ์‚ฌ์šฉํ•ด์•ผ ํ•œ๋‹ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ด๋Š” ์—ฌ๋Ÿฌ๋ถ„์˜ ์ฑ…์ž„์ž…๋‹ˆ๋‹ค!

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;
}[];

๋งŽ์€ ์ปฌ๋Ÿผ์ด ์žˆ๋Š” ํ…Œ์ด๋ธ”์„ joinํ•  ๋•Œ ์ˆ˜๋งŽ์€ nullable ํ•„๋“œ๋ฅผ ํ”ผํ•˜๊ธฐ ์œ„ํ•ด ์ค‘์ฒฉ select ๊ฐ์ฒด ๊ตฌ๋ฌธ์„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์Šค๋งˆํŠธ ํƒ€์ž… ์ถ”๋ก ์€ ๋ชจ๋“  ํ…Œ์ด๋ธ” ํ•„๋“œ๋ฅผ nullable๋กœ ๋งŒ๋“œ๋Š” ๋Œ€์‹  ์ „์ฒด ๊ฐ์ฒด๋ฅผ 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์€ selfjoin์ด ํ•„์š”ํ•  ๋•Œ ๋งค์šฐ ์œ ์šฉํ•œ ํ…Œ์ด๋ธ” ๋ณ„์นญ์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค.

์‚ฌ์šฉ์ž์™€ ๊ทธ๋“ค์˜ ๋ถ€๋ชจ๋ฅผ ๊ฐ€์ ธ์™€์•ผ ํ•œ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด๋ด…์‹œ๋‹ค:

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.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;
}[];

Aggregating results

Drizzle ORM์€ ๊ตฌ์กฐ๋ฅผ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š๊ณ  ๋“œ๋ผ์ด๋ฒ„์—์„œ ์ด๋ฆ„์ด ๋งคํ•‘๋œ ๊ฒฐ๊ณผ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค.

์›ํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ๊ฒฐ๊ณผ๋ฅผ ์ž์œ ๋กญ๊ฒŒ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๋‹ค์Œ์€ ๋‹ค๋Œ€์ผ ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ๋ฅผ ๋งคํ•‘ํ•˜๋Š” ์˜ˆ์ œ์ž…๋‹ˆ๋‹ค:

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[];
}>;

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();

const result = db.select().from(cities).leftJoin(users, eq(cities.id, users.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();