TypedEAV Documentation GitHub

ADR 0009: Application-owned trigram indexes for measured string workloads

Status

Accepted

Context

TypedEAV’s public string operators use three distinct SQL contracts: equality uses =, positive pattern operators use ILIKE, and not_contains uses NOT ILIKE. The shipped partial-covering text_pattern_ops B-tree preserves efficient equality. It does not make every ILIKE shape efficient.

The Phase 3 comparison kept those predicates unchanged while adding either a lower(string_value) text_pattern_ops B-tree or a partial pg_trgm GIN index. The deterministic dataset contained 250,000 target-field rows and 250,000 noise-field rows. Three candidate-order rotations ran on PostgreSQL 17.11 under resource-capped co-tenancy. Disposable PostgreSQL 15.19, 16.15, and 18.6 lanes tested extension lifecycle only; they are not planner evidence.

Decision

Keep the shipped partial-covering B-tree and public SQL unchanged. TypedEAV does not require pg_trgm, install it, create a trigram index, or provide an installer or generator for one.

Applications whose measured workload is dominated by positive ILIKE patterns with extractable trigrams may evaluate an application-owned partial GIN index:

CREATE INDEX CONCURRENTLY app_te_values_string_trgm
ON typed_eav_values USING gin (string_value gin_trgm_ops)
WHERE string_value IS NOT NULL;

The decision is operator- and workload-specific:

Public query Shipped B-tree Observed use of additive GIN Policy
eq (=) Index-only in the representative plan Not needed Retain the B-tree
starts_with (ILIKE 'value%') Not reliably pattern-indexed by this B-tree for public ILIKE Used for the measured extractable pattern Evaluate only with application plans/selectivity
contains (ILIKE '%value%') May scan/filter B-tree entries or the relation Used for the measured extractable pattern Primary candidate for workload-specific evaluation
ends_with (ILIKE '%value') Not suffix-indexed Used for the measured extractable pattern Evaluate only with application plans/selectivity
Escaped % and _ literals Semantics remain escaped ILIKE Used when the remaining literal supplied trigrams Do not infer support for every escaped pattern
not_contains (NOT ILIKE) No selective negative-search guarantee Not used Do not recommend GIN as acceleration
One- or two-character probes No useful trigram extraction Not used Do not recommend GIN as acceleration

The separate lower(string_value) LIKE ... prototype used its expression B-tree, but that predicate is not the public ILIKE contract. It does not justify changing public semantics or assuming collation equivalence. GiST was smoke-only and has no representative justification, so it is not recommended.

Application ownership and deployment

Before deployment, the application owner must confirm pg_trgm is available and that the deploy role may create it in the target database:

SELECT name, default_version, installed_version
FROM pg_available_extensions
WHERE name = 'pg_trgm';

CREATE EXTENSION IF NOT EXISTS pg_trgm;

The benchmark’s non-superuser database owner could manage the extension on stock PostgreSQL 15, 16, and 18. Hosted services may impose different privileges, so this is a preproduction check, not a portability promise.

The consuming application should own a nontransactional migration with a stable application-specific index name. It must create and drop the index with CONCURRENTLY, monitor invalid indexes after interruption, and verify the catalog definition rather than accepting a same-named but different index. Rollback drops only the application-owned index:

DROP INDEX CONCURRENTLY IF EXISTS app_te_values_string_trgm;

Do not drop pg_trgm during index rollback. Extensions are database-wide and may be shared by unrelated application objects. Extension removal belongs in a separate, explicitly owned operation only after catalog checks prove no remaining dependants.

Before keeping the index, compare representative before/after plans with EXPLAIN (ANALYZE, BUFFERS, WAL, SETTINGS), including real pattern lengths, selectivity, field distribution, and concurrency. Also measure index and total relation bytes, build and write WAL, insert/update throughput, and ongoing maintenance. A per-field partial predicate may be worth testing for a known hot field, but it was not the Phase 3 candidate and requires its own evidence.

Consequences and evidence limits

The additive GIN candidate increased median candidate index bytes by 61.417% and build WAL by 50.293% versus the current B-tree candidate. Median insert and update throughput fell 56.995% and 58.060%, while their WAL rose 156.499% and 195.441%. These costs make automatic installation inappropriate.

The retained artifact contains the full plans, checksums, sizes, and build measurements for trial 1 plus cross-trial metric arrays and rotation metadata. It does not retain the raw trial 2/3 plans, checksums, sizes, or build times, so those items are not independently auditable from the artifact. The result is relative evidence under active co-tenants; absolute timings are diagnostic, and PostgreSQL 17 plan choices must not be generalized to every version or workload. Applications should rerun the comparison in their own environment.