Skip to content

bulk-submit(sqlite): per-entry pipeline dominates ingest (~84%) — instrument and cut the non-index write volume #947

Description

@angela-helios

Context

SQLite counterpart of #945, from an instrumented run of the real 31 GB manifest (single worker, the safe default per #942). Binary = #944's partial indexes plus per-phase timing counters on the write path (cumulative, dumped every 1,000 resources). Windows host, WAL mode.

Host profile during ingest: the writer pegs one core at ~99% while the disk sits at ~12 MB/s. SQLite ingest is CPU-bound on the single writer thread — the exact opposite of PostgreSQL, which is round-trip-bound with idle CPU (#945). The two backends need different levers.

Measured phase breakdown (real data)

At 29,000 resources into the manifest (CarePlan / AllergyIntolerance segment, ~1.5 KB average resource, 13.8 index rows per resource):

Phase Per resource Share of wall
Wall clock (52/s in this window) 19.2 ms 100%
FHIRPath extraction 0.26 ms 1.4%
search_index row INSERTs 2.66 ms 13.9%
FTS (exists check + insert) 0.14 ms 0.7%
Everything else (unattributed) ~16 ms ~84%

Two conclusions:

  1. perf(sqlite): rebuild search_index family indexes as partial (schema v20) #944's partial indexes already paid off on real data: 0.19 ms per index row, down from 0.40 ms measured with the full-width indexes. Indexing is now a ~14% item, not the dominant cost.
  2. The dominant cost (~84%) is the per-entry pipeline outside indexing, and it is currently unmeasured. Known occupants, from a row-count audit of the same database:
resources                31,555     the resource row
resource_history         31,555     a FULL COPY of the data blob, written on create
bulk_entry_results       30,176     one bookkeeping row per manifest entry
bulk_submission_changes  30,176     one more bookkeeping row per manifest entry
search_index            437,723     ~14 rows per resource (instrumented above)

Every ingested entry writes four table rows (one of them duplicating the entire resource blob) plus its index rows, and the remainder of the 16 ms budget goes to the typed FHIR serde round-trip, savepoint/transaction bookkeeping, and NDJSON parsing — none of it yet individually timed.

Proposed work, in order

  1. Extend the phase instrumentation to the rest of the per-entry pipeline (resource INSERT, history INSERT, bookkeeping rows, serde round-trip, batch commit) and attribute the ~16 ms. This is a day of work and decides everything below.
  2. Stop double-writing the blob on create — evaluate deriving history v1 from the current resources row instead of materializing a full copy into resource_history at create time. On a fresh bulk load this halves the largest write.
  3. Batch the bookkeeping writesbulk_entry_results and bulk_submission_changes are one INSERT each per entry; multi-row INSERT per batch is mechanical.
  4. Multi-row INSERT for search_index rows — now a ~14% item after perf(sqlite): rebuild search_index family indexes as partial (schema v20) #944, still worth taking inside the same pass as (3), and it shortens the write-lock hold (mitigates bulk-submit(sqlite): import aborts with 'database is locked' at high file concurrency with the full search-parameter registry #942).
  5. Skip the typed serde round-trip where possible — if the stored representation is the canonical JSON bytes and validation is off, parsing NDJSON into the full typed FHIR model and re-serializing may be avoidable on the bulk path.

Known context (do not re-derive)

Refs: #945 (PostgreSQL roadmap), #944 (partial indexes, in review), #942, #904, #933.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

enhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions