[태그:] 쿼리

  • 데이터 분석의 지평을 넓히다, SQL 윈도우 함수(Window Function) 완벽 정복

    데이터 분석의 지평을 넓히다, SQL 윈도우 함수(Window Function) 완벽 정복

    데이터 분석가나 SQL을 다루는 개발자라면 누구나 한 번쯤은 이런 요구사항에 머리를 싸맨 경험이 있을 것입니다. “각 부서별로 직원들의 급여 순위를 매겨주세요.”, “월별 매출액과 함께 누적 매출액을 보여주세요.”, “각 직원의 급여를 바로 이전 입사자의 급여와 비교해주세요.” 기존의 GROUP BY를 활용한 집계 방식으로는 개별 행의 정보가 사라지기 때문에 이러한 요구사항을 해결하기가 매우 까다롭습니다. 여러 번의 서브쿼리나 복잡한 셀프 조인(Self-Join)을 사용해야만 겨우 원하는 결과를 얻을 수 있었죠. ‘윈도우 함수(Window Function)’는 바로 이러한 분석 쿼리의 한계를 극복하기 위해 탄생한 강력한 SQL 기능입니다.

    윈도우 함수는 행과 행 간의 관계를 쉽게 정의하고, 각 행의 위치에 기반하여 연산한 결과를 반환하는 함수입니다. 마치 데이터의 특정 범위(파티션)를 ‘창문(Window)’을 통해 들여다보며 계산하는 것과 같다고 해서 이런 이름이 붙었습니다. OLAP(Online Analytical Processing, 온라인 분석 처리) 환경에서 복잡한 분석 및 리포팅 쿼리를 작성하는 데 특화되어 있어 ‘OLAP 함수’라고도 불립니다. 이 글에서는 SQL 데이터 분석의 필수 스킬로 자리 잡은 윈도우 함수의 기본 개념부터 종류, 그리고 실전 활용법까지 체계적으로 알아보겠습니다.

    윈도우 함수의 핵심: OVER() 절 해부하기

    윈도우 함수의 모든 마법은 OVER()라는 키워드 안에서 이루어집니다. OVER() 절은 함수가 계산될 행의 집합, 즉 ‘윈도우’를 정의하는 역할을 합니다. 이 OVER() 절은 세 가지 주요 구성 요소로 나뉩니다.

    1. PARTITION BY: 창문의 구획 나누기

    PARTITION BY 절은 전체 데이터를 특정 기준에 따라 여러 개의 논리적인 그룹, 즉 ‘파티션’으로 분할합니다. 이는 GROUP BY 절과 역할이 유사하지만 결정적인 차이가 있습니다. GROUP BY는 그룹별로 하나의 결과 행만 반환하지만, PARTITION BY는 원본 데이터의 각 행은 그대로 유지한 채, 함수 계산을 위한 논리적인 경계만 설정합니다.

    • 예시: PARTITION BY department_id 라고 지정하면, 윈도우 함수는 각 부서(department_id) 안에서만 독립적으로 계산을 수행합니다. A 부서의 계산은 B 부서에 영향을 주지 않습니다.

    2. ORDER BY: 창문 안에서 순서 정하기

    ORDER BY 절은 파티션 내에서 어떤 순서로 데이터를 정렬하여 함수를 적용할지 결정합니다. 순위(Ranking)를 매기거나, 순서에 기반한 누적(Running Total) 값을 계산하는 등 순서가 중요한 윈도우 함수에서는 필수적인 요소입니다.

    • 예시: ORDER BY salary DESC 라고 지정하면, 파티션 내에서 급여(salary)가 높은 순서대로 정렬한 뒤, 그 순서에 따라 함수 계산이 이루어집니다.

    3. ROWS / RANGE (Frame): 계산 범위 지정하기

    ROWS 또는 RANGE 절은 파티션 내에서 현재 행을 기준으로 함수 계산에 포함될 구체적인 행의 범위를 지정합니다. 이를 ‘프레임(Frame)’이라고 부릅니다. 이 프레임은 동적으로 이동하며 계산을 수행합니다.

    • ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW: 파티션의 첫 번째 행부터 현재 행까지를 계산 범위로 지정합니다. (누적 합계를 구하는 데 주로 사용)
    • ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING: 현재 행의 바로 이전 행, 현재 행, 바로 다음 행, 이렇게 3개의 행을 계산 범위로 지정합니다. (이동 평균을 구하는 데 사용)

    이 세 가지 요소를 조합하여 함수() OVER (PARTITION BY ... ORDER BY ... ROWS/RANGE ...) 형태로 윈도우 함수를 사용하게 됩니다.


    윈도우 함수의 종류: 무엇을 할 수 있는가?

    윈도우 함수는 크게 순위 함수, 집계 함수, 행 순서 함수 세 가지 그룹으로 나눌 수 있습니다.

    1. 순위 함수 (Ranking Functions)

    파티션 내에서 각 행의 순서를 결정하는 함수들입니다.

    • RANK(): 일반적인 순위 함수. 동일한 값이면 같은 순위를 부여하고, 다음 순위는 공동 순위의 개수만큼 건너뜁니다. (예: 1, 2, 2, 4)
    • DENSE_RANK(): RANK()와 유사하지만, 공동 순위가 있어도 다음 순위를 건너뛰지 않고 연속적으로 부여합니다. (예: 1, 2, 2, 3)
    • ROW_NUMBER(): 동일한 값이라도 고유한 순위를 부여합니다. 공동 순위가 없습니다. (예: 1, 2, 3, 4)
    • NTILE(n): 파티션의 전체 행을 지정된 n개의 그룹(버킷)으로 나누고 각 행이 몇 번째 그룹에 속하는지 나타냅니다. (예: 고객을 매출 상위 4분위로 나눌 때 사용)
    함수설명예시 (점수: 100, 90, 90, 80)
    RANK()공동 순위 다음 순위 건너뜀1, 2, 2, 4
    DENSE_RANK()공동 순위 있어도 순위 연속적1, 2, 2, 3
    ROW_NUMBER()고유 순위 부여 (동점 없음)1, 2, 3, 4

    활용 예시: 각 부서(dept_no) 내에서 직원들의 급여(salary)가 높은 순으로 순위를 매기기

    SQL

    SELECT
        emp_name,
        dept_no,
        salary,
        RANK() OVER (PARTITION BY dept_no ORDER BY salary DESC) AS "부서별 급여 순위"
    FROM
        employees;
    

    2. 집계 함수 (Aggregate Functions)

    SUM(), AVG(), COUNT(), MAX(), MIN() 등 기존의 집계 함수들도 OVER() 절과 함께 사용되면 윈도우 함수로 작동합니다. 이 경우, GROUP BY와 달리 각 행의 원래 정보는 유지되면서 파티션별 집계 결과가 모든 행에 추가됩니다.

    활용 예시: 각 직원의 급여와 함께, 해당 직원이 속한 부서의 평균 급여를 함께 보여주기

    SQL

    SELECT
        emp_name,
        dept_no,
        salary,
        AVG(salary) OVER (PARTITION BY dept_no) AS "부서 평균 급여"
    FROM
        employees;
    

    활용 예시 2: 월별 매출과 함께 누적 매출(Running Total) 계산하기

    SQL

    SELECT
        sale_month,
        monthly_revenue,
        SUM(monthly_revenue) OVER (ORDER BY sale_month) AS "누적 매출"
    FROM
        sales;
    

    3. 행 순서 함수 (Value Functions)

    파티션 내에서 현재 행을 기준으로 특정 위치에 있는 행의 값을 가져오는 함수들입니다.

    • LAG(column, n, default): 현재 행을 기준으로 이전 n번째 행의 column 값을 가져옵니다.
    • LEAD(column, n, default): 현재 행을 기준으로 이후 n번째 행의 column 값을 가져옵니다.
    • FIRST_VALUE(column): 파티션 내에서 정렬 순서상 가장 첫 번째 행의 column 값을 가져옵니다.
    • LAST_VALUE(column): 파티션 내에서 정렬 순서상 가장 마지막 행의 column 값을 가져옵니다.

    활용 예시: 각 월의 매출을 바로 전월 매출과 비교하기

    SQL

    SELECT
        sale_month,
        monthly_revenue,
        LAG(monthly_revenue, 1, 0) OVER (ORDER BY sale_month) AS "전월 매출"
    FROM
        sales;
    

    이 쿼리를 통해 monthly_revenue - LAG(...) 와 같은 간단한 연산으로 전월 대비 매출 성장률을 쉽게 계산할 수 있습니다.


    윈도우 함수 vs. GROUP BY: 결정적 차이

    윈도우 함수와 GROUP BY는 데이터를 그룹화하여 집계한다는 점에서 유사해 보이지만, 그 결과와 목적은 완전히 다릅니다.

    • GROUP BY: 원본 데이터의 여러 행을 그룹별로 ‘요약’하여 그룹당 하나의 행만 남깁니다. 개별 행의 정보는 집계 과정에서 사라집니다. (예: 부서별 평균 급여)
    • 윈도우 함수: 원본 데이터의 모든 행을 그대로 유지하면서, 각 행에 대한 ‘추가 정보’를 계산하여 보여줍니다. (예: 각 직원의 급여와 그가 속한 부서의 평균 급여)

    즉, 개별 데이터의 상세 정보와 그룹 전체의 통계 정보를 함께 보고 싶을 때, 윈도우 함수는 그 진가를 발휘합니다. GROUP BY를 사용했다면, 부서별 평균 급여를 구한 뒤 원래 직원 테이블과 다시 조인해야 하는 번거로운 과정을 거쳐야 했을 것입니다.

    결론: 복잡한 분석 쿼리를 위한 우아한 해결책

    윈도우 함수는 현대적인 데이터 분석 환경에서 더 이상 선택이 아닌 필수 기능입니다. 복잡한 서브쿼리와 조인을 사용해야만 가능했던 데이터 순위, 누적 합계, 이동 평균, 기간별 비교 등의 분석 작업을 단 몇 줄의 직관적인 코드로 해결할 수 있게 해줍니다. 이는 SQL 쿼리의 가독성을 높이고 유지보수를 용이하게 만들 뿐만 아니라, 데이터베이스 시스템 내부에서 더 효율적으로 처리되어 성능상의 이점을 가져다주기도 합니다.

    처음에는 OVER (PARTITION BY ... ORDER BY ...) 구문이 다소 복잡하게 느껴질 수 있지만, 각 요소가 ‘어떤 그룹에서’, ‘어떤 순서로’, ‘어떤 범위를’ 계산할지 정의하는 논리적 흐름이라는 것을 이해하면 금방 익숙해질 수 있습니다. 데이터로부터 더 깊이 있는 인사이트를 빠르고 우아하게 추출하고 싶다면, 윈도우 함수라는 강력한 무기를 반드시 당신의 것으로 만드시길 바랍니다.

  • 데이터를 바라보는 새로운 창, 뷰(View)의 힘과 지혜

    데이터를 바라보는 새로운 창, 뷰(View)의 힘과 지혜

    데이터베이스의 세계에서 ‘뷰(View)’는 사용자가 데이터를 바라보는 방식을 재구성하는 강력하고 우아한 도구입니다. 뷰는 하나 이상의 기본 테이블(Base Table)로부터 유도된, 이름을 가지는 ‘가상 테이블(Virtual Table)’입니다. 여기서 가장 중요한 키워드는 ‘가상’입니다. 뷰는 실제 물리적인 데이터를 저장하고 있지 않으며, 단지 데이터베이스에 저장된 하나의 SQL 쿼리(SELECT 문)에 불과합니다. 하지만 사용자에게는 마치 실제 데이터가 존재하는 테이블처럼 보이고, 동일한 방식으로 데이터를 조회할 수 있는 편리함을 제공합니다.

    뷰를 사용하는 것은 마치 복잡한 도시의 풍경을 특정 목적에 맞게 편집하여 보여주는 ‘창문’을 만드는 것과 같습니다. 어떤 창문을 통해서는 도시의 아름다운 공원만 보이게 할 수 있고(단순성), 다른 창문을 통해서는 보안 시설을 제외한 전경만 보이도록 제한할 수 있으며(보안성), 도시의 일부 건물이 리모델링되더라도 창문의 풍경은 그대로 유지되도록 만들 수도 있습니다(독립성). 이처럼 뷰는 복잡한 데이터의 원본은 그대로 둔 채, 사용자에게 필요한 맞춤형 데이터 창을 제공함으로써 데이터베이스의 보안, 단순성, 그리고 독립성을 크게 향상시키는 핵심적인 역할을 수행합니다. 이 글에서는 뷰가 왜 필요한지, 어떻게 동작하며, 실제 업무에서 어떻게 활용될 수 있는지 그 힘과 지혜를 깊이 있게 탐구해 보겠습니다.

    뷰(View)를 사용하는 세 가지 핵심 이유

    뷰는 단순히 SQL 쿼리를 저장하는 것 이상의 중요한 가치를 제공합니다. 뷰를 활용함으로써 얻을 수 있는 이점은 크게 보안성 강화, 복잡성 완화, 그리고 논리적 데이터 독립성 확보라는 세 가지로 요약할 수 있습니다.

    보안성: 보여주고 싶은 것만 안전하게

    뷰의 가장 중요한 역할 중 하나는 데이터베이스 보안을 강화하는 것입니다. 기본 테이블에는 수많은 열(Column)이 존재하며, 그중에는 급여나 개인 연락처와 같은 민감한 정보가 포함될 수 있습니다. 모든 사용자에게 이 기본 테이블에 대한 접근 권한을 직접 부여하는 것은 심각한 보안 위험을 초래할 수 있습니다. 이때 뷰를 사용하면 특정 사용자 그룹에게 필요한 데이터만 선택적으로 노출하는 것이 가능합니다.

    예를 들어, 회사의 EMPLOYEES 테이블에 emp_id(사번), emp_name(이름), department(부서), salary(급여)라는 열이 있다고 가정해 봅시다. 일반 직원들에게는 다른 직원들의 급여 정보를 보여주어서는 안 됩니다. 이 경우, 급여 정보를 제외한 뷰를 생성하여 권한을 부여할 수 있습니다.

    CREATE VIEW VW_EMP_PUBLIC_INFO AS SELECT emp_id, emp_name, department FROM EMPLOYEES;

    — 일반 사용자 그룹(public_role)에게는 뷰에 대한 조회 권한만 부여 GRANT SELECT ON VW_EMP_PUBLIC_INFO TO public_role;

    이제 public_role을 가진 사용자들은 VW_EMP_PUBLIC_INFO 뷰를 통해 다른 직원들의 이름과 부서는 조회할 수 있지만, 원래 테이블인 EMPLOYEES에는 접근할 수 없으므로 민감한 salary 정보는 완벽하게 숨길 수 있습니다. 이처럼 뷰는 데이터 접근을 세밀하게 제어하는 효과적인 보안 계층(Security Layer)으로 작동합니다.

    단순성: 복잡한 쿼리를 감추다

    현대의 관계형 데이터베이스는 정규화를 통해 데이터가 여러 테이블에 나뉘어 저장되는 경우가 많습니다. 따라서 의미 있는 정보를 얻기 위해서는 여러 테이블을 JOIN하고, 데이터를 집계하며, 복잡한 조건을 거는 SQL 쿼리를 작성해야 합니다. 이러한 복잡한 쿼리는 작성하기 어려울 뿐만 아니라, 자주 사용될 경우 반복적인 작업으로 인해 생산성을 저하시킵니다. 뷰는 이처럼 복잡한 쿼리 자체를 데이터베이스에 저장하고 단순한 이름으로 대체하여 사용자의 편의성을 크게 높여줍니다.

    예를 들어, ‘고객별 총 주문 금액’을 계산하기 위해 CUSTOMERS, ORDERS, ORDER_DETAILS라는 세 개의 테이블을 조인하고 GROUP BY를 사용해야 하는 복잡한 쿼리가 있다고 가정해 봅시다.

    — 복잡한 원본 쿼리 SELECT c.customer_name, SUM(od.quantity * od.unit_price) AS total_purchase FROM CUSTOMERS c JOIN ORDERS o ON c.customer_id = o.customer_id JOIN ORDER_DETAILS od ON o.order_id = od.order_id GROUP BY c.customer_name;

    이 쿼리를 뷰로 만들어두면, 데이터 분석가나 일반 사용자들은 복잡한 JOIN 구조를 전혀 알 필요 없이 간단한 쿼리만으로 동일한 결과를 얻을 수 있습니다.

    CREATE VIEW VW_CUSTOMER_TOTAL_PURCHASE AS — (위의 복잡한 쿼리 내용)

    — 단순화된 쿼리 SELECT * FROM VW_CUSTOMER_TOTAL_PURCHASE ORDER BY total_purchase DESC;

    이처럼 뷰는 복잡한 데이터베이스 로직을 추상화하고 캡슐화하여, 사용자가 데이터의 물리적 구조가 아닌 논리적 구조에만 집중할 수 있도록 돕습니다.

    논리적 데이터 독립성: 변화에 유연하게 대응하다

    논리적 데이터 독립성은 데이터베이스의 스키마 구조가 변경되더라도, 기존의 응용 프로그램은 영향을 받지 않도록 하는 중요한 개념입니다. 뷰는 이러한 독립성을 확보하는 데 결정적인 역할을 합니다. 만약 응용 프로그램이 기본 테이블에 직접 접근하고 있다면, 해당 테이블의 이름이 바뀌거나 특정 열이 다른 테이블로 분리되는 등의 스키마 변경이 발생했을 때 모든 응용 프로그램의 코드를 수정해야 하는 대규모 작업이 필요합니다.

    하지만 응용 프로그램이 뷰를 통해 데이터에 접근하도록 설계되었다면, 상황은 달라집니다. 스키마가 변경되더라도, 관리자는 변경된 스키마 구조에 맞게 뷰의 정의(SELECT 문)만 수정해주면 됩니다. 응용 프로그램은 기존과 동일한 뷰의 이름을 계속 사용하면 되므로, 아무런 코드 변경 없이 서비스를 이어나갈 수 있습니다. 뷰가 기본 테이블과 응용 프로그램 사이에서 일종의 ‘어댑터’ 또는 ‘인터페이스’ 역할을 수행하여 양쪽의 변경으로부터 서로를 보호해주는 것입니다. 이는 시스템의 유지보수성과 유연성을 크게 향상시킵니다.

    뷰의 생성과 관리, 그리고 한계

    뷰를 생성하는 것은 DDL 명령어인 CREATE VIEW를 통해 이루어집니다. 한번 생성된 뷰는 DROP VIEW를 통해 삭제할 수 있으며, CREATE OR REPLACE VIEW 구문을 사용하면 기존에 뷰가 존재할 경우 내용을 덮어쓰고, 존재하지 않을 경우 새로 생성하여 편리하게 관리할 수 있습니다.

    CREATE OR REPLACE VIEW view_name AS select_statement;

    하지만 뷰는 만능이 아닙니다. 가장 큰 한계는 뷰를 통한 데이터 수정(INSERT, UPDATE, DELETE)에 제약이 많다는 점입니다. 데이터베이스 시스템은 뷰에 대한 수정 요청이 들어왔을 때, 이 요청을 기본 테이블의 어떤 행에 대한 요청인지 명확하게 추적할 수 있어야만 합니다. 따라서 다음과 같이 여러 기본 테이블의 행과 뷰의 행이 1:1로 매핑되지 않는 복잡한 뷰는 일반적으로 수정이 불가능합니다.

    • 여러 테이블을 JOIN한 뷰
    • GROUP BY, HAVING 절을 사용하거나 집계 함수(SUM, COUNT 등)를 포함한 뷰
    • DISTINCT 키워드를 사용한 뷰
    • 하위 쿼리(Subquery)를 포함하면서 기본 테이블의 행을 고유하게 식별할 수 없는 뷰

    따라서 뷰는 주로 데이터 ‘조회’의 용도로 사용되며, 뷰를 통해 데이터를 수정하는 것은 매우 제한적인 경우에만 신중하게 사용해야 합니다.

    특별한 뷰: 머티리얼라이즈드 뷰 (Materialized View)

    일반적인 뷰가 데이터를 저장하지 않는 ‘가상’ 테이블인 반면, ‘머티리얼라이즈드 뷰(Materialized View, 구체화된 뷰)’는 뷰의 정의에 따라 계산된 결과를 실제 물리적인 테이블로 저장하는 특별한 형태의 뷰입니다. 이는 데이터 웨어하우스(DW)나 대규모 데이터 분석 환경에서 성능 최적화를 위해 사용됩니다.

    매우 복잡하고 실행하는 데 시간이 오래 걸리는 쿼리가 있다면, 이 쿼리를 머티리얼라이즈드 뷰로 만들어두면 최초 한 번만 실행하여 결과를 저장해 둡니다. 그 이후부터 사용자는 이 뷰를 조회할 때, 복잡한 쿼리를 다시 실행하는 대신 이미 저장된 결과를 즉시 가져오므로 매우 빠른 응답 속도를 얻을 수 있습니다. 물론, 기본 테이블의 데이터가 변경되면 머티리얼라이즈드 뷰의 데이터도 언젠가는 갱신(Refresh)해주어야 하는 추가적인 관리 비용이 발생하며, 데이터가 최신 상태가 아닐 수 있다는 단점이 있습니다. 하지만 응답 속도가 매우 중요한 리포팅이나 대시보드 시스템에서 이 기법은 매우 효과적으로 사용됩니다.

    결론: 데이터의 복잡성을 다루는 현명한 방법

    뷰는 데이터베이스의 물리적 구조는 그대로 둔 채, 사용자에게 논리적으로 재구성된 데이터의 창을 제공하는 강력한 추상화 도구입니다. 뷰를 통해 우리는 민감한 데이터를 숨겨 보안을 강화하고, 복잡한 쿼리를 단순화하여 사용 편의성을 높이며, 데이터 구조의 변경으로부터 응용 프로그램을 보호하여 시스템의 유연성을 확보할 수 있습니다.

    물론, 뷰를 통한 데이터 수정의 제약이나 무분별한 뷰 사용이 초래할 수 있는 성능 문제 등 고려해야 할 점도 분명히 존재합니다. 하지만 이러한 특징과 한계를 명확히 이해하고 적재적소에 뷰를 활용한다면, 우리는 거대하고 복잡한 데이터의 세계를 훨씬 더 안전하고, 단순하며, 현명하게 다룰 수 있을 것입니다. 결국 뷰는 데이터를 어떻게 바라볼 것인가에 대한 기술적 해답이자, 데이터베이스를 설계하고 사용하는 지혜의 한 형태라고 할 수 있습니다.