Differential-privacy publishing in SQL — dp_count/dp_sum/dp_mean with the classic Laplace mechanism (pure SQL macros, deterministic reproducible noise)
Compatibility Matrix
This table covers all DuckDB releases that have not reached end-of-life, including LTS releases, and shows the 2 most recent patch releases of each. Older patch releases are not listed, but the extension may be available for them as well.
| Platform | 1.4.4 | 1.4.5 | 1.5.5 | 1.5.6 | 2.0.0 |
|---|---|---|---|---|---|
| Linux AMD64 | |||||
| Linux ARM64 | |||||
| macOS AMD64 | |||||
| macOS ARM64 | |||||
| Windows AMD64 | |||||
| Windows ARM64 | |||||
| WebAssembly |
Installing and Loading
INSTALL dp FROM community;
LOAD dp;
Example
-- (epsilon, 0)-differential-privacy publishing, straight in SQL.
-- The Rust extension is a thin shim: on LOAD it registers the dp_*
-- macros from a pure-SQL script. Zero per-row native work.
CREATE TABLE patients AS
SELECT range AS id,
20 + (hash('age' || range) % 60) AS age
FROM range(300);
-- Clip + Laplace(1/eps) count, deterministic per salt (reproducible)
D SELECT dp_count(array_agg(age), 0.1, 'pub_2026q4') AS dp_n FROM patients;
┌─────────────┐
│ dp_n │
│ double │
├─────────────┤
│ 307.23651554674376 │
└─────────────┘
-- Clipped mean: eps = privacy budget, lb/ub = clipping bounds
D SELECT dp_mean(array_agg(age), 0.1, 0, 100, 'pub_2026q4') AS dp_age FROM patients;
D SELECT dp_sum(array_agg(age), 0.01, 0, 100, 'pub_2026q4') AS dp_sum_tighter FROM patients;
About dp
duckdb-dp — differential privacy for DuckDB
Publish (epsilon, 0)-differentially-private aggregate statistics directly in
SQL. The mechanism is the classic Laplace mechanism; all the privacy math
is pure SQL macros (no per-row native code). The bundled Rust extension
is a thin shim that exists only so the community registry can build a
loadable binary: on LOAD it executes the SQL script and registers the
dp_* macros.
Functions
| function | mechanism | sensitivity |
|---|---|---|
dp_count(x, eps, salt) |
count + Laplace(1/eps) | 1 |
dp_sum(x, eps, lb, ub, salt) |
clipped sum + Laplace((ub-lb)/eps) | ub-lb |
dp_mean(x, eps, lb, ub, salt) |
clipped mean + Laplace((ub-lb)/(2n·eps)) | (ub-lb)/(2n) |
dp_noise_lap(b, salt) / dp_noise_gauss(sigma, salt) |
noise primitives | — |
dp_u(salt) / dp_v(salt) |
deterministic uniform (internal) | — |
Aggregates take a LIST (house convention, same as duckdb_stats):
SELECT dp_count(array_agg(age), 0.1, 'pub_2026q4') FROM t;
- eps = privacy budget;
eps <= 0returns NULL (nothing published). - salt = publication identifier. Same salt → same noise on re-run (reproducible dashboards); different salt → independent noise (assign a fresh salt per publication when budget is tracked per release). The salt must be a single constant shared by all rows.
- lb / ub = clipping bounds — semantically core, not optional: the
noise scale is
(ub - lb), so no clip means no sensitivity guarantee. - NULL rows are not counted (matches COUNT/AVG);
ub <= lb→ NULL.
Honest scope (prototype)
- Single-query mechanism, no composition accounting. Publishing several
dp_*values back-to-back — you sum the budgets yourself (basic composition, Σeps) or use advanced composition (Dwork-Roth Thm 3.7). - Non-adaptive. Noise derived from one salt is correlated across calls; reusing a salt across independent publications shares noise and breaks additivity.
- Numerics. 48-bit deterministic uniform; the log domain is clamped to
[2^-20, 1-2^-20](a 48-bit value within 2^-48 of 1 would otherwise round1-Vto 0 →ln(0)→ ±inf; regression-locked by the test suite). - Upgrade path. For RDP / ledger accounting / complex queries, move to a C++ extension over OpenDP (MIT / Rust / C ABI).
Verification
bash run_tests.sh— 12 assertions (determinism, salt divergence, eps bounds, empty, NULL semantics, clipping, numeric, grouped, ub<=lb, mean scale, tail-finite), all green → exit 0.python3 verify_dp.py— moment checks vs theory (Laplace σ≈b·√2, Gaussian σ, uniform, no collisions), N=2000, ±10%.- End-to-end: the compiled binary
LOADs in the DuckDB CLI and produces values bit-identical to the rawdp.sql, deterministic across loads.
Relation to the registry
Registry 370-extension rescan (2026-10-18): no differential-privacy
coverage. (differential = database versioning / time travel; table_guard
= table-level access control; semantic_profile = PII detection only —
none of them implement a DP mechanism.) duckdb_stats is the statistics
layer (inference / hypothesis tests); this is the publishing layer
(privacy budget) — no overlap.
Added Functions
| function_name | function_type | description | comment | examples |
|---|---|---|---|---|
| dp_clip | macro | NULL | NULL | |
| dp_count | macro | NULL | NULL | |
| dp_mean | macro | NULL | NULL | |
| dp_noise_gauss | macro | NULL | NULL | |
| dp_noise_lap | macro | NULL | NULL | |
| dp_non_null | macro | NULL | NULL | |
| dp_sum | macro | NULL | NULL | |
| dp_u | macro | NULL | NULL | |
| dp_v | macro | NULL | NULL |
Overloaded Functions
This extension does not add any function overloads.
Added Types
This extension does not add any types.
Added Settings
| name | description | input_type | scope | aliases | typed_value |
|---|---|---|---|---|---|
| temp_directory | Set the directory to which temporary files are written. | VARCHAR | GLOBAL | [] | post.db.tmp |