Skip to main content
Summary

긴 SQL을 매번 다시 쓰지 않기

앞 장들에서 우리는 고객, 주문, 상품 테이블을 연결하고 합계와 평균을 계산했습니다. 처음에는 한 줄씩 따라가면 됩니다. 하지만 실무에서는 곧 이런 문제가 생깁니다.

"지난달 매출 요약 SQL을 또 써야 하나?"
"상품별 판매 수량 보고서를 매주 복사해서 쓰고 있는데, 조건을 하나 빼먹으면 어떡하지?"
"팀원마다 SQL을 조금씩 다르게 써서 보고서 숫자가 다르게 나오네?"

이럴 때 쓰는 기능이 뷰입니다.

뷰는 자주 쓰는 조회문을 데이터베이스 안에 이름 붙여 저장해 둔 것입니다. 엑셀로 비유하면, 원본 시트는 그대로 두고 자주 보는 필터 화면을 저장해 둔 것과 비슷합니다. 폴더로 비유하면, 실제 파일을 복사해 새로 저장하는 것이 아니라 "이 조건으로 이 파일들을 보여 주세요"라는 바로가기를 만들어 둔 것에 가깝습니다.

중요한 점은 이것입니다.

일반 뷰는 실제 데이터를 새로 복사해 담아 두는 표가 아닙니다.
저장해 둔 SELECT 문을 필요할 때마다 실행해 보여 주는 가상 표입니다.

그래서 뷰를 조회하면 테이블처럼 보입니다. 하지만 그 안에는 원본 데이터가 따로 쌓여 있는 것이 아니라, 원본 테이블을 읽는 조회 규칙이 저장되어 있습니다.

반복 작업을 줄이는 뷰와 저장된 조회문AI 생성 이미지

뷰는 '저장된 조회문'이다

예를 들어 매출 보고서를 만들 때 이런 SQL을 자주 쓴다고 해 보겠습니다.

무엇을 하려는 문장인지
주문, 주문상세, 상품 테이블을 연결해서 상품별 판매 수량과 매출 합계를 보고 싶다.

SELECT
    p.product_id,
    p.product_name,
    SUM(oi.quantity) AS total_quantity,
    SUM(oi.quantity * oi.unit_price) AS total_sales
FROM order_items oi
JOIN products p
    ON oi.product_id = p.product_id
GROUP BY
    p.product_id,
    p.product_name
ORDER BY
    total_sales DESC;

이 SQL은 틀린 문장은 아닙니다. 하지만 매번 쓰기에는 깁니다. JOIN, GROUP BY, SUM이 함께 들어가 있어서 초보자에게는 복사하는 과정에서도 실수가 나기 쉽습니다.

이제 이 조회문에 sales_summary라는 이름을 붙여 보겠습니다.

무엇을 하려는 문장인지
상품별 판매 수량과 매출 합계를 계산하는 긴 조회문을 sales_summary라는 뷰로 저장하고 싶다.

CREATE VIEW sales_summary AS
SELECT
    p.product_id,
    p.product_name,
    SUM(oi.quantity) AS total_quantity,
    SUM(oi.quantity * oi.unit_price) AS total_sales
FROM order_items oi
JOIN products p
    ON oi.product_id = p.product_id
GROUP BY
    p.product_id,
    p.product_name;

결과 메시지는 도구에 따라 다르지만 보통 다음과 비슷합니다.

CREATE VIEW

이제부터는 긴 SQL을 다시 쓰지 않아도 됩니다. 뷰를 테이블처럼 조회하면 됩니다.

무엇을 하려는 문장인지
저장해 둔 상품별 매출 요약 뷰에서 매출이 큰 상품부터 보고 싶다.

SELECT
    product_id,
    product_name,
    total_quantity,
    total_sales
FROM sales_summary
ORDER BY total_sales DESC;

예상 결과는 다음과 같습니다.

product_id | product_name | total_quantity | total_sales
-----------+--------------+----------------+------------
        10 | 무선 마우스   |            120 |    1800000
        21 | 키보드        |             80 |    1600000
        15 | USB 허브      |             60 |     720000

결과 해석은 간단합니다. sales_summary라는 이름만 조회했지만, 실제로는 그 뒤에 저장된 조인과 집계 SQL이 실행된 것입니다.

뷰를 쓰면 무엇이 좋아질까

뷰의 장점은 단순히 타이핑을 줄이는 데서 끝나지 않습니다.

첫째, 복잡한 SQL을 숨길 수 있습니다.
팀원이 JOIN을 몰라도 sales_summary를 조회하면 상품별 매출 요약을 볼 수 있습니다.

