백엔드 데이터 인프라 20편. SQL 문법 전반 — 값 표현식·함수 호출·타입 변환·연산자 우선순위 풀어쓴 학습 노트.
이 글은 백엔드 데이터 인프라 시리즈 70편 중 20편이에요. 19편 어휘 구조 가 "단어" 였다면, 이번 20편은 "문장의 구조 + 표현식" 깊이.
값 표현식 — Value Expression
SQL의 거의 모든 곳에 박을 수 있는 "값을 만드는 단위". 6가지 형태:
- 상수 —
42·'Alice' - 컬럼 참조 —
users.name - 위치 파라미터 —
$1·$2(함수·prepared statement) - 연산자 표현식 —
price * 1.1 - 함수 호출 —
LENGTH(name) - 서브쿼리 —
(SELECT MAX(amount) FROM orders)
SELECT
42, -- 상수
name, -- 컬럼
price * 1.1 AS new_price, -- 연산자
LENGTH(description) AS desc_len, -- 함수
(SELECT AVG(amount) FROM orders) AS avg -- 서브쿼리
FROM products;
이 모두 가 "값 표현식". SQL의 SELECT·WHERE·INSERT VALUES 등 어디에나.
함수 호출
함수이름(인자1, 인자2, ...)
자주 쓰는 함수 — 10가지
LENGTH('hello') -- 5
LOWER('ABC') -- 'abc'
UPPER('abc') -- 'ABC'
TRIM(' hello ') -- 'hello'
SUBSTRING('hello' FROM 2 FOR 3) -- 'ell'
REPLACE('Hello World', 'World', 'SQL') -- 'Hello SQL'
COALESCE(email, 'no@email.com') -- 첫 NULL 아닌 값
COUNT(*) -- 행 수
SUM(amount) -- 합계
NOW() -- 현재 시각
명명 매개변수 — PG 특별
SELECT format_address(country => 'Korea', city => 'Seoul');
함수 매개변수에 이름. 함수 정의에 명시한 매개변수명 사용.
타입 캐스팅
::TYPE — PG 단축 (가장 흔함)
'123'::INTEGER
123::TEXT
'2026-05-17'::DATE
amount::DECIMAL(10, 2)
NOW()::DATE
CAST(... AS TYPE) — SQL 표준
CAST('123' AS INTEGER)
CAST(NOW() AS DATE)
둘 다 같은 동작. ::TYPE 가 더 짧아서 PG 코드에 압도적.
암시적 vs 명시적
SELECT 1 + '2'; -- 명시적 캐스팅 없이도 가능 (암시적)
SELECT '1' + 2; -- 가능 — '1' 자동 INT
SELECT 'abc' + 1; -- ❌ 자동 변환 불가
복잡한 표현식 = 명시적 캐스팅 권장 — 의도가 명확.
연산자 우선순위
. (테이블.컬럼)
:: (캐스팅)
[] (배열·JSON 접근)
+ - (단항)
^ (거듭제곱)
* / % (곱·나누기·나머지)
+ - (덧·뺄셈)
|| (문자열 연결)
~ ~* !~ !~* (정규식)
LIKE ILIKE (패턴)
< > <= >= = <> -- 비교
NOT
AND
OR
위에서 아래로 우선순위. 헷갈릴 때 = 괄호 박기.
WHERE a = 1 AND b = 2 OR c = 3
-- 실제 = WHERE (a = 1 AND b = 2) OR c = 3
WHERE a = 1 AND (b = 2 OR c = 3)
-- 명시
행 값 (ROW)
여러 컬럼을 한 번에 비교.
SELECT * FROM orders WHERE (status, user_id) = ('PAID', 1);
SELECT * FROM products WHERE (category, brand) IN (
('electronics', 'Samsung'),
('electronics', 'LG')
);
여러 컬럼 조합으로 매칭. PG·표준 SQL 모두 지원.
서브쿼리 종류
스칼라 서브쿼리 — 한 값
SELECT name,
(SELECT AVG(amount) FROM orders) AS avg_all
FROM users;
서브쿼리가 "한 행 한 컬럼". 외부 쿼리에 "값 하나" 처럼.
행 서브쿼리
SELECT * FROM orders
WHERE (user_id, amount) = (SELECT user_id, MAX(amount) FROM orders WHERE status = 'PAID');
테이블 서브쿼리
SELECT u.name, recent.last_order
FROM users u
JOIN (SELECT user_id, MAX(created_at) AS last_order FROM orders GROUP BY user_id) recent
ON u.id = recent.user_id;
FROM 절에 서브쿼리 → "임시 테이블". CTE (WITH) 가 더 깔끔할 때 많음.
EXISTS·NOT EXISTS
SELECT * FROM users u
WHERE EXISTS (SELECT 1 FROM orders WHERE user_id = u.id);
서브쿼리에 "존재 여부만" 검사. 결과 행이 1개 이상이면 TRUE.
IN·NOT IN
SELECT * FROM users WHERE id IN (SELECT user_id FROM premium);
IN과 EXISTS는 가끔 — EXISTS가 성능 더 좋은 경우 많음 (NULL 처리·짧은 회로).
조건 표현식
CASE — 깊이
-- CASE WHEN
SELECT
name,
CASE
WHEN age < 18 THEN '미성년'
WHEN age < 65 THEN '성인'
ELSE '시니어'
END AS age_group
FROM users;
-- CASE expr WHEN
SELECT
name,
CASE status
WHEN 'PENDING' THEN '대기'
WHEN 'PAID' THEN '결제완료'
ELSE '기타'
END
FROM orders;
COALESCE — 첫 NULL 아닌 값
COALESCE(nickname, name, 'Anonymous')
여러 후보 중 NULL 아닌 첫 값.
NULLIF — 같으면 NULL
NULLIF(price, 0) -- price = 0 이면 NULL (0 나눗셈 회피)
SELECT total / NULLIF(count, 0) FROM stats;
GREATEST·LEAST
GREATEST(1, 2, 3) -- 3
LEAST('a', 'b', 'c') -- 'a'
배열 표현
SELECT ARRAY[1, 2, 3]; -- {1,2,3}
SELECT '{"a", "b", "c"}'::TEXT[]; -- 리터럴
SELECT ARRAY[1, 2, 3] || ARRAY[4, 5]; -- 연결 {1,2,3,4,5}
SELECT 2 = ANY(ARRAY[1, 2, 3]); -- TRUE (포함)
PG는 배열 일급 지원. 32편에서 깊이.
JSON 표현
SELECT '{"name": "Alice"}'::JSONB -> 'name'; -- "Alice"
SELECT '{"name": "Alice"}'::JSONB ->> 'name'; -- Alice (텍스트)
33편에서 깊이.
함정 5가지
(1) 연산자 우선순위 헷갈림
명시 안 하면 — AND > OR 우선순위로 "의도와 다른 결과". 괄호.
(2) 정수 / 정수 = 정수
SELECT 1 / 2; -- 0
SELECT 1.0 / 2; -- 0.5
SELECT 1 / 2.0; -- 0.5
SELECT 1::FLOAT / 2; -- 0.5
소수 결과 원하면 한쪽은 FLOAT·NUMERIC.
(3) NULL과 연산
SELECT 1 + NULL; -- NULL
SELECT NULL = NULL; -- NULL (not TRUE)
SELECT 'a' || NULL; -- NULL
거의 모든 연산은 NULL과 NULL.
(4) 함수 명명 매개변수 vs 위치
format_address(country => 'Korea', city => 'Seoul'); -- 명명
format_address('Korea', 'Seoul'); -- 위치 (순서 의존)
명시 안 하면 위치 — 함수 매개변수 순서 바뀌면 깨짐.
(5) ::TYPE 우선순위
SELECT -1::INTEGER; -- 실제 = -(1::INTEGER) = -1 (OK)
SELECT 1 + 2::TEXT; -- 1 + (2::TEXT) = ERROR
SELECT (1 + 2)::TEXT; -- '3'
:: 가 매우 강하게 묶음. 의도 명시 = 괄호.
(1) 모든 값 = 표현식. (2) 캐스팅은 ::TYPE. (3) NULL과 연산은 NULL. (4) 우선순위 헷갈리면 괄호. (5) COALESCE·NULLIF·GREATEST·LEAST 4가지가 자주 쓰는 표현식 도구.
한 줄 정리 — SQL 표현식 6종 = 상수·컬럼·파라미터·연산자·함수·서브쿼리. 캐스팅은 ::TYPE 또는 CAST(... AS TYPE). CASE·COALESCE·NULLIF·GREATEST·LEAST가 표준 도구. EXISTS·IN 서브쿼리 활용. 우선순위 헷갈리면 괄호.
시험 직전 한 번 더 — SQL 표현식 입문자가 매번 헷갈리는 것
- 값 표현식 6종 = 상수·컬럼·파라미터·연산자·함수·서브쿼리
- 함수 호출 =
name(arg1, arg2) - 명명 매개변수 =
key => value - 타입 캐스팅 =
::TYPE(단축) 또는CAST(val AS TYPE) - ::TYPE 우선순위 매우 강함 — 괄호로 명시
- 암시적 캐스팅 = 자동 변환 (가능한 경우만)
- 정수 / 정수 = 정수 — FLOAT 캐스팅 필요
- NULL + 모든 것 = NULL
||= 문자열 연결 (NULL이면 NULL)- COALESCE = 첫 NULL 아닌
- NULLIF(a, b) = a == b면 NULL
- GREATEST·LEAST = 최대·최소
- 연산자 우선순위 =
::>^>* />+ ->||> 비교 > NOT > AND > OR - 헷갈리면 = 괄호
- CASE = SQL의 if-else
- 서브쿼리 4종 = 스칼라·행·테이블·EXISTS
- EXISTS vs IN — EXISTS 성능 더 좋은 경우 많음
- 행 값 =
(col1, col2) = (val1, val2) - 행 값 IN =
(col1, col2) IN (...) - 배열 =
ARRAY[1,2,3]또는'{1,2,3}'::INT[] - JSON =
->JSONB 추출,->>텍스트 - 함수 매개변수 순서 의존 = 명명 매개변수 안전
- 한국 백엔드 = COALESCE·NULLIF·CASE 매일
시리즈 다른 편
시리즈 다음 글
다음 글(21편)에서는 DDL 개요 — 데이터 정의 언어 — CREATE·ALTER·DROP의 큰 그림.
공식 문서: PostgreSQL 18 — SQL Syntax에서 더 자세한 사양을 확인할 수 있어요.