[강동7기 전Z전능 AI PM] 코멘토 청년취업사관학교 DAY 35

DAY 35 | SQL 기초 — 집계·GROUP BY로 요약하고 INSERT·UPDATE·DELETE로 바꾸기

SQL 기초 3일차. 앞의 이틀이 데이터를 “꺼내고, 거르고, 다듬는” 과정이었다면 오늘은 두 가지가 새로 붙었다. 하나는 여러 행을 하나로 요약하는 집계와 GROUP BY, 다른 하나는 데이터를 실제로 추가·수정·삭제하는 INSERT/UPDATE/DELETE다. 앞부분은 “국가별 매출 같은 요약 지표를 어떻게 뽑느냐”라서 PM이 대시보드에서 매일 보는 숫자와 바로 연결됐고, 뒷부분은 조회와 달리 데이터를 실제로 바꾸는 명령이라 긴장감이 다른 파트였다.

집계 함수 — 여러 행을 하나로 요약한다

집계 함수는 여러 행을 받아 하나의 값으로 줄여준다. 개수(COUNT), 합계(SUM), 평균(AVG), 최솟값(MIN), 최댓값(MAX)이 대표적이다.

SELECT SUM(Total), AVG(Total), MIN(Total), MAX(Total)
FROM Invoice;

여기서 놓치기 쉬운 게 SUM/AVG/MIN/MAX는 모두 NULL을 계산에서 제외한다는 점이다. 평균을 낼 때 NULL이 0으로 들어가는 게 아니라 아예 분모에서 빠진다.

COUNT(*)와 COUNT(컬럼)은 다르다

오늘 첫 번째 헷갈리는 포인트였다. COUNT(*)는 NULL 여부와 관계없이 행 자체를 세고, COUNT(컬럼)은 그 컬럼이 NULL이 아닌 행만 센다.

SELECT COUNT(*) FROM Customer;              -- 전체 행 수
SELECT COUNT(Company) FROM Customer;        -- Company가 있는 행만
SELECT COUNT(*) FROM Customer WHERE Country = 'Germany';  -- 조건에 맞는 행 수

Company가 비어 있는 고객이 있다면 두 값이 달라진다. “전체 고객 수”와 “회사명을 남긴 고객 수”는 다른 질문이라는 걸 숫자로 확인했다. COUNT(*) + WHERE는 조건에 맞는 행의 개수를 세는 조합으로 자주 쓰인다.

DISTINCT — 중복을 제거하고 고유값만

DISTINCT는 조회 결과에서 중복을 없애고 고유한 값만 남긴다. COUNT와 붙이면 “고유한 값이 몇 개인지”가 된다.

SELECT DISTINCT Country FROM Customer;         -- 어떤 국가들이 있나
SELECT COUNT(DISTINCT Country) FROM Customer;  -- 몇 개 국가에 고객이 있나

“우리 고객이 몇 개 나라에 퍼져 있나” 같은 질문이 COUNT(DISTINCT ...) 한 줄로 나온다.

집계 + 단위 변환

집계는 여러 개를 한 SELECT에 몰아넣을 수 있는데, 이때 SQL 안에서 단위 변환까지 처리할 수 있다는 걸 배웠다. 밀리초를 초·분으로, 바이트를 KB로 바꾸고 ROUND로 소수 자릿수를 정리한다.

SELECT
    COUNT(*) AS total_tracks,
    ROUND(AVG(Milliseconds) / 1000, 2) AS avg_duration_sec,
    ROUND(MAX(Milliseconds) / 1000 / 60, 2) AS longest_track_min,
    ROUND(AVG(Bytes) / 1024, 2) AS avg_size_kb
FROM Track;

원본이 밀리초·바이트라도 보고서에 올릴 땐 초·분·KB가 편하다. 가공을 애플리케이션까지 미루지 않고 쿼리에서 끝낼 수 있다는 감이 왔다.

GROUP BY — 기준을 정해 나눠서 집계한다

