mysql링크

STEP 1. SQL 기초 (10문제)

순서번호문제링크배우는 내용
1175Combine Two Tableshttps://leetcode.cn/problems/combine-two-tables/SELECT, LEFT JOIN
2584Find Customer Refereehttps://leetcode.cn/problems/find-customer-referee/WHERE
3595Big Countrieshttps://leetcode.cn/problems/big-countries/AND, OR
41148Article Views Ihttps://leetcode.cn/problems/article-views-i/DISTINCT
51683Invalid Tweetshttps://leetcode.cn/problems/invalid-tweets/CHAR_LENGTH
61378Replace Employee ID With The Unique Identifierhttps://leetcode.cn/problems/replace-employee-id-with-the-unique-identifier/LEFT JOIN
71068Product Sales Analysis Ihttps://leetcode.cn/problems/product-sales-analysis-i/JOIN
81581Customer Who Visited but Did Not Make Any Transactionshttps://leetcode.cn/problems/customer-who-visited-but-did-not-make-any-transactions/LEFT JOIN + NULL
9197Rising Temperaturehttps://leetcode.cn/problems/rising-temperature/Self Join
10607Sales Personhttps://leetcode.cn/problems/sales-person/NOT EXISTS

STEP 2. 정렬과 조건 (8문제)

순서번호문제링크학습 내용
11511Game Play Analysis Ihttps://leetcode.cn/problems/game-play-analysis-i/MIN
12619Biggest Single Numberhttps://leetcode.cn/problems/biggest-single-number/GROUP BY
132356Number of Unique Subjects Taught by Each Teacherhttps://leetcode.cn/problems/number-of-unique-subjects-taught-by-each-teacher/COUNT DISTINCT
141141User Activity for the Past 30 Days Ihttps://leetcode.cn/problems/user-activity-for-the-past-30-days-i/DATE
151527Patients With a Conditionhttps://leetcode.cn/problems/patients-with-a-condition/LIKE
161873Calculate Special Bonushttps://leetcode.cn/problems/calculate-special-bonus/CASE WHEN
171667Fix Names in a Tablehttps://leetcode.cn/problems/fix-names-in-a-table/UPPER, LOWER
18196Delete Duplicate Emailshttps://leetcode.cn/problems/delete-duplicate-emails/DELETE

STEP 3. GROUP BY & HAVING (10문제)

순서번호문제링크학습 내용
19182Duplicate Emailshttps://leetcode.cn/problems/duplicate-emails/GROUP BY, HAVING
20586Customer Placing the Largest Number of Ordershttps://leetcode.cn/problems/customer-placing-the-largest-number-of-orders/COUNT, GROUP BY
211050Actors and Directors Who Cooperated At Least Three Timeshttps://leetcode.cn/problems/actors-and-directors-who-cooperated-at-least-three-times/GROUP BY, HAVING
221729Find Followers Counthttps://leetcode.cn/problems/find-followers-count/COUNT, GROUP BY
231211Queries Quality and Percentagehttps://leetcode.cn/problems/queries-quality-and-percentage/AVG, ROUND, GROUP BY
241193Monthly Transactions Ihttps://leetcode.cn/problems/monthly-transactions-i/SUM, COUNT, CASE WHEN
251174Immediate Food Delivery IIhttps://leetcode.cn/problems/immediate-food-delivery-ii/MIN, GROUP BY
26550Game Play Analysis IVhttps://leetcode.cn/problems/game-play-analysis-iv/COUNT, JOIN, GROUP BY
271393Capital Gain/Losshttps://leetcode.cn/problems/capital-gain-loss/SUM, CASE WHEN
281398Customers Who Bought Products A and B but Not Chttps://leetcode.cn/problems/customers-who-bought-products-a-and-b-but-not-c/GROUP BY, HAVING

STEP 4. JOIN (15문제)

순서번호문제링크
291070Product Sales Analysis IIIhttps://leetcode.cn/problems/product-sales-analysis-iii/
301075Project Employees Ihttps://leetcode.cn/problems/project-employees-i/
31181Employees Earning More Than Their Managershttps://leetcode.cn/problems/employees-earning-more-than-their-managers/
32626Exchange Seatshttps://leetcode.cn/problems/exchange-seats/
33183Customers Who Never Orderhttps://leetcode.cn/problems/customers-who-never-order/
34184Department Highest Salaryhttps://leetcode.cn/problems/department-highest-salary/
35185Department Top Three Salarieshttps://leetcode.cn/problems/department-top-three-salaries/
36602Friend Requests II: Who Has the Most Friendshttps://leetcode.cn/problems/friend-requests-ii-who-has-the-most-friends/
37608Tree Nodehttps://leetcode.cn/problems/tree-node/
38601Human Traffic of Stadiumhttps://leetcode.cn/problems/human-traffic-of-stadium/
39180Consecutive Numbershttps://leetcode.cn/problems/consecutive-numbers/
40577Employee Bonushttps://leetcode.cn/problems/employee-bonus/
41585Investments in 2016https://leetcode.cn/problems/investments-in-2016/
421132Reported Posts IIhttps://leetcode.cn/problems/reported-posts-ii/
431158Market Analysis Ihttps://leetcode.cn/problems/market-analysis-i/

STEP 5. 서브쿼리 (8문제)

순서번호문제링크
44176Second Highest Salaryhttps://leetcode.cn/problems/second-highest-salary/
45177Nth Highest Salaryhttps://leetcode.cn/problems/nth-highest-salary/
46178Rank Scoreshttps://leetcode.cn/problems/rank-scores/
47262Trips and Usershttps://leetcode.cn/problems/trips-and-users/
481045Customers Who Bought All Productshttps://leetcode.cn/problems/customers-who-bought-all-products/
491164Product Price at a Given Datehttps://leetcode.cn/problems/product-price-at-a-given-date/
501204Last Person to Fit in the Bushttps://leetcode.cn/problems/last-person-to-fit-in-the-bus/
511321Restaurant Growthhttps://leetcode.cn/problems/restaurant-growth/

STEP 6. CASE WHEN (5문제)

순서번호문제링크
521873Calculate Special Bonushttps://leetcode.cn/problems/calculate-special-bonus/
531179Reformat Department Tablehttps://leetcode.cn/problems/reformat-department-table/
541205Monthly Transactions IIhttps://leetcode.cn/problems/monthly-transactions-ii/
551174Immediate Food Delivery IIhttps://leetcode.cn/problems/immediate-food-delivery-ii/
561587Bank Account Summary IIhttps://leetcode.cn/problems/bank-account-summary-ii/

STEP 7. 문자열 / 날짜 함수 (8문제)

순서번호문제링크
571667Fix Names in a Tablehttps://leetcode.cn/problems/fix-names-in-a-table/
581527Patients With a Conditionhttps://leetcode.cn/problems/patients-with-a-condition/
591141User Activity for the Past 30 Days Ihttps://leetcode.cn/problems/user-activity-for-the-past-30-days-i/
601225Report Contiguous Dateshttps://leetcode.cn/problems/report-contiguous-dates/
611454Active Usershttps://leetcode.cn/problems/active-users/
621084Sales Analysis IIIhttps://leetcode.cn/problems/sales-analysis-iii/
631795Rearrange Products Tablehttps://leetcode.cn/problems/rearrange-products-table/
641127User Purchase Platformhttps://leetcode.cn/problems/user-purchase-platform/

STEP 8. Window Function (10문제)

순서번호문제링크
65178Rank Scoreshttps://leetcode.cn/problems/rank-scores/
66185Department Top Three Salarieshttps://leetcode.cn/problems/department-top-three-salaries/
671204Last Person to Fit in the Bushttps://leetcode.cn/problems/last-person-to-fit-in-the-bus/
681321Restaurant Growthhttps://leetcode.cn/problems/restaurant-growth/
691341Movie Ratinghttps://leetcode.cn/problems/movie-rating/
701164Product Price at a Given Datehttps://leetcode.cn/problems/product-price-at-a-given-date/
711934Confirmation Ratehttps://leetcode.cn/problems/confirmation-rate/
721211Queries Quality and Percentagehttps://leetcode.cn/problems/queries-quality-and-percentage/
73601Human Traffic of Stadiumhttps://leetcode.cn/problems/human-traffic-of-stadium/
74180Consecutive Numbershttps://leetcode.cn/problems/consecutive-numbers/

STEP 9. CTE (4문제)

순서번호문제링크
75608Tree Nodehttps://leetcode.cn/problems/tree-node/
761225Report Contiguous Dateshttps://leetcode.cn/problems/report-contiguous-dates/
771070Product Sales Analysis IIIhttps://leetcode.cn/problems/product-sales-analysis-iii/
781321Restaurant Growthhttps://leetcode.cn/problems/restaurant-growth/

STEP 10. 실전 종합 (7문제)

순서번호문제링크
79262Trips and Usershttps://leetcode.cn/problems/trips-and-users/
80601Human Traffic of Stadiumhttps://leetcode.cn/problems/human-traffic-of-stadium/
81184Department Highest Salaryhttps://leetcode.cn/problems/department-highest-salary/
82185Department Top Three Salarieshttps://leetcode.cn/problems/department-top-three-salaries/
831321Restaurant Growthhttps://leetcode.cn/problems/restaurant-growth/
841341Movie Ratinghttps://leetcode.cn/problems/movie-rating/
851934Confirmation Ratehttps://leetcode.cn/problems/confirmation-rate/

자주 쓰는 SQL 명령 정리

위 문제들을 풀며 반복해서 만나는 문법과 함정을 한곳에 모았습니다. ⚠️ 는 에러 없이 조용히 틀린 답을 내는 함정입니다. 문법 에러보다 위험합니다. 💡 는 실무에서 사고를 막아주는 팁입니다.

1. 비교 연산자

