7일 이상 / 30일 이상 / 90일 이상 구간으로 변환LEFT JOINIFNULL(DISCOUNT_RATE, 0)으로 처리CASE는 큰 조건부터 검사SELECT
H.HISTORY_ID,
FLOOR(
C.DAILY_FEE
* (DATEDIFF(H.END_DATE, H.START_DATE) + 1)
* (100 - IFNULL(P.DISCOUNT_RATE, 0)) / 100
) AS FEE
FROM CAR_RENTAL_COMPANY_CAR C
JOIN CAR_RENTAL_COMPANY_RENTAL_HISTORY H
ON C.CAR_ID = H.CAR_ID
LEFT JOIN CAR_RENTAL_COMPANY_DISCOUNT_PLAN P
ON C.CAR_TYPE = P.CAR_TYPE
AND P.DURATION_TYPE =
CASE
WHEN DATEDIFF(H.END_DATE, H.START_DATE) + 1 >= 90 THEN '90일 이상'
WHEN DATEDIFF(H.END_DATE, H.START_DATE) + 1 >= 30 THEN '30일 이상'
WHEN DATEDIFF(H.END_DATE, H.START_DATE) + 1 >= 7 THEN '7일 이상'
END
WHERE C.CAR_TYPE = '트럭'
ORDER BY FEE DESC, H.HISTORY_ID DESC;
LEFT JOIN7, 30, 90을 추출하기보다 실제 기간을 CASE로 정책 구간에 매핑DATEDIFF(END_DATE, START_DATE) + 1로 대여일 계산MDDR_ID = DR_ID로 연결SELECT
A.APNT_NO,
P.PT_NAME,
P.PT_NO,
D.MCDP_CD,
D.DR_NAME,
A.APNT_YMD
FROM APPOINTMENT A
JOIN DOCTOR D
ON A.MDDR_ID = D.DR_ID
JOIN PATIENT P
ON A.PT_NO = P.PT_NO
WHERE A.MCDP_CD = 'CS'
AND A.APNT_CNCL_YN = 'N'
AND A.APNT_YMD >= '2022-04-13'
AND A.APNT_YMD < '2022-04-14'
ORDER BY A.APNT_YMD;
ON은 테이블 관계, WHERE는 조회 조건으로 나누면 읽기 좋음>= 시작일 AND < 다음날 방식이 안전CODE가 2의 거듭제곱으로 저장되어 있으므로 비트 AND 연산으로 스킬 보유 여부 확인DISTINCT 사용SELECT DISTINCT
D.ID,
D.EMAIL,
D.FIRST_NAME,
D.LAST_NAME
FROM SKILLCODES S
JOIN DEVELOPERS D
ON (S.CODE & D.SKILL_CODE) != 0
WHERE S.CATEGORY = 'Front End'
ORDER BY D.ID;
(SKILL_CODE & CODE) > 0
DISTINCT 고려COUNT(DISTINCT USER_ID)WITH USER_2021 AS (
SELECT USER_ID
FROM USER_INFO
WHERE YEAR(JOINED) = 2021
)
SELECT
YEAR(O.SALES_DATE) AS YEAR,
MONTH(O.SALES_DATE) AS MONTH,
COUNT(DISTINCT O.USER_ID) AS PURCHASED_USERS,
ROUND(
COUNT(DISTINCT O.USER_ID)
/ (SELECT COUNT(*) FROM USER_2021),
1
) AS PURCHASED_RATIO
FROM ONLINE_SALE O
JOIN USER_2021 U
ON O.USER_ID = U.USER_ID
GROUP BY
YEAR(O.SALES_DATE),
MONTH(O.SALES_DATE)
ORDER BY YEAR, MONTH;
WITH USER_2021 AS (
SELECT USER_ID
FROM USER_INFO
WHERE YEAR(JOINED) = 2021
),
TOTAL AS (
SELECT COUNT(*) AS CNT
FROM USER_2021
)
SELECT
YEAR(O.SALES_DATE) AS YEAR,
MONTH(O.SALES_DATE) AS MONTH,
COUNT(DISTINCT O.USER_ID) AS PURCHASED_USERS,
ROUND(COUNT(DISTINCT O.USER_ID) / T.CNT, 1) AS PURCHASED_RATIO
FROM ONLINE_SALE O
JOIN USER_2021 U
ON O.USER_ID = U.USER_ID
CROSS JOIN TOTAL T
GROUP BY YEAR(O.SALES_DATE), MONTH(O.SALES_DATE), T.CNT;
SELECT
CHILD.ITEM_ID,
CHILD.ITEM_NAME,
CHILD.RARITY
FROM ITEM_TREE T
JOIN ITEM_INFO PARENT
ON T.PARENT_ITEM_ID = PARENT.ITEM_ID
JOIN ITEM_INFO CHILD
ON T.ITEM_ID = CHILD.ITEM_ID
WHERE PARENT.RARITY = 'RARE'
ORDER BY CHILD.ITEM_ID DESC;
SELECT ITEM_ID, ITEM_NAME, RARITY
FROM ITEM_INFO
WHERE ITEM_ID IN (
SELECT T.ITEM_ID
FROM ITEM_INFO I
JOIN ITEM_TREE T
ON I.ITEM_ID = T.PARENT_ITEM_ID
WHERE I.RARITY = 'RARE'
)
ORDER BY ITEM_ID DESC;
PARENT는 부모 조건 확인용, CHILD는 출력 정보 조회용LEFT JOINSELECT
PARENT.ID,
COUNT(CHILD.ID) AS CHILD_COUNT
FROM ECOLI_DATA PARENT
LEFT JOIN ECOLI_DATA CHILD
ON PARENT.ID = CHILD.PARENT_ID
GROUP BY PARENT.ID
ORDER BY PARENT.ID;
COUNT(*) -- 행 자체의 개수
COUNT(CHILD.ID) -- CHILD.ID가 NULL이 아닌 행만
LEFT JOIN은 부모 행 하나를 남김COUNT(*)를 쓰면 1로 나올 수 있음LEFT JOIN 후 실제 매칭된 행 수를 세려면 오른쪽 테이블의 NOT NULL 컬럼을 COUNTSELECT
CHILD.ID,
CHILD.GENOTYPE,
PARENT.GENOTYPE AS PARENT_GENOTYPE
FROM ECOLI_DATA PARENT
JOIN ECOLI_DATA CHILD
ON PARENT.ID = CHILD.PARENT_ID
WHERE (CHILD.GENOTYPE & PARENT.GENOTYPE)
= PARENT.GENOTYPE
ORDER BY CHILD.ID;
(A & B) > 0
(A & B) = B
WITH RANKED AS (
SELECT
ID,
NTILE(4) OVER (
ORDER BY SIZE_OF_COLONY DESC
) AS GRADE
FROM ECOLI_DATA
)
SELECT
ID,
CASE
WHEN GRADE = 1 THEN 'CRITICAL'
WHEN GRADE = 2 THEN 'HIGH'
WHEN GRADE = 3 THEN 'MEDIUM'
ELSE 'LOW'
END AS COLONY_NAME
FROM RANKED
ORDER BY ID;
WITH RANKED AS (
SELECT
ID,
ROW_NUMBER() OVER (
ORDER BY SIZE_OF_COLONY DESC
) AS RN,
COUNT(*) OVER () AS TOTAL
FROM ECOLI_DATA
)
SELECT
ID,
CASE
WHEN RN <= TOTAL * 0.25 THEN 'CRITICAL'
WHEN RN <= TOTAL * 0.50 THEN 'HIGH'
WHEN RN <= TOTAL * 0.75 THEN 'MEDIUM'
ELSE 'LOW'
END AS COLONY_NAME
FROM RANKED
ORDER BY ID;
ROW_NUMBER() = 순번NTILE(N) = 정렬 결과를 N개 그룹으로 균등 분할상위 25%, 4등분, 사분위 → NTILE(4) 고려GROUP BY가 아니라 MAX() OVER (PARTITION BY ...) 사용SELECT
YEAR(DIFFERENTIATION_DATE) AS YEAR,
MAX(SIZE_OF_COLONY) OVER (
PARTITION BY YEAR(DIFFERENTIATION_DATE)
) - SIZE_OF_COLONY AS YEAR_DEV,
ID
FROM ECOLI_DATA
ORDER BY YEAR, YEAR_DEV;
GROUP BY
→ 그룹당 한 행으로 줄어듦
MAX(...) OVER (PARTITION BY ...)
→ 원본 행 수를 유지하면서 그룹별 집계값을 각 행에 붙임
PARENT_ITEM_ID에 등장하는 아이템은 다음 단계 아이템이 존재SELECT ITEM_ID, ITEM_NAME, RARITY
FROM ITEM_INFO
WHERE ITEM_ID NOT IN (
SELECT PARENT_ITEM_ID
FROM ITEM_TREE
WHERE PARENT_ITEM_ID IS NOT NULL
)
ORDER BY ITEM_ID DESC;
SELECT
I.ITEM_ID,
I.ITEM_NAME,
I.RARITY
FROM ITEM_INFO I
LEFT JOIN ITEM_TREE T
ON I.ITEM_ID = T.PARENT_ITEM_ID
WHERE T.ITEM_ID IS NULL
ORDER BY I.ITEM_ID DESC;
NOT IN, NOT EXISTS, LEFT JOIN ... IS NULLNOT IN 사용 시 서브쿼리의 NULL 제거LEFT JOIN은 왼쪽 테이블을 모두 살리므로 테이블 순서가 중요SELECT GRANDCHILD.ID
FROM ECOLI_DATA PARENT
JOIN ECOLI_DATA CHILD
ON PARENT.ID = CHILD.PARENT_ID
JOIN ECOLI_DATA GRANDCHILD
ON CHILD.ID = GRANDCHILD.PARENT_ID
WHERE PARENT.PARENT_ID IS NULL
ORDER BY GRANDCHILD.ID;
PARENT = 1세대
CHILD = 2세대
GRANDCHILD = 3세대
PARENT.PARENT_ID IS NULL로 시작점을 실제 1세대로 고정2세대 → 3세대 → 4세대 경로도 포함될 수 있음SELECT CODE
FROM SKILLCODES
WHERE NAME = 'Python';
SELECT SUM(CODE)
FROM SKILLCODES
WHERE CATEGORY = 'Front End';
SELECT
CASE
WHEN
(SKILL_CODE & (
SELECT SUM(CODE)
FROM SKILLCODES
WHERE CATEGORY = 'Front End'
)) > 0
AND
(SKILL_CODE & (
SELECT CODE
FROM SKILLCODES
WHERE NAME = 'Python'
)) > 0
THEN 'A'
WHEN
(SKILL_CODE & (
SELECT CODE
FROM SKILLCODES
WHERE NAME = 'C#'
)) > 0
THEN 'B'
ELSE 'C'
END AS GRADE,
ID,
EMAIL
FROM DEVELOPERS
WHERE
(SKILL_CODE & (
SELECT SUM(CODE)
FROM SKILLCODES
WHERE CATEGORY = 'Front End'
)) > 0
OR
(SKILL_CODE & (
SELECT CODE
FROM SKILLCODES
WHERE NAME = 'C#'
)) > 0
ORDER BY GRADE, ID;
Front End + Python, C가 Front End이므로 더 구체적인 A를 먼저 검사SELECT
SUM(G.SCORE) AS SCORE,
E.EMP_NO,
E.EMP_NAME,
E.POSITION,
E.EMAIL
FROM HR_GRADE G
JOIN HR_EMPLOYEES E
ON G.EMP_NO = E.EMP_NO
GROUP BY
E.EMP_NO,
E.EMP_NAME,
E.POSITION,
E.EMAIL
ORDER BY SCORE DESC
LIMIT 1;
SELECT
SUM(G.SCORE) AS SCORE,
E.EMP_NO,
E.EMP_NAME,
E.POSITION,
E.EMAIL
FROM HR_GRADE G
JOIN HR_EMPLOYEES E
ON G.EMP_NO = E.EMP_NO
GROUP BY
E.EMP_NO,
E.EMP_NAME,
E.POSITION,
E.EMAIL
HAVING SUM(G.SCORE) = (
SELECT MAX(T.SCORE)
FROM (
SELECT
EMP_NO,
SUM(SCORE) AS SCORE
FROM HR_GRADE
GROUP BY EMP_NO
) T
);
ORDER BY ... DESC LIMIT 1MAX와 비교하거나 RANK()SUM(SCORE)으로 필터링 → WHERE가 아닌 HAVINGCASE로 등급과 보너스 계산SELECT
E.EMP_NO,
E.EMP_NAME,
CASE
WHEN T.SCORE >= 96 THEN 'S'
WHEN T.SCORE >= 90 THEN 'A'
WHEN T.SCORE >= 80 THEN 'B'
ELSE 'C'
END AS GRADE,
CASE
WHEN T.SCORE >= 96 THEN E.SAL * 0.2
WHEN T.SCORE >= 90 THEN E.SAL * 0.15
WHEN T.SCORE >= 80 THEN E.SAL * 0.1
ELSE 0
END AS BONUS
FROM HR_EMPLOYEES E
JOIN (
SELECT
EMP_NO,
AVG(SCORE) AS SCORE
FROM HR_GRADE
GROUP BY EMP_NO
) T
ON E.EMP_NO = T.EMP_NO
ORDER BY E.EMP_NO;
먼저 그룹별 집계 → 집계 결과를 상세 테이블과 JOIN → CASE 계산HOST_ID 목록을 먼저 구함SELECT ID, NAME, HOST_ID
FROM PLACES
WHERE HOST_ID IN (
SELECT HOST_ID
FROM PLACES
GROUP BY HOST_ID
HAVING COUNT(*) >= 2
)
ORDER BY ID;
COUNT(*) = 행 개수COUNT(HOST_ID) = HOST_ID가 NULL이 아닌 행 개수SELECT CART_ID
FROM CART_PRODUCTS
WHERE NAME IN ('Milk', 'Yogurt')
GROUP BY CART_ID
HAVING COUNT(DISTINCT NAME) = 2
ORDER BY CART_ID;
GROUP BY CART_ID, NAME
HAVING COUNT(*) >= 2
COUNT(DISTINCT ...) 활용