๋ฉ”์ธ ์ฝ˜ํ…์ธ ๋กœ ๊ฑด๋„ˆ๋›ฐ๊ธฐ
Puri๋Š” SQL์˜ ๊ฐ•๋ ฅํ•œ ์ง‘๊ณ„ ํ•จ์ˆ˜๋ฅผ ํƒ€์ž… ์•ˆ์ „ํ•˜๊ฒŒ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์ด ๋ฌธ์„œ๋Š” COUNT, SUM, AVG, MAX, MIN๊ณผ GROUP BY, HAVING์„ ์„ค๋ช…ํ•ฉ๋‹ˆ๋‹ค.

์ง‘๊ณ„ ํ•จ์ˆ˜ ๊ฐœ์š”

COUNT

๋ ˆ์ฝ”๋“œ ๊ฐœ์ˆ˜ ์„ธ๊ธฐPuri.count()

SUM/AVG

ํ•ฉ๊ณ„์™€ ํ‰๊ท  ๊ณ„์‚ฐPuri.sum(), Puri.avg()

MAX/MIN

์ตœ๋Œ€๊ฐ’๊ณผ ์ตœ์†Œ๊ฐ’ ์ฐพ๊ธฐPuri.max(), Puri.min()

GROUP BY

๊ทธ๋ฃน๋ณ„๋กœ ์ง‘๊ณ„ํ•˜๊ธฐgroupBy(), having()

COUNT - ๊ฐœ์ˆ˜ ์„ธ๊ธฐ

๊ธฐ๋ณธ COUNT

const result = await db
  .table("employees")
  .select({
    total: Puri.count(),
  })
  .first();

console.log(result.total); // 150
// ํƒ€์ž…: { total: number }
Puri.count()๋Š” COUNT(*)๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. NULL์„ ํฌํ•จํ•œ ๋ชจ๋“  ํ–‰์„ ์…‰๋‹ˆ๋‹ค. Puri.count("column")์€ ํ•ด๋‹น ์ปฌ๋Ÿผ์ด NULL์ด ์•„๋‹Œ ํ–‰๋งŒ ์…‰๋‹ˆ๋‹ค.

COUNT with GROUP BY

const results = await db
  .table("employees")
  .select({
    departmentId: "department_id",
    employeeCount: Puri.count("id"),
  })
  .groupBy("department_id");

// ๋ถ€์„œ๋ณ„ ์ง์› ์ˆ˜
// [
//   { departmentId: 1, employeeCount: 20 },
//   { departmentId: 2, employeeCount: 15 },
//   ...
// ]

SUM - ํ•ฉ๊ณ„

์ˆซ์ž ์ปฌ๋Ÿผ์˜ ํ•ฉ๊ณ„๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค.
const result = await db
  .table("employees")
  .select({
    totalSalary: Puri.sum("salary"),
  })
  .first();

console.log(result.totalSalary); // 5000000
// ํƒ€์ž…: { totalSalary: number }

SUM with GROUP BY

const results = await db
  .table("employees")
  .select({
    departmentId: "department_id",
    totalSalary: Puri.sum("salary"),
    avgSalary: Puri.avg("salary"),
  })
  .groupBy("department_id")
  .orderBy("totalSalary", "desc");

// ๋ถ€์„œ๋ณ„ ๊ธ‰์—ฌ ํ•ฉ๊ณ„ & ํ‰๊ท 

AVG - ํ‰๊ท 

์ˆซ์ž ์ปฌ๋Ÿผ์˜ ํ‰๊ท ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค.
const result = await db
  .table("employees")
  .select({
    avgSalary: Puri.avg("salary"),
  })
  .first();

console.log(result.avgSalary); // 65000
// ํƒ€์ž…: { avgSalary: number }

MAX / MIN - ์ตœ๋Œ€๊ฐ’ / ์ตœ์†Œ๊ฐ’

์ตœ๋Œ€๊ฐ’

const result = await db
  .table("employees")
  .select({
    highestSalary: Puri.max("salary"),
    latestHireDate: Puri.max("hire_date"),
  })
  .first();

// ๊ฐ€์žฅ ๋†’์€ ๊ธ‰์—ฌ, ๊ฐ€์žฅ ์ตœ๊ทผ ์ž…์‚ฌ์ผ

์ตœ์†Œ๊ฐ’

const result = await db
  .table("employees")
  .select({
    lowestSalary: Puri.min("salary"),
    earliestHireDate: Puri.min("hire_date"),
  })
  .first();

// ๊ฐ€์žฅ ๋‚ฎ์€ ๊ธ‰์—ฌ, ๊ฐ€์žฅ ์˜ค๋ž˜๋œ ์ž…์‚ฌ์ผ

MAX/MIN with GROUP BY

const results = await db
  .table("employees")
  .select({
    departmentId: "department_id",
    minSalary: Puri.min("salary"),
    maxSalary: Puri.max("salary"),
    avgSalary: Puri.avg("salary"),
  })
  .groupBy("department_id");

// ๋ถ€์„œ๋ณ„ ์ตœ์ €/์ตœ๊ณ /ํ‰๊ท  ๊ธ‰์—ฌ

GROUP BY - ๊ทธ๋ฃนํ™”

๋ฐ์ดํ„ฐ๋ฅผ ํŠน์ • ์ปฌ๋Ÿผ ๊ธฐ์ค€์œผ๋กœ ๊ทธ๋ฃนํ™”ํ•˜์—ฌ ์ง‘๊ณ„ํ•ฉ๋‹ˆ๋‹ค.

๋‹จ์ผ ์ปฌ๋Ÿผ GROUP BY

const results = await db
  .table("employees")
  .select({
    departmentId: "department_id",
    count: Puri.count("id"),
    avgSalary: Puri.avg("salary"),
  })
  .groupBy("department_id");

// ๋ถ€์„œ๋ณ„ ํ†ต๊ณ„

์—ฌ๋Ÿฌ ์ปฌ๋Ÿผ GROUP BY

const results = await db
  .table("employees")
  .select({
    departmentId: "department_id",
    hireYear: Puri.rawNumber("EXTRACT(YEAR FROM hire_date)"),
    count: Puri.count("id"),
  })
  .groupBy("department_id", "hireYear")
  .orderBy("department_id", "asc")
  .orderBy("hireYear", "asc");

// ๋ถ€์„œ + ์—ฐ๋„๋ณ„ ์‹ ์ž… ์ง์› ์ˆ˜
GROUP BY ๊ทœ์น™:
  • SELECT์— ์žˆ๋Š” ์ง‘๊ณ„ ํ•จ์ˆ˜๊ฐ€ ์•„๋‹Œ ๋ชจ๋“  ์ปฌ๋Ÿผ์€ GROUP BY์— ์žˆ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค
  • GROUP BY์— ์—†๋Š” ์ปฌ๋Ÿผ์€ SELECTํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค (์ง‘๊ณ„ ํ•จ์ˆ˜ ์ œ์™ธ)

GROUP BY with JOIN

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"),
    avgSalary: Puri.avg("employees.salary"),
  })
  .groupBy("departments.id", "departments.name")
  .orderBy("empCount", "desc");

// ๋ถ€์„œ๋ณ„ ์ง์› ์ˆ˜ & ํ‰๊ท  ๊ธ‰์—ฌ

HAVING - ์ง‘๊ณ„ ๊ฒฐ๊ณผ ํ•„ํ„ฐ๋ง

HAVING์€ GROUP BY ํ›„ ์ง‘๊ณ„ ๊ฒฐ๊ณผ๋ฅผ ํ•„ํ„ฐ๋งํ•ฉ๋‹ˆ๋‹ค.

๊ธฐ๋ณธ HAVING

const results = await db
  .table("employees")
  .select({
    departmentId: "department_id",
    count: Puri.count("id"),
  })
  .groupBy("department_id")
  .having("count", ">=", 10);

// ์ง์› 10๋ช… ์ด์ƒ์ธ ๋ถ€์„œ๋งŒ
WHERE vs HAVING:
  • WHERE: GROUP BY ์ „์— ํ•„ํ„ฐ๋ง (๊ฐœ๋ณ„ ํ–‰ ๋Œ€์ƒ)
  • HAVING: GROUP BY ํ›„์— ํ•„ํ„ฐ๋ง (์ง‘๊ณ„ ๊ฒฐ๊ณผ ๋Œ€์ƒ)

HAVING with ์ง‘๊ณ„ ํ•จ์ˆ˜

const results = await db
  .table("employees")
  .select({
    departmentId: "department_id",
    avgSalary: Puri.avg("salary"),
    count: Puri.count("id"),
  })
  .groupBy("department_id")
  .having("avgSalary", ">", 70000)
  .having("count", ">=", 5);

// ํ‰๊ท  ๊ธ‰์—ฌ 7๋งŒ ์ด์ƒ & ์ง์› 5๋ช… ์ด์ƒ์ธ ๋ถ€์„œ

WHERE + HAVING ์กฐํ•ฉ

const results = await db
  .table("employees")
  .select({
    departmentId: "department_id",
    avgSalary: Puri.avg("salary"),
  })
  .where("hire_date", ">=", "2020-01-01")  // ๊ฐœ๋ณ„ ํ–‰ ํ•„ํ„ฐ
  .groupBy("department_id")
  .having("avgSalary", ">", 65000);         // ์ง‘๊ณ„ ๊ฒฐ๊ณผ ํ•„ํ„ฐ

// 2020๋…„ ์ดํ›„ ์ž…์‚ฌ์ž ์ค‘, ๋ถ€์„œ๋ณ„ ํ‰๊ท  ๊ธ‰์—ฌ 6.5๋งŒ ์ด์ƒ

๋ณตํ•ฉ ์ง‘๊ณ„ ์˜ˆ์ œ

๋ถ€์„œ๋ณ„ ์ƒ์„ธ ํ†ต๊ณ„

const stats = await db
  .table("departments")
  .leftJoin("employees", "departments.id", "employees.department_id")
  .select({
    deptId: "departments.id",
    deptName: "departments.name",
    
    // ์ง์› ์ˆ˜
    empCount: Puri.count("employees.id"),
    
    // ๊ธ‰์—ฌ ํ†ต๊ณ„
    minSalary: Puri.min("employees.salary"),
    maxSalary: Puri.max("employees.salary"),
    avgSalary: Puri.avg("employees.salary"),
    totalSalary: Puri.sum("employees.salary"),
    
    // ์ž…์‚ฌ์ผ ํ†ต๊ณ„
    earliestHire: Puri.min("employees.hire_date"),
    latestHire: Puri.max("employees.hire_date"),
  })
  .groupBy("departments.id", "departments.name")
  .having("empCount", ">", 0)
  .orderBy("empCount", "desc");

ํ”„๋กœ์ ํŠธ ์ง„ํ–‰ ํ˜„ํ™ฉ

const projectStats = await db
  .table("projects")
  .leftJoin("projects__employees", "projects.id", "projects__employees.project_id")
  .select({
    projectId: "projects.id",
    projectName: "projects.name",
    status: "projects.status",
    memberCount: Puri.count("projects__employees.employee_id"),
  })
  .groupBy("projects.id", "projects.name", "projects.status")
  .orderBy("status", "asc")
  .orderBy("memberCount", "desc");

// ํ”„๋กœ์ ํŠธ๋ณ„ ์ฐธ์—ฌ์ž ์ˆ˜ (์ƒํƒœ๋ณ„ ์ •๋ ฌ)

์›”๋ณ„ ๋งค์ถœ ๋ถ„์„

const monthlySales = await db
  .table("orders")
  .select({
    year: Puri.rawNumber("EXTRACT(YEAR FROM created_at)"),
    month: Puri.rawNumber("EXTRACT(MONTH FROM created_at)"),
    orderCount: Puri.count("id"),
    totalAmount: Puri.sum("amount"),
    avgAmount: Puri.avg("amount"),
  })
  .where("status", "completed")
  .groupBy("year", "month")
  .orderBy("year", "desc")
  .orderBy("month", "desc");

// ์›”๋ณ„ ์ฃผ๋ฌธ ํ†ต๊ณ„

์กฐ๊ฑด๋ถ€ ์ง‘๊ณ„

CASE WHEN์„ ์‚ฌ์šฉํ•œ ์กฐ๊ฑด๋ถ€ ์ง‘๊ณ„์ž…๋‹ˆ๋‹ค.

์ƒํƒœ๋ณ„ ์นด์šดํŠธ

const stats = await db
  .table("projects")
  .select({
    totalProjects: Puri.count("id"),
    
    activeProjects: Puri.rawNumber(`
      COUNT(CASE WHEN status = 'in_progress' THEN 1 END)
    `),
    
    completedProjects: Puri.rawNumber(`
      COUNT(CASE WHEN status = 'completed' THEN 1 END)
    `),
    
    cancelledProjects: Puri.rawNumber(`
      COUNT(CASE WHEN status = 'cancelled' THEN 1 END)
    `),
  })
  .first();

// ์ „์ฒด/์ง„ํ–‰์ค‘/์™„๋ฃŒ/์ทจ์†Œ ํ”„๋กœ์ ํŠธ ์ˆ˜

๊ตฌ๊ฐ„๋ณ„ ์ง‘๊ณ„

const salaryRanges = await db
  .table("employees")
  .select({
    under50k: Puri.rawNumber(`
      COUNT(CASE WHEN salary < 50000 THEN 1 END)
    `),
    
    range50to70k: Puri.rawNumber(`
      COUNT(CASE WHEN salary BETWEEN 50000 AND 70000 THEN 1 END)
    `),
    
    above70k: Puri.rawNumber(`
      COUNT(CASE WHEN salary > 70000 THEN 1 END)
    `),
  })
  .first();

// ๊ธ‰์—ฌ ๊ตฌ๊ฐ„๋ณ„ ์ง์› ์ˆ˜

์‹ค์ „ ์˜ˆ์ œ

๋Œ€์‹œ๋ณด๋“œ ํ†ต๊ณ„

async getDashboardStats() {
  // ์ „์ฒด ํ†ต๊ณ„
  const overall = await this.getPuri("r")
    .table("employees")
    .select({
      totalEmployees: Puri.count("id"),
      avgSalary: Puri.avg("salary"),
      maxSalary: Puri.max("salary"),
      minSalary: Puri.min("salary"),
    })
    .first();

  // ๋ถ€์„œ๋ณ„ ํ†ต๊ณ„
  const byDepartment = await this.getPuri("r")
    .table("departments")
    .leftJoin("employees", "departments.id", "employees.department_id")
    .select({
      deptName: "departments.name",
      empCount: Puri.count("employees.id"),
      avgSalary: Puri.avg("employees.salary"),
    })
    .groupBy("departments.id", "departments.name")
    .orderBy("empCount", "desc")
    .limit(10);

  return {
    overall,
    byDepartment,
  };
}

์ž…์‚ฌ ์—ฐ๋„๋ณ„ ํ†ต๊ณ„

async getHiringTrends() {
  const trends = await this.getPuri("r")
    .table("employees")
    .select({
      year: Puri.rawNumber("EXTRACT(YEAR FROM hire_date)"),
      hireCount: Puri.count("id"),
      avgSalary: Puri.avg("salary"),
    })
    .groupBy("year")
    .orderBy("year", "desc")
    .limit(5);

  return trends;
}

TOP 5 ๋ถ€์„œ

async getTop5Departments() {
  const top5 = await this.getPuri("r")
    .table("departments")
    .leftJoin("employees", "departments.id", "employees.department_id")
    .select({
      deptId: "departments.id",
      deptName: "departments.name",
      empCount: Puri.count("employees.id"),
      totalSalary: Puri.sum("employees.salary"),
      avgSalary: Puri.avg("employees.salary"),
    })
    .groupBy("departments.id", "departments.name")
    .having("empCount", ">", 0)
    .orderBy("totalSalary", "desc")
    .limit(5);

  return top5;
}

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

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

-- GROUP BYํ•  ์ปฌ๋Ÿผ์— ์ธ๋ฑ์Šค
CREATE INDEX idx_employees_department_id ON employees(department_id);
CREATE INDEX idx_employees_hire_date ON employees(hire_date);

-- ์ง‘๊ณ„ํ•  ์ปฌ๋Ÿผ์— ์ธ๋ฑ์Šค (์„ ํƒ์ )
CREATE INDEX idx_employees_salary ON employees(salary);

ํ•„์š”ํ•œ ์ง‘๊ณ„๋งŒ ์ˆ˜ํ–‰

// โŒ ๋‚˜์จ: ๋ถˆํ•„์š”ํ•œ ์ง‘๊ณ„
await db
  .table("employees")
  .select({
    count: Puri.count("id"),
    sum: Puri.sum("salary"),
    avg: Puri.avg("salary"),
    max: Puri.max("salary"),
    min: Puri.min("salary"),
    // ... ๋” ๋งŽ์€ ์ง‘๊ณ„
  })
  .groupBy("department_id");

// โœ… ์ข‹์Œ: ํ•„์š”ํ•œ ๊ฒƒ๋งŒ
await db
  .table("employees")
  .select({
    departmentId: "department_id",
    count: Puri.count("id"),
    avgSalary: Puri.avg("salary"),
  })
  .groupBy("department_id");

WHERE๋กœ ๋จผ์ € ํ•„ํ„ฐ๋ง

// โœ… ์ข‹์Œ: ์ง‘๊ณ„ ์ „ ๋ถˆํ•„์š”ํ•œ ํ–‰ ์ œ๊ฑฐ
await db
  .table("employees")
  .where("is_active", true)        // ๋จผ์ € ํ•„ํ„ฐ
  .where("hire_date", ">=", "2020-01-01")
  .select({
    departmentId: "department_id",
    avgSalary: Puri.avg("salary"),
  })
  .groupBy("department_id");

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

์ง‘๊ณ„ ํ•จ์ˆ˜์˜ ๊ฒฐ๊ณผ ํƒ€์ž…์ด ์ž๋™์œผ๋กœ ์ถ”๋ก ๋ฉ๋‹ˆ๋‹ค.
const result = await db
  .table("employees")
  .select({
    count: Puri.count("id"),           // number
    avgSalary: Puri.avg("salary"),     // number
    maxDate: Puri.max("hire_date"),    // string (๋‚ ์งœ๋Š” ๋ฌธ์ž์—ด๋กœ ๋ฐ˜ํ™˜)
  })
  .first();

result.count;      // number
result.avgSalary;  // number
result.maxDate;    // string

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