๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
Puri๋Š” ์—ฌ๋Ÿฌ ํ…Œ์ด๋ธ”์„ ์กฐ์ธํ•˜์—ฌ ๊ด€๋ จ ๋ฐ์ดํ„ฐ๋ฅผ ํ•จ๊ป˜ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ฌธ์„œ๋Š” INNER JOIN, LEFT JOIN, ๊ทธ๋ฆฌ๊ณ  ๋ณต์žกํ•œ ์กฐ์ธ ํŒจํ„ด์„ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.

์กฐ์ธ ๊ฐœ์š”

INNER JOIN

์–‘์ชฝ ํ…Œ์ด๋ธ”์— ๋ชจ๋‘ ์กด์žฌํ•˜๋Š” ๋ฐ์ดํ„ฐjoin()

LEFT JOIN

์™ผ์ชฝ ํ…Œ์ด๋ธ” ๊ธฐ์ค€, ์˜ค๋ฅธ์ชฝ์€ OptionalleftJoin()

Self JOIN

๊ฐ™์€ ํ…Œ์ด๋ธ”์„ ์กฐ์ธalias ์‚ฌ์šฉ

M:N JOIN

๋‹ค๋Œ€๋‹ค ๊ด€๊ณ„ ์กฐ์ธ์ค‘๊ฐ„ ํ…Œ์ด๋ธ” ํ™œ์šฉ

INNER JOIN

์–‘์ชฝ ํ…Œ์ด๋ธ”์— ๋ชจ๋‘ ์กด์žฌํ•˜๋Š” ๋ฐ์ดํ„ฐ๋งŒ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋ณธ JOIN

const results = await db
  .table("employees")
  .join("users", "employees.user_id", "users.id")
  .select({
    employeeId: "employees.id",
    employeeNumber: "employees.employee_number",
    userName: "users.username",
    userEmail: "users.email",
  });

// SQL:
// SELECT ...
// FROM employees
// INNER JOIN users ON employees.user_id = users.id
INNER JOIN ์‚ฌ์šฉ ์‹œ๊ธฐ:
  • ๊ด€๊ณ„๊ฐ€ ํ•„์ˆ˜์ธ ๊ฒฝ์šฐ (NOT NULL ์™ธ๋ž˜ ํ‚ค)
  • ์–‘์ชฝ์— ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์–ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ
  • ์˜ˆ: ์ง์› โ†’ ์‚ฌ์šฉ์ž (๋ชจ๋“  ์ง์›์€ ๋ฐ˜๋“œ์‹œ ์‚ฌ์šฉ์ž ๊ณ„์ •์ด ์žˆ์Œ)

์—ฌ๋Ÿฌ ํ…Œ์ด๋ธ” JOIN

const results = await db
  .table("employees")
  .join("users", "employees.user_id", "users.id")
  .join("departments", "employees.department_id", "departments.id")
  .join("companies", "departments.company_id", "companies.id")
  .select({
    employeeName: "users.username",
    departmentName: "departments.name",
    companyName: "companies.name",
  });

// employees โ†’ users (ํ•„์ˆ˜)
// employees โ†’ departments (ํ•„์ˆ˜)
// departments โ†’ companies (ํ•„์ˆ˜)

LEFT JOIN

์™ผ์ชฝ ํ…Œ์ด๋ธ”์˜ ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•˜๊ณ , ์˜ค๋ฅธ์ชฝ ํ…Œ์ด๋ธ”์€ ์žˆ์œผ๋ฉด ํฌํ•จํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋ณธ LEFT JOIN

const results = await db
  .table("employees")
  .leftJoin("departments", "employees.department_id", "departments.id")
  .select({
    employeeId: "employees.id",
    employeeNumber: "employees.employee_number",
    departmentName: "departments.name", // null ๊ฐ€๋Šฅ
  });

