실무에 강한 SQL 작성법: 비즈니스 로직을 쿼리로 설계하는 기술
1. 실무 SQL의 본질: 단순한 SELECT가 아니다
1-1. “조회”만 하는 쿼리는 실무에서 드물다
SQL을 학습할 땐 SELECT name FROM users 같은 단순 조회 쿼리로 시작한다. 하지만 실무에서 실제로 자주 마주치는 쿼리는 대부분 다음과 같다:
- 조건이 복잡하게 얽힌 검색 쿼리
- 통계를 위한 다단계 집계 쿼리
- 기간별 변화량, 사용자별 구간 비교
- 누적값이나 순위 기반 정렬
- 동적 정렬, 페이징, 검색 필터가 동시에 적용되는 목록 조회
이처럼 실무에서 SQL은 단순 조회 도구가 아니라, 복잡한 비즈니스 로직을 명확하게 표현해내는 언어로서 사용된다.
2. 쿼리 분할 vs 결합: 언제 하나로 묶고, 언제 나눠야 할까?
2-1. 단일 쿼리로 구성할 수 있는 기준
모든 로직을 하나의 SQL로 처리할 수 있다면 가장 좋겠지만, 실무에서는 쿼리를 나눠서 처리하는 경우도 많다. 다음 조건이 충족되면 단일 쿼리로 구성하는 것이 좋다.
- 하나의 결과만 필요하다
- 동일한 기준(WHERE, JOIN, 기간 등)으로 필터링할 수 있다
- 동일한 인덱스를 사용할 수 있는 범위다
- 중간 계산값을 따로 저장하지 않아도 된다
예를 들어, "사용자별 이번 달 주문 건수"는 다음과 같이 단일 쿼리로 처리할 수 있다.
SELECT user_id, COUNT(*) AS order_count
FROM orders
WHERE order_date BETWEEN '2024-05-01' AND '2024-05-31'
GROUP BY user_id;
2-2. 쿼리를 나눠야 할 때
다음 조건에 해당하면 쿼리를 여러 번 나누어 처리하는 것이 더 효율적이다:
- 중간 집계값을 여러 번 재활용해야 한다
- 서브쿼리가 중첩되어 가독성이 떨어진다
- 성능 최적화를 위해 중간 결과를 캐싱해야 한다
- 하나의 쿼리로는 모든 조건을 표현하기 어렵다
실무에서는 아래처럼 조회 쿼리 → 가공 쿼리 → 출력 쿼리를 나누는 방식으로 쿼리를 구성하는 경우가 많다.
3. 데이터 흐름을 따라 쿼리를 설계하는 습관
3-1. 데이터 흐름 먼저 생각하자
SQL은 문법보다도 데이터가 어떻게 흘러가는지를 먼저 설계하는 것이 더 중요하다. 즉, 원하는 결과를 얻기 위해 어떤 데이터를 어떤 순서로 필터링하고, 가공하고, 출력할지를 먼저 그려야 한다.
예: “지난달 기준으로 상태별 결제 건수를 구하고, 그 중 취소 비율이 높은 사용자만 찾고 싶다.”
→ 흐름을 나눠보면 다음과 같다:
- 조건에 맞는 결제 데이터 필터
- 사용자별 결제 통계 (성공, 실패, 취소 건수)
- 취소 비율이 일정 이상인 사용자 필터링
- 사용자 정보 JOIN
이 과정을 먼저 머릿속에 정리한 뒤, 이를 SQL로 옮겨가는 방식이 실무에서 유지보수하기 좋은 쿼리 작성법이다.
3-2. 쿼리를 설계할 때의 질문들
- 이 데이터는 어떤 기준으로 그룹화해야 하는가?
- 지금 필요한 건 행(Row) 기준인가, 컬럼(Column) 기준인가?
- 이 쿼리는 반복해서 재사용될 수 있는가?
- 지금 이 연산은 DB가 하는 게 좋을까, 앱 로직에서 처리하는 게 좋을까?
이런 질문을 먼저 던지고 설계한 SQL은 문제 발생 시에도 쉽게 수정할 수 있다.
4. 실무 쿼리 설계 시 자주 쓰이는 구조
4-1. WITH 절을 활용한 가독성 향상
복잡한 쿼리는 공통 부분을 WITH절로 추출하여 쿼리 가독성과 재사용성을 높일 수 있다. 실제로 MySQL 8.0 이상에서는 WITH (CTE, Common Table Expression)를 적극적으로 활용한다.
WITH recent_orders AS (
SELECT user_id, MAX(order_date) AS last_order
FROM orders
GROUP BY user_id
)
SELECT u.name, r.last_order
FROM users u
JOIN recent_orders r ON u.id = r.user_id;
이 구조는 실무에서 가장 많이 사용되는 패턴 중 하나이며, 모듈화된 쿼리 작성 습관을 키우는 데 효과적이다.
4-2. 윈도우 함수의 기본 적용
RANK(), ROW_NUMBER(), SUM() OVER(...) 등 윈도우 함수는 “모든 결과를 테이블 단위로 보고 싶은데, 그 안에서 구간별 통계를 내고 싶을 때” 매우 유용하다. 실무에서는 예를 들어 다음처럼 쓰인다.
- 사용자별 최근 활동일
- 카테고리별 판매 누적
- 날짜별 로그인 수 누적
윈도우 함수는 데이터 흐름을 자르지 않고 연속적인 분석을 가능하게 해주는 도구로, 실무에서는 필수다.
5. 복잡한 쿼리는 나누고 단순하게 구성하자
5-1. 쿼리가 길어진다고 무조건 좋은 건 아니다
실무에서 흔히 저지르는 실수 중 하나는 “모든 것을 한 줄의 SQL로 끝내려는 습관”이다. JOIN, WHERE, GROUP BY, HAVING, 윈도우 함수, 서브쿼리, ORDER BY까지 한 문장에 담기 시작하면 결과는 나오더라도 가독성은 무너지고 유지보수도 어렵다. 복잡한 쿼리는 가급적 단계별로 의미 있는 파트를 나누어 작성하는 것이 좋다.
5-2. 쿼리 나누기의 대표 구조
- 기초 필터링 → 필요한 범위만 추출
- 그룹화/집계 → 통계 수치 정리
- 가공/정렬 → UI에 맞게 최종 처리
이런 구조로 쿼리를 분리하면, 각 단계별 성능도 점검하기 쉬워지고, 오류 발생 시 디버깅도 훨씬 쉬워진다.
6. 중첩된 SELECT 구조를 제거하자
6-1. 중첩 SELECT가 왜 문제인가?
SELECT ... WHERE IN (SELECT ...)처럼 중첩 구조가 너무 많아지면 실행 순서가 명확하지 않고, 실행 계획도 비효율적으로 구성될 수 있다. MySQL이나 PostgreSQL의 Optimizer가 어느 정도 해결해주긴 하지만, 성능 튜닝 측면에서는 명확하게 JOIN 기반으로 구성하는 게 유리하다.
6-2. 서브쿼리 대신 JOIN 또는 CTE 사용
예전에는 서브쿼리를 많이 썼지만, 요즘은 다음과 같은 방식으로 리팩토링하는 것이 실무에서는 기본이다.
- 서브쿼리 → WITH절 (공통 테이블 식)
- 조건 필터링 → JOIN + WHERE로 통합
- 집계 서브쿼리 → 인라인 뷰 또는 TEMP VIEW로 분리
이렇게 구성하면 쿼리는 더 길어지지만, 명확하게 단계별로 역할이 구분된 SQL을 만들 수 있다.
7. 실시간 처리 쿼리 vs 비실시간 쿼리 구분
7-1. 즉시 처리와 배치 처리를 나눠야 하는 이유
모든 데이터를 실시간으로 처리하면 좋을 것 같지만, 실제로는 비효율적인 경우가 많다. 예를 들어 사용자의 접속 로그 전체를 매번 조인해서 조회하거나, 매일의 누적 주문 수를 실시간으로 계산하면 시스템에 과부하가 생길 수 있다. 이럴 때는 다음 기준으로 분리해야 한다.
- 사용자가 직접 보는 화면 → 실시간 쿼리
- 관리자, 보고서, 대시보드 → 비실시간 집계 테이블 또는 뷰
7-2. 실시간 쿼리는 가능한 가볍게
실시간으로 호출되는 쿼리는 속도가 가장 중요하다. JOIN 횟수 최소화, 정렬 제거, LIMIT 사용 등으로 성능을 유지해야 한다. 반면에 대시보드나 백오피스용 쿼리는 복잡해도 상관없지만 속도가 느리면 안 되는 유형이다. 이런 쿼리는 전처리 또는 배치 집계 테이블을 따로 만들어두는 것이 좋다.
8. 데이터 양에 따라 쿼리 구조는 달라진다
8-1. 소량일 때는 자유롭게, 대량일 때는 구조적으로
테스트 DB나 소규모 서비스에서는 어떤 쿼리를 써도 금방 결과가 나온다. 하지만 실무에서는 테이블 하나에 수백만~수억 건의 데이터가 쌓인다. 이때 단순한 ORDER BY, LIKE, JOIN이 심각한 속도 저하를 불러오게 된다.
8-2. 쿼리 구조를 데이터 양에 따라 바꾸는 기준
데이터 규모 | 권장 쿼리 방식 |
1~1000건 | 서브쿼리, LIKE 등 자유롭게 |
1만~10만건 | 인덱스 기반 WHERE, JOIN 최소화 |
100만건 이상 | 서브쿼리 제거, 커서 기반 처리, 정렬 제거, 캐싱 고려 |
실무에서는 항상 데이터 양을 고려하고, 같은 쿼리 구조를 모든 테이블에 똑같이 적용하지 않는 유연한 사고가 필요하다.
9. 유지보수 가능한 SQL을 위한 원칙
마지막으로, 실무에 강한 SQL을 쓴다는 건 나만 이해하는 쿼리를 쓰지 말고, 모두가 이해할 수 있도록 쓰는 것이다.
다음 원칙을 참고하자:
- 주석을 적극적으로 활용해 쿼리의 목적을 명시
- SELECT * 대신 필요한 컬럼만 명시
- 서브쿼리는 가독성을 기준으로 정리
- 임시 테이블이나 CTE 활용을 적극 고려
- 가능하면 프론트에서 복잡한 가공을 미루지 말고, SQL에서 해결