정렬과 제한
데이터를 조회하면 결과의 순서를 지정하고, 가져올 행의 수를 제한해야 하는 경우가 대부분입니다. 게시판의 최신 글 10개, 가격이 높은 순서대로 5개 — 이것이 정렬과 페이지네이션입니다.
ORDER BY
ORDER BY는 SELECT 결과를 특정 컬럼 기준으로 정렬합니다. SQL 처리 순서에서 ORDER BY는 거의 마지막에 실행됩니다(SELECT 이후, LIMIT/FETCH 직전).
-- 오름차순 (기본값, ASC 생략 가능)
SELECT * FROM products ORDER BY price ASC;
SELECT * FROM products ORDER BY price; -- 동일
-- 내림차순
SELECT * FROM products ORDER BY price DESC;-- 카테고리 오름차순 → 같으면 가격 내림차순
SELECT * FROM products
ORDER BY category ASC, price DESC;category name price
────────── ────────── ──────
가전 에어컨 1500000
가전 선풍기 50000
의류 코트 200000
의류 티셔츠 30000
전자 노트북 1200000
전자 마우스 25000-- 별칭으로 정렬
SELECT name, price * 0.9 AS sale_price
FROM products
ORDER BY sale_price DESC;
-- 컬럼 순서 번호로 정렬 (SELECT 절의 n번째 컬럼)
SELECT name, price, stock
FROM products
ORDER BY 2 DESC; -- 2번째 컬럼(price)으로 내림차순
-- 주의: 유지보수성이 낮아 실무에서는 권장하지 않음
-- 표현식으로 정렬
SELECT name, price, stock
FROM products
ORDER BY price * stock DESC; -- 재고 가치가 높은 순
-- CASE를 사용한 사용자 정의 정렬
SELECT name, status
FROM orders
ORDER BY
CASE status
WHEN 'PENDING' THEN 1
WHEN 'PROCESSING' THEN 2
WHEN 'SHIPPED' THEN 3
WHEN 'DELIVERED' THEN 4
WHEN 'CANCELLED' THEN 5
END;ORDER BY와 성능
ORDER BY는 데이터를 메모리(또는 디스크)에서 정렬하는 작업이므로 비용이 발생합니다 특히 대량 데이터에서는 주의가 필요합니다.
인덱스가 있는 컬럼으로 정렬
→ 인덱스가 이미 정렬되어 있으므로 추가 정렬 불필요
→ 매우 빠름 (Index Scan)
인덱스가 없는 컬럼으로 정렬
→ 전체 결과를 메모리에 올려 정렬 (Sort Operation)
→ sort_buffer_size(MySQL), SORT_AREA_SIZE(Oracle) 이내면 메모리 정렬
→ 초과하면 디스크 임시 파일 사용 (큰 성능 저하)
실행 계획 확인
EXPLAIN SELECT * FROM products ORDER BY price;
→ "Using filesort" 표시 = 추가 정렬 수행 (MySQL)
→ "Sort" 노드 표시 (PostgreSQL)NULL 정렬
NULL 값이 포함된 컬럼을 정렬하면, DBMS마다 기본 동작이 다릅니다.
| DBMS | NULL 위치 (ASC) | NULL 위치 (DESC) |
|---|---|---|
| Oracle | 마지막 | 처음 |
| MySQL | 처음 | 마지막 |
| PostgreSQL | 마지막 | 처음 |
| SQL Server | 처음 | 마지막 |
-- Oracle / PostgreSQL: NULLS FIRST / NULLS LAST 지원
SELECT * FROM products ORDER BY category NULLS FIRST;
SELECT * FROM products ORDER BY category NULLS LAST;
-- MySQL: NULLS FIRST/LAST 미지원, CASE로 우회
SELECT * FROM products
ORDER BY
CASE WHEN category IS NULL THEN 1 ELSE 0 END, -- NULL을 뒤로
category ASC;
-- IS NULL을 사용한 NULL 후순위 처리
SELECT * FROM products
ORDER BY category IS NULL, category ASC; -- MySQL 전용category name
────────── ──────────
가전 에어컨
의류 코트
전자 노트북
NULL 아이템X ← Oracle ASC에서 NULL은 마지막결과 제한
결과에서 특정 개수의 행만 가져오는 문법이 DBMS마다 다릅니다.
Oracle
-- 처음 10행
SELECT * FROM products
ORDER BY price DESC
FETCH FIRST 10 ROWS ONLY;
-- 11~20행 (페이지 2)
SELECT * FROM products
ORDER BY price DESC
OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY;
-- 상위 10% 행
SELECT * FROM products
ORDER BY price DESC
FETCH FIRST 10 PERCENT ROWS ONLY;
-- 동점 포함 (WITH TIES)
SELECT * FROM products
ORDER BY price DESC
FETCH FIRST 3 ROWS WITH TIES;
-- price가 같은 행이 있으면 3개 이상 반환될 수 있음-- ROWNUM은 WHERE보다 먼저 부여되므로 서브쿼리 필요
SELECT * FROM (
SELECT * FROM products ORDER BY price DESC
) WHERE ROWNUM <= 10;
-- 페이지네이션 (11~20행)
SELECT * FROM (
SELECT a.*, ROWNUM rn FROM (
SELECT * FROM products ORDER BY price DESC
) a WHERE ROWNUM <= 20
) WHERE rn >= 11;
-- ROW_NUMBER 방식 (더 깔끔)
SELECT * FROM (
SELECT p.*, ROW_NUMBER() OVER (ORDER BY price DESC) AS rn
FROM products p
) WHERE rn BETWEEN 11 AND 20;MySQL
-- 처음 10행
SELECT * FROM products
ORDER BY price DESC
LIMIT 10;
-- 11~20행 (페이지 2)
SELECT * FROM products
ORDER BY price DESC
LIMIT 10 OFFSET 10;
-- 또는 (LIMIT offset, count 형식)
SELECT * FROM products
ORDER BY price DESC
LIMIT 10, 10;
-- 첫 번째 숫자가 OFFSET, 두 번째가 COUNTPostgreSQL
-- LIMIT 방식 (MySQL과 동일)
SELECT * FROM products
ORDER BY price DESC
LIMIT 10 OFFSET 10;
-- SQL 표준 (FETCH)
SELECT * FROM products
ORDER BY price DESC
OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY;SQL Server
-- TOP (정렬 없이도 사용 가능하지만 비권장)
SELECT TOP 10 * FROM products
ORDER BY price DESC;
-- TOP WITH TIES (동점 포함)
SELECT TOP 3 WITH TIES * FROM products
ORDER BY price DESC;
-- OFFSET FETCH (SQL Server 2012+)
SELECT * FROM products
ORDER BY price DESC
OFFSET 10 ROWS FETCH NEXT 10 ROWS ONLY;DBMS별 결과 제한 문법 비교
┌────────────┬───────────────────────────────────────┐
│ DBMS │ 처음 10행 조회 문법 │
├────────────┼───────────────────────────────────────┤
│ Oracle 12c+│ FETCH FIRST 10 ROWS ONLY │
│ Oracle 11g │ WHERE ROWNUM <= 10 (서브쿼리 필요) │
│ MySQL │ LIMIT 10 │
│ PostgreSQL │ LIMIT 10 또는 FETCH FIRST 10 ROWS ONLY│
│ SQL Server │ TOP 10 또는 FETCH NEXT 10 ROWS ONLY │
│ SQL 표준 │ FETCH FIRST 10 ROWS ONLY │
└────────────┴───────────────────────────────────────┘페이지네이션 패턴
OFFSET 방식의 동작과 문제
OFFSET 방식의 내부 동작
OFFSET 990 LIMIT 10 → 1000행을 읽고 앞의 990행을 버린 후 10행 반환
페이지 1: OFFSET 0 → 10행 읽기 → 빠름
페이지 100: OFFSET 990 → 1000행 읽기 → 느림!
페이지 1000: OFFSET 9990 → 10000행 읽기 → 매우 느림!
페이지 10000: OFFSET 99990 → 100000행 읽기 → 극도로 느림!
문제 1: 성능 저하 (깊은 페이지일수록 심각)
문제 2: 데이터 변경 시 중복/누락 (INSERT/DELETE 발생 시)사용자가 페이지 1을 보는 동안 새 글이 1건 추가됨
페이지 1: [A, B, C, D, E] (OFFSET 0 LIMIT 5)
-- 새 글 F가 추가됨 (최신순 정렬)
페이지 2: [E, F, G, H, I] (OFFSET 5 LIMIT 5)
→ E가 페이지 1과 2에서 중복 노출!커서 기반 페이지네이션 (Keyset Pagination)
대량 데이터에서는 OFFSET 방식 대신 커서 기반 페이지네이션을 사용합니다.
-- 첫 페이지
SELECT product_id, name, price
FROM products
ORDER BY product_id
LIMIT 10;
-- 다음 페이지 (마지막으로 본 product_id가 10이면)
SELECT product_id, name, price
FROM products
WHERE product_id > 10
ORDER BY product_id
LIMIT 10;
-- 다음 페이지 (마지막 product_id가 20이면)
SELECT product_id, name, price
FROM products
WHERE product_id > 20
ORDER BY product_id
LIMIT 10;성능:
WHERE product_id > 10 → 인덱스로 바로 해당 위치 접근
→ OFFSET처럼 앞의 행을 읽고 버리지 않음
→ 페이지 깊이와 관계없이 일정한 성능
데이터 일관성:
→ 중간에 데이터가 추가/삭제되어도 중복/누락 없음
→ 이전 페이지의 마지막 키 값 기준이므로복합 정렬 키의 커서 페이지네이션
단일 키가 아닌 복합 정렬 기준에서의 커서 방식은 조금 복잡합니다.
-- 가격 내림차순, 같으면 ID 오름차순으로 정렬
-- 마지막으로 본 행: price=50000, product_id=15
SELECT product_id, name, price
FROM products
WHERE (price < 50000)
OR (price = 50000 AND product_id > 15)
ORDER BY price DESC, product_id ASC
LIMIT 10;
-- MySQL에서는 Row Constructor 비교도 가능
SELECT product_id, name, price
FROM products
WHERE (price, product_id) < (50000, 15)
ORDER BY price DESC, product_id ASC
LIMIT 10;OFFSET vs 커서 방식 비교
| 항목 | OFFSET 방식 | 커서 방식 |
|---|---|---|
| 구현 난이도 | 쉬움 | 보통 |
| 성능 (깊은 페이지) | 매우 느림 | 일정 |
| 데이터 변경 대응 | 중복/누락 가능 | 안전 |
| 특정 페이지 이동 | 가능 (OFFSET 계산) | 불가 (순차만) |
| 총 페이지 수 표시 | COUNT(*) 필요 | 별도 쿼리 필요 |
| 사용 사례 | 관리자 화면, 소규모 데이터 | 무한 스크롤, 대규모 데이터 |
TOP-N 분석 패턴
RANK / DENSE_RANK를 사용한 TOP-N
-- 카테고리별 가격 TOP 3 상품
SELECT * FROM (
SELECT
category,
name,
price,
RANK() OVER (PARTITION BY category ORDER BY price DESC) AS rnk
FROM products
) ranked
WHERE rnk <= 3;데이터: 100, 100, 90, 80
RANK(): 1, 1, 3, 4 (동점 후 건너뜀)
DENSE_RANK(): 1, 1, 2, 3 (동점 후 연속)
ROW_NUMBER(): 1, 2, 3, 4 (동점 무시, 고유)
TOP-3을 구할 때:
RANK: 동점을 모두 포함 (3개 이상 가능)
DENSE_RANK: 3번째 순위까지 포함
ROW_NUMBER: 정확히 3개만Oracle ROWNUM과 실행 순서 주의
-- 잘못된 방법: ORDER BY 전에 ROWNUM 부여
SELECT * FROM products
WHERE ROWNUM <= 5
ORDER BY price DESC;
-- → 임의의 5행을 먼저 선택한 후 정렬 (의도와 다름!)
-- 올바른 방법: 서브쿼리로 정렬 후 ROWNUM 적용
SELECT * FROM (
SELECT * FROM products ORDER BY price DESC
) WHERE ROWNUM <= 5;ORDER BY 없는 SELECT의 순서 보장
SELECT * FROM products;
→ 결과 순서가 보장되지 않음!
SQL 표준: ORDER BY가 없으면 결과 순서는 미정의
→ DBMS가 내부적으로 가장 효율적인 순서로 반환
→ 테이블 풀 스캔 → 저장된 순서 (보통 INSERT 순서)
→ 인덱스 스캔 → 인덱스 순서
→ 병렬 쿼리 → 스레드별 반환 순서
실무 원칙
→ 순서가 필요하면 반드시 ORDER BY 명시
→ "이전에는 ID 순서로 나왔으니까" 가정하지 말 것
→ DBMS 버전 업이나 통계 갱신으로 실행 계획이 바뀌면
결과 순서도 달라질 수 있음DISTINCT와 정렬
DISTINCT는 중복 행을 제거합니다. ORDER BY와 함께 사용할 때 주의할 점이 있습니다.
-- 중복 제거
SELECT DISTINCT category FROM products;
-- DISTINCT + ORDER BY
SELECT DISTINCT category FROM products ORDER BY category;
-- 주의: ORDER BY에 사용할 컬럼은 SELECT 목록에 있어야 함
-- 아래는 대부분의 DBMS에서 오류
SELECT DISTINCT category FROM products ORDER BY price;
-- → price가 SELECT에 없으므로 어떤 price로 정렬할지 모호-- DISTINCT는 내부적으로 정렬 또는 해싱을 수행
-- 대량 데이터에서 비용이 클 수 있음
-- 방법 1: DISTINCT (정렬 또는 해시 사용)
SELECT DISTINCT dept_id FROM employees;
-- 방법 2: GROUP BY (동일 결과, 인덱스 활용 가능)
SELECT dept_id FROM employees GROUP BY dept_id;
-- 방법 3: EXISTS (대량 데이터에서 가장 효율적일 수 있음)
SELECT dept_id FROM departments d
WHERE EXISTS (SELECT 1 FROM employees e WHERE e.dept_id = d.dept_id);SQL 처리 순서와 정렬
SQL 쿼리의 논리적 처리 순서를 이해하면 ORDER BY, LIMIT의 동작을 명확히 파악할 수 있습니다.
1. FROM → 테이블 접근
2. WHERE → 행 필터링
3. GROUP BY → 그룹화
4. HAVING → 그룹 필터링
5. SELECT → 컬럼 선택, 별칭 부여
6. DISTINCT → 중복 제거
7. ORDER BY → 정렬 (이 시점에 별칭 사용 가능)
8. LIMIT/FETCH → 결과 제한
핵심
→ ORDER BY는 SELECT 이후에 실행되므로 별칭 사용 가능
→ WHERE에서는 별칭 사용 불가 (SELECT 이전에 실행)
→ LIMIT은 ORDER BY 이후에 실행되므로 정렬된 결과에서 제한SELECT category, AVG(price) AS avg_price -- 5. 컬럼 선택
FROM products -- 1. 테이블 접근
WHERE stock > 0 -- 2. 재고있는 것만
GROUP BY category -- 3. 카테고리별 그룹
HAVING AVG(price) > 10000 -- 4. 평균 1만원 이상
ORDER BY avg_price DESC -- 7. 평균가 내림차순
LIMIT 5; -- 8. 상위 5개만정렬 관련 실무 팁
✅ 자주 사용하는 정렬 컬럼에 인덱스 생성
CREATE INDEX idx_products_price ON products(price);
✅ 복합 정렬 시 복합 인덱스 고려
ORDER BY category ASC, price DESC
→ CREATE INDEX idx_cat_price ON products(category ASC, price DESC);
✅ LIMIT과 함께 사용할 때 인덱스 커버링 활용
SELECT product_id, price FROM products
ORDER BY price DESC LIMIT 10;
→ INDEX(price, product_id)이면 테이블 접근 없이 인덱스만으로 처리
✅ 정렬 버퍼 크기 확인 및 조정
MySQL: SHOW VARIABLES LIKE 'sort_buffer_size';
Oracle: ALTER SESSION SET SORT_AREA_SIZE = 2097152;
✅ 불필요한 ORDER BY 제거
서브쿼리 내의 ORDER BY는 대부분 무의미
(외부 쿼리가 결과를 재정렬하므로)정리
| 개념 | 문법(표준/MySQL) | 용도 |
|---|---|---|
| 오름차순 정렬 | ORDER BY col ASC | 작은 값부터 |
| 내림차순 정렬 | ORDER BY col DESC | 큰 값부터 |
| 복합 정렬 | ORDER BY col1 ASC, col2 DESC | 다중 기준 |
| NULL 정렬 제어 | NULLS FIRST / NULLS LAST | NULL 위치 지정 |
| 결과 제한 | LIMIT n / FETCH FIRST n ROWS | TOP-N 조회 |
| 페이지네이션 | LIMIT n OFFSET m | 페이지 단위 조회 |
| 커서 페이지네이션 | WHERE id > last_id LIMIT n | 대용량 페이지 처리 |
| WITH TIES | FETCH FIRST n WITH TIES | 동점 포함 TOP-N |
정렬과 결과 제한은 SELECT 쿼리의 마무리 단계입니다. ORDER BY 없이는 결과 순서가 보장되지 않으며, 대용량 데이터에서는 OFFSET 방식의 한계를 인식하고 커서 기반 페이지네이션을 활용하는 것이 중요합니다. 인덱스가 정렬 순서와 일치하면 별도의 정렬 연산 없이 결과를 반환할 수 있으므로, 정렬 성능이 중요한 쿼리에서는 인덱스 설계도 함께 고려해야 합니다.
다음 절에서는 데이터를 넣고, 고치고, 지우는 INSERT, UPDATE, DELETE를 다루겠습니다.