TechAscent - DuckDB - Data power tools for your laptop, now in ClojureBlog Home<br>Contact
2023-09-02<br>DuckDB - Data power tools for your laptop, now in Clojure<br>Establishing the Need<br>Our in-memory column-major data processing platform, tech.ml.dataset (TMD), drives the future of functional data science. When data gets large enough not to fit in memory, one can continue with TMD by operating on samples of data, or otherwise filtering to relevant subsets to fit in bounds imposed by the working environment. Moreover, one can accomplish persistence, for data small and large, with nippy, arrow, or parquet.<br>When data becomes large enough, for example sets of .csv files on the order of ~100GB with relational aspects to them, the tools in their current state can become unwieldy. One is tempted to get involved in nonfunctional sparky cluster snafus. Of course, maintaining some level of transactional interaction and a simple disk IO model is still super-desirable. Local disks are big enough, and local chips are fast enough, no need to do anything rash.<br>Relational databases are well adapted for out-of-memory storage and fast relational queries - but, how to leverage this without giving up the advantages of functional programming, and TMD's column-major processing model? JDBC, along with Postgres, provide a good first answer to this question, but it's irritating to perform a full row-to-column conversion through an inefficient, non-batched API in order to get the data through JDBC and into TMD.<br>A New Challenger Appears<br>DuckDB showed up via a github issue in May of 2021 and tmducken was minimally integrated with their C bindings by December of that year. In that version, all query results were returned at once, and so needed to fit in memory. Also, in those early days of DuckDB there was not a specific high performance append or insert system, so IO was limiting potential performance, and Postgres persisted as the adjunct processing system to TMD. Much has changed since then.<br>In the last two years, DuckDB improved a lot. Importantly, the C interface now provides a batched system for both inserts and querying, which enables processing very large joins - more on that later. These improved capabilities can now be leveraged in Clojure, through TMD, to access DuckDB's state of the art vectorized SQL execution engine, and it's good.<br>Actual Use<br>Building on our previous post, there is a 50 gigabyte .csv file with 3 years of transaction data, totaling 400,000,000 rows:<br>$ ll -h data.csv<br>-rw-rw-r-- 1 harold harold 50G Aug 8 09:49 data.csv<br>Loading that into DuckDB is surprisingly easy - though, you do have to wait 2 minutes:<br>$ time duckdb data.ddb 'CREATE TABLE data AS FROM "data.csv";'<br>100% ▕████████████████████████████████████████████████████████████▏
real 1m50.091s<br>user 21m42.693s<br>sys 0m57.887s<br>$ ll -h data.ddb<br>-rw-rw-r-- 1 harold harold 18G Sep 6 10:57 data.ddb<br>So, that reduced the file to 18GB, which includes all of the indexes (!) created automatically by DuckDB.<br>The data is in there:<br>$ duckdb data.ddb<br>v0.8.1 6536a77232<br>Enter ".help" for usage hints.<br>D SELECT COUNT(*) AS n FROM data;<br>┌───────────┐<br>│ n │<br>│ int64 │<br>├───────────┤<br>│ 400000000 │<br>└───────────┘<br>D DESCRIBE TABLE data;<br>┌────────────────┬─────────────┬─────────┬─────────┬─────────┬─────────┐<br>│ column_name │ column_type │ null │ key │ default │ extra │<br>│ varchar │ varchar │ varchar │ varchar │ varchar │ varchar │<br>├────────────────┼─────────────┼─────────┼─────────┼─────────┼─────────┤<br>│ customer-id │ VARCHAR │ YES │ │ │ │<br>│ day │ BIGINT │ YES │ │ │ │<br>│ inst │ TIMESTAMP │ YES │ │ │ │<br>│ month │ BIGINT │ YES │ │ │ │<br>│ brand │ VARCHAR │ YES │ │ │ │<br>│ style │ VARCHAR │ YES │ │ │ │<br>│ sku │ VARCHAR │ YES │ │ │ │<br>│ year │ BIGINT │ YES │ │ │ │<br>│ transaction-id │ VARCHAR │ YES │ │ │ │<br>│ quantity │ BIGINT │ YES │ │ │ │<br>│ price │ DOUBLE │ YES │ │ │ │<br>├────────────────┴─────────────┴─────────┴─────────┴─────────┴─────────┤<br>│ 11 rows 6 columns │<br>└──────────────────────────────────────────────────────────────────────┘<br>Accessing this from Clojure, through TMD, is also easy:<br>user> (require '[tmducken.duckdb :as duckdb])<br>nil<br>user> (require '[tech.v3.dataset :as ds])<br>nil<br>user> (duckdb/initialize!)<br>Sep 06, 2023 11:00:12 AM clojure.tools.logging$eval7454$fn__7457 invoke<br>INFO: Attempting to load duckdb from "./binaries/libduckdb.so"<br>true<br>user> (def db (duckdb/open-db "data.ddb"))<br>#'user/db<br>user> (def conn (duckdb/connect db))<br>#'user/conn<br>user> (time (duckdb/sql->dataset conn "SELECT COUNT(*) AS n FROM data"))<br>"Elapsed time: 10.305756 msecs"<br>:_unnamed [1 1]:
| n |<br>|----------:|<br>| 400000000 |<br>Now, imagine management lets us know that there is another dataset, that captures information for each sku about what colors the product is. That needs to be in the database as well:<br>user> (-> (let [colors ["red" "green" "blue" "yellow" "purple" "black" "white"]]<br>(->> (for [brand (range 100)<br>style (range 10)<br>item (range 10)]<br>(let [sku (format "sku-%s-%s-%s" brand style item)<br>n (rand-int 8)]<br>(for [color...