둘째, 같은 기준의 보고서를 만들 수 있습니다.
누구는 취소 주문을 빼고, 누구는 포함하면 보고서 숫자가 달라집니다. 뷰 안에 기준을 정해 두면 팀 전체가 같은 계산식을 사용하게 됩니다.

셋째, 반복 업무가 줄어듭니다.
매주 같은 SQL을 복사해 붙여 넣는 대신, 짧은 조회문만 쓰면 됩니다.

넷째, 조회 결과를 업무용 표처럼 다룰 수 있습니다.
DBeaver나 pgAdmin에서는 뷰가 보통 Views 폴더 아래에 표시됩니다. 테이블처럼 클릭해서 컬럼을 확인하고, 간단한 조회를 실행할 수 있습니다.

다만 뷰는 마법이 아닙니다. 뷰 안의 SQL이 복잡하면 조회할 때 시간이 걸릴 수 있습니다. 일반 뷰는 결과를 미리 저장해 두는 것이 아니라, 조회할 때마다 원본 테이블을 읽어 계산하기 때문입니다.

조건을 붙여 필요한 부분만 보기

뷰를 만들었다고 해서 항상 전체 결과만 봐야 하는 것은 아닙니다. 테이블을 조회할 때처럼 WHERE, ORDER BY, LIMIT를 붙일 수 있습니다.

무엇을 하려는 문장인지
상품별 매출 요약 중에서 매출이 100만 원 이상인 상품만 보고 싶다.

SELECT
    product_name,
    total_quantity,
    total_sales
FROM sales_summary
WHERE total_sales >= 1000000
ORDER BY total_sales DESC;

예상 결과는 다음과 같습니다.

product_name | total_quantity | total_sales
-------------+----------------+------------
무선 마우스   |            120 |    1800000
키보드        |             80 |    1600000

결과 해석은 이렇습니다. sales_summary 뷰가 상품별 매출을 계산하고, 그중에서 total_sales가 100만 원 이상인 행만 보여 줍니다.

이번에는 최근 보고서 화면처럼 상위 5개만 볼 수도 있습니다.

무엇을 하려는 문장인지
상품별 매출 요약에서 매출 상위 5개 상품만 보고 싶다.

SELECT
    product_name,
    total_sales
FROM sales_summary
ORDER BY total_sales DESC
LIMIT 5;

이처럼 뷰는 저장된 보고서 원본처럼 사용할 수 있습니다. 긴 SQL은 뷰 안에 넣고, 그때그때 필요한 조건만 바깥에서 붙이면 됩니다.

기간 기준이 들어간 매출 뷰 만들기

실무 보고서는 보통 기간이 중요합니다. 이번에는 월별 매출 요약 뷰를 만들어 보겠습니다.

무엇을 하려는 문장인지
주문일을 월 단위로 묶고, 월별 주문 건수와 매출 합계를 계산하는 뷰를 만들고 싶다.

CREATE VIEW monthly_sales_summary AS
SELECT
    date_trunc('month', o.order_date)::date AS sales_month,
    COUNT(DISTINCT o.order_id) AS order_count,
    SUM(oi.quantity * oi.unit_price) AS total_sales
FROM orders o
JOIN order_items oi
    ON o.order_id = oi.order_id
WHERE o.order_status = '결제완료'
GROUP BY date_trunc('month', o.order_date)::date;

여기서는 세 가지를 눈여겨보면 됩니다.

  • date_trunc('month', o.order_date)는 주문일을 월 단위로 묶습니다.

  • COUNT(DISTINCT o.order_id)는 중복을 제외하고 주문 건수를 셉니다.

  • WHERE o.order_status = '결제완료'는 결제 완료 주문만 매출에 포함합니다.

이제 월별 매출을 조회해 보겠습니다.

무엇을 하려는 문장인지
저장해 둔 월별 매출 요약 뷰에서 최근 월부터 보고 싶다.

SELECT
    sales_month,
    order_count,
    total_sales
FROM monthly_sales_summary
ORDER BY sales_month DESC;

예상 결과는 다음과 같습니다.

sales_month | order_count | total_sales
------------+-------------+------------
2026-06-01  |          58 |    7250000
2026-05-01  |          64 |    8120000
2026-04-01  |          49 |    5900000

여기서 2026-06-01은 6월 1일 하루의 매출이라는 뜻이 아닙니다. 2026년 6월 전체를 대표하는 월 표시값입니다. 월 단위 보고서에서 흔히 쓰는 방식입니다.

일반 뷰와 물리화 뷰의 차이

