DuckDB 사용법(DuckDB Python + Jupyter Lab)


  • 이 글은 DuckDB 사용법을 작성한 글입니다
  • 예상 독자
    • DuckDB가 궁금하신 분
    • 데이터 분석가 : Pandas가 느리다고 생각해서 다른 대안을 찾고 있는 분
    • 데이터 엔지니어 : 데이터 엔지니어링의 Transform 영역에서 활용할 수 있는 수단을 찾고 있는 분. 작은 규모의 회사에서 데이터 웨어하우스를 고민하고 있는 분
  • 키워드 : Duckdb 사용법, Duckdb 설치, Duckdb Python

DuckDB 소개, DuckDB란?

  • Github Repo
  • DuckDB는 fast in-process Analytical Database
  • 왜 Duck인가?
    • Duck(오리)은 날 수도 있고, 수영도 하고, 걷는 놀라운 동물. 환경에 유연함
    • 다재다능하고 탄력적인 데이터 관리 시스템을 만들고 싶었는데, 이런 시스템이 오리와 유사하다고 생각해서 DuckDB라고 이름을 지음
  • C++로 작성되었고, Column-oriented 데이터베이스로 매우 빠름
  • Parquet, CSV, JSON 파일에 직접 쿼리할 수 있음
    • AWS S3, GCS, Azure Blob Storage 등에서도 쿼리할 수 있음
    • S3 Iceberg도 지원
  • 사용하기 쉬움
  • 로컬 Data Warehouse(나만을 위한 DW)라고 생각해도 될 듯
  • Portable Python Dataframe 라이브러리라고 나온 ibis의 백엔드 엔진이 DuckDB
  • 공식 Docker 이미지가 존재하지 않음
    • 애플리케이션에서 DuckDB를 동시에 실행하려고 함. 별도의 컨테이너화를 하지 않아도 됨
    • 웹페이지, 앱 등에서 그냥 DuckDB를 설치해서 사용하면 됨
  • DuckDB 기반 관리형 서비스 : MotherDuck
  • Live Demo를 보고 싶다면 DuckDB Live Demo에서 확인할 수 있음

DuckDB Live Demo



BIG DATA IS DEAD

  • Google Cloud의 BigQuery를 만든 멤버 중 한 명인 Jordan Tigani가 2023년 2월에 올린 BIG DATA IS DEAD라는 글을 보다가 MotherDuck을 알게 되고, DuckDB를 알게 되었음
  • BIG DATA IS DEAD 글의 핵심은 다음과 같음
    • 대부분의 기업은 “빅데이터”를 가지고 있지 않음
      • BigQuery 사용자 대부분은 1TB 미만의 데이터를 보유
      • 일반적인 기업의 데이터 웨어하우스 크기는 100GB가 일반적임
      • 특정 VC의 포트폴리오 회사 중 제일 큰 B2B 기업은 약 1TB, 제일 큰 B2C 기업은 약 10TB를 가지고 있었음. 그러나 대부분 이것보다 적은 데이터를 가짐
    • 데이터를 쿼리하는 패턴
      • 시간이 지나면서 데이터는 많아지지만, 컴퓨팅 리소스는 거의 변하지 않음
      • 1년에 1,000달러 이상을 지출하는 고객을 살펴보니, 쿼리의 90%가 100MB 미만의 데이터를 처리함
      • 데이터가 많은 고객도 많은 양의 데이터를 쿼리하지 않음
      • 최근 24시간 내의 데이터가 가장 자주 활용
      • 오래 된 데이터는 사용되지 않음
    • 싱글 머신의 성능 향상
      • 요즘 AWS의 표준 인스턴스는 64 Core, 256GB RAM 서버
        • 2006년에 AWS에서 EC2를 처음 출시할 땐 싱글 코어에 2GB RAM 서버였음
      • 예전엔 대형 서버가 비쌌으나, 요즘엔 저렴해짐
      • 과거에 분산 처리가 필요했던 작업들이 싱글 머신에서 처리될 수 있음
    • 데이터는 자산이 아닌 부채일 수 있음
      • 사람들이 빅데이터를 가지게 되는 이유는 필요해서가 아닌 단순히 삭제하는 것을 귀찮아해서
      • 같은 쿼리만 반복적으로 한다면 집계된 데이터만 저장하는 게 좋을 수 있음
      • 언젠가 쓸지 모른다는 생각으로 보관할 수 있음
    • 빅데이터의 1%인지 확인할 수 있는 질문
      • 정말 엄청난 양의 데이터를 생성하고 있는가?
      • 그렇다면 정말 한 번에 그 많은 데이터를 사용할 필요가 있는가?
      • 그렇다면 그 데이터가 정말 1대의 컴퓨터에 담기에 큰가?
      • 그렇다면 당신이 단순히 데이터를 쌓아두기만 하는 사람이 아닌지 확실할 수 있는가?
      • 그렇다면 데이터를 요약해서 보관하는 것이 더 나은 방법이 아닐까?
      • 하나라도 아니요라고 답하면 데이터 규모에 맞는 새로운 데이터 도구를 사용하는 것이 좋을 수 있음
  • 위와 같은 내용 때문에 복잡한 분산 시스템 대신 단순하고 효율적인 솔루션이 필요하다고 함
  • BIG DATA IS DEAD엔 직접적으로 나오지 않지만, DuckDB가 이런 상황에 적절한 도구라는 것을 알 수 있음
  • 시간이 지나면서 점점 싱글 머신이 확장되어 분산 처리까지 갈 필요가 없다는 의미 => DuckDB는 이 용도에 적합




DuckDB 목표 & DuckDB 장점

  • 세상에 다양한 DB가 있는데, 모든 상황에 적합한 데이터베이스는 없음. 특정 사용 사례에 따라 더 잘 맞도록 설계됨
  • 다음은 DuckDB의 목표 및 장점

1. Simple

  • SQLite처럼 설치하고 사용하기 쉬워야 함
  • 외부 종속성 없이 동작하고, 별도의 서버 설치나 관리가 필요 없음
  • DuckDB는 별도의 프로세스로 실행되지 않고 호스트 프로세스에서 실행됨


2. Portable

  • 모든 OS에서 동작해야 함 (Windows, MacOS, Linux)
  • 다양한 CPU 아키텍처(x86, ARM) 지원
  • 소형 디바이스, 웹 브라우저, 모바일에서도 실행 가능(DuckDB-Wasm 사용)
  • Java, C, C++, Go, Node.js, Python 등 다양한 API 제공


3. Feature Rich

  • 데이터 분석을 위한 복잡한 쿼리를 지원(윈도우 함수)
  • 커스텀한 MVCC(Multi-Version Concurrency Control)를 통해 ACID 트랜잭션을 보장한다고 주장
    • MVCC : 데이터베이스에서 여러 버전의 데이터를 동시에 관리. 수정할 때마다 새 버전 생성
    • HyPer의 serializable variant MVCC 방식을 구현
  • 쿼리의 속도를 높이기 위한 보조 인덱스를 지원
  • Python, R과 통합


4. Fast

  • OLAP 쿼리 워크로드를 지원
  • 전체 테이블에 대한 집계, 큰 테이블의 JOIN 등을 빠르게 처리
  • 개별 값당 소모되는 CPU 사이클의 양을 줄이는 것이 중요하고, 이를 위해 just-in-time 쿼리 실행 엔진, Vectorized(벡터화된) 쿼리 실행 엔진을 사용
  • TPC-H, TPC-DS 벤치마크로 테스트
    • TPC-H : Decision Support Benchmark, 공급망/구매 데이터로 구성된 비즈니스 의사결정 시나리오
    • TPC-DS : Data Warehouse Benchmark, 온라인 쇼핑 데이터로 구성된 데이터 웨어하우스 시나리오
    • 확인하는 것들 : 쿼리 응답 시간, 처리량, 사용자가 많을 때의 처리량, 데이터 크기별 성능


5. Extensible


6. Free

  • DuckDB의 개발자들은 네덜란드 공무원일 때 개발을 시작
  • 누구나 자유롭게 제공하는 것을 사회에 대한 책임과 의무로 봄
  • 따라서 MIT 라이선스를 채택