// SQL:
// SELECT ...
// FROM employees
// LEFT JOIN departments ON employees.department_id = departments.id
LEFT JOIN ์‚ฌ์šฉ ์‹œ๊ธฐ:
  • ๊ด€๊ณ„๊ฐ€ ์„ ํƒ์ ์ธ ๊ฒฝ์šฐ (NULLABLE ์™ธ๋ž˜ ํ‚ค)
  • ์™ผ์ชฝ ํ…Œ์ด๋ธ”์˜ ๋ชจ๋“  ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ
  • ์˜ˆ: ์ง์› โ†’ ๋ถ€์„œ (์ผ๋ถ€ ์ง์›์€ ๋ถ€์„œ๊ฐ€ ์—†์„ ์ˆ˜ ์žˆ์Œ)

INNER JOIN vs LEFT JOIN ๋น„๊ต

const results = await db
  .table("employees")
  .join("departments", "employees.department_id", "departments.id")
  .select({
    employeeId: "employees.id",
    departmentName: "departments.name",
  });

// ๋ถ€์„œ๊ฐ€ ๋ฐฐ์ •๋œ ์ง์›๋งŒ ์กฐํšŒ (๋ถ€์„œ ์—†๋Š” ์ง์› ์ œ์™ธ)

๋ณตํ•ฉ LEFT JOIN

const results = await db
  .table("employees")
  .leftJoin("departments", "employees.department_id", "departments.id")
  .leftJoin("companies", "departments.company_id", "companies.id")
  .select({
    employeeId: "employees.id",
    employeeNumber: "employees.employee_number",
    departmentName: "departments.name",  // null ๊ฐ€๋Šฅ
    companyName: "companies.name",       // null ๊ฐ€๋Šฅ
  });

// employees (ํ•„์ˆ˜)
// โ†’ departments (์„ ํƒ)
// โ†’ companies (์„ ํƒ)

INNER + LEFT JOIN ํ˜ผํ•ฉ

ํ•„์ˆ˜ ๊ด€๊ณ„๋Š” INNER JOIN, ์„ ํƒ ๊ด€๊ณ„๋Š” LEFT JOIN์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
const results = await db
  .table("employees")
  .join("users", "employees.user_id", "users.id")              // ํ•„์ˆ˜: ๋ชจ๋“  ์ง์›์€ ์‚ฌ์šฉ์ž ์žˆ์Œ
  .leftJoin("departments", "employees.department_id", "departments.id") // ์„ ํƒ: ๋ถ€์„œ ์—†์„ ์ˆ˜ ์žˆ์Œ
  .select({
    employeeName: "users.username",           // ํ•ญ์ƒ ์กด์žฌ
    departmentName: "departments.name",       // null ๊ฐ€๋Šฅ
  });
์กฐ์ธ ํƒ€์ž… ์„ ํƒ ๊ฐ€์ด๋“œ:
  1. ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์Šคํ‚ค๋งˆ ํ™•์ธ (NOT NULL vs NULLABLE)
  2. NOT NULL ์™ธ๋ž˜ ํ‚ค โ†’ INNER JOIN
  3. NULLABLE ์™ธ๋ž˜ ํ‚ค โ†’ LEFT JOIN
  4. ๋น„์ฆˆ๋‹ˆ์Šค ์š”๊ตฌ์‚ฌํ•ญ ๊ณ ๋ ค

Self JOIN - ์ž๊ธฐ ์ฐธ์กฐ

๊ฐ™์€ ํ…Œ์ด๋ธ”์„ ์กฐ์ธํ•  ๋•Œ๋Š” **๋ณ„์นญ(alias)**์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.

๋ถ€์„œ ๊ณ„์ธต๊ตฌ์กฐ ์˜ˆ์ œ

const results = await db
  .table({ child: "departments" })
  .leftJoin(
    { parent: "departments" },
    "child.parent_id",
    "parent.id"
  )
  .select({
    childId: "child.id",
    childName: "child.name",
    parentId: "parent.id",
    parentName: "parent.name", // null ๊ฐ€๋Šฅ (์ตœ์ƒ์œ„ ๋ถ€์„œ)
  });

