백엔드 데이터 인프라 11편. SELECT 데이터 조회의 표준 패턴 — WHERE·ORDER BY·LIMIT·LIKE·IN·BETWEEN·CASE까지 풀어쓴 학습 노트.
이 글은 백엔드 데이터 인프라 시리즈 70편 중 11편이에요. 10편 INSERT 까지 데이터를 박았으니, 이번 11편은 박힌 데이터를 어떻게 꺼내는가 — SQL 가장 자주 쓰이는 동사 SELECT.
SELECT 표준 형태
SELECT 컬럼1, 컬럼2, ...
FROM 테이블
WHERE 조건
GROUP BY 컬럼
HAVING 그룹 조건
ORDER BY 컬럼 [ASC|DESC]
LIMIT 개수
OFFSET 시작 위치;
8개 절(clause) 의 표준 순서. 실행 순서는 약간 다른데 — FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT. 익히면 복잡한 쿼리도 한 흐름으로 읽혀요.
기본 — 전체 / 컬럼 선택
-- 모든 컬럼
SELECT * FROM users;
-- 특정 컬럼
SELECT id, name, email FROM users;
-- 계산
SELECT
name,
EXTRACT(YEAR FROM AGE(birth)) AS age
FROM users;
-- 별칭
SELECT
name AS user_name,
email AS user_email
FROM users;
AS 별칭은 생략 가능. 명시 권장.
WHERE — 필터링
비교 연산자
WHERE id = 1
WHERE id <> 1 -- 같지 않음 (또는 !=)
WHERE age >= 18
WHERE created < NOW() - INTERVAL '7 days'
논리 연산자
WHERE age >= 18 AND status = 'ACTIVE'
WHERE city = 'Seoul' OR city = 'Busan'
WHERE NOT (age < 18)
AND·OR·NOT 표준.
IN — 여러 값 중 하나
WHERE city IN ('Seoul', 'Busan', 'Daegu')
WHERE id NOT IN (1, 2, 3)
WHERE user_id IN (SELECT id FROM premium_users)
OR 줄이는 깔끔한 표현. 서브쿼리도 가능.
BETWEEN — 범위
WHERE age BETWEEN 18 AND 65
WHERE created BETWEEN '2026-01-01' AND '2026-12-31'
>= + <= 단축. 양 끝 포함.
LIKE — 패턴 매칭
WHERE name LIKE 'A%' -- A로 시작
WHERE email LIKE '%@example.com' -- 끝
WHERE name LIKE '_lice' -- 5글자 + 끝 'lice' (단일 문자)
WHERE name ILIKE 'a%' -- 대소문자 무시 (PG 특별)
% = 임의 문자열, _ = 단일 문자. ILIKE = PG의 대소문자 무시.
IS NULL
WHERE email IS NULL
WHERE deleted_at IS NOT NULL
7편 의 NULL 룰 — = NULL 안 됨.
정규식 — ~·~*
WHERE name ~ '^[A-Z]' -- 정규식
WHERE name ~* '^a' -- 대소문자 무시 정규식
WHERE name !~ '\d' -- 매칭 X
PG 특별 — LIKE보다 강력.
ORDER BY — 정렬
-- 기본 = 오름차순
SELECT * FROM users ORDER BY created_at;
-- 명시
SELECT * FROM users ORDER BY created_at DESC;
-- 여러 컬럼
SELECT * FROM users ORDER BY age DESC, name ASC;
-- NULL 처리
SELECT * FROM users ORDER BY last_login NULLS LAST;
NULLS FIRST·NULLS LAST = NULL의 위치. ASC면 기본 NULLS LAST, DESC면 NULLS FIRST.
LIMIT·OFFSET — 페이지네이션
-- 처음 10건
SELECT * FROM users ORDER BY id LIMIT 10;
-- 다음 10건
SELECT * FROM users ORDER BY id LIMIT 10 OFFSET 10;
-- 페이지 3 (10건씩)
SELECT * FROM users ORDER BY id LIMIT 10 OFFSET 20;
페이지네이션 표준. 단 — OFFSET 깊으면 느림 (1000번째 페이지 = 9990건 건너뛰는 비용). 큰 데이터는 "커서 페이지네이션" (다음 ID > N) 권장.
-- 커서 페이지네이션
SELECT * FROM users WHERE id > 100 ORDER BY id LIMIT 10;
DISTINCT — 중복 제거
SELECT DISTINCT city FROM users;
SELECT DISTINCT city, country FROM users; -- 조합 unique
DISTINCT ON — PG 특별
-- 각 사용자별 최근 주문 1건
SELECT DISTINCT ON (user_id) *
FROM orders
ORDER BY user_id, created_at DESC;
표준 SQL엔 없는 PG 강력 기능. "그룹별 첫 행" 가져오기.
CASE — 조건 표현식
SELECT
name,
CASE
WHEN age < 18 THEN '미성년'
WHEN age < 65 THEN '성인'
ELSE '시니어'
END AS age_group
FROM users;
SQL 안 if-else. WHERE·ORDER BY 안에도 가능.
짧은 형태
CASE status
WHEN 'PENDING' THEN '대기'
WHEN 'PAID' THEN '결제완료'
ELSE '기타'
END
NULL 처리 함수
COALESCE(email, 'no-email@example.com') -- 첫 NULL 아닌 값
NULLIF(value, 0) -- value = 0이면 NULL
COALESCE = 백엔드에서 가장 자주. 디폴트 값 대체.
함수 — 자주 쓰는 30개 중 10개
| 함수 | 의미 | 예 |
|---|---|---|
LENGTH(s) |
문자열 길이 | LENGTH('hello') = 5 |
LOWER(s)·UPPER(s) |
대소문자 | |
TRIM(s) |
양 끝 공백 제거 | |
SUBSTRING(s FROM 1 FOR 3) |
부분 문자열 | |
CONCAT(s1, s2) 또는 s1 \|\| s2 |
연결 | |
NOW() |
현재 시각 | |
CURRENT_DATE |
오늘 날짜 | |
DATE_TRUNC('day', ts) |
단위 절삭 | |
EXTRACT(YEAR FROM ts) |
부분 추출 | |
AGE(birth) |
나이 (interval) |
집계 함수
SELECT COUNT(*) FROM users;
SELECT COUNT(DISTINCT city) FROM users;
SELECT AVG(amount) FROM orders;
SELECT SUM(amount), MIN(amount), MAX(amount) FROM orders;
다음 14편 GROUP BY 에서 깊이.
페이지네이션 실전 예
-- 사용자 목록, 최근 가입 순, 페이지당 20명, 3페이지
SELECT id, name, email, created_at
FROM users
WHERE deleted_at IS NULL
ORDER BY created_at DESC
LIMIT 20 OFFSET 40;
이게 한국 백엔드 "목록 조회 API" 의 99% 패턴.
Spring 백엔드 흐름
자바 백엔드 입문 49편 쿼리 메서드 의 JPA가 생성하는 SELECT:
// 메서드 이름 쿼리
List<User> findByCityAndAgeGreaterThan(String city, int age);
// → SELECT * FROM users WHERE city = ? AND age > ?
JPA 자동 생성 SQL은 단순한데 — "복잡한 SELECT는 직접 짜야". 자바 백엔드 입문 50편 QueryDSL 또는 native SQL.
함정 5가지
(1) SELECT * 운영 남용
SELECT * FROM users; -- ❌ 운영 코드에 X
새 컬럼 추가 시 — 코드가 깨짐. 운영 = 명시적 컬럼.
(2) ORDER BY 없는 LIMIT
SELECT * FROM users LIMIT 10; -- ❌ 어떤 10건일지 모름
LIMIT 박을 때는 ORDER BY 필수.
(3) LIKE 와일드카드 앞
WHERE name LIKE '%alice' -- ❌ 인덱스 사용 X — 풀스캔
WHERE name LIKE 'alice%' -- ✅ 인덱스 사용
앞에 % 박힌 LIKE는 인덱스 못 씀. 36편 인덱스 참고.
(4) OFFSET 깊음
LIMIT 10 OFFSET 1000000 -- 백만 건 스킵 — 느림
큰 데이터는 커서 페이지네이션.
(5) WHERE 안 NULL = NULL
WHERE col = NULL ❌
WHERE col IS NULL ✅
7편 참고.
(1) 컬럼 명시. (2) WHERE에 인덱스 가능한 조건. (3) ORDER BY + LIMIT 페이지네이션. (4) CASE·COALESCE로 표현식. (5) 깊은 OFFSET은 커서로.
한 줄 정리 — SELECT 8개 절 = SELECT·FROM·WHERE·GROUP BY·HAVING·ORDER BY·LIMIT·OFFSET. WHERE의 비교·LIKE·IN·BETWEEN·IS NULL 5종. ORDER BY는 LIMIT의 짝. DISTINCT ON·ILIKE·::·CASE 가 PG 강점.
시험 직전 한 번 더 — SELECT 입문자가 매번 헷갈리는 것
- 절 순서 = SELECT·FROM·WHERE·GROUP BY·HAVING·ORDER BY·LIMIT·OFFSET
- 실행 순서 = FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
SELECT *= 개발만, 운영은 명시- 비교 =
=·<>·>=·<=·>·< - IN = 여러 값 중 하나 (서브쿼리 가능)
- BETWEEN =
>=+<=단축 (양 끝 포함) - LIKE = 와일드카드
%·_ - ILIKE = PG 대소문자 무시 LIKE
- 정규식 =
~·~*(PG 특별) - IS NULL = NULL 비교 (
= NULL안 됨) - ORDER BY = ASC (기본) · DESC
- NULL 위치 = NULLS FIRST·NULLS LAST
- LIMIT 박을 때 = ORDER BY 필수
- OFFSET 깊으면 느림 — 커서 페이지네이션
- DISTINCT = 중복 제거
- DISTINCT ON = 그룹별 첫 행 (PG 특별)
- CASE WHEN ... THEN ... ELSE ... END
- COALESCE = 첫 NULL 아닌 값
- NULLIF(a, b) = a == b면 NULL
- 자주 쓰는 함수 = LENGTH·LOWER·SUBSTRING·NOW·DATE_TRUNC·COALESCE
- 페이지네이션 실전 =
WHERE deleted_at IS NULL ORDER BY created_at DESC LIMIT N OFFSET M - LIKE 앞
%= 인덱스 X - 백엔드 99% = SELECT 가 가장 자주
시리즈 다른 편
시리즈 다음 글
다음 글(12편)에서는 JOIN — 여러 테이블 합치기 — INNER·LEFT·RIGHT·FULL JOIN 표준 패턴.
공식 문서: PostgreSQL 18 — Tutorial: Querying a Table에서 더 자세한 사양을 확인할 수 있어요.