함수와 프로시저

함수와 프로시저

1. 내장 함수

PostgreSQL은 다양한 내장 함수를 제공합니다.

문자열 함수

함수 설명 예시 결과
LENGTH() 문자열 길이 LENGTH('Hello') 5
UPPER() 대문자 변환 UPPER('hello') HELLO
LOWER() 소문자 변환 LOWER('HELLO') hello
TRIM() 공백 제거 TRIM(' hi ') hi
SUBSTRING() 부분 문자열 SUBSTRING('Hello', 1, 3) Hel
REPLACE() 문자열 치환 REPLACE('Hello', 'l', 'L') HeLLo
CONCAT() 문자열 연결 CONCAT('A', 'B', 'C') ABC
SPLIT_PART() 구분자로 분리 SPLIT_PART('a,b,c', ',', 2) b
SELECT
    LENGTH('PostgreSQL') AS len,
    UPPER('hello') AS upper,
    LOWER('WORLD') AS lower,
    TRIM('  text  ') AS trimmed,
    SUBSTRING('PostgreSQL', 1, 8) AS sub,
    REPLACE('Hello', 'l', 'L') AS replaced,
    CONCAT('Post', 'gre', 'SQL') AS concat;

숫자 함수

함수 설명 예시 결과
ROUND() 반올림 ROUND(3.567, 2) 3.57
FLOOR() 내림 FLOOR(3.9) 3
CEIL() 올림 CEIL(3.1) 4
ABS() 절대값 ABS(-5) 5
MOD() 나머지 MOD(10, 3) 1
POWER() 거듭제곱 POWER(2, 3) 8
SQRT() 제곱근 SQRT(16) 4
RANDOM() 0~1 난수 RANDOM() 0.xxx
SELECT
    ROUND(123.456, 2),
    FLOOR(9.9),
    CEIL(1.1),
    ABS(-100),
    MOD(17, 5),
    POWER(2, 10),
    ROUND(RANDOM() * 100);

날짜/시간 함수

함수 설명
NOW() 현재 타임스탬프
CURRENT_DATE 현재 날짜
CURRENT_TIME 현재 시간
DATE_TRUNC() 날짜 자르기
EXTRACT() 날짜 요소 추출
AGE() 날짜 차이
TO_CHAR() 날짜 포맷팅
SELECT
    NOW(),
    CURRENT_DATE,
    DATE_TRUNC('month', NOW()),
    EXTRACT(YEAR FROM NOW()),
    EXTRACT(DOW FROM NOW()),  -- 0=일요일
    AGE('2024-12-31', '2024-01-01'),
    TO_CHAR(NOW(), 'YYYY-MM-DD HH24:MI:SS');

2. 사용자 정의 함수 기본

SQL 함수

-- 간단한 함수
CREATE FUNCTION add_numbers(a INTEGER, b INTEGER)
RETURNS INTEGER
AS $$
    SELECT a + b;
$$ LANGUAGE SQL;

-- 사용
SELECT add_numbers(5, 3);  -- 8

함수 삭제

DROP FUNCTION add_numbers(INTEGER, INTEGER);
DROP FUNCTION IF EXISTS add_numbers(INTEGER, INTEGER);

3. PL/pgSQL 함수

PL/pgSQL은 PostgreSQL의 절차적 언어입니다.

기본 구조

CREATE FUNCTION function_name(parameters)
RETURNS return_type
AS $$
DECLARE
    -- 변수 선언
BEGIN
    -- 함수 본문
    RETURN value;
END;
$$ LANGUAGE plpgsql;

변수와 할당

CREATE FUNCTION calculate_tax(price NUMERIC)
RETURNS NUMERIC
AS $$
DECLARE
    tax_rate NUMERIC := 0.1;  -- 10%
    tax_amount NUMERIC;
BEGIN
    tax_amount := price * tax_rate;
    RETURN tax_amount;
END;
$$ LANGUAGE plpgsql;

SELECT calculate_tax(10000);  -- 1000

IF-ELSE

CREATE FUNCTION get_grade(score INTEGER)
RETURNS VARCHAR
AS $$
BEGIN
    IF score >= 90 THEN
        RETURN 'A';
    ELSIF score >= 80 THEN
        RETURN 'B';
    ELSIF score >= 70 THEN
        RETURN 'C';
    ELSIF score >= 60 THEN
        RETURN 'D';
    ELSE
        RETURN 'F';
    END IF;
END;
$$ LANGUAGE plpgsql;

SELECT get_grade(85);  -- B

CASE 문