집계만 쓰면 항상 전체에 대한 하나의 값이 나온다. 그런데 실무 질문은 대부분 “전체”가 아니라 “무엇별”이다. 국가별 매출, 장르별 평균 재생시간, 월별 매출, 고객별 주문 수처럼. 이 “~별”을 담당하는 게 GROUP BY다.

SELECT Country, COUNT(*) AS CustomerCount
FROM Customer
GROUP BY Country;

같은 Country끼리 한 그룹으로 묶고, 그 그룹마다 COUNT를 낸다. 여러 컬럼을 쓰면 두 값이 모두 같은 행끼리 묶여서 “국가 → 도시”처럼 더 잘게 나뉜다.

SELECT Country, City, COUNT(*) AS CustomerCount
FROM Customer
GROUP BY Country, City;

GROUP BY의 규칙 — SELECT에 아무 컬럼이나 못 온다

두 번째 포인트. GROUP BY를 쓰면 SELECT에는 (1) GROUP BY에 들어간 컬럼이거나 (2) 집계 함수로 감싼 컬럼만 올 수 있다.

-- 문제가 되는 쿼리: City는 그룹 기준도, 집계도 아니다
SELECT Country, City
FROM Customer
GROUP BY Country;

Country로 묶으면 한 그룹 안에 City가 여러 개일 수 있는데, 그중 뭘 보여줘야 할지 SQL이 정할 수 없다. “그룹 기준 아니면 집계”라는 한 줄로 기억하면 편했다.

DISTINCT와 비슷해 보이지만 목적이 다르다는 것도 짚었다. DISTINCT는 중복을 지워 고유값을 보는 것이고, GROUP BY는 같은 값끼리 묶어 그 묶음마다 집계를 내는 것이다. GROUP BY의 핵심은 언제나 집계 함수와 함께 쓴다는 데 있다.

HAVING — 그룹을 걸러낸다 (WHERE와의 차이)

오늘 가장 중요한 지점이 WHERE와 HAVING의 차이였다. WHERE는 행을 거르고, HAVING은 그룹을 거른다.

SELECT Country, COUNT(*) AS CustomerCount
FROM Customer
GROUP BY Country
HAVING COUNT(*) >= 5;   -- 고객 5명 이상인 국가만

COUNT(*) >= 5 같은 조건은 그룹이 만들어진 뒤에야 판단할 수 있으니 WHERE에는 못 쓰고 HAVING에 써야 한다. 이 차이는 결국 실행 순서 한 줄에서 나온다.

FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY

WHERE는 GROUP BY보다 먼저라 아직 그룹이 없고, HAVING은 GROUP BY 다음이라 그룹의 집계 결과를 조건으로 쓸 수 있다. 어제 “WHERE에서 별칭을 못 쓰는 이유”도 같은 실행 순서였는데, 이 순서 하나가 계속 반복해서 답을 준다는 게 재밌었다.

세 개를 같이 쓰기 — “미국에서 주문 10건 이상 도시”

WHERE / GROUP BY / HAVING를 한 쿼리에서 순서대로 엮으면 꽤 구체적인 질문이 풀린다.

SELECT BillingCity, COUNT(*) AS OrderCount
FROM Invoice
WHERE BillingCountry = 'USA'   -- ① 미국 행만 남기고
GROUP BY BillingCity           -- ② 도시별로 묶어서
HAVING COUNT(*) >= 10          -- ③ 10건 이상 도시만
ORDER BY OrderCount DESC;      -- ④ 많은 순 정렬

전체 데이터 → WHERE로 미국만 → 도시별 묶기 → 도시별 COUNT → HAVING으로 10건 이상만. 실행 순서를 그대로 따라가면 쿼리가 왜 이 모양인지 자연스럽게 읽혔다. 정렬은 집계 결과에 별칭(OrderCount)을 붙여서 거는 게 안전하고, 그룹이 지나치게 잘게 쪼개질 것 같으면 WHERE로 먼저 데이터를 줄인 뒤 GROUP BY 하는 게 좋다고 배웠다.

