DuckDB가 무엇이며 어째서 2020년대 초에 이름을 얻게 되었는지를 알았으니, 이제 친해질 차례다. 저자들이 이 장의 무대를 명령행 인터페이스 하나로 좁힌 이유는 분명하다. 가장 빠르게 속도를 낼 수 있는 길이라고 보았기 때문이다. 여러 환경에 설치하는 법을 익히고, 내장 명령들을 배우고, 원격 CSV 파일을 질의하는 것으로 장을 맺는다.
순서만 놓고 보면 흔한 입문 장이다. 그러나 읽어가다 보면 이 장이 실은 하나의 주장을 반복해서 증명하고 있음을 알게 된다. DuckDB를 쓰기 위해 준비해야 할 것이 거의 없다는 주장이다. 설치에 바이너리 하나, 실행에 명령어 한 낱말, 원격 파일을 읽는 데 확장 하나. 그 절제된 목록이 이 장의 성격이다.
지원하는 환경Supported environments
DuckDB는 여러 프로그래밍 언어와 운영체제에서 쓸 수 있다. 리눅스, 윈도, macOS를 모두 지원하며, Intel/AMD와 ARM 아키텍처 양쪽을 아우른다. 집필 시점을 기준으로 지원 목록은 다음과 같다.
이 장은 그중 명령행만 다룬다. 독자를 가장 빨리 궤도에 올려놓는 방법이라고 저자들이 판단했기 때문이다. 여기서 제1장의 개념 하나가 실물로 확인된다. DuckDB CLI는 별도의 서버 설치를 요구하지 않는다. DuckDB가 임베디드 데이터베이스이며, CLI의 경우에는 CLI 실행 파일 자체에 임베드되어 있기 때문이다. 데이터베이스가 도구 안에 들어앉아 있는 것이다.
명령행 도구는 깃허브 릴리스로 배포된다. 운영체제와 아키텍처별로 다양한 패키지가 있고, 전체 목록은 설치 안내 페이지에서 확인할 수 있다.
CLI 설치Installing the DuckDB CLI
저자들은 이 설치를 "복사해 넣는(copy to)" 설치라고 부른다. 설치 프로그램도 필요 없고 라이브러리도 필요 없다는 뜻이다. CLI의 실체는 duckdb라는 이름의 단일 바이너리 하나다. 설치라고 부르기가 민망할 정도로 간소한데, 바로 그 간소함이 이 도구의 성격을 가장 잘 말해준다.
macOS 2.2.1
macOS에서는 Homebrew 패키지 설치 도구를 쓰라는 것이 공식 권고다.
# Homebrew 패키지 관리자 자체를 설치할 때만 필요하다. # 이미 갖고 있다면 이 줄은 실행하지 않는다. $ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/\ Homebrew/install/HEAD/install.sh)" $ brew install duckdb
리눅스와 윈도 2.2.2
리눅스와 윈도에는 아키텍처와 판본에 따라 여러 패키지가 준비되어 있다. 전체 목록은 깃허브 릴리스 페이지에 있다. 아래는 AMD64 아키텍처의 리눅스에서 CLI를 띄우는 절차다.
$ wget https://github.com/duckdb/duckdb/releases/download/v0.10.0/\ duckdb_cli-linux-amd64.zip $ unzip duckdb_cli-linux-amd64.zip $ ./duckdb -version
CLI 사용하기Using the DuckDB CLI
CLI를 띄우는 가장 간단한 방법은 duckdb 한 낱말이다. 저자들의 표현대로, 그렇다, 이토록 짧고 이토록 빠르다. 실행하면 앞의 표제부에서 본 네 줄이 나온다. 데이터베이스는 일시적이며 모든 데이터가 메모리에 놓인다. CLI를 나가면 사라진다.
SQL 문 2.3.1
SQL 문은 명령행에 직접 입력하거나 붙여 넣고, 세미콜론과 줄바꿈으로 끝낸다. 세미콜론이 없는 동안에는 줄바꿈을 계속 넣을 수 있다. 문장은 곧바로 실행되어 결과를 압축된 표 형식으로 출력한다. 출력 형식은 2.5.1절에서 설명하는 방법으로 바꿀 수 있다. 오래 걸리는 작업에는 진행 표시줄이 나타난다.
D select v.* from values (1),(3),(3),(7) as v;
| col0 |
|---|
| int32 |
| 1 |
| 3 |
| 3 |
| 7 |
점 명령 2.3.2
SQL 문과 일반 명령 외에, CLI에서만 쓸 수 있는 특별한 명령이 따로 있다. 점 명령(dot command)이다. 규칙은 엄격하다. 줄을 마침표(.)로 시작하고, 곧바로 명령 이름을 붙인다. 추가 인자는 명령 뒤에 공백으로 구분해 넣는다. 점 명령은 반드시 한 줄 안에 입력해야 하고, 마침표 앞에 공백이 있어서는 안 된다. 그리고 보통의 SQL 문이나 명령과 달리 줄 끝에 세미콜론이 필요하지 않다.
널리 쓰이는 것들은 다음과 같다.
| .open | 현재 데이터베이스 파일을 닫고 새 파일을 연다. |
| .read | CLI 안에서 SQL 파일을 읽어 실행한다. |
| .tables | 현재 사용할 수 있는 테이블과 뷰의 목록을 보여준다. |
| .timer on/off | SQL 실행 시간 출력을 켜고 끈다. |
| .mode | 출력 형식을 제어한다. |
| .maxrows | 기본으로 보여줄 행의 수를 제어한다(duckbox 형식에 해당). |
| .excel | 다음 명령의 출력을 스프레드시트로 보여준다. |
| .exit .quit ctrl-d | CLI를 나간다. |
전체 목록은 .help로 받아볼 수 있다.
CLI 인자 2.3.3
CLI는 인자를 받는다. 데이터베이스 모드를 조정하거나, 출력 형식을 제어하거나, 대화형 모드로 들어갈지 여부를 결정하는 데 쓴다. 사용법은 duckdb [OPTIONS] FILENAME [COMMANDS]다.
| -readonly | 데이터베이스를 읽기 전용으로 연다. |
| -json | 출력 모드를 json으로 설정한다. |
| -line | 출력 모드를 line으로 설정한다. |
| -unsigned | 서명되지 않은 확장의 적재를 허용한다. |
| -s COMMAND -c COMMAND | 주어진 명령을 실행한 뒤 종료한다. 주어진 파일에서 입력을 읽는 .read 점 명령과 함께 쓸 때 특히 요긴하다. |
다음은 질의 결과를 JSON으로 출력하도록 CLI에 매개변수를 준 예다.
$ duckdb --json -c 'select v.* from values (1),(3),(3),(7) as v;' [{"col0":1}, {"col0":3}, {"col0":3}, {"col0":7}]
DuckDB의 확장 체계DuckDB’s extension system
DuckDB에는 확장 체계가 있다. 데이터베이스의 핵심에 속하지 않는 기능을 여기에 담아둔다. 확장을 DuckDB와 함께 설치하는 패키지라고 생각하면 된다. 코어를 가볍게 유지하면서 필요한 사람만 필요한 것을 얹게 하는 이 설계가, 제1장에서 본 "긴 꼬리를 겨냥한다"는 말과 정확히 맞물린다.
DuckDB는 몇 가지 확장을 미리 적재한 상태로 배포되며, 그 목록은 사용하는 배포판에 따라 다르다. 설치되었든 아니든 사용할 수 있는 모든 확장의 목록은 duckdb_extensions 함수로 얻는다. 먼저 이 함수가 무슨 필드를 돌려주는지부터 확인한다.
D DESCRIBE SELECT * FROM duckdb_extensions();
| column_name | column_type |
|---|---|
| varchar | varchar |
| extension_name | VARCHAR |
| loaded | BOOLEAN |
| installed | BOOLEAN |
| install_path | VARCHAR |
| description | VARCHAR |
| aliases | VARCHAR[] |
그러면 이 기계에 무엇이 설치되어 있는지 들여다본다.
D SELECT extension_name, loaded, installed from duckdb_extensions() ORDER BY installed DESC, loaded DESC;
| extension_name | loaded | installed |
|---|---|---|
| varchar | boolean | boolean |
| autocomplete | true | true |
| fts | true | true |
| icu | true | true |
| json | true | true |
| parquet | true | true |
| tpch | true | true |
| httpfs | false | false |
| inet | false | false |
| jemalloc | false | false |
| motherduck | false | false |
| postgres_scanner | false | false |
| spatial | false | false |
| sqlite_scanner | false | false |
| tpcds | false | false |
| excel | true | |
| 15 rows 3 columns | ||
확장을 설치하려면 INSTALL 명령에 확장 이름을 붙여 입력한다. 그러면 확장은 데이터베이스에 설치되지만 적재되지는 않는다. 적재하려면 같은 이름에 LOAD를 붙인다. 이 기제는 멱등적(idempotent)이어서, 두 명령을 여러 번 내려도 오류가 나지 않는다.
DuckDB 0.8 버전 이후로는, 설치된 확장이 필요하다고 판단되면 데이터베이스가 그것을 자동으로 적재한다. 그러므로 LOAD 명령이 필요하지 않을 수도 있다.
httpfs — 인터넷 저편의 파일을 읽는 문
기본 상태의 DuckDB는 인터넷 다른 곳에 있는 파일을 질의하지 못한다. 그 능력은 공식 확장 httpfs를 통해 얻는다. 배포판에 들어 있지 않다면 설치하고 적재한다.
D INSTALL httpfs; D LOAD httpfs; D FROM duckdb_extensions() SELECT loaded, installed, install_path WHERE extension_name = 'httpfs';
| loaded | installed | install_path |
|---|---|---|
| boolean | boolean | varchar |
| true | true | /path/to/httpfs.duckdb_extension |
여기서 문장 하나가 눈에 걸린다. 방금 그 질의는 FROM으로 시작해서 SELECT가 뒤에 온다. SQL을 배운 사람이라면 순서가 뒤집힌 것처럼 보일 텐데, DuckDB는 이 FROM 우선 구문을 허용한다. 사람이 실제로 생각하는 순서—어디서 가져와서, 무엇을 볼 것인가—를 그대로 적게 해주는 것이다. 이 장 마지막에 한 번 더 등장한다.
CLI로 CSV 파일 분석하기Analyzing a CSV file with the DuckDB CLI
이제 시연이다. 저자들이 고른 과제는 데이터 엔지니어라면 누구나 겪는 일—CSV 파일 안의 데이터가 무엇인지 알아내는 일이다. 데이터가 어디에 저장되어 있든 상관없다. 원격 HTTP 서버든 클라우드 스토리지(S3, GCP, HDFS)든, DuckDB는 수동으로 내려받아 가져오는 절차 없이 그것을 직접 처리한다. 게다가 CSV와 Parquet처럼 지원되는 여러 파일 형식의 수집은 기본적으로 병렬화되어 있으므로, 데이터를 DuckDB로 들이는 일은 번개처럼 빠르다.
저자들은 깃허브에서 CSV 파일을 찾다가 여러 나라의 총인구 수치를 담은 데이터셋을 발견했다. 우선 레코드가 몇 개인지 센다.
D SELECT count(*) FROM 'https://github.com/bnokoro/Data-Science/raw/master/' 'countries%20of%20the%20world.csv';
| count_star() |
|---|
| int64 |
| 227 |
확장자가 사라진 자리 — 짧은 링크의 함정
URL이나 파일 이름이 특정 확장자로 끝나면—여기서는 .csv—DuckDB가 알아서 처리한다. 그러면 같은 CSV 파일의 짧은 링크를 넣으면 어떻게 되는가.
D SELECT count(*) FROM 'https://bit.ly/3KoiZR0'; Error: Catalog Error: Table with name https://bit.ly/3KoiZR0 does not exist! Did you mean "Player"? LINE 1: select count(*) from 'https://bit.ly/3KoiZR0';
해법은 read_csv_auto 함수다. 주어진 URI를 .csv 접미어가 없어도 CSV 파일인 것처럼 처리한다.
D SELECT count(*) FROM read_csv_auto("https://bit.ly/3KoiZR0");
결과 모드 2.5.1
결과를 표시하는 방식은 .mode <name>으로 고를 수 있다. 쓸 수 있는 모드의 목록은 .help mode로 본다. 이 장에서 지금까지 써온 것은 duckbox 모드로, 유연한 표 구조를 돌려준다. DuckDB에는 여러 모드가 딸려 있고, 크게 두 갈래로 나뉜다.
- 표 기반(table based) — 컬럼이 적을 때 잘 맞는다.
duckbox,box,csv,ascii,table,list,column이 여기 속한다. - 줄 기반(line based) — 컬럼이 많을 때 잘 맞는다.
json,jsonline,line이 여기 속한다. - 그 밖의 것 — 두 갈래에 들지 않는 것으로
html,insert, 그리고 아무것도 출력하지 않는trash가 있다.
같은 결과가 모드에 따라 어떻게 달라지는지는 말로 설명하기보다 직접 갈아 끼워 보는 편이 빠르다. 아래는 이 장 끝에서 만들게 되는 서유럽 데이터의 첫 다섯 행을 여러 모드로 옮겨본 것이다.
| Country | Population | Birthrate | Deathrate |
|---|---|---|---|
| varchar | varchar | varchar | varchar |
| Andorra | 71,201 | 8,71 | 6,25 |
| Austria | 8,192,880 | 8,74 | 9,76 |
| Belgium | 10,379,067 | 10,38 | 10,27 |
| Denmark | 5,450,661 | 11,13 | 10,36 |
| Faroe Islands | 47,246 | 14,05 | 8,7 |
| 5 rows 4 columns | |||
Country = Andorra Population = 71,201 Birthrate = 8,71 Deathrate = 6,25 Country = Austria Population = 8,192,880 Birthrate = 8,74 Deathrate = 9,76 Country = Belgium Population = 10,379,067 Birthrate = 10,38 Deathrate = 10,27
[{"Country":"Andorra","Population":"71,201","Birthrate":"8,71","Deathrate":"6,25"},
{"Country":"Austria","Population":"8,192,880","Birthrate":"8,74","Deathrate":"9,76"},
{"Country":"Belgium","Population":"10,379,067","Birthrate":"10,38","Deathrate":"10,27"}]
Country,Population,Birthrate,Deathrate Andorra,"71,201","8,71","6,25" Austria,"8,192,880","8,74","9,76" Belgium,"10,379,067","10,38","10,27" Denmark,"5,450,661","11,13","10,36" Faroe Islands,"47,246","14,05","8,7"
Country Population Birthrate Deathrate ------------- ----------- --------- --------- Andorra 71,201 8,71 6,25 Austria 8,192,880 8,74 9,76 Belgium 10,379,067 10,38 10,27 Denmark 5,450,661 11,13 10,36 Faroe Islands 47,246 14,05 8,7
첫 질의는 CSV 파일의 레코드 수를 셌을 뿐이니, 이제 어떤 컬럼이 있는지가 궁금해진다. 그런데 컬럼이 많아서 기본 모드로는 상당수가 잘려 나간다. 그래서 질의를 돌리기 전에 line 모드로 바꾼다.
D .mode line D SELECT * FROM read_csv_auto("https://bit.ly/3KoiZR0") LIMIT 1; Country = Afghanistan Region = ASIA (EX. NEAR EAST) Population = 31056997 Area (sq. mi.) = 647500 Pop. Density (per sq. mi.) = 48,0 Coastline (coast/area ratio) = 0,00 Net migration = 23,06 Infant mortality (per 1000 births) = 163,07 GDP ($ per capita) = 700 Literacy (%) = 36,0 Phones (per 1000) = 3,2 Arable (%) = 12,13 Crops (%) = 0,22 Other (%) = 87,65 Climate = 1 Birthrate = 46,6 Deathrate = 20,34 Agriculture = 0,38 Industry = 0,24 Service = 0,38
보다시피 line 모드는 duckbox보다 자리를 훨씬 많이 차지한다. 그러나 저자들의 경험으로는, 컬럼이 많은 데이터셋을 처음 탐색할 때 가장 좋은 모드다. 쓸 컬럼의 부분집합을 정한 다음에 다른 모드로 되돌아가면 된다.
이 데이터셋에는 여러 나라에 관한 흥미로운 정보가 많다. 나라의 수를 세고, 최대 인구와 전체 나라의 평균 면적을 구하는 질의를 써본다. 돌려받을 컬럼이 몇 개뿐이니 duckbox 모드로 되돌린다.
D .mode duckbox D SELECT count(*) AS countries, max(Population) AS max_population, round(avg(cast("Area (sq. mi.)" AS decimal))) AS avgArea FROM read_csv_auto("https://bit.ly/3KoiZR0");
| countries | max_population | avgArea |
|---|---|---|
| int64 | int64 | double |
| 227 | 1313973713 | 598227.0 |
대화형을 벗어나 파이프라인으로
앞의 예들은 모두 대화형 모드에서 돌렸다. 그러나 DuckDB CLI는 비대화형으로도 동작한다. 표준 입력에서 읽고 표준 출력으로 쓴다. 그래서 온갖 종류의 파이프라인을 지어 올릴 수 있다.
이 장을 맺는 예제는 서유럽 나라들의 인구, 출생률, 사망률을 뽑아 새 로컬 CSV 파일을 만드는 스크립트다. DuckDB CLI에서 .exit로 나가거나, 다른 탭을 열어 실행한다.
$ duckdb -csv \ -s "SELECT Country, Population, Birthrate, Deathrate FROM read_csv_auto('https://bit.ly/3KoiZR0') WHERE trim(region) = 'WESTERN EUROPE'" \ > western_europe.csv $ head -n6 western_europe.csv
| Country | Population | Birthrate | Deathrate |
|---|---|---|---|
| Andorra | 71,201 | 8,71 | 6,25 |
| Austria | 8,192,880 | 8,74 | 9,76 |
| Belgium | 10,379,067 | 10,38 | 10,27 |
| Denmark | 5,450,661 | 11,13 | 10,36 |
| Faroe Islands | 47,246 | 14,05 | 8,7 |
Parquet은 리다이렉션으로 만들 수 없다
Parquet 파일도 만들 수 있다. 다만 출력을 .parquet 확장자를 가진 파일로 그대로 흘려보낼 수는 없다. 대신 COPY … TO 절에 파일 이름을 목적지로 지정한다. 이진 컬럼 형식이니 표준 출력으로 밀어낼 수 있는 물건이 아니라는, 당연하지만 놓치기 쉬운 구분이다.
$ duckdb \ -s "COPY ( SELECT Country, Population, Birthrate, Deathrate FROM read_csv_auto('https://bit.ly/3KoiZR0') WHERE trim(region) = 'WESTERN EUROPE' ) TO 'western_europe.parquet' (FORMAT PARQUET)" # 만든 Parquet 파일은 어떤 Parquet 판독기로도 볼 수 있다. DuckDB 자신으로도. $ duckdb -s "FROM 'western_europe.parquet' LIMIT 5"
되풀이되는 설정을 위한 설정 파일
반복되는 설정과 사용법은 $HOME/.duckdbrc에 놓인 설정 파일에 담아둘 수 있다. 이 파일은 시작할 때 읽히며, 그 안의 모든 명령—점 명령이든 SQL 명령이든—이 하나의 .read 명령을 통해 실행된다. 그래서 CLI의 설정 상태와, SQL 명령으로 초기화하고 싶은 것을 함께 저장해둘 수 있다.
이 파일에 넣을 만한 것의 예로 저자들은 맞춤 프롬프트와 환영 메시지를 든다. 오리 머리 모양의 프롬프트라니, 이 프로젝트의 유머 감각이 문서 곳곳에 배어 있다.
-- Duck head prompt .prompt 'O> ' -- Example SQL statement select 'Begin quacking now '||cast(now() as string) as "Ready, Set, ...";
제2장이 남긴 다섯 문장Summary
- DuckDB는 Python, R, Java, JavaScript, Julia, C/C++, ODBC, WASM, Swift용 라이브러리로 제공된다.
- CLI는 출력 제어, 파일 읽기, 내장 도움말 등을 위한 점 명령을 추가로 지원한다.
.mode로 duckbox, line, ascii 등 여러 표시 모드를 쓸 수 있다.- httpfs 확장을 설치하면 HTTP 서버의 CSV 파일을 곧바로 질의할 수 있다.
- 외부 데이터셋을 질의하고 그 결과를 표준 출력이나 다른 파일로 써내면, 테이블을 만들지 않고도 CLI를 데이터 파이프라인의 한 단계로 쓸 수 있다.