CREATE FUNCTION day_name(day_num INTEGER)
RETURNS VARCHAR
AS $$
BEGIN
    RETURN CASE day_num
        WHEN 0 THEN '일요일'
        WHEN 1 THEN '월요일'
        WHEN 2 THEN '화요일'
        WHEN 3 THEN '수요일'
        WHEN 4 THEN '목요일'
        WHEN 5 THEN '금요일'
        WHEN 6 THEN '토요일'
        ELSE '잘못된 입력'
    END;
END;
$$ LANGUAGE plpgsql;

반복문

-- LOOP
CREATE FUNCTION factorial(n INTEGER)
RETURNS BIGINT
AS $$
DECLARE
    result BIGINT := 1;
    i INTEGER := 1;
BEGIN
    LOOP
        EXIT WHEN i > n;
        result := result * i;
        i := i + 1;
    END LOOP;
    RETURN result;
END;
$$ LANGUAGE plpgsql;

-- FOR 루프
CREATE FUNCTION sum_to_n(n INTEGER)
RETURNS INTEGER
AS $$
DECLARE
    total INTEGER := 0;
BEGIN
    FOR i IN 1..n LOOP
        total := total + i;
    END LOOP;
    RETURN total;
END;
$$ LANGUAGE plpgsql;

-- WHILE
CREATE FUNCTION count_digits(num INTEGER)
RETURNS INTEGER
AS $$
DECLARE
    n INTEGER := ABS(num);
    count INTEGER := 0;
BEGIN
    WHILE n > 0 LOOP
        n := n / 10;
        count := count + 1;
    END LOOP;
    RETURN CASE WHEN count = 0 THEN 1 ELSE count END;
END;
$$ LANGUAGE plpgsql;

4. 테이블 데이터 반환

RETURNS TABLE

CREATE FUNCTION get_users_by_city(p_city VARCHAR)
RETURNS TABLE (
    user_id INTEGER,
    user_name VARCHAR,
    user_email VARCHAR
)
AS $$
BEGIN
    RETURN QUERY
    SELECT id, name, email
    FROM users
    WHERE city = p_city;
END;
$$ LANGUAGE plpgsql;

-- 사용
SELECT * FROM get_users_by_city('서울');

RETURNS SETOF

CREATE FUNCTION get_expensive_products(min_price NUMERIC)
RETURNS SETOF products
AS $$
BEGIN
    RETURN QUERY
    SELECT * FROM products WHERE price >= min_price;
END;
$$ LANGUAGE plpgsql;

-- 사용
SELECT * FROM get_expensive_products(100000);

OUT 파라미터

CREATE FUNCTION get_user_stats(
    IN p_user_id INTEGER,
    OUT order_count INTEGER,
    OUT total_amount NUMERIC
)
AS $$
BEGIN
    SELECT COUNT(*), COALESCE(SUM(amount), 0)
    INTO order_count, total_amount
    FROM orders
    WHERE user_id = p_user_id;
END;
$$ LANGUAGE plpgsql;

-- 사용
SELECT * FROM get_user_stats(1);

5. 예외 처리

CREATE FUNCTION safe_divide(a NUMERIC, b NUMERIC)
RETURNS NUMERIC
AS $$
BEGIN
    IF b = 0 THEN
        RAISE EXCEPTION '0으로 나눌 수 없습니다.';
    END IF;
    RETURN a / b;
EXCEPTION
    WHEN division_by_zero THEN
        RAISE NOTICE '0으로 나누기 시도됨';
        RETURN NULL;
    WHEN OTHERS THEN
        RAISE NOTICE '예외 발생: %', SQLERRM;
        RETURN NULL;
END;
$$ LANGUAGE plpgsql;

RAISE 레벨

RAISE DEBUG 'Debug message';
RAISE LOG 'Log message';
RAISE INFO 'Info message';
RAISE NOTICE 'Notice message';     -- 기본 출력
RAISE WARNING 'Warning message';
RAISE EXCEPTION 'Error message';   -- 실행 중단

6. 프로시저 (PROCEDURE)

함수와 달리 값을 반환하지 않고 작업을 수행합니다 (PostgreSQL 11+).

프로시저 생성

CREATE PROCEDURE update_user_status(p_user_id INTEGER, p_status VARCHAR)
AS $$
BEGIN
    UPDATE users SET status = p_status WHERE id = p_user_id;
    RAISE NOTICE '사용자 % 상태가 %로 변경됨', p_user_id, p_status;
END;
$$ LANGUAGE plpgsql;

-- 호출
CALL update_user_status(1, 'active');

트랜잭션 제어

CREATE PROCEDURE transfer_money(
    from_account INTEGER,
    to_account INTEGER,
    amount NUMERIC
)
AS $$
BEGIN
    UPDATE accounts SET balance = balance - amount WHERE id = from_account;
    UPDATE accounts SET balance = balance + amount WHERE id = to_account;
    COMMIT;