연산자의미비고
=같다NULL 과 비교하면 결과가 UNKNOWN
<> (!=)다르다NULL 과 비교하면 결과가 UNKNOWN
> < >= <=크다 / 작다 / 이상 / 이하문자열은 콜레이션 기준, 날짜는 시간순
<=>NULL-safe 같다절대 UNKNOWN 을 안 냄. 항상 1 또는 0
IS NULL / IS NOT NULLNULL 여부NULL 비교는 오직 이것으로만
IS [NOT] TRUE/FALSE불린 판정x IS TRUE = x = 1 과 유사
BETWEEN x AND yx 이상 y 이하양 끝 포함
IN (...)목록에 속함OR 의 축약형
LIKE패턴 매칭%(0글자+), _(1글자)
REGEXP (RLIKE)정규식 매칭⚠️ 인덱스를 절대 못 탐 → 풀스캔
  • =<=> 의 차이: NULL = NULLNULL(UNKNOWN), NULL <=> NULL1(TRUE). 값이 NULL 일 수 있는 컬럼을 정확히 비교할 땐 <=>.
  • 부등호는 숫자뿐 아니라 문자열·날짜에도 됩니다. 문자열은 'a' < 'b' 처럼 콜레이션 정렬 순서로 비교합니다.
-- % 자체를 검색하기 (ESCAPE)
-- 상품명에 % 가 들어 있는 "다크초콜릿 72% 100g" 을 찾으려면 % 를 이스케이프해야 합니다.
SELECT * FROM products
WHERE name LIKE '%72\%%';            -- 기본 이스케이프 문자는 백슬래시(\)

SELECT * FROM products
WHERE name LIKE '%72#%%' ESCAPE '#'; -- ESCAPE 로 이스케이프 문자를 직접 지정

2. 데이터베이스 / 테이블 만들기 — CREATE / ALTER / DROP

-- 데이터베이스
CREATE DATABASE shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
DROP DATABASE IF EXISTS shop;          -- 존재하지 않아도 에러 안 남

-- 테이블
CREATE TABLE customers (
  id         BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
  name       VARCHAR(50)  NOT NULL,
  email      VARCHAR(100) NOT NULL,
  created_at DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
  PRIMARY KEY (id),
  UNIQUE KEY uk_email (email)
) ENGINE=InnoDB;

-- 구조 변경 (ALTER)
ALTER TABLE customers ADD COLUMN phone VARCHAR(20) NULL;        -- 컬럼 추가
ALTER TABLE customers MODIFY COLUMN phone VARCHAR(30) NOT NULL; -- 타입/속성 변경
ALTER TABLE customers CHANGE COLUMN phone tel VARCHAR(30) NULL; -- 이름까지 변경
ALTER TABLE customers DROP COLUMN phone;                        -- 컬럼 삭제
ALTER TABLE customers ADD INDEX idx_name (name);               -- 인덱스 추가

-- 삭제 (DROP / TRUNCATE)
DROP TABLE IF EXISTS customers;   -- 테이블 자체를 없앰(구조+데이터)
TRUNCATE TABLE customers;         -- 데이터만 전부 비움(구조 유지, AUTO_INCREMENT 초기화)
  • DROP 은 테이블/DB 자체를 삭제, TRUNCATE데이터만 비웁니다. DELETE 는 행 단위로 지우며 WHERE 로 조건을 줄 수 있습니다.
  • 💡 대형 테이블의 ALTER 는 잠금·복제 지연을 유발할 수 있어, 운영에서는 pt-online-schema-change(Percona) / gh-ost(GitHub) 를 씁니다.

3. 데이터 타입 함정

  • BOOLEAN 은 TINYINT(1) 의 별칭일 뿐입니다. 진짜 불린 타입은 없습니다. TRUE=1, FALSE=0 으로 저장됩니다. WHERE is_active = 2 도 문법상 통과합니다.
  • 주문 ID처럼 무한히 늘어나는 값은 처음부터 BIGINT 로. INT 의 상한(약 21억)은 트래픽 많은 서비스에서 실제로 넘칩니다. 나중에 INT → BIGINT 로 바꾸는 마이그레이션은 테이블 전체를 재작성하는 대공사가 됩니다.
  • ⚠️ 돈·정확한 수치 계산에 FLOAT/DOUBLE 을 쓰면 안 됩니다. 부동소수점은 근사값이라 0.1 + 0.2 ≠ 0.3 이 되어 정산이 어긋납니다. 반드시 DECIMAL(p, s)(예: 원화 DECIMAL(12, 2))를 쓰세요.
  • ⚠️ CHAR 는 뒤 공백을 조용히 삼킵니다. CHAR(10)'abc ' 를 넣고 꺼내면 뒤 공백이 사라진 'abc' 가 나옵니다. 공백이 의미 있는 값(비밀번호 해시 등)은 VARCHAR/BINARY 를 쓰세요.
  • ⚠️ ENUM 은 값 추가 시 ALTER TABLE 이 필요합니다. 값이 자주 늘어나는 도메인(예: 결제수단이 계속 추가됨)이라면 ENUM 대신 별도 코드 테이블 + FK 가 낫습니다. 또 하나: ENUM 의 정렬 순서는 알파벳순이 아니라 선언 순서입니다. ORDER BY grade 가 사전순이 아닌 이유입니다.
  • ⚠️ 2038년 문제: TIMESTAMP 는 32비트라서 2038-01-19 에 오버플로우합니다. 구독 만료일, 보증 기간처럼 먼 미래 날짜를 다룬다면 TIMESTAMP 대신 DATETIME 을 쓰세요.

