Search Shortcut cmd + k | ctrl + k

Differential-privacy publishing in SQL — dp_count/dp_sum/dp_mean with the classic Laplace mechanism (pure SQL macros, deterministic reproducible noise)

Maintainer(s): alitrack

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 <= 0 returns 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)

  1. 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).
  2. Non-adaptive. Noise derived from one salt is correlated across calls; reusing a salt across independent publications shares noise and breaks additivity.
  3. 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 round 1-V to 0 → ln(0) → ±inf; regression-locked by the test suite).
  4. 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 raw dp.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