정렬과 제한
데이터를 조회하면 결과의 순서를 지정하고, 가져올 행의 수를 제한해야 하는 경우가 대부분입니다. 게시판의 최신 글 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는 데이터를 메모리(또는 디스크)에서 정렬하는 작업이므로 비용이 발생합니다 특히 대량 데이터에서는 주의가 필요합니다.
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별 결과 제한 문법 비교
페이지네이션 패턴
OFFSET 방식의 동작과 문제
커서 기반 페이지네이션 (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의 순서 보장
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의 동작을 명확히 파악할 수 있습니다.
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개만정렬 관련 실무 팁
정리
| 개념 | 문법(표준/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를 다루겠습니다.