백엔드 데이터 인프라 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(PostgreSQL)에서 제공하는 대소문자 무시 버전이고요.
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(자바 ORM 표준)가 생성하는 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 가 가장 자주
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 6편 — PostgreSQL 데이터베이스 접속·전환
- 7편 — PostgreSQL 관계형 모델 핵심 개념
- 8편 — SQL 기초 5가지 동사
- 9편 — CREATE TABLE 첫 테이블 만들기
- 10편 — INSERT 데이터 입력 표준 패턴
다음 글: