백엔드 데이터 인프라 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;
이 6가지가 모두 "값 표현식" 이고, 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 (PostgreSQL) 특별
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 (텍스트)
JSONB (이진 JSON 저장 타입) 깊이는 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 매일
시리즈 다른 편 (앞뒤 글 모음)
이전 글:
- 15편 — 외래 키 참조 무결성
- 16편 — 뷰 VIEW와 MATERIALIZED VIEW
- 17편 — 트랜잭션 BEGIN COMMIT ROLLBACK
- 18편 — 윈도우 함수와 고급 SQL
- 19편 — PostgreSQL SQL 어휘 구조
다음 글: