깔끔한 쿼리를 위한 SQL 포맷팅 모범 사례
데이터베이스 전문가라면 누구나 한 번쯤 키워드를 벽에 던진 것 같은 쿼리를 물려받은 경험이 있을 겁니다. 적절한 SQL 포맷팅은 읽을 수 없는 텍스트 덩어리를 구조화되고 한눈에 파악할 수 있는 코드로 변환하여, 팀의 누구나 몇 초 만에 이해할 수 있게 만들어 줍니다. 간단한 임시 쿼리를 작성하든, 수년간 프로덕션에서 사용될 저장 프로시저를 구축하든, SQL을 어떻게 포맷팅하느냐는 매우 중요합니다.
지저분한 쿼리를 SQL Formatter에 붙여넣으면 깔끔하고 적절하게 들여쓰기된 결과를 즉시 얻을 수 있습니다. 하지만 왜 특정 포맷팅 방식이 더 효과적인지 이해하면, 전반적으로 더 뛰어난 SQL 개발자가 될 수 있습니다.
SQL 포맷팅이 중요한 이유
잘못된 SQL 포맷팅은 전체 워크플로우에 연쇄적인 문제를 일으킵니다:
- 디버깅에 세 배의 시간이 걸립니다 — 쿼리가 잘못된 결과를 반환할 때 로직을 시각적으로 추적해야 합니다. 지저분한 포맷팅은 논리적 오류를 숨깁니다.
- 코드 리뷰가 정체됩니다 — 리뷰어가 로직을 평가하는 대신 구조를 해독하는 데 시간을 씁니다.
- 머지 충돌이 증가합니다 — 일관성 없는 포맷팅은 팀원마다 다르게 재포맷팅하게 되어 불필요한 git diff를 만들어냅니다.
- 온보딩이 어려워집니다 — 새로운 팀원이 엉킨 쿼리 속에 묻힌 비즈니스 로직을 이해하는 데 애를 먹습니다.
- 프로덕션 장애가 확대됩니다 — 긴급 상황에서 줄바꿈도 없는 200줄짜리 쿼리를 풀어헤치고 싶은 사람은 아무도 없습니다.
핵심은 이것입니다: SQL 포맷팅은 미관의 문제가 아닙니다. 인지 부하를 줄이는 것입니다. 잘 포맷팅된 쿼리는 컬럼 이름을 읽기도 전에 의도를 전달합니다.
키워드 대소문자: 규칙을 정하고 일관되게 지키기
SQL 포맷팅에서 가장 논쟁이 많은 주제는 키워드 대소문자입니다. 일반적으로 세 가지 접근 방식이 있습니다:
대문자 키워드 (가장 일반적):
SELECT
u.first_name,
u.last_name,
o.order_total
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.order_date >= '2025-01-01'
ORDER BY o.order_total DESC;
소문자 키워드:
select
u.first_name,
u.last_name,
o.order_total
from users u
inner join orders o on u.id = o.user_id
where o.order_date >= '2025-01-01'
order by o.order_total desc;
타이틀 케이스 키워드:
Select
u.first_name,
u.last_name,
o.order_total
From users u
Inner Join orders o On u.id = o.user_id
Where o.order_date >= '2025-01-01'
Order By o.order_total Desc;
대문자 키워드는 여전히 업계 표준이며, 그럴 만한 이유가 있습니다. SQL 키워드와 테이블/컬럼 이름 사이에 즉각적인 시각적 분리를 만들어 줍니다. 왼쪽 여백을 훑어보기만 해도 쿼리 구조를 바로 파악할 수 있습니다: SELECT, FROM, WHERE, ORDER BY.
참고로: 어떤 규칙을 선택하든 자동화 도구로 강제하세요. SQL Formatter는 키워드 대소문자를 자동으로 처리하므로, 팀 전체가 신경 쓰지 않아도 일관성을 유지할 수 있습니다.
주요 절을 위한 들여쓰기 전략
좋은 들여쓰기는 가독성 높은 SQL의 기본입니다. 각 주요 절은 왼쪽 여백에서 시작하고, 내용은 한 단계 들여쓰기해야 합니다.
SELECT 절
각 컬럼을 별도의 줄에 배치하세요. 이렇게 하면 컬럼을 추가, 제거, 주석 처리하기가 매우 쉬워집니다:
SELECT
e.employee_id,
e.first_name,
e.last_name,
e.department_id,
d.department_name,
e.hire_date,
e.salary
FROM employees e
INNER JOIN departments d ON e.department_id = d.id;
이런 흔한 안티패턴은 피하세요:
-- 한눈에 파악하기 어렵고, 수정하기도 어려움
SELECT e.employee_id, e.first_name, e.last_name, e.department_id, d.department_name, e.hire_date, e.salary
FROM employees e INNER JOIN departments d ON e.department_id = d.id;
FROM과 JOIN 절
각 JOIN은 별도의 줄에 배치합니다. ON 조건은 해당 JOIN과 함께 두되, 여러 조건에 걸치는 경우 추가로 들여쓰기합니다:
SELECT
o.order_id,
c.customer_name,
p.product_name,
oi.quantity,
oi.unit_price
FROM orders o
INNER JOIN customers c
ON o.customer_id = c.id
INNER JOIN order_items oi
ON o.id = oi.order_id
LEFT JOIN products p
ON oi.product_id = p.id
WHERE o.status = 'completed'
AND o.order_date >= '2025-01-01';
WHERE 절
각 조건을 별도의 줄에 배치하세요. 불리언 연산자(AND/OR)를 각 새 줄의 시작 부분에 두면, 디버깅 중에 개별 조건을 주석 처리하기가 아주 간편해집니다:
WHERE o.status = 'completed'
AND o.order_date >= '2025-01-01'
AND o.order_date < '2026-01-01'
AND c.region IN ('US', 'CA', 'UK')
-- AND o.total > 100 (일시적으로 비활성화)
좋은 소식은: 이 패턴을 내면화하면 논리적 오류를 훨씬 빠르게 발견할 수 있다는 것입니다. 각 조건이 시각적으로 분리되어 있어, 누락되거나 잘못된 필터가 즉시 눈에 띕니다.
복잡한 JOIN 포맷팅
실무 쿼리에서 단순한 단일 컬럼 JOIN은 드뭅니다. 다중 조건 JOIN을 깔끔하게 처리하는 방법은 다음과 같습니다:
SELECT
s.sale_id,
s.sale_date,
p.product_name,
w.warehouse_name,
i.quantity_on_hand
FROM sales s
INNER JOIN products p
ON s.product_id = p.id
LEFT JOIN inventory i
ON p.id = i.product_id
AND i.warehouse_id = s.warehouse_id
AND i.snapshot_date = s.sale_date
LEFT JOIN warehouses w
ON i.warehouse_id = w.id
WHERE s.sale_date >= '2025-06-01';
추가 JOIN 조건이 첫 번째 ON 조건과 같은 수준으로 들여쓰기되어 있는 것에 주목하세요. 이렇게 하면 세 조건 모두 같은 JOIN에 속한다는 것이 명확해지며, 실수로 잘못 배치된 WHERE 절과 혼동할 일이 없습니다.
셀프 조인의 경우, 의미 없는 단일 문자 대신 의미 있는 별칭을 사용하세요:
SELECT
mgr.first_name AS manager_name,
emp.first_name AS employee_name,
emp.hire_date
FROM employees emp
INNER JOIN employees mgr
ON emp.manager_id = mgr.employee_id
WHERE mgr.department_id = 10;
CTE 포맷팅 (WITH 절)
공통 테이블 표현식(CTE)은 복잡한 쿼리의 가독성을 크게 좌우할 수 있으므로, 특별한 포맷팅 주의가 필요합니다. 각 CTE는 독립적으로 포맷팅된 블록으로 취급해야 합니다:
WITH monthly_revenue AS (
SELECT
DATE_TRUNC('month', order_date) AS revenue_month,
SUM(order_total) AS total_revenue,
COUNT(DISTINCT customer_id) AS unique_customers
FROM orders
WHERE order_date >= '2025-01-01'
GROUP BY DATE_TRUNC('month', order_date)
),
customer_segments AS (
SELECT
customer_id,
SUM(order_total) AS lifetime_value,
CASE
WHEN SUM(order_total) >= 10000 THEN 'platinum'
WHEN SUM(order_total) >= 5000 THEN 'gold'
WHEN SUM(order_total) >= 1000 THEN 'silver'
ELSE 'bronze'
END AS segment
FROM orders
GROUP BY customer_id
)
SELECT
mr.revenue_month,
mr.total_revenue,
mr.unique_customers,
COUNT(CASE WHEN cs.segment = 'platinum' THEN 1 END) AS platinum_count,
COUNT(CASE WHEN cs.segment = 'gold' THEN 1 END) AS gold_count
FROM monthly_revenue mr
CROSS JOIN customer_segments cs
GROUP BY mr.revenue_month, mr.total_revenue, mr.unique_customers
ORDER BY mr.revenue_month;
핵심 CTE 포맷팅 규칙:
- 각 CTE 사이에 빈 줄을 넣으세요 — 닫는 괄호와 쉼표 뒤에 빈 줄을 추가합니다
- 각 CTE의 본문을 들여쓰기하세요 — 일반 쿼리와 동일하게 처리합니다
- 설명적인 CTE 이름을 사용하세요 —
monthly_revenue가cte1보다 항상 낫습니다 - 최종 SELECT는 WITH 키워드와 같은 들여쓰기 수준으로 유지하세요
서브쿼리 포맷팅
서브쿼리는 외부 쿼리보다 한 단계 더 들여쓰기해야 합니다. 괄호를 시각적 경계로 활용하세요:
SELECT
d.department_name,
d.budget,
dept_stats.avg_salary,
dept_stats.employee_count
FROM departments d
INNER JOIN (
SELECT
department_id,
AVG(salary) AS avg_salary,
COUNT(*) AS employee_count
FROM employees
WHERE status = 'active'
GROUP BY department_id
HAVING COUNT(*) >= 5
) dept_stats
ON d.id = dept_stats.department_id
WHERE d.budget > 100000
ORDER BY dept_stats.avg_salary DESC;
중첩된 서브쿼리가 3~4단계까지 깊어진다면, 이는 보통 CTE로 리팩토링해야 한다는 신호입니다. CTE는 중첩을 평탄화하고 각 논리적 단계에 읽기 쉬운 이름을 부여합니다. SQL Formatter를 사용하면 리팩토링이 필요한 깊은 중첩 구조를 쉽게 식별할 수 있습니다.
CASE 문 포맷팅
CASE 문은 SELECT 목록, WHERE 절, ORDER BY, 심지어 JOIN 조건에서도 곳곳에 등장합니다. 일관된 들여쓰기로 가독성을 유지하세요:
SELECT
order_id,
order_total,
CASE
WHEN order_total >= 1000 THEN 'high-value'
WHEN order_total >= 100 THEN 'medium-value'
ELSE 'low-value'
END AS order_tier,
CASE status
WHEN 'shipped' THEN 'In Transit'
WHEN 'delivered' THEN 'Complete'
WHEN 'returned' THEN 'Refund Pending'
ELSE 'Processing'
END AS display_status
FROM orders;
WHEN과 ELSE 키워드는 서로 정렬하고, END 키워드는 CASE와 정렬합니다. 이렇게 하면 한눈에 파악하고 수정하기 쉬운 깔끔한 시각적 블록이 만들어집니다.
SQL 주석 작성 모범 사례
SQL 주석은 미래의 자신에게 주는 최고의 선물입니다. 전략적으로 사용하세요:
쿼리 목적을 위한 블록 주석:
/*
* 월별 매출 리포트
* 제품 카테고리별 매출 분석을 생성합니다
* 사용처: 재무 대시보드 (Tableau)
* 최종 수정일: 2025-06-15
*/
SELECT
pc.category_name,
SUM(oi.quantity * oi.unit_price) AS revenue
FROM order_items oi
INNER JOIN products p ON oi.product_id = p.id
INNER JOIN product_categories pc ON p.category_id = pc.id
GROUP BY pc.category_name;
명확하지 않은 로직을 위한 인라인 주석:
WHERE o.order_date >= '2025-01-01'
AND o.status != 'cancelled'
AND o.payment_verified = TRUE -- 결제 검토 대기 건 제외
AND c.account_type != 'internal' -- 직원 테스트 주문은 내부 계정 사용
긴 쿼리를 위한 섹션 주석:
-- === 매출 계산 ===
...
-- === 고객 필터링 ===
...
-- === 최종 집계 ===
뻔한 것에 대한 과도한 주석은 피하세요. SELECT 문 위에 -- 모든 컬럼 선택이라고 쓰는 것은 명확성이 아니라 잡음만 추가합니다.
SQL 방언별 포맷팅
SQL 방언마다 고유한 문법 특성이 있지만, 포맷팅 원칙은 일관되게 유지됩니다. 방언별 고려사항은 다음과 같습니다:
MySQL
SELECT
product_name,
price,
IFNULL(discount, 0) AS discount,
price - IFNULL(discount, 0) AS final_price
FROM products
WHERE category_id IN (1, 3, 5)
LIMIT 50 OFFSET 100;
PostgreSQL
SELECT
product_name,
price,
COALESCE(discount, 0) AS discount,
price - COALESCE(discount, 0) AS final_price
FROM products
WHERE category_id = ANY(ARRAY[1, 3, 5])
LIMIT 50 OFFSET 100;
SQL Server
SELECT TOP 50
product_name,
price,
ISNULL(discount, 0) AS discount,
price - ISNULL(discount, 0) AS final_price
FROM products
WHERE category_id IN (1, 3, 5)
ORDER BY price DESC
OFFSET 100 ROWS FETCH NEXT 50 ROWS ONLY;
방언에 관계없이 구조적 포맷팅은 동일합니다: 키워드는 별도의 줄에, 일관된 들여쓰기, 한 줄에 하나의 컬럼. alltools.one의 SQL Formatter는 모든 주요 방언을 지원하므로, 어떤 데이터베이스를 사용하든 일관된 결과를 얻을 수 있습니다.
자동 포맷팅 vs. 수동 포맷팅
수동 포맷팅은 짧은 쿼리에는 잘 작동합니다. 하지만 팀이 두 명 이상으로 성장하면 자동화된 적용이 필요합니다. 이유는 다음과 같습니다:
자동 포맷팅의 장점:
- 논쟁 제로 — 포맷터가 결정하고, 모두가 따릅니다
- 일관된 git diff — 공백만 변경된 커밋이 풀 리퀘스트를 어지럽히지 않습니다
- 속도 — 500줄짜리 저장 프로시저를 수동으로 재포맷팅하면 몇 분이 걸립니다. 도구는 밀리초 만에 처리합니다.
- 온보딩 — 새로운 팀원이 스타일 가이드를 외울 필요가 없습니다
수동 포맷팅이 더 나은 경우:
- INSERT 문이나 복잡한 CASE 블록에서 특정 컬럼의 정렬이 필요할 때
- 문서화용 쿼리의 의도적인 포맷팅을 보존해야 할 때
- 자동화 도구가 가독성을 해치는 엣지 케이스가 있을 때 (드물지만 발생합니다)
실용적인 접근법: 자동 포맷팅을 기본으로 사용하고, 중요한 몇몇 쿼리만 수동으로 미세 조정하세요. 먼저 SQL Formatter로 SQL을 처리한 다음, 필요한 경우 특정 부분을 조정하면 됩니다.
빠른 참고: SQL 포맷팅 체크리스트
코드베이스에 SQL을 커밋하기 전에 이 체크리스트를 점검하세요:
- 키워드의 대소문자가 일관적인가 (대문자 권장)
- 각 주요 절(SELECT, FROM, WHERE, GROUP BY, ORDER BY)이 새 줄에서 시작하는가
- SELECT의 컬럼이 한 줄에 하나씩인가
- JOIN이 각각 별도의 줄에 있고 ON 조건이 들여쓰기되어 있는가
- WHERE 조건이 한 줄에 하나씩이고 AND/OR가 줄 시작에 있는가
- CTE의 이름이 설명적이고 들여쓰기가 일관적인가
- CASE 문에서 WHEN/ELSE가 정렬되어 있는가
- 주석이 무엇을이 아니라 왜를 설명하는가
- 테이블 별칭이 의미 있는가 (복잡한 쿼리에서 단일 문자만 사용하지 않는가)
- 2단계 이상 깊은 서브쿼리가 CTE로 리팩토링되었는가
관련 리소스
더 넓은 코드 품질을 향상시키고 싶다면, 아래 가이드가 SQL 포맷팅과 잘 어울립니다:
- JSON 포맷팅 모범 사례 — JSON 데이터 구조에 대한 유사한 포맷팅 원칙
- 텍스트 Diff 비교 가이드 — SQL 파일 간 포맷팅 변경 사항을 검토할 때 유용합니다
- 정규 표현식 치트 시트 — LIKE 및 SIMILAR TO를 사용한 SQL 패턴 매칭 작성 시 유용합니다
깔끔한 SQL 포맷팅은 도입 비용이 거의 들지 않지만 매일 성과를 돌려주는 습관입니다. 미래의 여러분 — 그리고 여러분의 쿼리를 다루는 모든 팀원이 — 감사할 것입니다.
🛠️ 지금 바로 사용해 보세요: SQL Formatter — SQL 쿼리를 즉시 포맷팅하고 정리하세요. 100% 무료이며, 모든 처리는 브라우저에서 이루어집니다. 데이터가 업로드되지 않습니다.