You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
|`COUNT(DISTINCT x)`| per-PU distinct count, clip, sum, Laplace | per-PU distinct count, clip, sample-sum, median/mean; single DISTINCT may use the lane-mask specialization |
345
348
|`SUM(x)`| clip `x` to `dp_sum_bound`, sum, Laplace | per-lane sum; average release supports fixed-lane or privately-counted non-empty-lane estimators |
346
349
|`AVG(x)`| decomposed → `SUM(clip x)` + `COUNT`; released as `noised_sum / max(noised_count, 1)` (Google bounded-mean shape) |**three estimators** (`dp_sass_avg_method`): `lane_average` (default) fills empty lanes before releasing the lane mean; `nonempty_lane_average` divides a noisy shifted sum of populated-lane averages by their noisy count; `ratio` releases independent row SUM and COUNT SAA cells and divides them |
@@ -378,21 +381,22 @@ distinct values per group, and the top aggregate releases an `as_sample_sum` ove
378
381
bounded per-PU counts. Single `COUNT(DISTINCT)` queries may use the optimized
379
382
`RewriteDistinctCountToSampleMedian` lane-mask path: it groups by `[groups..., PU,
380
383
distinct value]`, computes `bit_or(dp_sample_mask(pu_hash))`, applies the same caps, then
381
-
merges masks by `[groups..., distinct value]` before releasing 64 lane counts.
384
+
merges masks by `[groups..., distinct value]` before releasing 64 lane counts. The
385
+
variable-`m` path represents lane assignments directly instead of using a bit mask.
382
386
383
387
---
384
388
385
389
## 14. NULL handling
386
390
387
391
### Empty-lane filling (`dp_sass`)
388
392
389
-
A `dp_sass` aggregate computes 64 lane answers and releases the median (or mean) of
390
-
those 64. Some lanes can be **empty** — no PU was assigned to that lane, or everything
393
+
A `dp_sass` aggregate computes `m` lane answers and releases their median or mean.
394
+
Some lanes can be **empty** — no PU was assigned to that lane, or everything
391
395
in it was filtered out. An empty lane has no natural answer: an empty `COUNT`/`SUM` is
392
396
`0`, but an empty `AVG`/`MIN`/`MAX` is undefined (`NULL`, i.e. ±∞).
393
397
394
398
The danger is that the *number* of non-empty lanes is **data-dependent** — a group with
395
-
3 PUs populates far fewer of the 64 lanes than a group with 3000. If we released "the
399
+
3 PUs populates far fewer lanes than a group with 3000. If we released "the
396
400
median over just the non-empty lanes," that effective sample size would itself be a
397
401
signal about the group's support, and it is not covered by the noise calibration — a
398
402
leak.
@@ -417,7 +421,7 @@ Because the fill depends only on the public bounds (`dp_sass_count_output_bound`
417
421
`dp_sass_minmax_lower/upper_bound`, …), swapping one PU in or out cannot change what an
418
422
empty lane receives — it leaks nothing. Were the fill taken from the data (e.g. the
419
423
observed minimum) it *would* leak, which is why §5 lists "empty-lane fill is public" as
420
-
a load-bearing assumption. The effect: a 3-PU group and a 3000-PU group both yield 64
424
+
a load-bearing assumption. The effect: a 3-PU group and a 3000-PU group both yield `m`
421
425
fully-defined lanes, so the support signal never reaches the released median. This is
422
426
the GUPT "fixed number of blocks, bounded block outputs" property.
423
427
@@ -480,6 +484,10 @@ releasing the raw value; `privacy_noise = false` zeroes noise for deterministic
480
484
query that never named the PU table; now the lanes are derived from the linked table's
481
485
FK column and the result matches the
482
486
explicit join (§13).
487
+
-**Configurable `m`.**`dp_sass_m` supports `64`, `128`, `256`, and `512`. The
488
+
optimized `m = 64` path uses a machine-word membership mask. Larger values use
489
+
direct lane identifiers and variable-size aggregate states while preserving the
490
+
same disjoint assignment for `dp_sample_lanes = 1`.
483
491
484
492
### Still open
485
493
@@ -489,16 +497,13 @@ releasing the raw value; `privacy_noise = false` zeroes noise for deterministic
489
497
Bernoulli-`1/m` inclusion model, under which a PU appears in up to O(√m) subsamples
490
498
(Nissim et al.) and that factor must enter the analysis. Confirm our hard-cap scaling is
491
499
sound, or restrict `sample_lanes > 1`. An alternative derivation provides pseudocode for this case.
492
-
2.**Configurable `m`.**`m` is fixed at 64 (SIMD/perf). SAA supports general `m`; `m ≤ 64`
493
-
is achievable by using a subset of the 64-bit mask, `m > 64` needs a wider multi-word
494
-
representation. Worth exposing `m` as a setting (64 the recommended default) — future work.
495
-
3.**τ under very small δ.** A very small `δ_η` drives τ large enough to suppress all
500
+
2.**τ under very small δ.** A very small `δ_η` drives τ large enough to suppress all
496
501
groups. One possible mitigation is to give partition selection a relatively larger
497
502
`δ_η` (an uneven δ split) so τ stays practical. Currently `δ_η = δ/(c+1)` (even split).
498
-
4.**User-level FLEX.** Extending `dp_elastic` from row-level to user-level needs new
503
+
3.**User-level FLEX.** Extending `dp_elastic` from row-level to user-level needs new
499
504
per-user statistics + a sensitivity analysis and proof (future work).
500
-
5.**`FILTER` on `COUNT(DISTINCT)`** (§3) — implementation gap, not a DP restriction: the
505
+
4.**`FILTER` on `COUNT(DISTINCT)`** (§3) — implementation gap, not a DP restriction: the
501
506
distinct rewrite already uses the aggregate's `FILTER` slot, so a user `FILTER` would
502
507
have to be composed with it. Fails closed for now; plain aggregates support `FILTER`.
503
-
6.**Fail-open side tables** — an unannotated private table is treated as public (§5.6);
508
+
5.**Fail-open side tables** — an unannotated private table is treated as public (§5.6);
0 commit comments