7. Thorough Testing

  • 최초엔 Research 그룹에서 만들었지만, 안정적인 데이터베이스 시스템이 되는 것을 목표로 함
  • 여러 플랫폼과 컴파일러에서 반복적으로 테스트
  • Github Actions을 보면 여러 테스트가 돌아가고 있는 것을 확인할 수 있음


DuckDB 성능 벤치마크

DuckDB GROUP BY Benchmark

DuckDB JOIN Benchmark

  • Benchmarking Ourselves over Time at DuckDB 이 글도 확인해보니, 시간의 흐름에 따라 DuckDB가 더욱 빨라지고 있는 내용을 담고 있었음. 2023년 말에 급격한 성능 향상이 존재
    • 최근 3년 동안 3~25배 빨라지고, 10배 많은 데이터를 분석할 수 있게 됨

DuckDB benchmark results over time

  • Pandas보단 확실히 빠르고, Rust로 만들어진 Polars와 많이 비교됨
    • DuckDB와 Polars, Pandas를 비교한 Repo : 이 결과에서도 DuckDB가 더 빠르다고 나옴
    • 회사에 도입하려고 하면 우리 데이터로 테스트를 충분히 하고 선택할 것. 벤치마크 결과를 항상 신뢰하는 것보다, 다시 점검하는 것이 필요


DuckDB 설치

  • 공식 문서에 내용을 확인할 수 있음
  • Mac OS 기준

    brew install duckdb
    
  • 윈도우

    winget install DuckDB.cli
    
  • Linux : 최신 버전은 Github에서 확인

    wget https://github.com/duckdb/duckdb/releases/download/v1.1.2/duckdb_cli-linux-aarch64.zip
    
    unzip duckdb_cli-linux-aarch64.zip
    
    ./duckdb
    



DuckDB 실행하기

  • CLI에서 실행은 간단함

    duckdb
    

DuckDB in CLI

  • 데이터 분석가 관점에서 DuckDB를 사용한다면 Jupyter Lab을 사용할 텐데, brew로 설치하면 파이썬 패키지도 설치되어 바로 사용할 수 있음
  • Jupyter Lab 실행
jupyter lab


간단한 실행(DuckDB Python)

  • DuckDB를 사용할 때, duckdb.sql로 실행하는 방법과 duckdb.connect로 Connection을 생성해서 쓰는 방법이 존재
    • 1) duckdb.sql : 전역 메서드로 사용
      • 간단한 스크립트나 빠른 프로토타이핑, 노트북에서 유용
      • 메모리에 데이터를 저장함
      import duckdb
      duckdb.sql("SELECT 1")
      
    • 2) duckdb.connect : Connection 객체를 생성해서 사용
      • 커넥션을 생성할 때 In-Memory 모드와 Both Persistent 모드를 선택할 수 있음
        • Disk에 Spilling을 사용해 메모리보다 큰 워크로드에서 데이터를 처리할 수 있음
          • 특정 작업이나 메모리가 부족할 때 데이터를 디스크에 임시로 저장하는 기법
      • 별도의 옵션을 지정하지 않거나 :memory: 옵션을 사용하면 In-Memory 모드로 생성
      • 파일 경로를 지정하면 파일이 생성되거나 사용됨
      # 데이터를 메모리에 저장해서 사용
      import duckdb
      
      con = duckdb.connect(":memory:")
      
      # 이 케이스에는 test.db 파일을 생성
      import duckdb
      
      con = duckdb.connect("test.db")
      
  • .show()는 결과를 출력만 하고 None을 반환
  • .show()를 사용하지 않으면 Relation 객체로 반환되어서 다른 곳에서 사용 가능
  • .df()를 사용하면 Pandas DataFrame으로 변경. 그러나 DuckDB를 사용하면 .df()는 거의 사용하지 않거나 가공을 DuckDB로 최대한 한 후에 사용하는 것이 좋을 듯(실행 시간이 느린 것이 체감됨)

    import duckdb
    duckdb.sql("SELECT 1").show()
    output = duckdb.sql("SELECT 1 AS a")
    duckdb.sql("SELECT a+1 AS b FROM output").show()
    new_df = duckdb.sql("SELECT 1 AS a").df()
    