// departments (์ž์‹) LEFT JOIN departments (๋ถ€๋ชจ)
// child.parent_id = parent.id
๊ฒฐ๊ณผ ์˜ˆ์‹œ:
childIdchildNameparentIdparentName
1๋ณธ๋ถ€nullnull
2๊ฐœ๋ฐœํŒ€1๋ณธ๋ถ€
3๋””์ž์ธํŒ€1๋ณธ๋ถ€
4๋ฐฑ์—”๋“œํŒ€2๊ฐœ๋ฐœํŒ€

์‚ฌ์šฉ์ž ์ถ”์ฒœ์ธ ์˜ˆ์ œ

const results = await db
  .table({ user: "users" })
  .leftJoin(
    { referrer: "users" },
    "user.referrer_id",
    "referrer.id"
  )
  .select({
    userId: "user.id",
    userName: "user.username",
    referrerId: "referrer.id",
    referrerName: "referrer.username", // null ๊ฐ€๋Šฅ
  })
  .where("user.role", "normal");

// ์ผ๋ฐ˜ ์‚ฌ์šฉ์ž + ์ถ”์ฒœ์ธ ์ •๋ณด

Many-to-Many ์กฐ์ธ

๋‹ค๋Œ€๋‹ค(M:N) ๊ด€๊ณ„๋Š” ์ค‘๊ฐ„ ํ…Œ์ด๋ธ”์„ ํ†ตํ•ด ์กฐ์ธํ•ฉ๋‹ˆ๋‹ค.

ํ”„๋กœ์ ํŠธ โ†” ์ง์› ์˜ˆ์ œ

const results = await db
  .table("projects")
  .leftJoin("projects__employees", "projects.id", "projects__employees.project_id")
  .leftJoin("employees", "projects__employees.employee_id", "employees.id")
  .leftJoin("users", "employees.user_id", "users.id")
  .select({
    projectId: "projects.id",
    projectName: "projects.name",
    projectStatus: "projects.status",
    employeeId: "employees.id",
    employeeName: "users.username",
  })
  .where("projects.status", "in_progress");

// projects โ†” projects__employees โ†” employees โ†” users
๊ฒฐ๊ณผ ํŠน์ง•:
  • ํ”„๋กœ์ ํŠธ 1๊ฐœ์— ์ง์› N๋ช… โ†’ N๊ฐœ ํ–‰
  • ์ง์›์ด ์—†๋Š” ํ”„๋กœ์ ํŠธ โ†’ 1๊ฐœ ํ–‰ (employee ํ•„๋“œ๋Š” null)
M:N ์กฐ์ธ ์ฃผ์˜์‚ฌํ•ญ:
  1. ๊ฒฐ๊ณผ๊ฐ€ ์ค‘๋ณต๋จ (ํ”„๋กœ์ ํŠธ๋‹น ์ง์› ์ˆ˜๋งŒํผ ํ–‰ ์ƒ์„ฑ)
  2. ์ง‘๊ณ„ ์‹œ ์ฃผ์˜ (COUNT(DISTINCT ...) ์‚ฌ์šฉ)
  3. ํŽ˜์ด์ง€๋„ค์ด์…˜ ๋ณต์žกํ•ด์ง

M:N ์ง์›๋ณ„๋กœ ๊ทธ๋ฃนํ™”

const results = await db
  .table("employees")
  .leftJoin("projects__employees", "employees.id", "projects__employees.employee_id")
  .leftJoin("projects", "projects__employees.project_id", "projects.id")
  .leftJoin("users", "employees.user_id", "users.id")
  .select({
    employeeId: "employees.id",
    employeeName: "users.username",
    projectCount: Puri.count("projects.id"),
  })
  .groupBy("employees.id", "users.username")
  .having("projectCount", ">", 0);

// ์ง์›๋ณ„ ์ฐธ์—ฌ ํ”„๋กœ์ ํŠธ ์ˆ˜

์„œ๋ธŒ์ฟผ๋ฆฌ ์กฐ์ธ

์„œ๋ธŒ์ฟผ๋ฆฌ๋ฅผ ํ…Œ์ด๋ธ”์ฒ˜๋Ÿผ ์กฐ์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

๋ถ€์„œ๋ณ„ ํ†ต๊ณ„ ์กฐ์ธ