PostgreSQL에는 일반 뷰뿐 아니라 물리화 뷰도 있습니다. 이름이 조금 어렵지만 차이는 간단합니다.

일반 뷰는 실시간 계산표입니다.
조회할 때마다 원본 테이블을 읽고 계산합니다. 원본 데이터가 바뀌면 뷰 조회 결과도 바로 바뀐 내용으로 보입니다.

물리화 뷰는 미리 만들어 둔 결과표입니다.
복잡한 조회 결과를 실제로 저장해 둡니다. 그래서 조회는 빠를 수 있지만, 원본 데이터가 바뀌어도 자동으로 최신 상태가 되지는 않습니다. 필요할 때 REFRESH MATERIALIZED VIEW로 새로 고쳐야 합니다. PostgreSQL 공식 문서도 물리화 뷰를 조회 결과를 테이블과 비슷한 형태로 보관하고, 필요할 때 다시 생성할 수 있는 구조로 설명합니다.1

엑셀로 비유하면 이렇습니다.

구분일반 뷰물리화 뷰
비유원본 시트를 읽는 저장된 필터한 번 계산해서 붙여 넣은 요약표
데이터 저장결과를 따로 저장하지 않음결과를 저장함
최신성조회할 때 원본 기준으로 계산새로 고침 전까지 예전 결과일 수 있음
장점항상 최신 데이터 조회반복 조회가 빠를 수 있음
주의점복잡하면 매번 느릴 수 있음새로 고침 관리가 필요함

초보자라면 먼저 일반 뷰를 쓰면 됩니다. 조회가 너무 느리거나, 매일 아침 한 번만 갱신해도 되는 큰 보고서라면 물리화 뷰를 검토합니다.

물리화 뷰로 미리 계산해 두기

이번에는 월별 매출 요약을 물리화 뷰로 만들어 보겠습니다.

무엇을 하려는 문장인지
월별 매출 요약 결과를 미리 계산해서 저장하는 물리화 뷰를 만들고 싶다.

CREATE MATERIALIZED VIEW monthly_sales_summary_mv AS
SELECT
    date_trunc('month', o.order_date)::date AS sales_month,
    COUNT(DISTINCT o.order_id) AS order_count,
    SUM(oi.quantity * oi.unit_price) AS total_sales
FROM orders o
JOIN order_items oi
    ON o.order_id = oi.order_id
WHERE o.order_status = '결제완료'
GROUP BY date_trunc('month', o.order_date)::date;

결과 메시지는 보통 다음과 비슷합니다.

SELECT 12

도구나 버전에 따라 표현은 다를 수 있지만, 핵심은 물리화 뷰가 만들어졌고 그 안에 계산 결과가 저장되었다는 뜻입니다.

조회는 일반 테이블처럼 합니다.

무엇을 하려는 문장인지
미리 계산해 둔 월별 매출 요약 결과를 조회하고 싶다.

SELECT
    sales_month,
    order_count,
    total_sales
FROM monthly_sales_summary_mv
ORDER BY sales_month DESC;

물리화 뷰를 사용할 때 가장 중요한 문장은 새로 고침입니다.

무엇을 하려는 문장인지
원본 주문 데이터가 바뀌었으므로 물리화 뷰의 저장된 결과를 최신 기준으로 다시 계산하고 싶다.

REFRESH MATERIALIZED VIEW monthly_sales_summary_mv;

결과 해석은 이렇습니다. orders와 order_items의 최신 데이터를 다시 읽어 monthly_sales_summary_mv 안의 결과를 갱신합니다. PostgreSQL의 REFRESH MATERIALIZED VIEW는 물리화 뷰의 기존 내용을 버리고, 저장해 둔 조회문을 다시 실행해 새 데이터로 채우는 명령입니다.2

물리화 뷰는 편리하지만 "언제 새로 고쳤는가"가 중요합니다. 매일 오전 9시에 보는 보고서라면, 오전 8시 50분에 새로 고쳤는지 확인해야 합니다. 새로 고치지 않은 물리화 뷰는 빠르지만 오래된 보고서일 수 있습니다.

뷰를 만들 때 이름을 잘 지어야 한다

뷰는 팀이 함께 쓰기 시작하면 데이터 자산이 됩니다. 그래서 이름을 대충 지으면 나중에 헷갈립니다.

좋은 이름은 짧고 의미가 분명해야 합니다.

좋은 예
- sales_summary
- monthly_sales_summary
- customer_order_summary
- low_stock_products

아쉬운 예
- test_view
- view1
- sales_new_final
- report_20260626_temp

이름만 보고도 무엇을 보여 주는지 알 수 있어야 합니다. 특히 summary, report, list, status 같은 단어를 일관되게 쓰면 좋습니다.