EXCEPTION
    WHEN OTHERS THEN
        ROLLBACK;
        RAISE;
END;
$$ LANGUAGE plpgsql;

7. 함수 vs 프로시저

특성 함수 (FUNCTION) 프로시저 (PROCEDURE)
반환값 반드시 반환 반환 없음
SELECT에서 사용 가능 사용 불가
호출 방법 SELECT func() CALL proc()
트랜잭션 외부 트랜잭션 자체 트랜잭션 가능
COMMIT/ROLLBACK 불가능 가능

8. 실습 예제

실습 1: 유틸리티 함수

-- 1. 이메일 도메인 추출
CREATE FUNCTION get_email_domain(email VARCHAR)
RETURNS VARCHAR
AS $$
BEGIN
    RETURN SPLIT_PART(email, '@', 2);
END;
$$ LANGUAGE plpgsql;

SELECT get_email_domain('user@gmail.com');  -- gmail.com

-- 2. 나이 계산
CREATE FUNCTION calculate_age(birth_date DATE)
RETURNS INTEGER
AS $$
BEGIN
    RETURN EXTRACT(YEAR FROM AGE(CURRENT_DATE, birth_date));
END;
$$ LANGUAGE plpgsql;

SELECT calculate_age('1990-05-15');  -- 34 (2024년 기준)

-- 3. 가격 포맷팅
CREATE FUNCTION format_price(price NUMERIC)
RETURNS VARCHAR
AS $$
BEGIN
    RETURN TO_CHAR(price, 'FM999,999,999') || '원';
END;
$$ LANGUAGE plpgsql;

SELECT format_price(1500000);  -- 1,500,000원

실습 2: 비즈니스 로직 함수

-- 1. 주문 총액 계산
CREATE FUNCTION calculate_order_total(p_order_id INTEGER)
RETURNS NUMERIC
AS $$
DECLARE
    total NUMERIC;
BEGIN
    SELECT SUM(p.price * oi.quantity)
    INTO total
    FROM order_items oi
    JOIN products p ON oi.product_id = p.id
    WHERE oi.order_id = p_order_id;

    RETURN COALESCE(total, 0);
END;
$$ LANGUAGE plpgsql;

-- 2. 사용자 등급 결정
CREATE FUNCTION get_user_tier(p_user_id INTEGER)
RETURNS VARCHAR
AS $$
DECLARE
    total_spent NUMERIC;
BEGIN
    SELECT COALESCE(SUM(amount), 0)
    INTO total_spent
    FROM orders
    WHERE user_id = p_user_id;

    RETURN CASE
        WHEN total_spent >= 1000000 THEN 'VIP'
        WHEN total_spent >= 500000 THEN 'Gold'
        WHEN total_spent >= 100000 THEN 'Silver'
        ELSE 'Bronze'
    END;
END;
$$ LANGUAGE plpgsql;

실습 3: 데이터 검증 함수

-- 1. 이메일 유효성 검사
CREATE FUNCTION is_valid_email(email VARCHAR)
RETURNS BOOLEAN
AS $$
BEGIN
    RETURN email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$';
END;
$$ LANGUAGE plpgsql;

SELECT is_valid_email('test@email.com');  -- true
SELECT is_valid_email('invalid-email');   -- false

-- 2. 전화번호 포맷팅
CREATE FUNCTION format_phone(phone VARCHAR)
RETURNS VARCHAR
AS $$
DECLARE
    cleaned VARCHAR;
BEGIN
    cleaned := REGEXP_REPLACE(phone, '[^0-9]', '', 'g');
    IF LENGTH(cleaned) = 11 THEN
        RETURN SUBSTRING(cleaned, 1, 3) || '-' ||
               SUBSTRING(cleaned, 4, 4) || '-' ||
               SUBSTRING(cleaned, 8, 4);
    ELSE
        RETURN phone;
    END IF;
END;
$$ LANGUAGE plpgsql;

SELECT format_phone('01012345678');  -- 010-1234-5678

9. 함수 관리

함수 목록 확인

-- psql 명령
\df

-- SQL 쿼리
SELECT routine_name, routine_type
FROM information_schema.routines
WHERE routine_schema = 'public';

함수 정의 확인

-- 함수 소스 코드 보기
\sf function_name

-- 또는
SELECT prosrc FROM pg_proc WHERE proname = 'function_name';

함수 수정

CREATE OR REPLACE FUNCTION function_name(...)
RETURNS ...
AS $$
    -- 수정된 내용
$$ LANGUAGE plpgsql;

다음 단계

11_Transactions.md에서 트랜잭션과 동시성 제어를 배워봅시다!

to navigate between lessons