// ๋ถ€์„œ๋ณ„ ์ง์› ์ˆ˜ ์„œ๋ธŒ์ฟผ๋ฆฌ
const empStats = db
  .table("employees")
  .select({
    department_id: "department_id",
    emp_count: Puri.count("id"),
    avg_salary: Puri.avg("salary"),
  })
  .groupBy("department_id");

// ๋ถ€์„œ + ํ†ต๊ณ„ ์กฐ์ธ
const results = await db
  .table("departments")
  .leftJoin(
    { stats: empStats },
    "departments.id",
    "stats.department_id"
  )
  .select({
    deptId: "departments.id",
    deptName: "departments.name",
    empCount: "stats.emp_count",
    avgSalary: "stats.avg_salary",
  });

์ตœ์‹  ๋กœ๊ทธ์ธ ์กฐ์ธ

// ์‚ฌ์šฉ์ž๋ณ„ ์ตœ์‹  ๋กœ๊ทธ์ธ ์„œ๋ธŒ์ฟผ๋ฆฌ
const latestLogins = db
  .table("login_logs")
  .select({
    user_id: "user_id",
    latest_login: Puri.max("logged_in_at"),
  })
  .groupBy("user_id");

// ์‚ฌ์šฉ์ž + ์ตœ์‹  ๋กœ๊ทธ์ธ ์กฐ์ธ
const results = await db
  .table("users")
  .leftJoin(
    { latest: latestLogins },
    "users.id",
    "latest.user_id"
  )
  .select({
    userId: "users.id",
    userName: "users.username",
    latestLogin: "latest.latest_login",
  });

์กฐ์ธ + WHERE ์กฐ๊ฑด

์กฐ์ธ ์ „ ํ•„ํ„ฐ๋ง

const results = await db
  .table("employees")
  .join("users", "employees.user_id", "users.id")
  .leftJoin("departments", "employees.department_id", "departments.id")
  .select({
    employeeId: "employees.id",
    userName: "users.username",
    deptName: "departments.name",
  })
  .where("users.is_active", true)          // users ํ•„ํ„ฐ
  .where("employees.salary", ">=", 50000)  // employees ํ•„ํ„ฐ
  .where("departments.name", "like", "%๊ฐœ๋ฐœ%"); // departments ํ•„ํ„ฐ

์กฐ์ธ ํ›„ ์ง‘๊ณ„

const results = await db
  .table("departments")
  .leftJoin("employees", "departments.id", "employees.department_id")
  .select({
    deptId: "departments.id",
    deptName: "departments.name",
    empCount: Puri.count("employees.id"),
  })
  .groupBy("departments.id", "departments.name")
  .having("empCount", ">", 5)
  .orderBy("empCount", "desc");

์‹ค์ „ ์˜ˆ์ œ

์‚ฌ์šฉ์ž ํ”„๋กœํ•„ ์กฐํšŒ

async getUserProfile(userId: number) {
  const profile = await this.getPuri("r")
    .table("users")
    .join("employees", "users.id", "employees.user_id")
    .leftJoin("departments", "employees.department_id", "departments.id")
    .leftJoin("companies", "departments.company_id", "companies.id")
    .select({
      userId: "users.id",
      username: "users.username",
      email: "users.email",
      employeeNumber: "employees.employee_number",
      departmentName: "departments.name",
      companyName: "companies.name",
      salary: "employees.salary",
    })
    .where("users.id", userId)
    .first();

  if (!profile) {
    throw new Error("User not found");
  }

  return profile;
}

ํ”„๋กœ์ ํŠธ ์ƒ์„ธ + ์ฐธ์—ฌ์ž ๋ชฉ๋ก

async getProjectDetails(projectId: number) {
  const project = await this.getPuri("r")
    .table("projects")
    .select({
      id: "projects.id",
      name: "projects.name",
      status: "projects.status",
      description: "projects.description",
    })
    .where("projects.id", projectId)
    .first();

  if (!project) {
    throw new Error("Project not found");
  }

  // ์ฐธ์—ฌ์ž ๋ชฉ๋ก
  const members = await this.getPuri("r")
    .table("projects__employees")
    .join("employees", "projects__employees.employee_id", "employees.id")
    .join("users", "employees.user_id", "users.id")
    .leftJoin("departments", "employees.department_id", "departments.id")
    .select({
      employeeId: "employees.id",
      employeeName: "users.username",
      employeeNumber: "employees.employee_number",
      departmentName: "departments.name",
      role: "projects__employees.role",
    })
    .where("projects__employees.project_id", projectId);

  return {
    ...project,
    members,
  };
}