컬럼 이름도 마찬가지입니다.

좋은 예
- total_sales
- order_count
- sales_month
- customer_name

아쉬운 예
- sum
- count
- date
- name

sum이라고만 쓰면 무엇의 합계인지 모릅니다. total_sales라고 쓰면 매출 합계라는 뜻이 바로 보입니다. count보다 order_count가 낫습니다.

팀을 위한 뷰 설명 문서 만들기

뷰는 SQL을 줄여 주지만, 설명이 없으면 또 다른 혼란이 됩니다. 최소한 다음 네 가지는 적어 두는 것이 좋습니다.

뷰 이름: monthly_sales_summary
목적: 월별 결제 완료 주문의 매출과 주문 건수를 확인한다.
주요 기준: order_status = '결제완료'인 주문만 포함한다.
주요 컬럼:
- sales_month: 매출 월
- order_count: 해당 월 주문 건수
- total_sales: 해당 월 매출 합계
주의사항: 취소 주문과 결제 대기 주문은 제외된다.

이 정도 설명만 있어도 팀원이 숫자를 해석하기 쉬워집니다. 특히 보고서 숫자가 중요한 업무에서는 "무엇을 포함했고 무엇을 제외했는가"를 반드시 남겨야 합니다.

가능하다면 뷰를 만든 SQL도 함께 보관하세요. 회사의 문서 도구, 공유 폴더, 노션, 위키, 또는 단순한 텍스트 파일이어도 괜찮습니다. 중요한 것은 팀원이 같은 기준을 확인할 수 있어야 한다는 점입니다.

뷰 사용 전 점검할 것

뷰를 만들기 전에는 먼저 원본 SELECT 문을 충분히 확인해야 합니다. 뷰는 저장된 조회문이므로, 잘못된 SELECT를 저장하면 잘못된 보고서가 계속 재사용됩니다.

추천 순서는 다음과 같습니다.

  1. 원본 SQL을 작성한다.

  2. LIMIT로 일부 결과를 먼저 확인한다.

  3. 합계, 건수, 기간 조건이 맞는지 검토한다.

  4. 팀 기준과 맞는지 확인한다.

  5. 문제가 없으면 CREATE VIEW로 저장한다.

  6. 뷰를 다시 SELECT해서 결과를 확인한다.

예를 들어 월별 매출 뷰를 만들기 전에는 먼저 다음처럼 조회해 보는 것이 좋습니다.

무엇을 하려는 문장인지
뷰로 저장하기 전에 월별 매출 계산 결과가 맞는지 일부만 미리 확인하고 싶다.

SELECT
    date_trunc('month', o.order_date)::date AS sales_month,
    COUNT(DISTINCT o.order_id) AS order_count,
    SUM(oi.quantity * oi.unit_price) AS total_sales
FROM orders o
JOIN order_items oi
    ON o.order_id = oi.order_id
WHERE o.order_status = '결제완료'
GROUP BY date_trunc('month', o.order_date)::date
ORDER BY sales_month DESC
LIMIT 3;

이 결과를 확인한 뒤에 뷰로 저장해야 합니다. 뷰를 먼저 만들고 나중에 생각하는 방식은 좋지 않습니다.

이번 장에서 기억할 것

뷰는 자주 쓰는 SQL을 이름 붙여 저장한 가상 표입니다. 실제 데이터를 복사하지 않고, 저장된 SELECT 문을 통해 결과를 보여 줍니다. 그래서 복잡한 조인과 집계 SQL을 sales_summary, monthly_sales_summary 같은 이름으로 저장해 두면 팀원이 훨씬 쉽게 데이터를 조회할 수 있습니다.

일반 뷰는 조회할 때마다 계산하는 실시간 계산표에 가깝습니다. 물리화 뷰는 계산 결과를 미리 저장해 둔 결과표에 가깝습니다. 일반 뷰는 최신성이 좋고, 물리화 뷰는 반복 조회 속도에 유리할 수 있습니다. 대신 물리화 뷰는 새로 고침 시점을 관리해야 합니다.

마지막으로 뷰는 단순한 SQL 단축키가 아닙니다. 팀이 함께 쓰는 보고서 기준입니다. 이름, 컬럼, 포함 조건, 제외 조건을 분명히 남겨야 좋은 데이터 자산이 됩니다.

참고

1PostgreSQL Materialized Views

2PostgreSQL REFRESH MATERIALIZED VIEW

책 목록

비개발자를 위한 PostgreSQL 입문: 업무 데이터를 안전하게 다루는 가장 쉬운 방법

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기