4. SELECT 기본

  • ⚠️ SELECT * 를 애플리케이션 코드에 넣지 마세요. 컬럼이 추가되면 네트워크·메모리 낭비, 인덱스 커버링 실패, 컬럼 순서 의존 버그가 생깁니다. *탐색할 때만 쓰세요.
  • 별칭에 공백·괄호·예약어가 들어가면 백틱(`)으로 감싸야 합니다. 예: SELECT COUNT(*) AS 주문 수``.
  • ⚠️ 콤마 누락 주의: SELECT name price 는 에러가 아니라 pricename 의 별칭으로 해석해 컬럼이 조용히 사라집니다. 별칭엔 항상 AS 를 명시하세요.
  • WHERE 는 조건이 참(TRUE)인 행만 남깁니다. 거짓(FALSE)뿐 아니라 NULL(unknown)인 행도 버립니다.
  • SQL 의 결과에는 기본 순서가 없습니다. 정렬이 필요하면 반드시 ORDER BY 를 명시하세요.
  • MySQL 은 NULL 을 가장 작은 값으로 취급합니다. ORDER BY col ASC 면 NULL 이 맨 앞에 옵니다. 💡 MySQL 엔 NULLS LAST 문법이 없어 ORDER BY (col IS NULL), col 로 우회합니다.

LIMIT / DISTINCT / 실행 순서

  • ⚠️ ORDER BY 없는 LIMIT 은 아무것도 보장하지 않습니다. "상위 10개" 를 원한다면 반드시 ORDER BY 를 함께 쓰세요.
  • DISTINCT 는 함수가 아닙니다. DISTINCT(city) 라고 써도 동작하지만, 그건 DISTINCT (city) — 괄호가 그냥 무시된 것일 뿐입니다. DISTINCTSELECT 목록 전체에 걸립니다. SELECT DISTINCT city, country 는 (city, country) 조합의 유일값입니다.
SQL 쓰는 순서 :  SELECT → FROM → WHERE → GROUP BY → HAVING → ORDER BY → LIMIT
DB 평가 순서  :  FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
  • 평가 순서를 알면 "WHERE 에서 SELECT 별칭을 못 쓰는 이유"(WHERE 가 SELECT 보다 먼저 실행됨), "HAVING/ORDER BY 에선 별칭을 쓸 수 있는 이유" 가 자연스럽게 이해됩니다.

5. AND / OR / BETWEEN / IN

  • ANDOR 보다 먼저 묶입니다. 산술에서 *+ 보다 먼저인 것과 같습니다.
  • ⚠️ OR 를 쓸 때는 무조건 괄호를 치세요. 우선순위를 외우고 있더라도 치세요. 6개월 뒤의 나와 동료를 위한 것입니다. 실무 버그 리포트의 상당수가 "OR 괄호 누락"입니다.
-- 위험: category_id = 21 인 행 + (모든) 재고 있는 행 이 다 걸림
WHERE category_id = 21 OR category_id = 22 AND stock > 0
-- 안전: 의도대로 괄호
WHERE (category_id = 21 OR category_id = 22) AND stock > 0
  • BETWEENa BETWEEN x AND ya >= x AND a <= y 와 완전히 같습니다. 양 끝을 포함합니다.
  • ⚠️ 날짜에 BETWEEN 을 쓰면 반드시 사고가 납니다. logged_at BETWEEN '2024-01-01' AND '2024-01-31'2024-01-31 00:00:00 까지만 포함해 그날 낮 시간대를 통째로 놓칩니다. → logged_at >= '2024-01-01' AND logged_at < '2024-02-01' 처럼 ">= 시작, < 다음날" 패턴을 쓰세요.
  • INOR 를 짧게 쓴 것입니다. category_id IN (21, 22)category_id = 21 OR category_id = 22 와 같습니다.

6. NULL 과 3값 논리

  • NULL 은 "0" 도 "빈 문자열" 도 아닙니다. "값을 모른다" 입니다.
  • 모르는 값과 무엇을 비교하든 답은 "모른다" 입니다. 그래서 SQL 의 논리값은 참/거짓 2개가 아니라 TRUE / FALSE / UNKNOWN 3개입니다.
  • NULL = NULL 조차 TRUE 가 아닙니다(→ UNKNOWN). NULL 은 전염됩니다 — 산술이든 문자열 연결이든 NULL 이 하나 끼면 결과가 통째로 NULL 이 됩니다. (1 + NULLNULL, CONCAT('a', NULL)NULL)
  • ⚠️ NULL 이 가능한 컬럼에 부정 조건(<>, NOT LIKE, NOT IN)을 쓸 때는 항상 OR ... IS NULL 을 붙일지 결정하세요. "제외" 요구사항을 받으면 "NULL 인 행은 포함인가요, 제외인가요?" 를 반드시 물어보세요. 대개 기획자는 이 질문을 생각해 본 적이 없습니다.
  • <=>= 와 똑같지만 NULL 을 정상적으로 비교합니다. 절대 UNKNOWN 을 돌려주지 않고 항상 1 또는 0 입니다.
  • ⚠️ NOT IN (서브쿼리) 대신 NOT EXISTS 를 기본값으로 쓰세요. (아래 서브쿼리 절 참고)

NULL 처리 함수

함수동작
IFNULL(a, b)a 가 NULL 이면 b. 인자 2개 고정. MySQL 전용
COALESCE(a, b, c, ...)왼쪽부터 첫 번째 NULL 아닌 값. 표준 SQL, 인자 개수 무제한
NULLIF(a, b)a = b 이면 NULL, 아니면 a

7. 집계 함수

함수설명NULL 처리
COUNT(*)행의 개수NULL 포함, 무조건 셈
COUNT(col)col 이 NULL 이 아닌 행의 개수NULL 제외
COUNT(DISTINCT col)col 의 서로 다른 값의 개수NULL 제외
SUM(col)합계NULL 무시
AVG(col)평균NULL 무시(분모에서도 제외)
MIN(col) / MAX(col)최소 / 최대NULL 무시. 숫자·문자·날짜 모두 가능
GROUP_CONCAT(col)그룹 내 값들을 문자열로 이어붙임NULL 무시. SEPARATOR, ORDER BY 지정 가능
STDDEV / VARIANCE표준편차 / 분산NULL 무시
  • ⚠️ AVG 는 NULL 을 분모에서도 제외합니다. NULL 을 0으로 치고 평균 내고 싶다면 AVG(IFNULL(col, 0)) 또는 SUM(col) / COUNT(*) 를 쓰세요.

WHERE vs HAVING

  • 집계와 무관한 조건은 HAVING 이 아니라 WHERE 에 쓰세요. WHERE 는 집계 에 행을 걸러 더 효율적입니다.
  • 집계 결과(COUNT(*) 등)로 거르는 건 WHERE 로는 불가능하고 HAVING 만 할 수 있습니다. (평가 순서상 WHERE 는 GROUP BY 보다 먼저라 아직 집계값이 없음)
SELECT customer_id, COUNT(*) AS cnt
FROM orders
WHERE status <> 'CANCELLED'   -- 집계 전 필터 (행 단위 조건)
GROUP BY customer_id
HAVING COUNT(*) >= 3;         -- 집계 후 필터 (그룹 단위 조건)

8. JOIN

  • 주문(orders)에는 customer_id 만 있고 고객 이름은 없습니다. 이름을 붙이려면 customers 와 이어야 합니다. 양쪽에 짝이 있는 행만 남기는 것이 INNER JOIN 입니다.
  • INNER JOIN 은 짝이 없으면 버립니다. 하지만 "상품이 하나도 없는 카테고리" 처럼 짝이 없는 쪽도 보고 싶을 때가 많습니다. LEFT JOIN 은 왼쪽 테이블의 행을 전부 남기고, 오른쪽에 짝이 없으면 그 자리를 NULL 로 채웁니다.
  • ⚠️ LEFT JOIN 뒤의 COUNT 는 반드시 오른쪽 테이블 컬럼을 대상으로 하세요. NULL 확장으로 "상품이 전부 NULL 인 행" 이 1줄 생기는데 COUNT(*) 는 그 행도 셉니다. COUNT(오른쪽.컬럼) 을 쓰면 그 컬럼이 NULL 인 행은 안 세므로 올바른 0 이 나옵니다.
  • ⚠️ 핵심 규칙: LEFT JOIN 에서 오른쪽 테이블 조건은 ON 에, 왼쪽 테이블 조건은 WHERE 에. 오른쪽 조건을 WHERE 에 쓰면 NULL 행이 걸러져 사실상 INNER JOIN 이 되어버립니다.
-- 카테고리별 상품 수 (상품 0개인 카테고리도 0으로 표시)
SELECT c.id, c.name, COUNT(p.id) AS product_cnt   -- COUNT(p.id): 오른쪽 컬럼
FROM categories c
LEFT JOIN products p ON p.category_id = c.id
GROUP BY c.id, c.name;
  • ⚠️ JOIN 결과에 WHERE 를 걸면, 조건에 맞는 행이 하나도 없는 그룹은 결과에서 통째로 사라집니다. "취소 제외 매출" 은 맞게 나오지만, "전 고객 목록" 을 기대했다면 3명이 조용히 빠집니다. 이걸 피하려면 (1) 필터를 SUM(o.status <> 'CANCELLED') 같은 조건부 집계로 옮기거나, (2) LEFT JOIN 으로 바꾸고 조건을 ON 절이나 집계 안으로 넣어야 합니다.
  • CROSS JOIN 은 조건 없이 두 테이블의 모든 조합(곱집합)을 만듭니다. A 가 m 행, B 가 n 행이면 결과는 m × n 행입니다.
  • MySQL 엔 FULL OUTER JOIN 이 없습니다LEFT JOINRIGHT JOINUNION 으로 합쳐 우회합니다.
SELECT * FROM a LEFT  JOIN b ON a.id = b.a_id
UNION
SELECT * FROM a RIGHT JOIN b ON a.id = b.a_id;

9. 서브쿼리

  • 쿼리 안에 들어 있는 또 다른 SELECT 를 서브쿼리라고 합니다. 왜 필요할까요? "평균보다 비싼 상품" 을 찾으려면 평균을 먼저 알아야 합니다. 그런데 평균은 그 자체로 또 하나의 SELECT 입니다. 즉 "질문에 답하기 위해 먼저 답해야 하는 작은 질문" 이 있을 때 서브쿼리를 씁니다.
  • 스칼라 서브쿼리 — 값 1개를 돌려주면 = 로 비교: WHERE price > (SELECT AVG(price) FROM products).
  • 값이 여러 개면 = 대신 IN 을 씁니다. "서울에 사는 고객이 낸 주문" 처럼 목록에 속하는가를 묻는 형태입니다.
  • ROW 서브쿼리 — 컬럼 여러 개를 한 묶음으로 비교합니다. (a, b) = (서브쿼리) 형태입니다.
  • 파생 테이블(Derived Table) — 집계한 결과를 다시 필터링하고 싶을 때가 있습니다. WHERE 는 집계 전에 실행되므로 쓸 수 없고, HAVING 으로도 되지만 조인까지 얽히면 읽기 어려워집니다. 이럴 때 "집계 결과를 하나의 테이블처럼" 취급하는 것이 파생 테이블(FROM (SELECT ...) AS t)입니다.
  • 상관 서브쿼리(Correlated) — 서브쿼리 안에서 바깥 테이블의 컬럼을 참조하면, 서브쿼리는 바깥 행마다 새로 평가됩니다.
  • EXISTS 는 "그런 행이 하나라도 있으면 참" 입니다. 값을 가져오는 게 아니라 존재 여부만 봅니다. 그래서 안쪽 SELECT 에 무엇을 쓰든(1, *, NULL) 성능은 같습니다.
  • > ANY (...) 는 "서브쿼리 결과 중 하나라도 보다 크면", > ALL (...) 은 "전부보다 크면" 입니다. 결국 > ANY = > MIN(...), > ALL = > MAX(...) 와 같습니다.

IN vs EXISTS vs JOIN — 어떤 걸 써야 하나

상황권장
존재 여부만 확인EXISTS / IN
서브쿼리 쪽 컬럼도 결과에 필요JOIN
없는 것을 찾기(안티 조인)NOT EXISTS (NOT IN 은 위험, 아래 참조)
1:N 조인인데 개수를 세야 함JOIN + COUNT(DISTINCT ...) 또는 파생 테이블
  • ⚠️ 서브쿼리 컬럼이 NULL 을 허용한다면 NOT IN 을 쓰지 마세요. 에러도 안 나고 조용히 0행을 반환합니다. "왜 결과가 안 나오지?" 하며 몇 시간을 날리는 대표적 버그입니다. 습관적으로 NOT EXISTS 를 쓰는 것이 가장 안전합니다. (반대로 IN 은 NULL 이 있어도 "있는 것" 은 정상적으로 찾아주므로 상대적으로 안전합니다. 문제는 부정형뿐입니다.)

10. CTE (Common Table Expression)

  • CTE 란: 파생 테이블을 쿼리 맨 앞으로 빼내고 이름을 붙이는 문법(WITH name AS (...))입니다. 결과는 같지만, 읽는 사람은 "위에서 아래로" 자연스럽게 따라갈 수 있습니다. CTE 는 한 번 정의하고 여러 번 참조할 수 있습니다.
  • CTE 체이닝: CTE 는 콤마로 여러 개를 이어 쓸 수 있고, 뒤의 CTE 가 앞의 CTE 를 참조할 수 있습니다. 복잡한 집계를 "1단계 → 2단계 → 3단계" 파이프라인으로 표현하면 리뷰하기도 디버깅하기도 쉬워집니다.
WITH paid AS (
  SELECT * FROM orders WHERE status = 'PAID'
), by_customer AS (
  SELECT customer_id, SUM(amount) AS total
  FROM paid                         -- 앞의 CTE 를 참조
  GROUP BY customer_id
)
SELECT * FROM by_customer WHERE total >= 100000;
  • 재귀 CTE 는 항상 두 부분으로 이루어집니다.
WITH RECURSIVE cte_name AS (
    <앵커(anchor)>          -- 시작점. 자기 자신을 참조하지 않는다
    UNION ALL
    <재귀(recursive)>       -- cte_name 을 참조한다. 여기서 결과가 늘어난다
)
SELECT * FROM cte_name;

11. 집합 연산 (UNION / INTERSECT / EXCEPT)

  • 조인은 가로(컬럼을 옆으로 붙임), 집합 연산은 세로(행을 아래로 붙임) 입니다.
  • UNION 은 두 결과를 붙인 뒤 중복을 제거합니다. UNION ALL 은 중복 제거 없이 그냥 이어붙입니다.
  • UNION ALL 이 빠른 이유: "중복 제거" 는 공짜가 아닙니다. 모든 행을 서로 비교해야 하고, 그러려면 정렬하거나 해시 테이블을 만들어야 합니다. 중복이 없다는 걸 안다면 항상 UNION ALL 을 쓰세요(실행계획에서 차이가 확연합니다).

UNION 사용 규칙

  • 컬럼 개수는 반드시 같아야 한다
  • 컬럼 이름은 첫 번째 SELECT 를 따른다
  • 타입이 달라도 에러가 안 난다(암묵적 형변환)
  • 맨 끝의 ORDER BY / LIMIT 는 전체 결과에 적용됩니다. 개별 SELECT 에 걸고 싶다면 괄호로 감쌉니다.
  • 맨 끝 ORDER BY 에는 첫 번째 SELECT 의 결과 컬럼명만 쓸 수 있습니다. ORDER BY p.price 처럼 테이블 별칭을 붙이면 에러입니다. 두 번째 SELECT 의 별칭도 못 씁니다. 별칭을 붙였다면 그 별칭으로 정렬하세요.
  • ⚠️ 괄호 없는 개별 ORDER BY 는 의미가 없습니다. MySQL 은 UNION ALL 중간의 ORDER BY 를(LIMIT 이 없으면) 최적화 과정에서 버립니다. 정렬을 기대하고 썼다가 순서가 뒤죽박죽 나오는 원인입니다.
(SELECT name, price FROM products WHERE category_id = 1 ORDER BY price LIMIT 3)
UNION ALL
(SELECT name, price FROM products WHERE category_id = 2 ORDER BY price LIMIT 3)
ORDER BY price;   -- 전체 결과에 적용

INTERSECT / EXCEPT (MySQL 8.0.31+)

  • INTERSECT — 양쪽에 모두 존재하는 행만 남깁니다. 예: "10만원 이상 상품" ∩ "후기가 달린 상품".
  • EXCEPT — 왼쪽에는 있고 오른쪽에는 없는 행만 남깁니다. 다른 DB에서는 MINUS(Oracle) 라고 부르기도 합니다.
  • INTERSECT ALL / EXCEPT ALLALL 을 붙이면 중복을 남깁니다. 남는 개수는 "양쪽 중 적은 쪽"(INTERSECT ALL) 또는 "왼쪽 개수 − 오른쪽 개수"(EXCEPT ALL) 입니다.
  • 연산 우선순위: INTERSECTUNION / EXCEPT 보다 먼저 평가됩니다(곱셈이 덧셈보다 먼저인 것과 같습니다). 순서를 바꾸려면 괄호를 쓰세요.

12. INSERT / UPSERT

  • ⚠️ 컬럼 목록을 생략하지 마세요. INSERT INTO orders VALUES (...) 처럼 컬럼을 생략하면, 나중에 테이블에 컬럼이 하나 추가되는 순간 모든 INSERT 문이 깨집니다. 항상 INSERT INTO orders (a, b, c) VALUES (...) 로 컬럼을 명시하세요.
  • 💡 다중 행 INSERT 는 행마다 따로 INSERT 하는 것보다 훨씬 빠릅니다(네트워크 왕복과 트랜잭션 오버헤드가 1회로 줄어듦). 단, LAST_INSERT_ID()배치의 첫 번째 행 ID 를 돌려줍니다. AUTO_INCREMENT 는 연속이므로 나머지는 first + 1, first + 2 … 로 계산할 수 있습니다.
-- 다중 행 INSERT (컬럼 명시)
INSERT INTO order_items (order_id, product_id, qty) VALUES
  (1, 101, 2),
  (1, 102, 1),
  (1, 103, 5);
SELECT LAST_INSERT_ID();   -- 첫 번째 행(=101 아이템)의 id

UPSERT — 이미 있으면 수정, 없으면 삽입

  • INSERT ... ON DUPLICATE KEY UPDATE — 진짜 UPSERT. PK/UNIQUE 충돌 시 지정한 컬럼만 UPDATE 합니다. VALUES(col)(8.0.20+ 부터는 별칭)로 삽입하려던 값을 참조합니다.
  • ⚠️ REPLACE 는 사실 DELETE + INSERT 입니다. 이름은 "replace" 지만 UPDATE 가 아닙니다. 충돌하는 기존 행을 DELETE 하고 새 행을 INSERT 합니다. 이 차이가 사고를 부릅니다 — 기존 행의 AUTO_INCREMENT id 가 바뀌고, 명시하지 않은 컬럼은 DEFAULT 로 되돌아가며, 그 행을 참조하던 ON DELETE CASCADE FK 가 연쇄 삭제될 수 있습니다.
-- 재고 upsert: 있으면 누적, 없으면 삽입
INSERT INTO stock (product_id, qty) VALUES (101, 10)
ON DUPLICATE KEY UPDATE qty = qty + VALUES(qty);

13. UPDATE / DELETE / TRUNCATE — 안전하게

  • ⚠️ 실수로 UPDATE products SET stock = 0 (WHERE 없음!)을 실행하면 전 상품 재고가 0 이 됩니다. 이런 참사를 막는 스위치가 sql_safe_updates 입니다.
    • SET sql_safe_updates = 1; 이면 WHERE 가 있어도 키(인덱스) 컬럼을 쓰지 않으면 막습니다(전체 스캔 = 사실상 전체 갱신 위험). 키를 쓰거나 LIMIT 을 붙이면 통과합니다.
    • 💡 운영 DB에 붙을 땐 습관적으로 켜두세요.
  • DELETE vs TRUNCATE: DELETE 는 행 단위로 지우며 WHERE·트랜잭션·롤백이 됩니다. TRUNCATE 는 테이블을 통째로 비우고 AUTO_INCREMENT 를 초기화하며 훨씬 빠릅니다.
  • ⚠️ TRUNCATE 는 DDL 이라 실행하는 순간 암묵적 커밋이 일어납니다. 트랜잭션으로 감싸도 롤백되지 않습니다. "전체 삭제니까 TRUNCATE 가 빠르지" 하고 운영에서 무심코 썼다가, 롤백을 기대할 수 없어 사고가 커지는 경우가 있습니다. 되돌릴 여지가 필요하면 DELETE, 확실히 비우고 초기화할 거면 TRUNCATE.

14. 내장 함수

문자열 함수

함수설명
CONCAT(a, b, ...)문자열 이어붙이기⚠️ 인자 하나라도 NULL 이면 결과 NULL
CONCAT_WS(sep, ...)구분자로 이어붙이기NULL 인자는 건너뜀
SUBSTRING(s, pos, len)부분 문자열(1-base)SUBSTRING('abcdef', 2, 3)bcd
LEFT(s, n) / RIGHT(s, n)앞/뒤 n글자
REPLACE(s, from, to)부분 문자열 치환대소문자 구분
TRIM(s) / LTRIM / RTRIM공백 제거TRIM(BOTH 'x' FROM ...) 도 가능
LPAD(s, len, pad) / RPAD채워서 고정 길이LPAD('7', 3, '0')007
LOCATE(sub, s) (INSTR)위치 찾기(없으면 0)1-base
LENGTH(s) / CHAR_LENGTH(s)바이트 길이 / 글자 수⚠️ utf8mb4 한글은 둘이 다름
UPPER / LOWER대/소문자
REGEXP_REPLACE(s, pat, rep)정규식 치환8.0+

숫자 함수

함수설명
ROUND(x, d)반올림(자릿수 d)ROUND(3.14159, 2)3.14
CEIL(x) (CEILING)올림CEIL(3.1)4
FLOOR(x)내림FLOOR(3.9)3
TRUNCATE(x, d)버림(반올림 아님)TRUNCATE(3.99, 1)3.9
MOD(a, b) (a % b)나머지MOD(10, 3)1
ABS / POWER / SQRT / SIGN절댓값 / 거듭제곱 / 제곱근 / 부호
  • ⚠️ 함수 TRUNCATE(x, d) 와 문장 TRUNCATE TABLE 은 이름만 같고 전혀 다릅니다.

날짜 / 시간 함수

함수설명
NOW() / CURDATE() / CURTIME()현재 일시 / 날짜 / 시각
DATE_ADD(d, INTERVAL n unit)날짜 더하기DATE_ADD(d, INTERVAL 7 DAY)
DATE_SUB(d, INTERVAL n unit)날짜 빼기INTERVAL 1 MONTH
DATEDIFF(a, b)두 날짜의 일수a - b (일 단위)
TIMESTAMPDIFF(unit, a, b)단위 지정 차이TIMESTAMPDIFF(YEAR, 생일, NOW()) → 나이
DATE_FORMAT(d, fmt)날짜 → 문자열DATE_FORMAT(d, '%Y-%m')2024-01
STR_TO_DATE(s, fmt)문자열 → 날짜DATE_FORMAT 의 역
LAST_DAY(d)그 달의 마지막 날월말 계산
YEAR/MONTH/DAY/HOUR(d)구성요소 추출
YEARWEEK(d) / WEEK(d, mode)주차⚠️ 주 시작 요일(mode)에 주의
  • ⚠️ WHERE DATE(created) = '2024-01-01' 처럼 컬럼에 함수를 씌우면 인덱스를 못 탑니다.WHERE created >= '2024-01-01' AND created < '2024-01-02' 로 바꾸세요(§5 날짜 BETWEEN 함정과 같은 맥락).

조건 함수 / 형변환

함수설명
IF(cond, a, b)조건이 참이면 a, 아니면 b (MySQL 전용)
IFNULL(a, b)a 가 NULL 이면 b (§6 참고)
NULLIF(a, b)a = b 이면 NULL, 아니면 a
COALESCE(a, b, ...)첫 번째 NULL 아닌 값 (표준)
CASE WHEN ... THEN ... ELSE ... END다분기 조건. 집계와 결합해 조건부 집계로 자주 씀
CAST(x AS type)표준 형변환. CAST('123' AS SIGNED), AS DECIMAL(10,2), AS DATE
CONVERT(x, type)CAST 의 MySQL 문법. CONVERT(x USING utf8mb4) 로 문자셋 변환도
-- CASE 로 조건부 집계 (취소 제외 매출을 그룹 손실 없이)
SELECT customer_id,
       SUM(CASE WHEN status <> 'CANCELLED' THEN amount ELSE 0 END) AS net_sales
FROM orders
GROUP BY customer_id;