답사에 앞서 — pandas의 시대에 SQL을 다시 묻는다
WHY THIS BOOK BEGINS WITH SQL
데이터 분석이라 하면 많은 개발자가 먼저 pandas를 떠올린다. 근래에는 Polars가 더 빠르고 효율적인 DataFrame 처리 대안으로 부상했다. 그러나 이런 라이브러리들의 인기와는 별개로, 개발자 사회에서 가장 널리 알려지고 가장 많이 쓰이는 언어는 여전히 SQL이다. 데이터가 SQL을 지원하는 데이터베이스에 들어 있다면, 그 데이터를 질의하고 가공하는 가장 직관적이고 효과적인 방법은 대개 SQL이다.
파이썬이 데이터 과학의 지배 언어가 된 것은 사실이다. 특히 DataFrame 객체를 통해 표 형식 데이터를 다루는 영역에서 그렇다. 그럼에도 SQL은 데이터의 보편어(universal language)로 남아 있다. 그렇다면 이미 SQL에 익숙한 개발자가 데이터 가공에 SQL을 곧바로 쓰는 편이 더 효율적이지 않겠는가. 원저자가 제1장의 문을 여는 물음이 바로 이것이다.
DuckDB가 빛을 발하는 지점이 여기다. DuckDB는 2018년 빠른 분석 질의에 최적화된 OLAP(online analytical processing) 데이터베이스로 처음 구상되었다. 목표는 완전한 형태를 갖춘 데이터베이스 시스템과 SQLite 같은 내장형 DB의 간결함 사이에 놓인 간극을 메우는 것, 다만 초점은 트랜잭션이 아니라 분석 작업 부하에 두는 것이었다.
- 2018 OLAP 전용 데이터베이스로 최초 구상. 완전한 DBMS와 내장형 DB 사이의 간극을 메우는 것이 목표였다.
- 2019 첫 안정 릴리스. 파이썬과 R에 손쉽게 통합된다는 성질이 데이터 과학·분석 진영에서 폭넓은 지지를 얻는 계기가 되었다.
- 2021 DuckDB는 오픈소스이지만, 상업적 지원과 지속적 개발을 담당할 DuckDB Labs가 설립되었다.
- 이후 DuckDB를 클라우드로 옮기기 위해 MotherDuck이 그 위에 세워졌다. SaaS 형태로 접근이 가능해지면서 분산·관리형 환경에서의 사용, 더 큰 데이터셋으로의 확장, 협업 사례가 열렸다. 이 대목은 원서 제9장에서 본격적으로 다룬다.
제1장이 답하려는 것은 세 가지다. DuckDB란 무엇인가, 왜 데이터 분석에서 강력한 도구인가, 그 역량을 어떻게 끌어와 분석 작업을 간결하게 만들 것인가. DuckDB는 SQL의 성능과 유연성을 파이썬 환경 안에서 그대로 제공한다. 데이터 과학자와 분석가에게 이는 값을 매기기 어려운 도구다.
DuckDB란 무엇인가
INTRODUCTION TO DUCKDB
DuckDB는 SQL을 지원하는 관계형 데이터베이스 관리 시스템(RDBMS)이며, OLAP를 위해 특별히 설계되었다. 따라서 데이터 분석 과제에 적합하다. 이 한 문장에 네 개의 성질이 겹쳐 있다. 하나하나 뜯어 보면 이 건물의 구조가 드러난다.
설치가 없는 데이터베이스
in-process별도 설치 과정을 요구하는 전통적 데이터베이스 시스템과 달리 DuckDB는 전적으로 프로세스 안에서(in-process) 동작한다. 설치나 설정을 걱정할 필요가 없다.
pandas 데이터에 직접 질의
zero-copy on DataFrames가장 설득력 있는 특징 하나는 pandas 데이터에 대해 SQL 질의를 곧바로 실행한다는 점이다. 데이터를 별도로 가져오거나 복제할 필요가 없다. pandas 생태계에 익숙한 이에게는 대단히 강력한 성질이다.
벡터화 데이터 처리
vectorized processing단일 기계 안에서 CPU에 친화적인 덩어리(chunk) 단위로 데이터를 처리해 효율을 크게 높인다. 여러 노드에 데이터와 계산을 분산해 대규모 클러스터의 병렬성으로 확장성을 얻는 Spark나 Flink 같은 빅데이터 프레임워크와는 대조적인 접근이다.
열 기반 저장 형식
columnar storageMySQL이나 SQLite 같은 데이터베이스의 전통적 행 기반 저장 형식 대신 열 기반 저장 형식을 채택한다. 이 열 지향 구조가 고성능의 열쇠다. 특히 대규모 분석 질의에서, 속도와 효율이 중요한 상황에서 두각을 드러낸다.
왜 DuckDB인가 — 서버라는 우회로를 지운다
WHY USE DUCKDB?
오늘날 데이터셋은 대개 다음 가운데 하나 이상에서 온다. CSV(comma-separated values) 파일, 엑셀 스프레드시트, XML 파일, JSON 파일, Parquet 파일, 그리고 데이터베이스.
ELT(extract, load, transform) 과정에서 SQL을 쓰려 한다면, 통상 절차는 이렇다. 먼저 CSV 파일 같은 데이터셋을 데이터베이스 서버에 적재한다. 그다음 파이썬 등으로 작성한 응용에서 SQL을 통해 데이터를 pandas DataFrame으로 불러온다. 목적지에 이르기 전에 반드시 거쳐야 하는 관문이 하나 서 있는 셈이다.
ELT는 데이터 파이프라인에서 데이터를 옮기고 분석에 쓸 수 있도록 준비하는 데이터 통합 과정이다. 세 단계로 이루어진다.
- Extract — 데이터베이스, API, 평문 파일 등 여러 원천에서 데이터를 추출한다.
- Load — 추출한 데이터를 데이터 웨어하우스나 데이터 레이크 같은 대상 시스템에 곧바로 적재한다.
- Transform — 여과, 집계, 정제 등으로 데이터를 변환한다.
DuckDB는 데이터셋을 데이터베이스 서버에 적재할 필요를 없애고, SQL로 데이터셋을 직접 다루게 한다. 일단 pandas DataFrame이 메모리에 올라와 있다면 DuckDB와 SQL로 그 데이터를 계속 잘게 나누고 조합할 수 있다. 별도 데이터베이스 서버의 부담 없이 SQL의 강점만 파이썬 환경 안에서 취하는 구조다.
성능을 떠받치는 일곱 기둥
HIGH-PERFORMANCE ANALYTICAL QUERIES
DuckDB의 강점 하나는 분석 질의를 빠르게 수행하는 능력이며, 이 덕분에 데이터 집약적 과제에 유효한 도구가 된다. 그 성능은 몇 가지 핵심 설계 특징에서 나온다. 원저자가 꼽은 일곱 항목을 순서대로 따라가면, 앞의 열 기반 저장이라는 주초 위에 어떤 구조가 얹혔는지 보인다.
열 기반 저장 형식
columnar storage format행의 모든 필드를 함께 저장하는 행 기반 형식과 달리, 데이터를 행 단위가 아니라 열 단위로 저장한다. 질의가 여러 행에 걸쳐 소수의 열만 읽고 분석하는 분석 작업 부하에 특히 유리하다. 필요한 열만 디스크에서 읽으므로 메모리로 옮겨야 하는 데이터량이 크게 줄고, 질의 실행이 빨라진다. 열 열 가운데 두 열만 필요한 질의라면 DuckDB는 나머지를 무시하지만, 행 기반 데이터베이스는 모든 행의 모든 열을 적재해야 한다.
벡터화 실행 엔진
vectorized execution engine한 행씩 처리하지 않고 행 덩어리를 묶은 벡터 단위로 데이터를 처리한다. 이 기법을 벡터화 실행이라 부르며, CPU를 더 효율적으로 쓰게 한다. 여러 행을 한 번에 다루므로 메모리 접근과 명령 디스패치처럼 행 단위 처리에 따르는 부대 비용이 줄어든다. 또한 CPU 캐시를 더 잘 활용해 캐시 미스 빈도를 낮춘다. 현대 하드웨어에 맞춘 설계이며, 복잡한 분석 질의에서 특히 실행 시간이 짧아진다.
효율적 메모리 사용
efficient memory usage메모리 내 자료 구조를 직접 다루도록 설계되어, 연산을 지연시킬 수 있는 불필요한 데이터 복사를 만들지 않는다. 과도한 메모리 없이 대용량 데이터셋을 처리하며, 질의 실행 중 병목이 생기지 않도록 메모리를 지능적으로 관리한다. 덩어리 단위 처리 능력도 여기에 기여한다. 한 번에 메모리에 다 올릴 수 없는 데이터도 조금씩 나누어 처리하므로 자원 사용이 더욱 최적화된다.
병렬 실행
parallel execution현대 CPU는 통상 여러 코어를 갖추어 여러 연산을 동시에 수행한다. DuckDB는 이 점을 온전히 활용해 서로 다른 CPU 코어에서 질의를 병렬로 실행한다. 질의의 일부가 데이터의 서로 다른 부분에서 동시에 수행되므로 대용량 데이터셋 처리가 빨라진다. 예컨대 대규모 데이터셋에 대한 집계나 조인이라면, 작업을 더 작은 덩어리로 쪼개 동시에 처리하며 가용한 처리 능력을 모두 끌어 쓴다.
지연 실체화
late materialization데이터를 꼭 필요한 시점에만 가져오고 처리하는 지연 실체화 기법을 쓴다. 전통적 데이터베이스는 최종 결과에 일부 열만 필요해도 질의 실행 초기에 전체 행을 메모리로 적재하는 실체화를 수행한다. DuckDB는 이 단계를 가능한 한 뒤로 미루고, 실제 행 데이터 대신 열 인덱스 같은 메타데이터를 다루다가 결과에 필요한 특정 열만 실체화한다. 불필요한 데이터 이동과 처리가 최소화되며, 대규모 데이터셋의 여과나 조인이 얽힌 복잡한 질의에서 성능 향상이 크다.
최적화된 질의 계획기
optimized query planner질의를 실행하기 전에 분석하고 재구성하는 질의 계획기를 갖추었다. 계획기의 임무는 질의를 실행할 가장 효율적인 방법을 찾는 것이며, 특히 조인·집계·여과처럼 자원을 많이 쓰는 연산에서 중요하다. 질의 계획을 재편하고 predicate pushdown(여과 조건을 데이터 원천에 최대한 가깝게 밀어 내리기), join reordering(테이블 조인의 가장 효율적인 순서 선택) 같은 고급 최적화를 적용해 계산 부담을 줄인다.
이식성
portability복잡한 서버 구성이나 외부 의존을 요구하는 전통적 데이터베이스와 달리 DuckDB는 응용 안에서 곧바로 실행되는 in-process 데이터베이스다. 별도 서버가 없으므로 이식성이 매우 높고, 지역 응용부터 데이터 과학 노트북까지 특별한 설정 없이 폭넓은 환경에 임베드된다. 무거운 기반 시설에 의존하지 않고 자기 기계에서 데이터를 분석하려는 이에게 특히 유용하다. 파이썬·R은 물론 다른 응용 안에도 최소한의 노력으로 심을 수 있다. 작은 설치 규모와 CSV·Parquet 등 다양한 파일 형식과의 매끄러운 연동 덕에 Windows·Linux·macOS를 넘나들며 분석 작업 흐름을 어디로든 가지고 다닐 수 있다.
여러 언어에 걸친 통합과 쓰임의 편의
VERSATILE INTEGRATION AND EASE OF USE
DuckDB는 SELECT, INSERT, UPDATE, DELETE를 포함한 표준 SQL 구문을 온전히 지원한다. CSV, Parquet, JSON, pandas DataFrame 등 여러 데이터 형식과 매끄럽게 통합된다. Jupyter Notebook이든 파이썬 스크립트든, 응용과 같은 프로세스 안에서 직접 실행되므로 복잡한 설정이나 네트워크 통신이 필요 없다.
기본 연산을 다루는 손쉬움 덕에 초심자와 숙련자 모두에게 좋은 선택이 된다. 단순한 질의 실행이든 데이터 적재든 빠른 변환이든, 빠르고 효율적이며 사용자 친화적인 경험을 제공해 생산성을 높이고 폭넓은 데이터 처리 과제를 지원한다.
또한 DuckDB는 여러 프로그래밍 언어와 매끄럽게 동작하도록 설계되어, 다양한 환경에서 데이터 분석과 처리를 수행할 수 있는 다재다능한 선택지가 된다. 지원하는 언어는 다음과 같다.
- Python
- R
- C/C++
- Julia
- Java
- Go
- Node.js
- Rust
오픈소스라는 마당
폭넓은 언어 지원에 더해, DuckDB는 오픈소스라는 점에서 매력이 한층 커진다. 누구나 자유롭게 쓰고 수정하고 개발에 기여할 수 있다. 소스 코드가 공개되어 있으므로 개발자와 데이터 전문가가 코드를 살펴보고 개선하고 자기 필요에 맞게 다듬을 수 있다. 여기서 네 가지 이점이 따라 나온다.
투명성
transparencyDuckDB가 어떻게 구현되었는지 그대로 볼 수 있다. 열려 있고 투명한 설계가 개발자들 사이의 신뢰와 확신을 키운다.
빠른 순환과 갱신
rapid iteration오픈소스의 성질이 빠른 반복과 새로운 기능의 지속적 추가를 가능하게 한다. 공동체가 개선안을 제안하고 시험하고 신속히 구현하므로 소프트웨어가 기술 발전의 최전선에 머문다.
비용 효율
cost-effective완전히 무료이며 라이선스 비용이 없다. 비용을 걱정하지 않고 어떤 환경에든 배포할 수 있다.
튼튼한 생태계
strong ecosystem오픈소스 모형이 DuckDB의 기능을 확장하는 도구·라이브러리·확장 기능의 활발한 생태계를 기른다. 문서, 자습서, 플러그인 등 공동체가 기여한 자원을 풍부하게 이용할 수 있다.
답사 실습 — 마당을 한 바퀴 돌아본다
A QUICK LOOK AT DUCKDB
이제 실제로 들어가 볼 차례다. 원저자는 데이터베이스 생성, 테이블 생성, 레코드 삽입, 레코드 조회, 집계, 다중 테이블 조인, pandas DataFrame에서의 직접 적재라는 일곱 걸음으로 DuckDB의 마당을 한 바퀴 돈다.
원서는 별도 언급이 없는 한 코딩 환경으로 Jupyter Notebook을 사용한다. Windows, macOS, Linux 어디서든 무관하다.
대문 — 설치와 연결
DuckDB를 쓰려면 먼저 duckdb 패키지를 설치한다. Jupyter Notebook에서는 pip 명령으로 가능하다.
!pip install duckdbDuckDB 데이터베이스를 만들려면 duckdb 패키지의 connect() 함수를 쓴다.
import duckdb # create a connection to a new DuckDB database file conn = duckdb.connect('my_duckdb_database.db')
또는 connect() 함수에 :memory: 인수를 넘겨 메모리 상의 데이터베이스를 만들 수도 있다.
# alternatively, to create an in-memory database conn = duckdb.connect(':memory:')
메모리 데이터베이스에 가한 변경은 데이터베이스를 종료하면 모두 사라진다. 세션 사이에 데이터를 남기려면 메모리 데이터베이스 대신 영속 DuckDB 데이터베이스 파일을 써야 한다.
주칸 세우기 — 테이블 생성
데이터베이스를 만들었으면, 연결 객체의 execute() 메서드에 CREATE TABLE SQL 문을 넘겨 테이블을 만든다.
# create a table conn.execute(''' CREATE TABLE employees ( id INTEGER PRIMARY KEY, name VARCHAR, age INTEGER, department VARCHAR ) ''')
테이블이 제대로 만들어졌는지 확인하려면 SHOW TABLES 문을 쓴다. execute() 메서드는 SQL 질의를 실행하고 DuckDB 결과 집합을 돌려주므로, 결과를 눈으로 보려면 DataFrame으로 변환해야 한다.
conn.execute('SHOW TABLES').df()
| name | |
|---|---|
| 0 | employees |
레코드 삽입
INSERT INTO 문으로 테이블에 몇 개의 행을 넣는다.
# insert data into the table conn.execute(''' INSERT INTO employees VALUES (1, 'Alice', 30, 'HR'), (2, 'Bob', 35, 'Engineering'), (3, 'Charlie', 28, 'Marketing'), (4, 'David', 40, 'Engineering') ''')
테이블 조회
레코드가 들어갔으면 SELECT 문으로 꺼내 온다.
conn.execute(''' SELECT * FROM employees ''').df()
| id | name | age | department | |
|---|---|---|---|---|
| 0 | 1 | Alice | 30 | HR |
| 1 | 2 | Bob | 35 | Engineering |
| 2 | 3 | Charlie | 28 | Marketing |
| 3 | 4 | David | 40 | Engineering |
집계 — 큰 것을 작게 줄여 뜻을 얻는다
테이블에 흔히 수행하는 연산이 집계다. 하나 이상의 열을 기준으로 데이터를 묶고 COUNT, SUM, AVERAGE, MIN, MAX 같은 함수를 적용해 데이터를 요약하는 일이다. 집계는 통찰을 끌어내는 데 필수적이다. 큰 데이터셋을 뜻이 있는 요약으로 압축해 분석을 단순하게 만든다.
부서별 인원 수 — COUNT와 GROUP BY
conn.execute(''' SELECT department, COUNT(*) AS employee_count FROM employees GROUP BY department ''').df()
| department | employee_count | |
|---|---|---|
| 0 | HR | 1 |
| 1 | Engineering | 2 |
| 2 | Marketing | 1 |
전사 평균 연령 — AVG
conn.execute(''' SELECT AVG(age) AS average_age FROM employees ''').df()
| average_age | |
|---|---|
| 0 | 33.25 |
부서별 최고 연령 — MAX
conn.execute(''' SELECT department, MAX(age) AS oldest_age FROM employees GROUP BY department ''').df()
| department | oldest_age | |
|---|---|---|
| 0 | HR | 30 |
| 1 | Engineering | 40 |
| 2 | Marketing | 28 |
부서별 평균 연령
conn.execute(''' SELECT department, AVG(age) AS average_age FROM employees GROUP BY department ''').df()
| department | average_age | |
|---|---|---|
| 0 | HR | 30.0 |
| 1 | Engineering | 37.5 |
| 2 | Marketing | 28.0 |
조인 — 두 채의 건물을 회랑으로 잇는다
DuckDB는 단일 테이블 작업에 그치지 않고 여러 테이블에 대한 조인도 수행한다. 기존 데이터베이스에 두 개의 테이블을 만들고 레코드를 채워 이를 확인한다.
# create an in-memory copy of the database conn = duckdb.connect() # create first table - orders conn.execute(''' CREATE TABLE orders ( order_id INTEGER, customer_id INTEGER, amount FLOAT) ''') # add some records to the orders table conn.execute(''' INSERT INTO orders VALUES (1, 1, 100.0), (2, 2, 200.0), (3, 1, 150.0) ''') # create second table - customers conn.execute(''' CREATE TABLE customers ( customer_id INTEGER, name VARCHAR) ''') conn.execute(''' INSERT INTO customers VALUES (1, 'Alice'), (2, 'Bob') ''')
| order_id | customer_id | amount | |
|---|---|---|---|
| 0 | 1 | 1 | 100.0 |
| 1 | 2 | 2 | 200.0 |
| 2 | 3 | 1 | 150.0 |
| customer_id | name | |
|---|---|---|
| 0 | 1 | Alice |
| 1 | 2 | Bob |
고객별 지출 내역 목록을 얻으려면 두 테이블의 customer_id 필드를 기준으로 orders와 customers를 조인한다.
# join the two tables conn.execute(''' SELECT customers.customer_id, customers.name, orders.amount, FROM orders JOIN customers ON orders.customer_id = customers.customer_id ORDER BY customers.customer_id ''').df()
| customer_id | name | amount | |
|---|---|---|---|
| 0 | 1 | Alice | 100.0 |
| 1 | 1 | Alice | 150.0 |
| 2 | 2 | Bob | 200.0 |
고객별 총 지출액을 알고자 한다면 SUM 함수로 금액을 집계하고, 총액 집계를 위해 GROUP BY 문을 함께 쓴다.
# join the two tables conn.execute(''' SELECT customers.customer_id, customers.name, SUM(orders.amount) as total_spent FROM orders JOIN customers ON orders.customer_id = customers.customer_id GROUP BY customers.customer_id, customers.name ORDER BY customers.customer_id ''').df()
| customer_id | name | total_spent | |
|---|---|---|---|
| 0 | 1 | Alice | 250.0 |
| 1 | 2 | Bob | 200.0 |
pandas에서 데이터 읽기 — 이름만 부르면 된다
여기까지의 예는 모두 DuckDB 안에서 데이터베이스를 직접 만든 경우다. 데이터가 이미 pandas DataFrame에 들어 있다면 어떨까. DuckDB는 이미 메모리에 있는 pandas DataFrame과 직접 동작한다. 답사자가 가장 감탄하는 대목이 여기다. 별도의 등록 절차도, 임시 테이블도 없다. SQL 문 안에서 DataFrame을 그 이름으로 부르면 그만이다.
import pandas as pd # Employee DataFrame employees = pd.DataFrame({ 'employee_id': [1, 2, 3, 4], 'name': ['Alice', 'Bob', 'Charlie', 'David'], 'age': [30, 35, 28, 40], 'department': ['HR', 'Engineering', 'Marketing', 'Engineering'] }) # Sales DataFrame sales = pd.DataFrame({ 'sale_id': [101, 102, 103, 104, 105], 'employee_id': [1, 2, 1, 3, 4], 'sale_amount': [200, 500, 150, 300, 700], 'sale_date': ['2023-01-01', '2023-01-03', '2023-01-04', '2023-01-05', '2023-01-07'] }) display(employees) display(sales)
| employee_id | name | age | department | |
|---|---|---|---|---|
| 0 | 1 | Alice | 30 | HR |
| 1 | 2 | Bob | 35 | Engineering |
| 2 | 3 | Charlie | 28 | Marketing |
| 3 | 4 | David | 40 | Engineering |
| sale_id | employee_id | sale_amount | sale_date | |
|---|---|---|---|---|
| 0 | 101 | 1 | 200 | 2023-01-01 |
| 1 | 102 | 2 | 500 | 2023-01-03 |
| 2 | 103 | 1 | 150 | 2023-01-04 |
| 3 | 104 | 3 | 300 | 2023-01-05 |
| 4 | 105 | 4 | 700 | 2023-01-07 |
부서별 총 매출과 부서별 직원 1인당 평균 매출을 구한다고 하자. 두 DataFrame을 조인하고 집계를 수행해야 한다. 가장 중요한 점은, DuckDB에서는 DataFrame을 그 이름으로 참조하기만 하면 된다는 것이다.
# create an in-memory copy of the database conn = duckdb.connect() # join the DataFrames, group by department, and aggregate query = ''' SELECT e.department, SUM(s.sale_amount) AS total_sales, AVG(s.sale_amount) AS average_sale_per_employee, COUNT(DISTINCT e.employee_id) AS number_of_employees FROM employees e LEFT JOIN sales s ON e.employee_id = s.employee_id GROUP BY e.department ''' conn.execute(query).df()
| department | total_sales | average_sale_per_employee | number_of_employees | |
|---|---|---|---|---|
| 0 | HR | 350.0 | 175.0 | 1 |
| 1 | Engineering | 1200.0 | 600.0 | 2 |
| 2 | Marketing | 300.0 | 300.0 | 1 |
회사의 최고 실적자와 그 소속 부서를 찾는 일도 같은 방식으로 처리된다.
query = ''' SELECT e.department, e.name AS top_employee, MAX(s.sale_amount) AS top_sale_amount FROM employees e LEFT JOIN sales s ON e.employee_id = s.employee_id GROUP BY e.department, e.name ORDER BY top_sale_amount DESC ''' conn.execute(query).df()
| department | top_employee | top_sale_amount | |
|---|---|---|---|
| 0 | Engineering | David | 700 |
| 1 | Engineering | Bob | 500 |
| 2 | Marketing | Charlie | 300 |
| 3 | HR | Alice | 200 |
이로써 확인된 바와 같이, DuckDB는 SQL 문 안에서 pandas DataFrame을 직접 참조할 수 있게 한다.
효율의 증거 — 5.8백만 행을 두고 겨룬다
WHY DUCKDB IS MORE EFFICIENT
앞에서 DuckDB가 효율적이고 고성능이라 말했다. 말은 쉽다. 증거를 봐야 한다. CSV 파일을 다룰 때 DuckDB는 파일 전체를 메모리에 적재한 뒤 처리하지 않는다. 파일에서 데이터를 읽으며 그 자리에서(on the fly) 처리한다. 이 차이를 눈으로 보기 위해 원저자는 2015 Flight Delays and Cancellations 데이터셋을 끌어온다.
사용하는 파일은 데이터셋 안의 flights.csv로, 2015년 미국 내 모든 항공편의 상세 내역이 담겼다. 거의 600MB에 이르고 580만 행이 넘으므로 DuckDB의 효율을 평가할 좋은 후보다. 필드는 다음과 같다.
YEAR · MONTH · DAY · DAY_OF_WEEK · AIRLINE · FLIGHT_NUMBER · TAIL_NUMBER · ORIGIN_AIRPORT · DESTINATION_AIRPORT · SCHEDULED_DEPARTURE · DEPARTURE_TIME · DEPARTURE_DELAY · TAXI_OUT · WHEELS_OFF · SCHEDULED_TIME · ELAPSED_TIME · AIR_TIME · DISTANCE · WHEELS_ON · TAXI_IN · SCHEDULED_ARRIVAL · ARRIVAL_TIME · ARRIVAL_DELAY · DIVERTED · CANCELLED · CANCELLATION_REASON · AIR_SYSTEM_DELAY · SECURITY_DELAY · AIRLINE_DELAY · LATE_AIRCRAFT_DELAY · WEATHER_DELAY
이 예에서 살필 두 국면은 실행 속도와 메모리 사용량이다. 서른한 개의 필드 가운데 질의가 손대는 것은 AIRLINE과 ARRIVAL_DELAY 단 둘이다. 앞서 본 기둥 그림이 여기서 실측치로 되돌아온다.
이 데이터셋은 원서 제2장에서 더 본격적으로 쓰이며, 데이터셋의 여러 CSV 파일을 내려받는 방법도 그곳에서 다룬다.
실행 속도
먼저 pandas로 CSV 파일을 다루는 전통적 방식을 본다. CSV 파일을 pandas DataFrame으로 적재해야 한다.
import pandas as pd # load the CSV file and time it %timeit df = pd.read_csv('flights.csv') 7.46 s ± 568 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
Jupyter Notebook에서 퍼센트 기호(%)는 매직 명령을 나타내는 접두어다. 매직 명령은 Jupyter 환경에서 작업할 때 여러 기능과 편의를 제공하는 특별한 명령이다.
다음으로 항공사별 평균 도착 지연 시간을 구하는 간단한 집계를 수행한다.
df = pd.read_csv('flights.csv') %timeit df.groupby('AIRLINE')['ARRIVAL_DELAY'].mean() 186 ms ± 8.74 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) # %timeit 없이 실행하면 항공사별 평균 도착 지연 결과가 나온다 df.groupby('AIRLINE')['ARRIVAL_DELAY'].mean().reset_index()
이제 같은 집계를 DuckDB로 시도한다. DuckDB에는 CSV 파일을 읽는 read_csv_auto() 함수가 있다.
import duckdb conn = duckdb.connect() query = ''' SELECT AIRLINE, AVG(ARRIVAL_DELAY) AS MEAN_ARRIVAL_DELAY FROM read_csv_auto('flights.csv') GROUP BY AIRLINE ORDER BY AIRLINE; ''' %timeit df = conn.execute(query).df() 496 ms ± 29.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
read_csv_auto() 함수는 CSV 파일을 메모리에 적재하지 않는다. 데이터를 그 자리에서 처리하므로, 데이터셋 전체를 올릴 때 따라오는 메모리 소비 부담 없이 효율적으로 질의할 수 있다. 덕분에 더 큰 데이터셋도 매끄럽게 다루며, 낮은 메모리 사용을 유지한 채 디스크 I/O를 분석 연산에 활용한다. 위 문장은 그와 동시에 데이터 집계까지 수행한다. 소요 시간은 약 0.5초였다.
메모리 사용량
DuckDB의 메모리 사용을 살피기 위해, 먼저 psutil 패키지로 프로세스가 쓴 메모리를 계산하는 함수를 만든다.
import psutil def memory_usage(): process = psutil.Process() return process.memory_info().rss / (1024 ** 2)
CSV 파일을 DataFrame으로 적재하기 전과 후에 현재 프로세스가 쓴 메모리를 측정한다.
import pandas as pd # measure memory before query execution memory_before = memory_usage() print(f"Memory used before query: {memory_before:.2f} MB") # load the CSV file df = pd.read_csv('flights.csv') # measure memory after query execution memory_after = memory_usage() print(f"Memory used after query: {memory_after:.2f} MB") Memory used before query: 130.64 MB Memory used after query: 4362.61 MB
DataFrame이 쓴 메모리를 더 정확히 보려면 Jupyter Notebook의 커널을 반드시 재시작한다.
이제 DuckDB와 비교한다. DuckDB에서는 처리 전에 CSV 파일 전체를 메모리에 올릴 필요가 없다.
import duckdb conn = duckdb.connect() query = ''' SELECT AIRLINE, AVG(ARRIVAL_DELAY) AS MEAN_ARRIVAL_DELAY FROM read_csv_auto('flights.csv') GROUP BY AIRLINE ORDER BY AIRLINE; ''' # measure memory before query execution memory_before = memory_usage() print(f"Memory used before query: {memory_before:.2f} MB") # run the query df = conn.execute(query).df() # measure memory after query execution memory_after = memory_usage() print(f"Memory used after query: {memory_after:.2f} MB") Memory used before query: 72.19 MB Memory used after query: 348.48 MB
이 실측에서 읽어 낼 것
원저자가 이 간단한 예에서 끌어낸 관찰은 셋이다. 세 번째 관찰이 특히 정직하다. 좋은 답사기는 대상을 예찬만 하지 않고 한계도 적어 둔다.
- 하나 DuckDB를 쓰면 데이터 분석에 드는 시간이 극적으로 짧아진다. 집계를 시작하기 전에 CSV를 메모리로 올리는 여분의 시간을 쓰지 않기 때문이다. CSV 파일에 대해 일회성 연산을 수행하는 경우에 유용하다.
- 둘 데이터에 여러 연산을 수행해야 한다면, 시스템 메모리가 데이터를 담기에 충분한 경우 pandas DataFrame으로 적재하는 편이 더 효율적일 수 있다.
- 셋 전체적으로 DuckDB는 대용량 데이터셋에서 효율적으로 동작한다.