๋ถ€์„œ ๊ณ„์ธต๊ตฌ์กฐ ์กฐํšŒ

async getDepartmentHierarchy(companyId: number) {
  const departments = await this.getPuri("r")
    .table({ dept: "departments" })
    .leftJoin(
      { parent: "departments" },
      "dept.parent_id",
      "parent.id"
    )
    .leftJoin("companies", "dept.company_id", "companies.id")
    .select({
      id: "dept.id",
      name: "dept.name",
      parentId: "parent.id",
      parentName: "parent.name",
      companyName: "companies.name",
      level: Puri.rawNumber(`
        CASE
          WHEN dept.parent_id IS NULL THEN 0
          WHEN parent.parent_id IS NULL THEN 1
          ELSE 2
        END
      `),
    })
    .where("dept.company_id", companyId)
    .orderBy("level", "asc")
    .orderBy("dept.name", "asc");

  return departments;
}

ํƒ€์ž… ์•ˆ์ „์„ฑ

Puri์˜ ์กฐ์ธ์€ ํƒ€์ž… ์•ˆ์ „ํ•ฉ๋‹ˆ๋‹ค.
const results = await db
  .table("employees")
  .join("users", "employees.user_id", "users.id")
  .leftJoin("departments", "employees.department_id", "departments.id")
  .select({
    employeeId: "employees.id",        // number
    userName: "users.username",         // string
    departmentName: "departments.name", // string | null (LEFT JOIN)
  });

// ํƒ€์ž…์ด ์ž๋™์œผ๋กœ ์ถ”๋ก ๋จ
const first = results[0];
first.employeeId;     // number
first.userName;        // string
first.departmentName;  // string | null
LEFT JOIN๊ณผ ํƒ€์ž…:
  • LEFT JOIN๋œ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ์€ T | null ํƒ€์ž…
  • INNER JOIN๋œ ํ…Œ์ด๋ธ”์˜ ์ปฌ๋Ÿผ์€ T ํƒ€์ž…
  • ํƒ€์ž…์Šคํฌ๋ฆฝํŠธ๊ฐ€ ์ž๋™์œผ๋กœ null ์ฒดํฌ ๊ฐ•์ œ

์„ฑ๋Šฅ ์ตœ์ ํ™”

์ธ๋ฑ์Šค ํ™œ์šฉ

-- ์กฐ์ธ ํ‚ค์— ์ธ๋ฑ์Šค ์ƒ์„ฑ
CREATE INDEX idx_employees_user_id ON employees(user_id);
CREATE INDEX idx_employees_department_id ON employees(department_id);

ํ•„์š”ํ•œ ์ปฌ๋Ÿผ๋งŒ ์„ ํƒ

// โŒ ๋‚˜์จ: ๋ชจ๋“  ์ปฌ๋Ÿผ ์กฐํšŒ
await db
  .table("employees")
  .join("users", "employees.user_id", "users.id")
  .selectAll();

// โœ… ์ข‹์Œ: ํ•„์š”ํ•œ ์ปฌ๋Ÿผ๋งŒ ์„ ํƒ
await db
  .table("employees")
  .join("users", "employees.user_id", "users.id")
  .select({
    id: "employees.id",
    name: "users.username",
  });

JOIN ์ˆœ์„œ ์ตœ์ ํ™”

// ์ž‘์€ ํ…Œ์ด๋ธ” โ†’ ํฐ ํ…Œ์ด๋ธ” ์ˆœ์„œ
await db
  .table("departments")        // 10๊ฐœ
  .join("employees", ...)      // 100๊ฐœ
  .join("projects__employees", ...); // 1000๊ฐœ

๋‹ค์Œ ๋‹จ๊ณ„