여기서부터는 데이터를 실제로 바꾼다

70페이지쯤부터 성격이 바뀌었다. 지금까지의 SELECT는 아무리 돌려도 데이터가 안 바뀌는데, INSERT/UPDATE/DELETE는 실제 테이블을 건드린다. 그래서 실습도 원본을 직접 만지지 않도록 복사본을 만들어서 했다.

CREATE TABLE customer_copy AS
SELECT * FROM customer;

원본 customer를 그대로 복제한 customer_copy를 대상으로 추가·수정·삭제를 연습했다. “실수해도 원본은 안전하다”는 안전장치를 먼저 깔고 시작한다는 습관 자체가 인상적이었다.

INSERT — 행 추가

-- 모든 컬럼에 순서대로
INSERT INTO sample41 VALUES (1, 'abc', '2025-01-01');
 
-- 원하는 컬럼만 지정 (나머지는 기본값 또는 NULL)
INSERT INTO sample41 (No, a) VALUES (2, 'xyz');

모든 컬럼을 채울 필요는 없고, 컬럼명을 명시하면 지정한 것만 넣고 나머지는 기본값이나 NULL이 들어간다.

UPDATE — 수정, 그리고 WHERE 누락의 위험

UPDATE는 어떤 테이블을(UPDATE) 무엇으로(SET) 어떤 행에서(WHERE) 바꿀지로 읽으면 된다.

UPDATE customer_copy
SET Country = 'Korea', City = 'Seoul'
WHERE CustomerId = 105;

여기서 두 가지가 핵심이었다. 첫째, SET의 =는 비교가 아니라 대입이다. Country = 'Korea'는 “Country가 Korea냐”가 아니라 “Country에 Korea를 넣어라”다. 둘째, 그리고 가장 무서운 것 — WHERE가 없으면 모든 행이 바뀐다.

UPDATE customer_copy SET Country = 'Korea';  -- 전체 행의 Country가 다 Korea가 된다

그래서 실무에서는 UPDATE 전에 같은 조건으로 SELECT * ... WHERE 조건을 먼저 돌려서 바뀔 대상을 눈으로 확인하는 습관이 강조됐다.

UPDATE + CASE — 조건별로 한 번에

조건에 따라 다른 값으로 바꿔야 할 때 UPDATE를 여러 번 쓰지 않고 CASE로 한 번에 처리한다.

UPDATE customer_copy
SET Country = CASE
    WHEN Country = 'USA'    THEN 'United States'
    WHEN Country = 'Canada' THEN 'CA'
    ELSE 'Other'
END;

어제 조회에서 배운 CASE가 여기서 그대로 재활용됐다. ELSE에 기존 컬럼을 두면 조건에 안 걸린 행은 원래 값을 유지시킬 수도 있다.

DELETE — 삭제, 역시 WHERE가 생명

DELETE FROM customer_copy WHERE CustomerId = 105;  -- 조건에 맞는 행만
DELETE FROM customer_copy;                          -- WHERE 없으면 전체 삭제

UPDATE와 똑같이 WHERE가 없으면 전체가 날아간다. 반대로 조건에 맞는 행이 하나도 없으면 아무것도 안 지워진다.

오늘 헷갈리기 쉬웠던 것

  • COUNT(*)는 행을 세고 COUNT(컬럼)은 NULL이 아닌 값을 센다 — 두 결과가 다를 수 있다.
  • WHERE는 행을, HAVING은 그룹을 거른다. 집계 결과에 거는 조건은 HAVING.
  • GROUP BY를 쓰면 SELECT에는 그룹 기준 컬럼이나 집계 함수만 올 수 있다.
  • UPDATE / DELETE에서 WHERE를 빼면 전체 행이 바뀌거나 지워진다.

취업 부트캠프 청년취업사관학교 새싹 aipm dx교육 ai교육 실무프로젝트 실무경험 취업포트폴리오 포트폴리오 전z전능 코멘토 모비니티