Data Load

  • CSV, Parquet, JSON에 바로 접근할 수 있음
    • read_csv, read_parquet, read_json 함수를 사용
  • NYC 택시 데이터를 다운받아 테스트(약 300만 Row)

    wget https://d37ci6vzurychx.cloudfront.net/trip-data/yellow_tripdata_2024-01.parquet
    
  • 데이터 DESCRIBE

    duckdb.sql("DESCRIBE SELECT * FROM 'yellow_tripdata_2024-01.parquet'")
    

  • Parquet 파일 Load
    • 노트북 파일과 동일한 경로에 파일이 존재함
    • read_parquet를 사용하는 것과 사용하지 않는 것의 차이는 없음
    • 경로에서 glob 패턴을 사용할 수 있음 : SELECT * FROM ‘test/*.parquet’;
    duckdb.sql("SELECT * FROM 'yellow_tripdata_2024-01.parquet' LIMIT 3")
    
    duckdb.sql("SELECT * FROM read_parquet('yellow_tripdata_2024-01.parquet') LIMIT 3")
    

  • Parquet 파일을 읽고, 쿼리문에서 사용하고 싶다면 다음과 같이 사용할 수 있음
  nyc_data = duckdb.read_parquet("yellow_tripdata_2024-01.parquet")
  duckdb.sql("SELECT * FROM nyc_data")


jupysql을 사용해 더 편하게 실행하기

  • 위 코드를 보면 항상 duckdb 또는 conn을 입력하고 쿼리를 실행해야 함. 반복적으로 하다 보면 이런 것들이 번거로움
  • jupysql을 사용하면 간단하게 실행할 수 있음
  • jupysql 설치

    !pip install jupysql
    
  • SQL 쿼리를 실행한 후, Pandas DataFrame으로 항상 저장하는 옵션 설정

    %config SqlMagic.autopandas = True
    
  • Extension Load
    • 메모리 모드로 conn 생성
    %load_ext sql
    conn = duckdb.connect(':memory:')
    %sql conn --alias duckdb
    
  • 별도의 셸에서 실행. %sql, %%sql을 사용. %%sql은 여러 줄 쿼리 실행 가능
    • 한 셸에서 여러 명령을 실행하려면 세미콜론으로 구분해야 함
    %%sql 
    SELECT
      DATE_TRUNC('hour', tpep_pickup_datetime) AS pickup_hour,
      COUNT(*) AS cnt
    FROM read_parquet('yellow_tripdata_2024-01.parquet')
    GROUP BY ALL
    

  • 위에 사용한 쿼리의 결과를 저장하지 않아서 사용할 수 없고, <<을 사용해 변수에 저장할 수 있음

    %%sql hour_cnt << 
    SELECT
      DATE_TRUNC('hour', tpep_pickup_datetime) AS pickup_hour,
      COUNT(*) AS cnt
    FROM read_parquet('yellow_tripdata_2024-01.parquet')
    GROUP BY ALL
    
    hour_cnt
    


SQL 문법

  • DuckDB SQL 문서를 확인하면 자세한 문법을 확인할 수 있음
  • 일반적인 SQL 문법은 대부분 지원하고, PostgreSQL과 유사함
  • SQL 문법이 궁금하시면 제가 인프런에 무료로 공개해둔 초보자를 위한 BigQuery(SQL) 입문 강의를 추천
    • 문법 중에 일부만 다르고, 대부분 큰 흐름은 유사함
  • SQL 문법은 기억하고 싶은 일부분만 기록


GROUPING SETS, ROLLUP, CUBE

  • 여러 차원에 대한 그룹화를 실행할 때 사용
  • GROUP BY에 포함됨
  • GROUPING SETS : 여러 차원에 대한 그룹화
    • 어떤 차원을 그룹화할지 직접 지정
  • ROLLUP : GROUPING SETS의 특수한 형태. 주어진 차원의 Sub Group을 생성
    • (country, city, zip) -> (country, city), (country), ()이 생성
    • () : 전체를 의미
  • CUBE : GROUPING SETS의 또 다른 특수한 형태. 모든 가능한 조합을 생성
    • (country, city, zip) -> (country, city, zip), (country, city), (country, zip), (city, zip), (country), (city), (zip), ()
  • GROUPING() 함수는 해당 컬럼이 그룹화되었는지 아닌지를 비트로 표현
    • 3개의 컬럼일 경우 0 = 000, 1 = 001, 2 = 010, 3 = 011, 4 = 100, 5 = 101, 6 = 110, 7 = 111
  • 샘플 데이터 생성

    %%sql
    CREATE TABLE sales (
        category VARCHAR,
        product_name VARCHAR,
        region VARCHAR,
        sale_date DATE,
        sales_amount INT
    );
    
    -- 샘플 데이터 삽입
    INSERT INTO sales VALUES
        ('전자기기', '스마트폰', '서울', '2024-01-01', 1200000),
        ('전자기기', '스마트폰', '부산', '2024-01-01', 800000),
        ('전자기기', '노트북', '서울', '2024-01-01', 2000000),
        ('전자기기', '노트북', '부산', '2024-01-01', 1500000),
        ('의류', '청바지', '서울', '2024-01-01', 89000),
        ('의류', '청바지', '부산', '2024-01-01', 78000),
        ('의류', '티셔츠', '서울', '2024-01-01', 45000),
        ('의류', '티셔츠', '부산', '2024-01-01', 35000);
    
    %sql SELECT * FROM sales
    

  • GROUPING SETS 실행

    %%sql
    
    # GROUPING SETS
    SELECT 
        category,
        region,
        SUM(sales_amount) AS total_sales,
        COUNT(*) AS transaction_count
    FROM sales
    GROUP BY GROUPING SETS (
        (category, region),
        (region),
        ()
    )
    ORDER BY category NULLS LAST, region NULLS LAST;
    

    • category None은 region의 SUM, category, region 둘 다 None은 전체 합
  • ROLLUP

    %%sql
    
    # ROLLUP
    SELECT 
        category,
        product_name,
        region,
        SUM(sales_amount) AS total_sales
    FROM sales
    GROUP BY ROLLUP(category, product_name, region)
    ORDER BY category NULLS LAST, product_name NULLS LAST, region NULLS LAST;
    

  • (category, product_name, region) => (category, product_name, region), (category, product_name), (category), ()로 나타남

  • CUBE

    %%sql
    
    # CUBE
    SELECT 
        category,
        product_name,
        SUM(sales_amount) AS total_sales,
        COUNT(*) AS transaction_count,
        GROUPING(category, product_name) AS grouping_level
    FROM sales
    GROUP BY CUBE(category, product_name)
    ORDER BY category NULLS LAST, product_name NULLS LAST;
    

  • (category, product_name) => (category, product_name), (category), (product_name), ()
  • GROUPING 함수를 사용해 grouping_level을 구함(2개라서 0~3)


ORDER BY NULLS LAST

  • 위 쿼리에서 나온 예시
  • 정렬할 때 NULLS의 위치를 지정할 수 있음
  • NULLS {FIRST, LAST}


DATE 함수

  • 다양한 Database에서 차이가 있는 것은 대부분 Date 함수
  • 이 내용들은 공식 문서를 확인하는 것이 제일 좋은 방법
  • date_part(part, date) : BigQuery와 반대
    • date_partdatepart로도 사용할 수 있음


FILTER

  • SELECT 절의 집계 함수에서 사용할 수 있음. 특정 집계 함수에서만 적용되는 필터링 기능
    • FILTER가 없으면 CASE WHEN을 사용하는 것이 일반적
%%sql

SELECT 
    category,
    SUM(sales_amount) as total_sales,
    SUM(sales_amount) FILTER (WHERE region = '서울') AS seoul_sales,
    SUM(sales_amount) FILTER (WHERE region = '부산') AS busan_sales,
    -- 각 지역의 매출 비중도 계산
    ROUND(SUM(sales_amount) FILTER (WHERE region = '서울') * 100.0 / SUM(sales_amount), 1) AS seoul_percentage,
    ROUND(SUM(sales_amount) FILTER (WHERE region = '부산') * 100.0 / SUM(sales_amount), 1) AS busan_percentage
FROM sales
GROUP BY category;


UDF

  • 보통 SQL로 UDF를 만들 수 있는데, DuckDB는 24년 10월 기준 Python 함수로 UDF를 생성
  • DuckDB python Function API
  • 파이썬 함수를 만들고, duckdb.create_function 함수를 사용해 등록
    • con.create_function(name, function, parameters, return_type)
    • 이름, 함수, 파라미터, 리턴 타입을 명시해야 함
  • 예시: 텍스트에서 이모지를 추출하는 UDF 만들기

    !pip install emoji
    
    import duckdb
    import emoji  # pip install emoji 필요
    
    def extract_emojis(text):
        if text is None:
            return None
        return ' '.join([c for c in text if emoji.is_emoji(c)])
    
    con_udf = duckdb.connect()
    
    # Python 함수를 DuckDB UDF로 등록
    con_udf.create_function('extract_emojis', extract_emojis, ['VARCHAR'], 'VARCHAR')
    
    %sql con_udf
    # con_udf 연결
    
    %%sql
    # 테스트용 데이터 생성
    CREATE OR REPLACE TABLE messages AS 
      SELECT * FROM (
        VALUES 
        ('안녕하세요 👋 오늘도 화이팅! 🔥'),
        ('맛있는 저녁 먹었어요 🍜 🍖'),
        ('날씨가 좋네요 ☀️'),
        ('축하해요! 🎉 🎊 🎈')
    ) AS t(message);
    
    %%sql
    SELECT 
        message,
        extract_emojis(message) AS emojis
    FROM messages
    

Secrets Manager

  • Secrets Manager 문서
  • DuckDB에서 S3, GCS 등에 접근할 수 있음(내부적으로 httpfs Extension을 사용)
  • SECRET을 생성하면, 그 후에 사용할 수 있음
  • S3 Temporary Secrets
    • 메모리에 저장
    • DuckDB가 실행된 동안에만 활용 가능
    CREATE SECRET my_secret (
        TYPE S3,
        KEY_ID 'my_secret_key',
        SECRET 'my_secret_value',
        REGION 'my_region'
    );
    
  • S3 Persistent Secrets
    • 파일에 저장(~/.duckdb/stored_secrets)
    • 다음 세션에서도 자동으로 로드
    • 자주 사용해야 하면 Persistent Secret을 사용하는 것이 편할 수 있음. 그러나 보안에 취약해질 수 있는 점(Temporary Secret은 메모리에 저장되기 때문에 세션이 종료되면 사라짐)
    • 디스크에 암호화되지 않은 바이너리 형식으로 저장됨
    CREATE PERSISTENT SECRET my_persistent_secret (
        TYPE S3,
        KEY_ID 'my_secret_key',
        SECRET 'my_secret_value'
    );
    
  • 위 설정이 정상적으로 되었다면 s3:// 프리픽스를 사용할 수 있음

    SELECT *
    FROM 's3://my-bucket/file.parquet';
    
  • Secrets 삭제

    DROP PERSISTENT SECRET my_persistent_secret;
    



DuckDB Extension

  • Extension 문서
  • DuckDB는 익스텐션 기능을 동적으로 Load 가능
  • DuckDB Extension은 아직 많이 없어서 오픈소스에 기여하고 싶은 분들이 시도해도 좋을 것 같음
  • 설치된 목록 확인하기

    %%sql
    SELECT 
        extension_name, 
        installed, 
        description
    FROM duckdb_extensions()
    

  • 설치 후, Load
    • INSTALL : 설치는 1회만 하면 됨
    • LOAD : 세션마다 Load 필요
    %%sql
    INSTALL spatial;
    LOAD spatial;
    


bigquery

  • BigQuery 데이터에 쿼리할 수 있는 Extension
    • ATTACH로 접근한 후, D를 붙여서 쿼리 실행
    %%sql
    INSTALL bigquery FROM community;
    LOAD bigquery;
    
    %%sql
    D ATTACH 'project=my_gcp_project' AS bq (TYPE bigquery, READ_ONLY);
    D SELECT * FROM bq.quacking_dataset.duck_tbl;
    


h3


pg_duckdb

  • 이 글을 작성하기 2일 전에 나온 글 : pg_duckdb beta release : Even faster analytics in Postgres
  • PostgreSQL에 DuckDB 분석 엔진을 통합
  • PostgreSQL은 분석 목적이 아닌 트랜잭션 데이터베이스로 만들었지만, 종종 분석에 사용됨
    • 처음엔 괜찮지만 데이터가 많아지면서 한계에 부딪힘
    • 이럴 때 DuckDB를 사용하면 이점을 볼 수 있음
  • 글에서 나온 인덱스 없이 TPC-DS 데이터셋을 로드하는 경우, 기본 PostgreSQL에 pg_duckdb가 1,500배 빨랐다고 함. 스크립트
  • 나온지 얼마 안되서 아직 사용하긴 애매하지만, 더 발전하면 PostgreSQL에서 간단한 분석을 하거나 제품의 사용자가 많지 않을 때 DW 대용으로 사용할 수 있을 것 같음
    • 만약 DW가 없어진다면 과거에 만들었던 데이터 파이프라인은 꽤 바뀔 수 있을 것 같음




DuckDB 활용 방안


BigQuery에서 사용하는 예시

  • (1) BigQuery의 SQL 쿼리로 데이터를 전처리 => GCS에 Parquet로 저장 => Local에서 DuckDB에서 GCS에 접근해서 분석
  • (2) pd.read_gbq를 사용해 BigQuery의 데이터를 Pandas Dataframe으로 가져온 후, 그 데이터를 DuckDB에 연결해서 분석
  • How to combine BigQuery with DuckDB 글을 보면 gsutil을 사용해서 GCS에 데이터를 추출하고, 그 데이터에 접근. 그 후에 다시 bq 커맨드를 사용해 BigQuery에 Load


로컬 데이터 웨어하우스처럼 사용(Pandas 대체)

  • 기존에 로컬에서 데이터 분석을 하거나, 데이터 전처리를 할 때 Pandas를 많이 사용할 텐데 이젠 DuckDB로 대체할 수 있을 것
  • SQL로 데이터를 전처리하는 것이 더 좋은 방법이라고 생각
  • S3, GCS 등에 Parquet로 데이터 저장하고, 거기에서 쿼리를 실행하는 방식


필요할 때 사용하는 가벼운 분석 엔진 용도

  • OLTP의 가벼운 버전인 SQLite의 OLAP 버전이 DuckDB
  • Duckdb-wasm을 사용해 웹에서도 사용 가능


ETL, ELT 파이프라인에서 Transform 단계에서 사용

  • Spark나 Pandas로 데이터를 처리하는 과정에서 DuckDB를 사용할 수 있을 것
  • 데이터가 엄청 많은 경우엔 Spark를 사용할 텐데, 이때는 아직은 어려울 수 있을 것(테스트를 꼭 해보고 판단해보는 것을 추천)


GCS에 있는 Parquet 쿼리하기


httpfs 설치(brew로 설치하면 기본으로 설치됨)

%%sql
INSTALL httpfs FROM community;
LOAD httpfs;


Credential 설정

  • HMAC Keys 생성
    • HMAC Key에 대해 궁금하면 Google Cloud 문서 참고
    • S3와 호환되는 라이브러리를 사용해 GCS에 접근할 때 사용
    • Access Key ID와 Secret Access Key로 구성
    • 서비스 계정의 인증 수단으로 활용이라고 볼 수 있고, GCS 접근용으로만 사용
  • 아래 이미지에서 서비스 계정의 키 만들기 -> 서비스 계정 생성(Cloud Storage 권한 부여 : 스토리지 객체 사용자) -> 액세스 키 생성

  • 아래 코드를 실행해서 DuckDB에 Secret 등록
%%sql
CREATE OR REPLACE TEMPORARY SECRET gcs_secret (
    TYPE GCS,
    KEY_ID 'KEY ID를 넣어주세요',
    SECRET 'SECRET를 넣어주세요'
);


GCS의 Parquet를 불러와서 DuckDB에 저장

  • GCS의 Parquet를 불러올 때 read_parquet 함수를 사용
  • 다만 매번 GCS에서 불러오는 것은 네트워크 비용을 생각하면 느린 일이기 때문에, DuckDB의 Table로 생성해서 사용
%%sql  
CREATE TABLE yello_taxi AS
SELECT *
FROM read_parquet('gs://여러분들의_GCS_BUCKET/파일_경로.parquet');
  • warning 메시지는 sqlglot parser에서 발생하는 것 같으나, 동작에 문제는 없음


그 후에 쿼리 실행

  • 당연한 이야기지만 yello_taxi 테이블을 만들어서 속도가 빠름
  • 매번 read_parquet(‘gcs://~’)를 실행하면 데이터를 가지고 오는 시간이 소요되어서 느렸음
  • 약 300만 Row를 GROUP BY 하는데 Wall time이 65.5ms => 0.06초
%%time
%%sql

SELECT
  DATE_TRUNC('hour', tpep_pickup_datetime) as pickup_hour,
  COUNT(*) as cnt
FROM yello_taxi
GROUP BY ALL;


참고 삼아 BigQuery로 실행

  • BigQuery 테이블을 생성한 후, 동일한 쿼리를 실행
  • 경과 시간에 954ms로 DuckDB가 15배 빠름
  • 성능 비교할 땐 하나의 케이스만 보고 판단하면 안되고, 데이터가 점점 많아질 때나 쿼리의 복잡도에 따라 여러번 실행해야 함
    • DuckDB는 1인 사용자를 위해 최적화가 되어있고, BigQuery는 여러 사용자가 실행해도 문제가 없음
    • 다시 생각해도 회사의 DW가 존재하고 => DW에서 데이터를 가지고 와서 DuckDB에서 분석하는 흐름이 좋을 것 같음
  • BigQuery와 DuckDB 성능 비교를 한 글도 보면 유용함



정리

  • DuckDB가 데이터 분석, 데이터 엔지니어링에서 영향력을 점점 넓힐 것으로 예상
  • 과거부터 존재했던 데이터 웨어하우스를 사용하면서, 로컬에서 빠르게 처리할 때 활용할 수 있을 것
    • DW 비용도 더욱 줄일 수 있음
  • 아예 새롭게 파이프라인을 만든다고 하면 Spark를 사용하지 않고 DuckDB를 사용하는 것도 가능할 것으로 보임. PostgreSQL을 사용하면 규모가 커지기 전까지 모두 DuckDB를 활용
  • Reddit에서 검색해보니 다양한 토론이 존재
  • AWS Lambda에서 DuckDB를 사용하는 케이스도 존재 : serverless-duckdb. Duckdb-wasm이 있으니 프론트엔드에서도 활용할 수 있음
  • MotherDuck 가격도 확인해보니, STANDARD 가격이 월 25달러부터 시작해서 생각보다 저렴하게 사용할 수 있을지도(멤버 별 과금이 아니라 사용량에 기반한 과금)
  • DuckDB의 단점은 하나의 프로세스에서 실행되는 점인데, 이건 애초에 설계할 때 염두하지 않은 내용. 여러 프로세스에서 READ는 가능, WRITE는 불가능. Handling Concurrency
  • DuckDB의 성능 이슈가 있다면 Performance Guide, My Workload Is Slow 글을 보면 도움이 될 것 같음
    • FAQ에도 성능, 어떻게 사용할지가 나옴



참고 자료



  • 글 작성하는데 걸린 시간 : 7시간 20분
    • 하고자 하는 이야기, 개요 정리 : 20분
    • 초안 글 작성 : 6시간 10분
    • 클로드/Cursor와 셀프 글 피드백 : 30분
    • 2차 글 작성 : 20분
  • 간단 회고
    • DuckDB는 어느정도 사용하고, 공부도 이미 한 주제인데 방대한 내용을 정리하고 예시를 추가하는 과정에서 시간이 소요됨

카일스쿨 유튜브 채널을 만들었습니다. 데이터 사이언스, 성장, 리더십, BigQuery 등을 이야기할 예정이니, 관심 있으시면 구독 부탁드립니다 :)

PM을 위한 데이터 리터러시 강의를 만들었습니다. 문제 정의, 지표, 실험 설계, 문화 만들기, 로그 설계, 회고 등을 담은 강의입니다

이 글이 도움이 되셨거나 다양한 의견이 있다면 댓글 부탁드립니다 :)

Buy me a coffeeBuy me a coffee





© 2017. by Seongyun Byeon

Powered by zzsza