SQL은 문법을 외우는 것이 전부가 아니다. 오히려 실무에서 자주 마주치는 요구사항에 맞게 적절한 쿼리 구조를 떠올리고 재활용할 수 있는 감각이 더 중요하다. 이번 글에서는 복잡한 비즈니스 로직을 구성하지 않더라도, 대부분의 프로젝트에서 반복적으로 활용되는 쿼리 패턴들을 정리한다.
1. 최신 데이터 가져오기
1-1. 최근 작성된 게시글 1건
가장 많이 쓰이는 패턴 중 하나는, 어떤 조건에 해당하는 가장 최근 데이터를 가져오는 것이다.
SELECT *
FROM posts
WHERE category_id = 3
ORDER BY created_at DESC
LIMIT 1;
이 패턴은 다음과 같이 사용된다
- 최근 주문 내역
- 최근 접속 기록
- 최신 알림 or 최근 공지사항
실무에서는 각 사용자의 최신 상태를 확인하는 용도로 거의 매일 쓰인다.
1-2. GROUP BY와 함께 쓰는 최신 데이터
조금 더 복잡하게, 사용자별 가장 최근 주문을 뽑고 싶을 경우, 서브쿼리 또는 윈도우 함수가 필요하다.
SELECT o.*
FROM orders o
INNER JOIN (
SELECT user_id, MAX(created_at) AS last_order
FROM orders
GROUP BY user_id
) recent ON o.user_id = recent.user_id AND o.created_at = recent.last_order;
이 구조는 단순하지만, 실무에서는 개인별 활동 이력 중 가장 마지막 상태를 확인하는 데 유용하다.
2. 집계 + 조건 조합
2-1. 카테고리별 게시글 수
데이터를 단순히 불러오는 것이 아니라 카테고리별로 몇 건인지 세어보는 구조는 다음과 같다.
SELECT category_id, COUNT(*) AS cnt
FROM posts
GROUP BY category_id;
이는 거의 모든 대시보드 통계에서 기본이 되며, 사용자 수, 상품 수, 결제 수, 방문 수 등 단위 기준으로 묶고, 카운트하는 구조는 반복해서 쓰인다.
2-2. 조건 포함한 통계
SELECT status, COUNT(*) AS cnt
FROM orders
WHERE created_at >= CURDATE() - INTERVAL 30 DAY
GROUP BY status;
이 패턴은 특정 기간의 데이터만 집계하고 싶을 때 사용한다. 실무에서는 항상 시간 조건이 포함되기 때문에, 이 구조는 거의 모든 보고서에 포함된다.
3. 날짜 필터링과 범위 처리
3-1. 하루 단위 필터
WHERE created_at BETWEEN '2024-01-01 00:00:00' AND '2024-01-01 23:59:59'
단순하지만 정확한 하루 필터를 구성하려면 BETWEEN 구문을 위와 같이 명시해야 한다. DATE(created_at) = '2024-01-01'처럼 함수로 감싸면 인덱스를 사용할 수 없으므로 비효율적이다.
3-2. 주/월/분기 필터
실무에서는 다음과 같은 구조로 자주 변형된다:
- 이번 주: YEARWEEK(created_at) = YEARWEEK(CURDATE())
- 이번 달: DATE_FORMAT(created_at, '%Y-%m') = DATE_FORMAT(CURDATE(), '%Y-%m')
- 지난 분기: 직접 시작일/종료일 계산 후 BETWEEN
날짜 필터는 모든 통계 시스템에서 기본 구조가 되므로 반드시 숙지해야 한다.
4. 순위와 랭킹 구하기
4-1. 사용자별 랭킹
SELECT name, point,
RANK() OVER (ORDER BY point DESC) AS ranking
FROM users;
실무에서는 다음과 같은 랭킹이 필요할 수 있다:
- 포인트 랭킹
- 판매 랭킹
- 조회수 기반 인기 게시글 랭킹
이때 RANK(), DENSE_RANK(), ROW_NUMBER() 등 윈도우 함수는 핵심 도구가 된다.
4-2. 사용자 기준 내에서 순서 지정
SELECT user_id, created_at,
ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) AS rn
FROM logins;
이 구조는 사용자별로 최근 3건의 로그인 이력만 보여줄 때 자주 사용된다.
5. 조건 기반 존재 여부 확인
5-1. EXISTS vs IN 구조
SELECT *
FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.id
);
EXISTS는 다른 테이블에서 해당 조건을 만족하는 데이터가 존재하는지 확인할 때 사용된다. 특히 데이터가 많은 테이블에선 IN보다 EXISTS가 성능상 더 유리하다.
5-2. 조건 충족 유무를 컬럼으로 변환
SELECT name,
EXISTS (
SELECT 1 FROM orders WHERE user_id = u.id AND status = 'PENDING'
) AS has_pending
FROM users u;
이 구조는 단순 필터링이 아니라, 존재 여부를 플래그 값으로 표현해야 할 때 자주 사용된다.
예: 배송 대기중 여부, 신규 알림 존재 여부 등
6. 실무 패턴 적용 시 주의사항
6-1. INDEX 고려
패턴을 반복 사용하더라도, WHERE, JOIN, ORDER BY에 등장하는 컬럼이 인덱스를 타고 있는지 반드시 확인해야 한다.
특히 기간 필터링과 조인 조건은 대부분 성능의 핵심이다.
6-2. LIMIT과 OFFSET의 조합
실무에서는 항상 화면에 일부만 보여주므로 LIMIT을 쓰게 되는데, 데이터가 많아질수록 OFFSET을 활용한 페이지 이동은 느려지므로, 가능하면 커서 기반 페이징으로 전환하는 게 좋다.
6-3. 중첩 쿼리보단 WITH절 사용 권장
서브쿼리가 많아지면 SQL 가독성이 낮아지기 때문에, MySQL 8.0 이상에서는 WITH절(공통 테이블 식)을 사용해 쿼리를 모듈화하고 디버깅하기 쉽게 구성하는 것이 좋다.
7. 다건 INSERT 및 UPSERT 패턴
7-1. 여러 행을 한 번에 INSERT
실무에서는 사용자 업로드나 다건 입력 처리 등으로 인해 여러 건의 데이터를 한 번에 INSERT해야 할 때가 많다. 단순 반복 INSERT는 DB 연결을 반복하게 되므로, 속도와 자원 측면에서 매우 비효율적이다.
INSERT INTO products (name, price)
VALUES
('상품1', 1000),
('상품2', 2000),
('상품3', 3000);
이 방식은 훨씬 빠르고 효율적이며, 트랜잭션으로 묶으면 중간 실패에 대한 처리도 가능하다.
특히 10건 이상을 처리할 때는 한 번에 묶는 방식이 기본이다.
7-2. 중복 시 갱신: UPSERT
중복된 키가 있을 때는 기존 데이터를 갱신해야 하는 경우가 많다. MySQL에서는 ON DUPLICATE KEY UPDATE, PostgreSQL에서는 ON CONFLICT 구문을 사용해 UPSERT 처리를 한다.
INSERT INTO users (id, name)
VALUES (1, '홍길동')
ON DUPLICATE KEY UPDATE name = '홍길동';
이 구조는 회원 정보 갱신, 포인트 누적, 카운트 업데이트 등 실시간 갱신이 필요한 업무에 자주 사용된다.
8. 상태값 처리 패턴과 ENUM 설계
8-1. 숫자 상태값으로 관리
실무에서는 게시글 상태, 주문 상태, 회원 상태 등 상태값이 있는 컬럼을 반드시 정수형으로 설계하는 것이 좋다. 문자열보다 비교 연산과 정렬이 빠르고, 정수형 인덱스는 성능적으로 유리하기 때문이다.
예시)
- 0: 작성 중
- 1: 검토 중
- 2: 승인됨
- 3: 반려됨
- 4: 삭제됨
이런 방식은 코드만 보면 의미를 알 수 없기 때문에, 상태값은 별도의 상태 테이블이나 ENUM 매핑으로 관리해야 한다.
8-2. ENUM의 장단점
ENUM은 제한된 값만 허용하기 때문에 데이터 오입력을 막을 수 있다. 하지만 단점도 분명하다
- 새로운 상태를 추가하려면 테이블 구조(DDL)를 바꿔야 한다
- ENUM 순서가 바뀌면 정렬 기준이 흔들릴 수 있다
- ENUM 값에 정해진 정수 값을 명시할 수 없어 확장성이 떨어진다
실무에서는 ENUM보다는 TINYINT + 상태 테이블을 따로 관리하는 구조가 더 안전하고 유연하다.
9. 조건별 통계 조합 쿼리
9-1. 단일 쿼리로 다중 조건 통계
통계를 구할 때 가장 많이 쓰이는 패턴 중 하나는, 하나의 SELECT로 서로 다른 조건에 대한 통계를 동시에 구하는 구조다.
SELECT
COUNT(*) AS total,
COUNT(IF(status = 'DONE', 1, NULL)) AS done_count,
COUNT(IF(status = 'PENDING', 1, NULL)) AS pending_count
FROM orders;
이 구조는 관리자 대시보드에서 전체 수와 함께 조건별 통계를 한 번에 보여주어야 할 때 매우 유용하다.
9-2. 시간 조건 + 상태 조건 조합
SELECT
COUNT(*) AS last_week_total,
COUNT(IF(status = 'CANCELLED', 1, NULL)) AS cancelled
FROM orders
WHERE created_at BETWEEN CURDATE() - INTERVAL 7 DAY AND CURDATE();
시간과 상태 조건을 함께 넣는 이 구조는 주간, 월간 단위 통계에서 업무 현황을 빠르게 파악하는 데 필수적이다. 대부분의 관리자 페이지가 이 형태로 구성된다.
10. 실시간 분석을 위한 VIEW 또는 서브쿼리 설계
10-1. VIEW로 복잡 쿼리 단순화
실무에서 SELECT문이 너무 복잡해질 경우, 이를 VIEW로 추상화하여 마치 하나의 테이블처럼 사용할 수 있다.
CREATE VIEW recent_orders AS
SELECT user_id, MAX(order_date) AS last_order
FROM orders
GROUP BY user_id;
이렇게 만든 VIEW는 향후 SELECT 쿼리에서 JOIN하거나 WHERE 조건으로 쉽게 사용 가능하다.
SELECT u.name, r.last_order
FROM users u
JOIN recent_orders r ON u.id = r.user_id;
10-2. 뷰 대신 WITH 절 활용
MySQL 8.0 이상에서는 WITH절(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;
실시간으로 복잡한 데이터를 처리해야 할 경우, VIEW를 활용한 추상화 또는 WITH절을 통한 논리적 분리 방식이 가독성과 유지보수 측면에서 큰 장점을 가진다.
'컴퓨터공학' 카테고리의 다른 글
윈도우 함수 심화 정리: 실무에서 꼭 써먹는 핵심 기술 (0) | 2025.05.16 |
---|---|
실무에 강한 SQL 작성법: 비즈니스 로직을 쿼리로 설계하는 기술 (0) | 2025.05.16 |
SQL 트랜잭션과 롤백 처리 완전 정복: 데이터 신뢰성을 지키는 핵심 기술 (0) | 2025.05.15 |
SQL 인덱스와 성능 최적화 기본기: 데이터베이스를 빠르게 만드는 기술 (0) | 2025.05.15 |
SQL 데이터 타입과 제약 조건 완전 정복: 테이블 설계의 기본기 (0) | 2025.05.14 |