백엔드 데이터 인프라 20편 — PostgreSQL SQL 문법 전반

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

백엔드 데이터 인프라 20편. SQL 문법 전반 — 값 표현식·함수 호출·타입 변환·연산자 우선순위 풀어쓴 학습 노트.

📚 백엔드 데이터 인프라 · 20편 — PostgreSQL SQL 문법 전반

이 글은 백엔드 데이터 인프라 시리즈 70편 중 20편이에요. 19편 어휘 구조"단어" 였다면, 이번 20편은 "문장의 구조 + 표현식" 을 다뤄요.

값 표현식 — Value Expression

SQL의 거의 모든 자리에 박을 수 있는 "값을 만드는 단위" 예요. 형태가 6가지로 나뉘어요.

  1. 상수42·'Alice'
  2. 컬럼 참조users.name
  3. 위치 파라미터$1·$2 (함수·prepared statement (준비된 문장)에서 인자 자리)
  4. 연산자 표현식price * 1.1
  5. 함수 호출LENGTH(name)
  6. 서브쿼리(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'

:: 는 매우 강하게 묶여요. 의도를 분명히 하려면 괄호.

🎯 SQL 표현식 5룰

(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 매일

시리즈 다른 편 (앞뒤 글 모음)

이전 글:

다음 글:

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

답글 남기기

error: Content is protected !!