SQLite 윈도우 함수
SQLite는 작고 빠른 데이터베이스 엔진이다. 다른 DBMS 처럼 서버
프로세스들 띄울 필요 없이 명령행 셸(sqlite3
)로 데이터베이스 파일을
열어 SQL을 실행할 수 있다. CSV 파일을 바로 SQLite 테이블로 로드할 수
있어, 간단한 데이터 분석 시 유용하게 사용할 수 있다.
SQLite에서 지원하는 SQL 수준은 매우 초보적이었던 것으로 기억하는 데, 최근 다시 보니 윈도우 함수뿐 아니라 재귀적 CTE(Common Table Expressin)까지 지원한다는 사실을 알게 되었다. 이 글에서는 예전 기억을 되살리며 SQLite에서 윈도우 함수를 간단히 테스트 해보려 한다.
Oracle 데이터베이스로 SQL을 배울 때 사용했던 HR 스키마(Human Resources Sample Schema)를 사용할 것이다. ERD는 다음과 같다. 상용 ERD 도구를 이용하면 테이블 관계를 나타낼 때 어떤 컬럼이 이용되는지까지 표시할 수 있지만, PlantUML에서는 그렇게까지는 못 하는 것 같다.
관계에 레이블을 넣을 수 있지만, 그 경우 레이블이 테이블과 겹쳐 그림을
알아보기가 더 어려워져서 넣지 않기로 했다. 관계에 어떤 컬럼이
사용되는지는 각 테이블의 PK 컬럼 이름을 보면 쉽게 알 수 있다. 사실
여기서는 Employees
와 Departments
만 사용하므로 크게 중요하지도 않다.
데이터베이스 준비
쿼리를 테스트하려면 먼저 테이블을 만들고 데이터를 로드해야 한다. Human Resources Sample Schema에서 SQL 스크립트를 다운로드한 다음 SQLite에서 실행해 테이블과 인덱스를 생성하고 데이터를 로드할 수 있다. Oracle에서만 동작하는 코드는 모두 삭제한다.
SQLite 셸의 디폴트 모드는 list
인데, 알아보기가 쉽지 않다. 모드를
column
또는 table,
box
로 바꾸면 쿼리 실행 결과가 알아보기 쉬운 형태로
표시된다. HR 스키마에서 다운로드한 스크립트는 .read
명령을 사용해
실행할 수 있다.
$ sqlite3 hr.db
SQLite version 3.43.2 2023-10-10 13:08:14
Enter ".help" for usage hints.
sqlite> .mode column
sqlite> .read hr_create.sql
sqlite> .read hr_populate.sql
윈도우 함수 테스트
윈도우 함수는 입력 값을 윈도우에서 취하는 함수다. 여기서 윈도우는
select
결과에서 하나 또는 그 이상의 행을 말한다. 윈도우 함수에는 over
절이 따르며 이를 통해 파티션, 프레임 등을 설정한다. 윈도우 함수를
이용하면 쿼리로 해결하기 매우 어려워 보이는 문제도 쉽게 해결할 수
있다.
부서별 최고 급여 수령자
각 부서별로 급여가 가장 높은 직원 목록을 알고 싶다면 어떻게 쿼리를
작성해야 할까? 윈도우 함수를 쓰지 않고 작업하려면 매우 복잡하고
비직관적인 쿼리를 작성해야 할 것이다. 윈도우 함수 rank
를 사용하면
다음과 같이 쉽게 구할 수 있다. over
절을 보면 부서 별로 파티션을 하고
급여로 정렬해 rank
를 구하는 것을 볼 수 있다.
select department_name dept_name
, employee_id emp_id
, emp_name
, job_title
, salary
from (
select employee_id
, first_name ||' '||last_name emp_name
, department_name
, job_title
, salary
, rank() over (partition by department_id order by salary desc) rnk
from employees
join departments using (department_id)
join jobs using (job_id)
)
where rnk = 1
order by salary desc
;
dept_name emp_id emp_name job_title salary ---------------- ------ ---------------- ------------------------------- ------ Executive 100 Steven King President 24000 Sales 145 John Singh Sales Manager 14000 Marketing 201 Michael Martinez Marketing Manager 13000 Finance 108 Nancy Gruenberg Finance Manager 12008 Accounting 205 Shelley Higgins Accounting Manager 12008 Purchasing 114 Den Li Purchasing Manager 11000 Public Relations 204 Hermann Brown Public Relations Representative 10000 IT 103 Alexander James Programmer 9000 Shipping 121 Adam Fripp Stock Manager 8200 Human Resources 203 Susan Jacobs Human Resources Representative 6500 Administration 200 Jennifer Whalen Administration Assistant 4400
연도별 누적 직원 수
회사가 성장함에 따라 직원수가 늘어났다. 시간이 지남에 따라 직원이
어떻게 늘어났는지 확인하기 위해 연도별 누적 직원 수가 필요하다고
생각해보자. 이 문제 역시 윈도우 함수를 사용하면 간단히 해결할 수
있다. 연도별로 group by
해 연도별 직원수를 구한 다음, 윈도우 함수로
누적 합계를 구하면 된다.
날짜 함수는 DB마다 다르다. SQLite에서는 strftime
함수를 사용해
날짜에서 연도를 뽑아낼 수 있다.
select yyyy
, sum(cnt) over (order by yyyy) cnt
from (
select strftime('%Y', hire_date) yyyy
, count() cnt
from employees
group by strftime('%Y', hire_date)
)
;
yyyy cnt ---- --- 2011 1 2012 8 2013 14 2014 24 2015 53 2016 77 2017 96 2018 107
전년 대비 증가 직원 수
여기에 조금 더해서, 연도별로 전년 대비 증가 직원수도 알고 싶다면 어떻게
해야 할까? 위 쿼리에서 sum
함수에 대한 프레임을 설정할 때 파티션을
지정하지 않았으므로, 프레임은 처음부터 현재 행까지가 된다. 처음부터
현재 행 바로 전까지만 합계를 구할 수 있다면 전년 대비 증가 직원수를
쉽게 구할 수 있다.
윈도우 함수에서는 다음과 같은 식으로 프레임을 정의해, 처음부터 현재 행 바로 전까지만 합을 구하는 것이 가능하다. 현재 행을 기준으로 해당 연도까지 누적 직원 수와 그 전년도까지 누적 직원수를 구했다면, 증가 직원수는 그 차가 된다.
select yyyy
, cnt1 cnt
, cnt1 - cnt2 delta
from (
select yyyy
, sum(cnt) over (order by yyyy) cnt1
, sum(cnt) over (order by yyyy
range between unbounded preceding
and current row
exclude current row) cnt2
from (
select strftime('%Y', hire_date) yyyy
, count() cnt
from employees
group by strftime('%Y', hire_date)
)
)
;
yyyy cnt delta ---- --- ----- 2011 1 2012 8 7 2013 14 6 2014 24 10 2015 53 29 2016 77 24 2017 96 19 2018 107 11
윈도우 함수에서 프레임 설정에 대한 상세 사항은 Window Functions > Frame Specifications를 참조하기 바란다.
위 쿼리에서 두 번째 sum
은 over
절이 너무 복잡한 것 같다. 만약 연도별
누적 직원수 쿼리 결과에서 바로 전 행을 참조할 수 있다면 어떨까? 다음과
같이 lag
함수를 사용하면 쿼리를 조금 간단하게 만들 수 있다. 현재 행
누적 직원수에서 바로 전 행 누적 직원수를 빼면 증가한 직원수가 된다.
select yyyy
, cnt
, cnt - lag(cnt, 1) over (order by yyyy) delta
from (
select yyyy
, sum(cnt) over (order by yyyy) cnt
from (
select strftime('%Y', hire_date) yyyy
, count() cnt
from employees
group by strftime('%Y', hire_date)
)
)
;
yyyy cnt delta ---- --- ----- 2011 1 2012 8 7 2013 14 6 2014 24 10 2015 53 29 2016 77 24 2017 96 19 2018 107 11
결론
SQLite에서 몇 가지 예제를 통해 윈도우 함수 사용법을 간단히
확인해봤다. 한 가지 주목할 점은 sum
, count
와 같은 일반 집계
함수(aggregate function) 뒤에 over
절을 추가하면, 집계 함수가 over
절로
명시한 윈도우 프레임 안의 행에 대해 동작한다는 점이다.
여기서 살펴본 예제만으로 윈도우 함수를 제대로 알기에는 무리가 있겠지만, 윈도우, 파티션, 프레임 등의 개념을 이해하고 나면 윈도우 함수를 활용하는 것이 재미있어진다. 윈도우 함수를 활용하면 쿼리로 불가능해 보이는 작업도 간단히 해결할 수 있는 경우가 많다.
사족
이 글은 Emacs에서 org-mode(ob-sqlite)를 사용해 작성했다. 쿼리를 실행하면 결과과 본문에 삽입되며, 쿼리를 쉽게 수정하고 실행할 수 있다. SQLite 셸에서 복잡한 쿼리를 편집하는 것은 매우 불편하므로, Emacs에서 쿼리를 편집/실행할 수 있다는 것은 큰 장점이 아닐 수 없다.
그러나 몇 가지 불편한 점도 있었다. 일단 Emacs에 내장된 sql-mode가 별로 훌륭하지 않은 듯 하다. 쿼리를 편집할 때 제멋대로 들여쓰기를 바꿔버리는 문제가 있는데, org-mode에서 편집할 때만 그런 것 같다. 내 Emacs 설정이 잘못되어 그런 것인지도 모르겠다.
코드블록에서 Sqlite에 대해 쿼리를 실행하려면 언어를 sqlite
로 설정해야
하는데, 이렇게 하면 HTML로 렌더링했을 때 언어가 SQL로 인식되지 않아
문법 강조가 되지 않는다. 또 쿼리를 실행하면 결과가 본문에 삽입되긴
하는데, 이게 올바른 결과 형식으로 들어가지 않는다. HTML로 제대로
렌더링 되게 하려면 약간 수정이 필요하다.