https://duckdb.org/2024/06/20/cli-data-processing-using-duckdb-as-a-unix-tool.html DuckDB Logo for Download Documentation * Installation * How-to guides * Data import * Client APIs * SQL * Why DuckDB * Media * FAQ Blog GitHub 19.9k Support Support Command Line Data Processing: Using DuckDB as a Unix Tool Author Avatar Gabor Szarnyas2024-06-20 TL;DR: DuckDB's CLI client is portable to many platforms and architectures. It handles CSV files conveniently and offers users the same rich SQL syntax everywhere. These characteristics make DuckDB an ideal tool to complement traditional Unix tools for data processing in the command line. In this blog post, we dive into the terminal to compare DuckDB with traditional tools used in Unix shells (Bash, Zsh, etc.). We solve several problems requiring operations such as projection and filtering to demonstrate the differences between using SQL queries in DuckDB versus specialized command line tools. In the process, we will show off some cool features such as DuckDB's powerful CSV reader and the positional join operator. Let's get started! Table of Contents * Table of Contents * The Unix Philosophy * Portability and Usability * Data Processing with Unix Tools and DuckDB + Datasets + Projecting Columns + Sorting Files + Intersecting Columns + Pasting Rows Together + Filtering + Joining Files + Replacing Strings + Reading JSON * Performance * Summary The Unix Philosophy To set the stage, let's recall the Unix philosophy. This states that programs should: * do one thing and do it well, * work together, and * handle text streams. Unix-like systems such as macOS, Linux and WSL in Windows have embraced this philosophy. Tools such as grep, sed, and sort are ubiquitious and widely used in shell scripts. As a purpose-built data processing tool, DuckDB fits the Unix philosophy quite well. First, it was designed to be a fast in-process analytical SQL database system (do one thing and do it well). Second, it has a standalone command line client, which can consume and produce CSV files (work together), and also supports reading and writing text streams (handle text streams). Thanks to these, DuckDB works well in the ecosystem of Unix CLI tools, as shown in several posts. Portability and Usability While Unix CLI tools are fast, robust, and available on all major platforms, they often have cumbersome syntax that's difficult to remember. To make matters worse, these tools often come with slight differences between systems - think of the differences between GNU sed and macOS's sed or the differences between regex syntax among programs, which is aptly captured by Donald Knuth's quip "I define Unix as 30 definitions of regular expressions living under one roof." While there are shells specialized specifically for dataframe processing, such as the Nushell project, older Unix shells (e.g., the Bourne shell sh and Bash) are still the most wide-spread, especially on servers. At the same time, we have DuckDB, an extremely portable database system which uses the same SQL syntax on all platforms. With version 1.0.0 released recently, DuckDB's syntax - based on the proven and widely used PostgeSQL dialect - is now in a stable state. Another attractive feature of DuckDB is that it offers an interactive shell, which aids quick debugging. Moreover, DuckDB is available in several host languages as well as in the browser via WebAssembly, so if you ever decide to use your SQL scripts outside of the shell, DuckDB SQL scripts can be ported to a wide variety of environments without any changes. --------------------------------------------------------------------- Data Processing with Unix Tools and DuckDB In the following, we give examples for implementing simple data processing tasks using the CLI tools provided in most Unix shells and using DuckDB SQL queries. We use DuckDB v1.0.0 and run it in in-memory mode. This mode makes sense for the problems we are tackling, as we do not create any tables and the operations are not memory-intensive, so there is no data to persist or to spill on disk. --------------------------------------------------------------------- Datasets We use the four input files capturing information on cities and airports in the Netherlands. pop.csv, the population of each of the top-10 most populous cities. city,province,population Amsterdam,North Holland,905234 Rotterdam,South Holland,656050 The Hague,South Holland,552995 Utrecht,Utrecht,361924 Eindhoven,North Brabant,238478 Groningen,Groningen,234649 Tilburg,North Brabant,224702 Almere,Flevoland,218096 Breda,North Brabant,184716 Nijmegen,Gelderland,179073 area.csv, the area of each of the top-10 most populous cities. city,area Amsterdam,219.32 Rotterdam,324.14 The Hague,98.13 Utrecht,99.21 Eindhoven,88.92 Groningen,197.96 Tilburg,118.13 Almere,248.77 Breda,128.68 Nijmegen,57.63 cities-airports.csv, the IATA codes of civilian airports serving given cities. city,IATA Amsterdam,AMS Haarlemmermeer,AMS Eindhoven,EIN Groningen,GRQ Eelde,GRQ Maastricht,MST Beek,MST Rotterdam,RTM The Hague,RTM airport-names.csv, the airport names belonging to given IATA codes. IATA,airport name AMS,Amsterdam Airport Schiphol EIN,Eindhoven Airport GRQ,Groningen Airport Eelde MST,Maastricht Aachen Airport RTM,Rotterdam The Hague Airport You can download all input files as a single zip file. --------------------------------------------------------------------- Projecting Columns Projecting columns is a very common data processing step. Let's take the pop.csv file and project the first and last columns, city and population. Unix Shell: cut In the Unix shell, we use the cut command and specify the file's delimiter (-d) and the columns to be projected (-f). cut -d , -f 1,3 pop.csv This produces the following output: city,population Amsterdam,905234 Rotterdam,656050 The Hague,552995 Utrecht,361924 Eindhoven,238478 Groningen,234649 Tilburg,224702 Almere,218096 Breda,184716 Nijmegen,179073 DuckDB: SELECT In DuckDB, we can use the CSV reader to load the data, then use the SELECT clause with column indexes (#i) to designate the columns to be projected: SELECT #1, #3 FROM 'pop.csv'; Note that we did not have to define any schema or load the data to a table. Instead, we simply used 'pop.csv' in the FROM clause as we would do with a regular table. DuckDB detects that this is a CSV file and invokes the read_csv function, which automatically infers the CSV file's dialect (delimiter, presence of quotes, etc.) as well as the schema of the table. This allows us to simply project columns using SELECT #1, #3. We could also use the more readable syntax SELECT city, population. To make the output of the solutions using Unix tools and DuckDB equivalent, we wrap the query into a COPY ... TO statement: COPY ( SELECT #1, #3 FROM 'pop.csv' ) TO '/dev/stdout/'; This query produces the same result as the Unix command's output shown above. To turn this into a standalone CLI command, we can invoke the DuckDB command line client with the -c argument, which runs the SQL query and exits once it's finished. Using this technique, the query above can be turned into the following one-liner: duckdb -c "COPY (SELECT #1, #3 FROM 'pop.csv') TO '/dev/stdout/'" In the following, we'll omit the code blocks using the standalone duckdb command: all solutions can be executed in the duckdb -c template and yield the same result as the solutions using Unix tools. --------------------------------------------------------------------- Sorting Files Another common task is to sort files based on given columns. Let's rank the cities within provinces based on their populations. To do so, we need to sort the pop.csv file first based on the name of the province using an ascending order, then on the population using a descending order. We then return the province column first, followed by the city and the population columns. Unix Shell: sort In the Unix shell, we rely on the sort tool. We specify the CSV file's separator with the -t argument and set the keys to sort on using -k arguments. We first sort on the second column (province) with -k 2,2. Then, we sort on the third column (population), setting the ordering to be reversed (r) and numeric (n) with -k 3rn. Note that we need to handle the header of the file separately: we take the first row with head -n 1 and the rest of the rows with tail -n +2, sort the latter, and glue them back together with the header. Finally, we perform a projection to reorder the columns. Unfortunately, the cut command cannot reorder the columns, so we use awk instead: (head -n 1 pop.csv; tail -n +2 pop.csv \ | sort -t , -k 2,2 -k 3rn) \ | awk -F , '{ print $2 "," $1 "," $3}' The result is the following: province,city,population Flevoland,Almere,218096 Gelderland,Nijmegen,179073 Groningen,Groningen,234649 North Brabant,Eindhoven,238478 North Brabant,Tilburg,224702 North Brabant,Breda,184716 North Holland,Amsterdam,905234 South Holland,Rotterdam,656050 South Holland,The Hague,552995 Utrecht,Utrecht,361924 DuckDB: ORDER BY In DuckDB, we simply load the CSV and specify the column ordering via SELECT province, city, population, then set the sorting criteria on the selected columns (province ASC and population DESC). The CSV reader automatically detects types, so the sorting is numeric by default. Finally, we surround the query with a COPY statement to print the results to the standard output. COPY ( SELECT province, city, population FROM 'pop.csv' ORDER BY province ASC, population DESC ) TO '/dev/stdout/'; --------------------------------------------------------------------- Intersecting Columns A common task is to calculate the intersection of two columns, i.e., to find entities that are present in both. Let's find the cities that are both in the top-10 most populous cities and have their own airports. Unix Shell: comm The Unix solution for intersection uses the comm tool, intended to compare two sorted files line-by-line. We first cut the relevant colum from both files. Due to the sorting requirement, we apply sort on both inputs before performing the intersection. The intersection is performed using comm -12 where the argument -12 means that we only want to keep lines that are in both files. We again rely on head and tail to treat the headers and the rest of the files separately during processing and glue them together at the end. head -n 1 pop.csv | cut -d , -f 1; \ comm -12 \ <(tail -n +2 pop.csv | cut -d , -f 1 | sort) \ <(tail -n +2 cities-airports.csv | cut -d , -f 1 | sort) The script produces the following output: city Amsterdam Eindhoven Groningen Rotterdam The Hague DuckDB: INTERSECT ALL The DuckDB solution reads the CSV files, projects the city fields and applies the INTERSECT ALL clause to calculate the intersection: COPY ( SELECT city FROM 'pop.csv' INTERSECT ALL SELECT city FROM 'cities-airports.csv' ) TO '/dev/stdout/'; --------------------------------------------------------------------- Pasting Rows Together Pasting rows together line-by-line is a recurring task. In our example, we know that the pop.csv and the area.csv files have an equal number of rows, so we can produce a single file that contains both the population and the area of every city in the dataset. Unix Shell: paste In the Unix shell, we use the paste command and remove the duplicate city field using cut: paste -d , pop.csv area.csv | cut -d , -f 1,2,3,5 The output is the following: city,province,population,area Amsterdam,North Holland,905234,219.32 Rotterdam,South Holland,656050,324.14 The Hague,South Holland,552995,98.13 Utrecht,Utrecht,361924,99.21 Eindhoven,North Brabant,238478,88.92 Groningen,Groningen,234649,197.96 Tilburg,North Brabant,224702,118.13 Almere,Flevoland,218096,248.77 Breda,North Brabant,184716,128.68 Nijmegen,Gelderland,179073,57.63 DuckDB: POSITIONAL JOIN In DuckDB, we can use a POSITIONAL JOIN. This join type is one of DuckDB's SQL extensions and it provides a concise syntax to combine tables row-by-row based on each row's position in the table. Joining the two tables together using POSITIONAL JOIN results in two city columns - we use the EXCLUDE clause to remove the duplicate column: COPY ( SELECT pop.*, area.* EXCLUDE city FROM 'pop.csv' POSITIONAL JOIN 'area.csv' ) TO '/dev/stdout/'; --------------------------------------------------------------------- Filtering Filtering is another very common operation. For this, we'll use cities-airports.csv file. For each airport, this file contains its IATA code and the main cities that it serves: city,IATA Amsterdam,AMS Haarlemmermeer,AMS Eindhoven,EIN ... Let's try to formulate two queries: 1. Find all cities whose name ends in dam. 2. Find all airports whose IATA code is equivalent to the first three letters of a served city's name, but the city's name does not end in dam. Unix Shell: grep To answer the first question in the Unix shell, we use grep and the regular expression ^[^,]*dam,: grep "^[^,]*dam," cities-airports.csv In this expression, ^ denotes the start of the line, [^,]* searches for a string that does not contain the comma character (the separator). The expression dam, ensures that the end of the string in the first field is dam. The output is: Amsterdam,AMS Rotterdam,RTM Let's try to answer the second question. For this, we need to match the first three characters in the city field to the IATA field but we need to do so in a case-insensitive manner. We also need to use a negative condition to exclude the lines where the city's name ends in dam. Both of these requirements are difficult to achieve with a single grep or egrep command as they lack support for two features. First, they do not support case-insensitive matching using a backreference (grep -i alone is not sufficient to ensure this). Second, they do not support negative lookbehinds. Therefore, we use pcregrep, and formulate our question as follows: pcregrep -i '^([a-z]{3}).*?(?