012. Read validated duration fractions as ASCII bytes
- Status: Measured
- Date: 2026-10-13
Problem and mechanism
The PostgreSQL duration parser sums fractional components one decimal digit at a time so that nanosecond integrality and int64 bounds remain exact for long fractions. The candidate converts each already-validated ASCII fraction to UTF-8 bytes once, then reads digits by byte offset in the recursive carry query. It retains the existing grammar, component summation, integrality and overflow checks, and null propagation.
Applicability
| Execution profile | Applied / Partial / Not applied / Not applicable | Reason |
|---|---|---|
PostgreSQL duration() with fractional components |
Applied | Fractional digits use byte indexing after validation; exact summation and result checks are retained. |
| Integer-unit and compound-unit durations | Partial | These parse through the same SQL expression; measured compound inputs show a small execution regression. |
| String predicate control | Not applicable | It does not call the duration parser and keeps byte-identical SQL. |
| Local evaluation and other database workloads | Not applicable | Measurements cover the selected HFF fixtures and PostgreSQL profile only. |
Reproduction
The baseline is ef9945b. The candidate backends/sql/duration.go source snapshot has SHA-256 9eba6b1d5ed1e28c2596766baeefa503d0cbf430bf1b0f09838cd00b918124aa. The same benchmark harness was used for both source versions:
- Build benchmark, SHA-256
e5c4dd065e2adb2a430c216c5615cc8f33bd91537d7205ed1e4bcf7c62dcb04b - Execution benchmark, SHA-256
ed55515e863a11c5cc436e35536038fd1ed2d8d61cc6299456310f7660e169c9
Build covers four query shapes—StringControl, IntegerUnits, Repeated512,
and Serialized512—for native text, JSON, and JSONB. Its inputs do not vary by
stored fraction length. Database execution also covers CompoundUnits,
fractions with 16, 512, and 4,096 digits, and a 20,000-digit fraction. The
20,000-digit database case has one positive row. Two-row fixtures contain
positive and negative values; 25-row fixtures also include invalid, zero, and
null values. Fractional fixtures combine tiny decimal components that sum
exactly to one nanosecond, with alternating negative cases. Each execution
sample compares SQL-matched IDs with local HFF matches before collecting
PostgreSQL planning and execution fields.
Build command:
TMPDIR=/private/tmp GOMAXPROCS=16 GOTOOLCHAIN=go1.27.1 \
go test ./backends/sql -run '^$' \
-bench '^BenchmarkPostgreSQLDurationBuild$' -benchmem \
-benchtime=200ms -count=3
PostgreSQL execution command (use a separate plan directory for each stage):
TMPDIR=/private/tmp GOMAXPROCS=16 \
HFF_POSTGRES_CONTAINER=hff-pg-conformance-20261012 \
HFF_POSTGRES_PLAN_DIR=/private/tmp/hff-duration-evidence/baseline-plans \
GOTOOLCHAIN=go1.27.1 \
go test -tags=integration ./backends/sql -run '^$' \
-bench '^BenchmarkPostgreSQLDurationExecution$' -benchtime=1x -count=3
Change the plan directory for the candidate and repeated stages. Use the same PostgreSQL 18.6 profile when reproducing the comparison.
Runs were serialized in this order: baseline, candidate, baseline repeat,
candidate repeat. Each stage has three samples, for six measurements per side.
The host was Darwin 25.2.0 arm64, Apple M3 Max with 64 GiB RAM. Go was 1.27.1
with GOMAXPROCS=16. PostgreSQL was 18.6 in a Debian aarch64 container using
image digest sha256:5a5a84b19854a9ffaa54082c166ff4ec27473a361e496e5ea167f298f2da9722.
The container had no network, explicit CPU, memory, or cpuset limits; its
database directory used tmpfs. JIT was off, parallel workers were disabled,
custom plans were forced, and statement timeout was 60 seconds. The local
parity oracle used MaxSteps=math.MaxInt; that is not a PostgreSQL execution
budget. Raw logs, plans, IDs, and aggregate data remain outside Git in
/private/tmp/hff-duration-evidence/ on the measurement host.
Results
Build time is median ns/op with six-sample min–max ranges. Allocations are
cumulative B/op and allocs/op; SQL bytes are generated predicate length.
Allocation counts were unchanged in all 12 Build cases. B/op ranged from
effectively unchanged to 3.33% higher, and the candidate added 53 SQL bytes
per duration expression. Build timing was mixed and does not show a general
reduction.
| Format / workload | Baseline ns/op (range) → candidate ns/op (range) | B/op baseline → candidate | SQL bytes baseline → candidate |
|---|---|---|---|
| Native / StringControl | 20,837 (19,952–21,790) → 19,806 (19,445–21,226) | 24,097 → 24,098 | 339 → 339 |
| JSON / StringControl | 20,816 (19,119–23,709) → 20,758 (20,516–20,961) | 25,348 → 25,348 | 498 → 498 |
| JSONB / StringControl | 21,607 (20,770–22,541) → 20,852 (20,707–22,186) | 25,349 → 25,348 | 499 → 499 |
| Native / IntegerUnits | 25,643 (24,016–27,522) → 25,249 (24,762–25,624) | 41,449 → 42,475 | 2,112 → 2,165 |
| JSON / IntegerUnits | 27,048 (25,815–28,219) → 26,343 (25,992–27,234) | 42,380 → 43,792 | 2,273 → 2,326 |
| JSONB / IntegerUnits | 27,364 (26,489–28,216) → 26,018 (25,613–26,447) | 42,381 → 43,791 | 2,274 → 2,327 |
| Native / Repeated512 | 34,692 (32,400–37,301) → 34,914 (34,170–39,363) | 65,138 → 66,676 | 4,947 → 5,053 |
| JSON / Repeated512 | 37,026 (36,144–39,213) → 34,508 (33,923–35,055) | 66,068 → 67,605 | 5,108 → 5,214 |
| JSONB / Repeated512 | 36,080 (35,060–37,557) → 34,742 (33,868–34,990) | 66,069 → 67,606 | 5,109 → 5,215 |
| Native / Serialized512 | 27,433 (24,933–31,033) → 27,596 (27,131–29,966) | 45,940 → 47,094 | 3,056 → 3,109 |
| JSON / Serialized512 | 29,514 (28,747–30,400) → 27,692 (27,055–28,016) | 47,415 → 48,698 | 3,221 → 3,274 |
| JSONB / Serialized512 | 28,022 (26,316–34,067) → 27,630 (27,476–27,978) | 47,416 → 48,697 | 3,222 → 3,275 |
The execution table reports six-sample medians and min–max ranges in milliseconds. Planning medians are also shown because execution alone does not represent all measured server cost. All 51 workloads kept the same plan-node count. Planning changes were mixed, from 17.1% lower to 18.6% higher across the matrix, with no systematic reduction. The six-sample string-control execution medians drifted from 11.1% lower to 8% higher across tiny timings; their SQL was byte-identical.
| Format / rows / workload | Planning ms baseline → candidate | Execution ms baseline (range) → candidate (range) |
|---|---|---|
| Native / 25 / StringControl | 0.0445 → 0.0465 | 0.0215 (0.018–0.024) → 0.021 (0.016–0.023) |
| JSON / 25 / StringControl | 0.058 → 0.0525 | 0.0295 (0.027–0.034) → 0.028 (0.023–0.050) |
| JSONB / 25 / StringControl | 0.050 → 0.046 | 0.0265 (0.021–0.031) → 0.0255 (0.024–0.028) |
| Native / 25 / CompoundUnits | 0.248 → 0.235 | 0.2765 (0.256–0.312) → 0.296 (0.259–0.333) |
| JSON / 25 / CompoundUnits | 0.250 → 0.267 | 0.289 (0.257–0.352) → 0.308 (0.261–0.327) |
| JSONB / 25 / CompoundUnits | 0.264 → 0.261 | 0.2695 (0.249–0.315) → 0.2985 (0.273–0.382) |
| Native / 25 / Fraction512 | 0.265 → 0.258 | 34.58 (34.23–36.83) → 9.809 (9.697–10.10) |
| JSON / 25 / Fraction512 | 0.305 → 0.329 | 34.25 (33.54–34.70) → 9.736 (9.457–10.11) |
| JSONB / 25 / Fraction512 | 0.303 → 0.278 | 34.40 (33.97–37.20) → 9.886 (9.526–10.15) |
| Native / 25 / Fraction4096 | 0.293 → 0.296 | 1,620 (1,599–1,698) → 93.75 (92.28–94.12) |
| JSON / 25 / Fraction4096 | 0.305 → 0.317 | 1,621 (1,608–1,704) → 92.72 (91.11–93.86) |
| JSONB / 25 / Fraction4096 | 0.299 → 0.305 | 1,621 (1,605–1,711) → 92.56 (91.57–92.94) |
| Native / 1 / Fraction20000 | 0.292 → 0.279 | 1,960 (1,935–1,966) → 48.86 (48.30–49.29) |
| JSON / 1 / Fraction20000 | 0.315 → 0.342 | 1,962 (1,944–2,017) → 49.24 (48.80–50.60) |
| JSONB / 1 / Fraction20000 | 0.300 → 0.314 | 1,952 (1,944–2,086) → 48.78 (48.25–49.57) |
For 25-row compound inputs, execution increased 6.6% to 10.8%. The two-row compound samples were mixed, from 4.2% lower to 15.7% higher. For 512-digit fractions, the 25-row samples were about 71–72% lower; for 4,096 digits, they were about 94% lower. The single-row 20,000-digit samples were about 97.5% lower. These are measurements of the tested inputs and local PostgreSQL container, not a guarantee for other data or deployments.
Correctness and tradeoffs
The candidate passed 8,930 duration comparisons, eight long-decimal boundary cases, six SQL string escaping probes, and 87 shared conformance pairs. These cover exact fractional summation, integral-nanosecond checks, int64 bounds, invalid and null inputs, and parameter escaping. No parser grammar or result-conversion behavior changed.
The byte-index path materially reduced execution time for tested long fractional strings but added generated SQL and cumulative Build allocation. The short compound-path regressions are retained above. The database timing is from a single host/container profile with six samples per side; it is not a production latency or service-level claim. Peak and retained memory were not measured. PostgreSQL does not enforce the local parser's step budget; database timeouts remain caller-owned.
Decision and limits
The byte-index candidate is retained for its measured long-fraction results, with the short compound overhead and Build tradeoff documented. Continue to compare representative ordinary inputs alongside long fractions; do not extrapolate the measured reduction to other PostgreSQL versions, plans, or workloads.