백엔드 데이터 인프라 29편. SELECT 절의 깊이 — 컬럼·표현식·별칭·DISTINCT·집계의 표현 능력 풀어쓴 학습 노트.
이 글은 백엔드 데이터 인프라 시리즈 70편 중 29편이에요. 28편 쿼리 개요 의 9개 절 중 — "무엇을 가져올지" 정하는 SELECT 절의 깊이.
SELECT 절이 받는 것
SELECT [DISTINCT [ON (...)]] 표현식1 [AS 별칭1], 표현식2, ...
각 표현식은 20편 SQL 표현식 에서 다룬 모든 형태: - 상수 - 컬럼 - 함수 호출 - 산술·문자열·논리 연산 - CASE 표현식 - 서브쿼리
컬럼 선택 패턴
* — 모든 컬럼
SELECT * FROM users; -- 모든 컬럼
SELECT users.* FROM users; -- 명시적
SELECT u.* FROM users u; -- 별칭 *
운영 코드 = * 회피 권장. 11편 SELECT 의 함정 참고.
명시적 컬럼
SELECT id, name, email FROM users;
SELECT u.id, u.name, o.amount
FROM users u JOIN orders o ON ...;
별칭 (AS)
SELECT
id AS user_id,
name AS user_name,
created_at AS signed_up_at
FROM users;
AS 생략 가능 (id user_id) 이지만 명시 권장. 또한 — "공백 포함 별칭" 은 큰 따옴표:
SELECT id AS "User ID" FROM users;
표현식 컬럼
산술
SELECT
amount,
amount * 1.1 AS with_tax,
amount + shipping_fee AS total
FROM orders;
문자열 조작
SELECT
UPPER(name) AS upper_name,
name || ' (' || email || ')' AS display,
SUBSTRING(description FROM 1 FOR 100) AS summary
FROM users;
CASE
SELECT
name,
CASE
WHEN age < 18 THEN '미성년'
WHEN age < 65 THEN '성인'
ELSE '시니어'
END AS age_group
FROM users;
COALESCE·NULLIF
SELECT
name,
COALESCE(nickname, name, 'Anonymous') AS display,
NULLIF(score, 0) AS score_or_null
FROM users;
함수
SELECT
NOW() AS current_time,
DATE_TRUNC('day', created_at) AS day,
EXTRACT(YEAR FROM birthday) AS birth_year,
AGE(birthday) AS age
FROM users;
서브쿼리 컬럼 — 스칼라
SELECT
name,
(SELECT COUNT(*) FROM orders WHERE user_id = u.id) AS order_count,
(SELECT MAX(amount) FROM orders WHERE user_id = u.id) AS max_amount
FROM users u;
각 행마다 "한 값" 을 서브쿼리로. 단 — 큰 데이터엔 성능 위험 (N+1). LEFT JOIN + GROUP BY 또는 LATERAL 이 더 효율.
ROW·복합 컬럼
SELECT ROW(id, name, email) FROM users;
-- (1,Alice,alice@example.com)
-- 복합 타입으로
SELECT (id, name)::user_summary FROM users;
특수 시나리오 — JSON 응답에 묶인 객체 박을 때.
배열·JSON 컬럼
SELECT
name,
ARRAY_AGG(tag) AS tags, -- 그룹 내 배열로
JSONB_AGG(JSONB_BUILD_OBJECT(
'id', id, 'name', name
)) AS items
FROM users
GROUP BY name;
API 응답 직접 만들기에 강력.
DISTINCT 깊이
단순 DISTINCT
SELECT DISTINCT city FROM users;
SELECT DISTINCT city, country FROM users;
DISTINCT ON — PG 특별
SELECT DISTINCT ON (user_id) *
FROM orders
ORDER BY user_id, created_at DESC;
"각 user_id 별 첫 행". ORDER BY 의 첫 컬럼이 DISTINCT ON 과 일치해야.
활용 — 각 카테고리 최신 가격
SELECT DISTINCT ON (category) category, name, price, updated_at
FROM products
ORDER BY category, updated_at DESC;
GROUP BY + window function 의 깔끔한 단축.
집계 표현식
SELECT
user_id,
COUNT(*) AS total,
COUNT(*) FILTER (WHERE status = 'PAID') AS paid,
SUM(amount) AS total_amount,
AVG(amount)::INTEGER AS avg_amount,
STRING_AGG(product_name, ', ') AS products,
ARRAY_AGG(id ORDER BY created_at DESC) AS order_ids
FROM orders
GROUP BY user_id;
자주 쓰는 집계 5종:
- COUNT(*) · COUNT(DISTINCT col)
- SUM·AVG·MIN·MAX
- STRING_AGG(text, separator) — 문자열 연결
- ARRAY_AGG(col) — 배열로
- JSONB_AGG(expr) — JSON 배열로
윈도우 함수 컬럼
18편 윈도우 함수 의 OVER 절.
SELECT
user_id, order_id, amount,
RANK() OVER (PARTITION BY user_id ORDER BY amount DESC) AS rank_in_user,
SUM(amount) OVER (PARTITION BY user_id) AS user_total,
LAG(amount) OVER (PARTITION BY user_id ORDER BY created_at) AS prev_amount
FROM orders;
각 행 결과 + 그룹 컨텍스트.
VALUES 절 — 임시 행 만들기
SELECT 단독이 아닌 — VALUES 로 임시 데이터.
SELECT * FROM (VALUES
(1, 'Alice'),
(2, 'Bob'),
(3, 'Charlie')
) AS t(id, name);
테스트·소량 마스터 데이터 매핑에 자주.
-- 매핑 테이블 안 만들고 한 SQL로
SELECT
u.name,
cat.label
FROM users u
JOIN (VALUES
('ADMIN', '관리자'),
('USER', '일반'),
('GUEST', '게스트')
) AS cat(code, label) ON u.role = cat.code;
컬럼 순서·중복
위치로 ORDER BY
SELECT name, COUNT(*) FROM users GROUP BY name ORDER BY 2 DESC;
-- 2 = SELECT 의 2번째 컬럼 (COUNT(*))
가독성 떨어짐 — 명시 권장.
같은 컬럼 여러 번
SELECT id, id * 2, id + 100 FROM users;
-- 한 컬럼을 여러 표현식으로
TABLE 명령 — 단축
TABLE users;
-- = SELECT * FROM users;
거의 안 씀 — 명시적 SELECT 권장.
함정 5가지
(1) SELECT 별칭을 WHERE 에서
28편 의 실행 순서 함정.
(2) 스칼라 서브쿼리 N+1
SELECT name, (SELECT ... FROM orders WHERE user_id = u.id) FROM users u;
-- 사용자 수만큼 서브쿼리 실행
LEFT JOIN + GROUP BY 또는 LATERAL 권장.
(3) * 운영 코드
새 컬럼 추가 시 깨짐. 명시.
(4) STRING_AGG 순서
STRING_AGG(name, ', ') -- 순서 비결정
STRING_AGG(name, ', ' ORDER BY created_at) -- 명시
ORDER BY 명시 권장.
(5) 큰 따옴표 별칭
SELECT id AS "User Id" FROM users; -- "User Id" 공백 포함 별칭
SELECT id FROM users WHERE "User Id" = 1; -- ERROR — 컬럼명이 "User Id"가 아님
큰 따옴표 별칭은 클라이언트·도구 호환성 함정.
(1) 명시적 컬럼 + AS 별칭. (2) 표현식·CASE·COALESCE 자유롭게. (3) 스칼라 서브쿼리 N+1 주의. (4) STRING_AGG·ARRAY_AGG·JSONB_AGG 로 API 응답 직접. (5) DISTINCT ON 으로 그룹별 첫.
한 줄 정리 — SELECT 절 = 컬럼·표현식·서브쿼리·집계·윈도우 함수 + 별칭. ARRAY_AGG·JSONB_AGG 가 API 응답 직접 도구. DISTINCT ON 그룹별 첫. VALUES 임시 데이터. 스칼라 서브쿼리는 LATERAL·JOIN 우선.
시험 직전 한 번 더 — SELECT 절 깊이 입문자가 매번 헷갈리는 것
- SELECT 가 받는 표현식 = 모든 20편 표현식
*운영 Xtable.*명시- AS 별칭 = 명시 권장
- 큰 따옴표 별칭 = 공백 포함 (호환성 위험)
- 표현식 = 산술·문자열·CASE·COALESCE·함수
- 스칼라 서브쿼리 = 각 행마다 한 값 (N+1 주의)
- LATERAL 또는 LEFT JOIN 권장
- ROW(...) = 복합 컬럼
- ARRAY_AGG = 그룹 → 배열
- JSONB_AGG + JSONB_BUILD_OBJECT = API 응답 직접
- STRING_AGG = 문자열 연결 집계
- STRING_AGG 순서 =
ORDER BY 명시 - DISTINCT col = 단순 중복 제거
- DISTINCT ON (col) = 그룹별 첫 (PG 특별)
- DISTINCT ON + ORDER BY 컬럼 일치
- 집계 5종 = COUNT·SUM·AVG·MIN·MAX
- FILTER 집계 = 조건부 카운트
- 윈도우 함수 = OVER (PARTITION BY ORDER BY)
- VALUES 절 = 임시 행
- VALUES + JOIN = 매핑 테이블 안 만들고
- 위치 ORDER BY =
ORDER BY 2(가독성 X) - TABLE 명령 = 단축
- SELECT 별칭을 WHERE X (실행 순서)
- ORDER BY 에 SELECT 별칭 OK
시리즈 다른 편
시리즈 다음 글
다음 글(30편)에서는 테이블 표현식 — FROM 절의 깊이 + JOIN 종류 + LATERAL.
공식 문서: PostgreSQL 18 — Queries: Select Lists에서 더 자세한 사양을 확인할 수 있어요.