Puri๋ ์ฌ๋ฌ ํ
์ด๋ธ์ ์กฐ์ธํ์ฌ ๊ด๋ จ ๋ฐ์ดํฐ๋ฅผ ํจ๊ป ์กฐํํ ์ ์์ต๋๋ค. ์ด ๋ฌธ์๋ INNER JOIN, LEFT JOIN, ๊ทธ๋ฆฌ๊ณ ๋ณต์กํ ์กฐ์ธ ํจํด์ ์ค๋ช
ํฉ๋๋ค.
์กฐ์ธ ๊ฐ์
INNER JOIN ์์ชฝ ํ
์ด๋ธ์ ๋ชจ๋ ์กด์ฌํ๋ ๋ฐ์ดํฐ join()
LEFT JOIN ์ผ์ชฝ ํ
์ด๋ธ ๊ธฐ์ค, ์ค๋ฅธ์ชฝ์ Optional leftJoin()
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 ๋น๊ต
INNER JOIN - ์์ชฝ ๋ชจ๋ ์๋ ๊ฒ๋ง
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
๊ฒฐ๊ณผ ์์ :
childId childName parentId parentName 1 ๋ณธ๋ถ null null 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๊ฐ
๋ค์ ๋จ๊ณ
Aggregations ์ง๊ณ ํจ์๋ก ๋ฐ์ดํฐ ๋ถ์ํ๊ธฐ
Advanced Patterns ์๋ธ์ฟผ๋ฆฌ์ ๋ณต์กํ ํจํด
Type Safety ์กฐ์ธ ์ ํ์
์์ ์ฑ
Basic Queries ๊ธฐ๋ณธ ์ฟผ๋ฆฌ๋ก ๋์๊ฐ๊ธฐ