백엔드 데이터 인프라 29편 — SELECT 절과 표현식의 깊이

2026-05-17백엔드 데이터 인프라

백엔드 데이터 인프라 29편. SELECT 절의 깊이 — 컬럼·표현식·별칭·DISTINCT·집계의 표현 능력 풀어쓴 학습 노트.

📚 백엔드 데이터 인프라 · 29편 — SELECT 절과 표현식의 깊이

이 글은 백엔드 데이터 인프라 시리즈 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"가 아님

큰 따옴표 별칭은 클라이언트·도구 호환성 함정.

🎯 SELECT 절 활용 5

(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편 표현식
  • * 운영 X
  • table.* 명시
  • 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에서 더 자세한 사양을 확인할 수 있어요.

※ 이 포스팅은 쿠팡 파트너스 활동의 일환으로, 이에 따른 일정액의 수수료를 제공받습니다.

답글 남기기

error: Content is protected !!