Research
DuckDB Under the Microscope
DuckDB Under the Microscope: An Experimental Evaluation of Query Performance and Analytical Workloads
Rajesh Meher
Data Engineering & Architecture, Independent Researcher, San Antonio, Texas, USA
Abstract
DuckDB is an embedded analytical database designed to execute SQL workloads within an application process. Its in-process architecture and support for analytical execution make it a candidate for local analytics, exploratory data analysis, data transformation, and querying columnar files. However, performance depends on data volume, selectivity, join cardinality, memory limits, storage format, cache state, and engine configuration. This paper defines a reproducible experimental evaluation focused on query latency, scaling behavior, memory consumption, and workload type. The proposed benchmark covers scans, selective filters, aggregations, joins, sorting, and direct Parquet queries across multiple data scales. DuckDB is compared first with PostgreSQL on a single host; a distributed-engine extension is optional. The methodology specifies controlled conditions, repeated trials, correctness checks, reporting statistics, and validity safeguards. No performance measurements are asserted in this draft: results tables are intentionally left for values collected by running the protocol. This distinction is essential to avoid unsupported speedup claims and make the eventual paper auditable and reproducible.
Keywords—DuckDB; analytical database; OLAP; query performance; benchmark methodology; Parquet; PostgreSQL; reproducibility.
1. Introduction
Analytical workloads increasingly run outside centralized data warehouses. Engineers query local extracts, object-store files, lakehouse tables, notebooks, transformation pipelines, and application data. These workflows create demand for database systems that provide expressive SQL without routing every analytical task through a remote database service.
DuckDB addresses this use case through an embedded analytical database model. Rather than requiring a separate database server for typical single-process workflows, it can execute in the same process as the application. The original system paper describes DuckDB as an embeddable database designed for analytical SQL [1]. Work on embedded analytics further discusses the integration challenges that arise when data management is brought closer to data-science workflows [2]. These design choices motivate an empirical question: under which workload conditions does DuckDB execute analytical queries efficiently, and how does behavior change as the workload grows?
A useful performance study should not repeat vendor-published results or report a single query duration. Database comparisons are sensitive to benchmark construction, data loading, indexing, caching, configuration, query equivalence, and system familiarity. Raasveldt et al. identify common pitfalls that can produce misleading comparisons even when measurements appear objective [3]. This study therefore treats experimental control and transparent reporting as core contributions.
The paper defines a workload matrix that separates common analytical operators, specifies a repeatable procedure for collecting latency and resource metrics, and provides a reporting structure to connect observed performance with data scale, query shape, and execution plans.
2. Research Questions and Scope
RQ1: How do DuckDB query latency and resource consumption change as input data scales across scans, aggregations, joins, and sorting?
RQ2: How strongly does query shape affect performance, particularly selectivity, grouping cardinality, join multiplicity, and sort volume?
RQ3: Under a documented single-host setup and equivalent query semantics, how does DuckDB compare with PostgreSQL for analytical workloads, including Parquet queries where comparable access paths are available?
The primary comparison is DuckDB versus PostgreSQL on the same host. This is narrower than comparing embedded, server-based, and distributed engines in one experiment. PostgreSQL is a useful relational baseline, but its general-purpose design differs from DuckDB's analytical focus. Results should be interpreted as comparisons of tested configurations, not universal rankings. Spark SQL or Trino can be added in a separate extension, where startup, scheduling, shuffle, networking, and cluster resources are documented.
3. Background and Related Work
3.1 Embedded analytical execution
Raasveldt and Mühleisen introduced DuckDB as an embeddable analytical database at SIGMOD 2019 [1]. Its embedded model reduces the operational boundary between application code and query execution. Their later CIDR work frames embedded analytics as a response to workflows that combine independent tools for storage and processing [2]. These papers establish the system context but do not substitute for a workload-specific evaluation on the hardware and versions used here.
3.2 Profiling and workload behavior
DuckDB provides EXPLAIN and EXPLAIN ANALYZE for inspecting physical plans and runtime behavior [4]. Its performance guidance identifies grouping, joining, sorting, and windowing as blocking operators and discusses larger-than-memory execution limitations [5]. This motivates measuring both elapsed time and resource use.
3.3 Fair benchmarking
Fair comparison requires equivalent logical work, explicit configuration, repeated trials, and careful treatment of cache state. A benchmark can favor one engine through data preparation, query translation, indexing, or tuning choices that are not disclosed [3]. This protocol records versions, machine specifications, thread counts, memory limits, storage format, loading time, query text, and run order.
3.4 Study positioning
This study is a practical workload evaluation rather than a new database architecture. Its potential contribution is a transparent comparison across operator categories and data scales, including direct Parquet querying. Any novelty claim should be finalized only after a focused literature review establishes which benchmark combinations and versions have already been studied.
4. Experimental Design
4.1 Systems under test
System | Role | Required details |
DuckDB | Primary system | Exact version/build; API; thread count; memory limit; database mode; extensions |
PostgreSQL | Single-host baseline | Exact version; configuration; work_mem; shared_buffers; parallel workers; indexes |
Optional Spark SQL | Separate distributed extension | Version; cluster topology; executor/driver memory; cores; shuffle settings |
Run DuckDB and PostgreSQL on the same physical or virtual host for the primary comparison. Record OS, CPU model, physical core count, RAM, storage type, filesystem, and available disk space. If internal settings cannot be aligned, document the differences rather than implying perfect equivalence.
4.2 Dataset design
Use deterministic synthetic data with a fixed seed so row count, value distribution, and key cardinality can be controlled. An initial scale set is 1 million, 10 million, and 50 million fact rows, subject to hardware limits. If the largest scale cannot be run reliably, reduce it and document why. Use a fact table with an integer identifier, date or timestamp, categorical dimension key, numeric measure, and additional attributes; generate dimension tables with controlled key uniqueness and cardinality.
Scale | Fact rows | Purpose | Status |
S1 | 1,000,000 | Baseline and quick iteration | To generate |
S2 | 10,000,000 | Moderate-scale growth | To generate |
S3 | 50,000,000 or feasible alternative | Larger-scale behavior | To generate |
For file-based tests, export the same logical data to Parquet and record compression codec, row-group size, column types, file count, and total bytes. Do not compare a heavily indexed PostgreSQL table with unindexed DuckDB data without explicitly treating that as a separate, application-oriented configuration.
4.3 Workload matrix
ID | Workload | Representative operation | Main factors |
Q1 | Full scan / aggregation | SUM and COUNT over all fact rows | Rows processed; scan throughput |
Q2 | Selective filter | Date/category filter followed by aggregate | Selectivity; predicate pushdown |
Q3 | Group-by aggregation | GROUP BY category or date bucket | Group cardinality; aggregate state |
Q4 | Equi-join | Fact-to-dimension join and aggregate | Join key; cardinality |
Q5 | Sort / top-N | ORDER BY measure with LIMIT | Sort volume; top-N optimization |
Q6 | Direct Parquet scan | Aggregate directly from Parquet | File size; projection; filters |
Define canonical logical queries. Engine-specific SQL differences are acceptable only when needed to preserve equivalent semantics and must be documented. Exclude client-side serialization from query timing unless measuring end-to-end application latency. Prefer scalar or small aggregate outputs to reduce client-transfer effects.
5. Measurement Protocol
5.1 Controlled environment
Record operating system, CPU, RAM, storage, filesystem, power mode, and whether the test uses a container or virtual machine.
Record exact engine versions and configuration. Use the same thread limit where meaningful and document engine-specific settings.
Use deterministic dataset generation with fixed seeds; preserve the generation script and parameters.
Separate data loading and index creation from query execution; report setup time independently.
Close competing workloads and record environmental interruptions.
5.2 Cache state
Cache state can materially influence elapsed time. Report warm-cache and cold-cache measurements separately where cold-cache conditions can be established consistently. Define warm runs as execution after a controlled warm-up. A cold-cache run requires a documented procedure that clears or bypasses relevant caches; if the environment does not permit a trustworthy reset, do not label a run cold-cache. Never combine both states into one average.
5.3 Repetition and run order
For each engine, dataset scale, query, and cache condition, perform at least five measured runs after a warm-up; ten measured runs are recommended where execution time permits. Exclude the warm-up from summary statistics. Rotate or randomize the order of engine/query combinations to reduce systematic bias from thermal state or background load. Preserve raw run-level data.
5.4 Metrics
Metric | Definition | Reporting guidance |
Elapsed query time | Wall-clock duration | Median, IQR, and raw runs |
CPU utilization | Process/system CPU use | Tool, sample interval, aggregation rule |
Peak resident memory | Maximum resident memory during query | Measurement tool and shared-memory caveat |
Input size | Rows/bytes processed where available | Compressed vs uncompressed bytes |
Throughput | Rows processed per second | Use only for comparable query semantics |
Execution plan | Physical plan and operator profile | Archive EXPLAIN/EXPLAIN ANALYZE output |
Report median and interquartile range (IQR), not only the arithmetic mean. For paired comparisons, calculate the per-query latency ratio T_PostgreSQL / T_DuckDB. A value above 1 means lower elapsed time for DuckDB in that specific configuration; below 1 means lower elapsed time for PostgreSQL. Do not aggregate ratios across unrelated queries without stating the method.
5.5 Correctness validation
Validate row counts, aggregate values, join output counts, null behavior, and sorting/tie semantics. Use deterministic ordering only when order is part of the query contract. Floating-point aggregates can differ due to evaluation order; define a numeric tolerance before testing. Exclude or separately report queries whose semantics cannot be shown equivalent.
6. Hypotheses
H1: DuckDB's relative performance, if present, will vary by operator category rather than remain constant across all queries.
H2: Increasing input size will increase elapsed time, but scaling curves will differ among scans, aggregations, joins, and sorting.
H3: Direct Parquet queries will have a distinct performance profile from queries over materialized database tables because metadata, projection, predicate pushdown, and data layout affect the work performed.
H4: High-cardinality joins and blocking operators will increase memory pressure and may cause nonlinear changes at larger scales.
These are hypotheses, not findings. They should be retained, refined, or rejected based on measurements rather than rewritten after results are observed.
Interpretation: these are results reported by an independent published study, not measurements from the experiments proposed in this manuscript. The study used a specific clinical-data pipeline and workload, so its results motivate the present research question but cannot be generalized to every DuckDB query or hardware configuration.
Published metric | DuckDB | PostgreSQL / CSV baseline | Interpretation |
Scenario-based SQL workload wall time | 90.2 s | PostgreSQL: 2,546.4 s | ≈28.2× lower elapsed time for DuckDB in that study's tested configuration |
Approximate wall-time reduction vs PostgreSQL | 96.5% | Reference baseline | Calculated from the published totals; not a result of this manuscript's experiment |
CPU utilization | 72.1% | PostgreSQL: 39.6% | DuckDB used more CPU during the reported workload |
Memory use | 13.7 GB | PostgreSQL: 14.8 GB | Comparable order of magnitude in that experiment |
Read throughput | 554.7 MB/s | PostgreSQL: 365.2 MB/s | Reported throughput was higher for DuckDB |
Disk-access time in CSV vs Parquet workflow | Parquet: 44.2 s | CSV: 940.2 s | ≈95.3% reduction reported for Parquet disk access, not a DuckDB-vs-PostgreSQL-only result |
Dataset size in the study | 13.76 million rows; ~3.2 GB; 43 columns | Same clinical dataset context | Provides a real-world scale reference for designing our own benchmark |
Repeated runs | 20 paired runs per condition (reported) | Same run identifiers | Supports the value of repeated measurements and distribution reporting |
A recent empirical study by Lee, Hwang, and Lee (2026) evaluated a clinical dataset containing approximately 13.76 million rows and reported a 90.2-second total for DuckDB versus 2,546.4 seconds for PostgreSQL across its scenario-based SQL workload. The ratio is approximately 28.2× (2,546.4 ÷ 90.2), corresponding to about a 96.5% reduction in elapsed time relative to that PostgreSQL total. The same study reported DuckDB/PostgreSQL CPU utilization of 72.1%/39.6%, memory use of 13.7/14.8 GB, and read throughput of 554.7/365.2 MB/s. Separately, it reported disk-access time of 940.2 seconds for CSV versus 44.2 seconds for Parquet, a reduction of approximately 95.3%. These figures are included as contextual evidence from the literature; they must not be represented as results generated by the author of this manuscript.
The cited study is domain-specific and its aggregate totals depend on its query mix, environment, storage layout, and configuration. A single aggregate result does not establish that DuckDB is 28× faster for every query, nor does it replace a benchmark with fully documented hardware and workload details. The present study will test whether similar patterns appear across a more explicitly separated matrix of scans, filters, group-by operations, joins, sorting, and Parquet queries.
7. Results from a Published Benchmark
Because this manuscript does not include newly executed benchmark runs, this section reports quantitative results from a peer-reviewed, published study by Lee, Hwang, and Lee (2026) as contextual evidence. The values below are attributed to that study and must not be interpreted as original measurements collected by the present author. The source evaluated a clinical-data pipeline with approximately 13.76 million rows and compared scenario-based analytical SQL workloads in DuckDB and PostgreSQL.
In that study, DuckDB's reported wall-clock time was 90.2 seconds compared with 2,546.4 seconds for PostgreSQL. The ratio is approximately 28.2, and the reduction in elapsed time is approximately 96.5% relative to the PostgreSQL total. These results apply to the specific workload mix, storage design, software versions, and execution environment used in the published study; they should not be generalized to every query or deployment.
7.1 Environment Summary
Study characteristic | Published value / description |
Study | Lee, Hwang, and Lee (2026), JMIR Medical Informatics |
Dataset | Approximately 13.76 million rows, 43 columns, and 3.2 GB |
Workload | Scenario-based analytical SQL, adapted from TPC-DS patterns; included window functions, multidimensional aggregation, and ROLLUP |
Systems | DuckDB and PostgreSQL |
Repeated measurements | 20 paired runs per condition were reported |
Interpretation boundary | Published study results; not measurements newly collected for this manuscript |
7.2 Latency Results
Metric | DuckDB | PostgreSQL | Derived comparison | Unit | Source / note |
Wall-clock time | 90.2 | 2546.4 | ≈28.2× lower time for DuckDB | seconds | Published scenario-based workload |
CPU utilization | 72.1 | 39.6 | 32.5 percentage points higher for DuckDB | % | Reported CPU utilization |
Memory use | 13.7 | 14.8 | 1.1 GB lower for DuckDB | GB | Reported memory use |
Read throughput | 554.7 | 365.2 | ≈1.52× higher for DuckDB | MB/s | Reported read throughput |
Write throughput | 1116.0 | 998.0 | ≈1.12× higher for DuckDB | MB/s | Reported write throughput |
7.3 Memory and Scaling
Metric | CSV | Parquet | Derived comparison | Interpretation |
Disk-access time | 940.2 s | 44.2 s | ≈95.3% lower with Parquet | Storage-format comparison in the published clinical pipeline; not a direct DuckDB-versus-PostgreSQL result |
Dataset size | 13.76 million rows; ~3.2 GB; 43 columns | Same logical dataset | — | Reported study context |
Predictive-performance equivalence | Reported as within prespecified equivalence margins | Reported as within prespecified equivalence margins | — | The article reports equivalent predictive performance across selected metrics; this is not a database query-performance metric |
Plot median latency against row count and peak memory by query type. Explain whether observed changes align with data volume, plan changes, spilling, or resource limits. Any explanation based on query plans should cite archived plan evidence.
The reported Parquet-versus-CSV disk-access reduction is approximately 95.3% (940.2 seconds to 44.2 seconds). This measures a storage-format change within the published clinical pipeline and should not be confused with the DuckDB-versus-PostgreSQL comparison. The present paper's contribution should be framed as an analysis and synthesis of these published findings unless a separate reproducible benchmark is conducted.
The published aggregate figures indicate lower elapsed time and memory consumption, and higher reported throughput, for DuckDB in that specific workload. Because hardware, software versions, and query-level results are not fully reproduced here, these findings are context-specific. Query-level performance and scaling conclusions for S1–S3 remain undetermined without running the proposed benchmark.
8. Discussion
The final discussion should translate measured results into bounded guidance. If DuckDB performs well on local scans and aggregations in the tested setup, that supports considering it for local analytical processing; it does not establish suitability for every concurrent multi-user or distributed workload. If PostgreSQL performs competitively on a query class, describe the workload and configuration instead of treating it as an anomaly.
Distinguish logical work, physical execution choices, and environmental effects. Query plans can help explain differences, but an operator's presence alongside a longer runtime does not prove causation. Treat direct Parquet queries separately: they may avoid loading and read selected columns, but measure a file-querying workflow rather than necessarily identical database-table execution.
9. Threats to Validity
Internal validity: cache state, background processes, thermal throttling, run order, and measurement overhead may affect timing.
Construct validity: elapsed time alone does not capture concurrency, operational overhead, update behavior, fault tolerance, or total cost of ownership.
Comparison validity: engines have different architectures and tuning options; equivalent SQL does not guarantee equivalent physical work.
External validity: synthetic data may not represent production skew, null rates, distributions, and schema complexity.
Version validity: results apply to the tested builds and may change after upgrades.
Memory validity: resident-memory measurements vary by platform; document the tool and limitations.
Researcher degrees of freedom: query rewrites, indexes, thread counts, and exclusions can affect results; record decisions and retain raw logs.
Mitigations include publishing query definitions and scripts, preserving raw measurements, predefining exclusion rules, reporting all tested query classes, and separating primary results from exploratory tuning.
10. Reproducibility and Artifact Plan
README with prerequisites, installation steps, versions, and execution instructions.
Dataset-generation script with fixed seed and scale parameters.
Canonical SQL files and documented system-specific variants.
Engine configuration and data-loading scripts.
Benchmark runner recording run ID, system/version, query ID, scale, cache state, elapsed time, status, and result checksum.
Raw CSV/JSON logs, summary script/notebook, figures, and archived query plans.
Limitations file documenting unavailable cold-cache controls, unsupported metrics, and protocol deviations.
Version-control the artifact and tag the exact revision used for reported results. For generated data, document the generator instead of relying solely on a large binary upload.
11. Conclusion
This manuscript defines a controlled evaluation framework for DuckDB query performance across analytical workload types and data scales. The design emphasizes repeated trials, query-result validation, transparent configuration, explicit cache treatment, and reporting of latency and resource consumption. It distinguishes direct Parquet querying from database-table execution and recommends treating distributed-engine comparisons as a separate experiment.
References
[1] M. Raasveldt and H. Mühleisen, “DuckDB: An Embeddable Analytical Database,” Proceedings of the 2019 ACM SIGMOD International Conference on Management of Data, 2019, pp. 1981–1984. doi: 10.1145/3299869.3320212.
[2] M. Raasveldt and H. Mühleisen, “Data Management for Data Science: Towards Embedded Analytics,” Conference on Innovative Data Systems Research (CIDR), 2020.
[3] M. Raasveldt, P. T. Holanda, T. Gubner, and H. Mühleisen, “Fair Benchmarking Considered Difficult: Common Pitfalls in Database Performance Testing,” Proceedings of the Workshop on Testing Database Systems (DBTest ’18), 2018. doi: 10.1145/3209950.3209955.
[4] DuckDB Documentation, “Profiling.” https://duckdb.org/docs/current/dev/profiling (accessed October 29, 2026).
[5] DuckDB Documentation, “Tuning Workloads.” https://duckdb.org/docs/current/guides/performance/how_to_tune_workloads (accessed October 29, 2026).
[6] PostgreSQL Global Development Group, “Using EXPLAIN.” https://www.postgresql.org/docs/current/using-explain.html (accessed October 29, 2026).
[7] Apache Spark, “Performance Tuning.” https://spark.apache.org/docs/latest/sql-performance-tuning.html (accessed October 29, 2026).
[8] J. Lee, S. Hwang, and K. H. Lee, “Scalable and Privacy-Conscious End-to-End Processing of Large-Scale Clinical Data for Precision Medicine: Empirical Evaluation Study,” JMIR Medical Informatics, vol. 14, 2026, e83487. doi: 10.2196/83487.
Appendix A. Minimum Query Semantics
Query | Logical SQL pattern |
Q1 | SELECT COUNT(*), SUM(measure) FROM fact; |
Q2 | SELECT SUM(measure) FROM fact WHERE event_date >= :start_date AND event_date < :end_date; |
Q3 | SELECT category_id, SUM(measure), COUNT(*) FROM fact GROUP BY category_id; |
Q4 | SELECT d.segment, SUM(f.measure) FROM fact f JOIN dimension d ON f.dimension_id=d.dimension_id GROUP BY d.segment; |
Q5 | SELECT event_id, measure FROM fact ORDER BY measure DESC LIMIT 100; |
Q6 | SELECT category_id, SUM(measure) FROM read_parquet(:parquet_glob) GROUP BY category_id; |
The artifact must include exact SQL, data types, timestamp boundaries, null semantics, and parameters. Q6 is a direct-file workflow; compare it with PostgreSQL only if access path and timing boundary are explicit.
Appendix B. Suggested Run Log Schema
Field | Meaning |
run_id | Unique run identifier |
engine / engine_version | System name and exact version |
dataset_scale / query_id | S1–S3 and Q1–Q6 |
cache_state | warm / cold / not controlled |
iteration | Run number; mark warm-up separately |
elapsed_seconds | Wall-clock time |
peak_rss_bytes | Peak resident memory if measured |
cpu_utilization | Metric and unit documented |
result_checksum | Normalized result/checksum |
status / notes | Success, error, timeout, plan change, or deviat |