Skip to content

Database Tuning

You have a PostgreSQL server for IdentityScribe. These settings size memory, storage, and WAL for IdentityScribe's workload.

For caller-side index selection and query-shape guidance, see Query performance.

All formulas assume a dedicated database server. If PostgreSQL shares a host with IdentityScribe, reduce memory fractions accordingly.

IdentityScribe's database workload has distinct characteristics that inform tuning:

  • Three isolated connection pools — a default total of ~43 connections serving different workloads: write-heavy transcription tasks, schema maintenance, and latency-sensitive query serving (LDAP, REST, GraphQL).
  • Partition-pruned leaf tablesentries_data is sub-partitioned by entry type and attribute, producing dozens of leaf partitions. At 10M entries, a typical deployment has 27+ indexes across 16 partitions.
  • Multiple index types per attribute — equality and range B-tree indexes, sort-order B-tree indexes, and GIN trigram indexes for substring search. GIN indexes dominate disk usage (the largest can exceed 1 GB per attribute).
  • Read-dominant steady state — equality lookups, range scans with cursor pagination, substring search, and sorted result sets. All served concurrently from LDAP, REST, and GraphQL channels.
  • Bursty writes — event-sourced single-row inserts at steady state; multi-row bulk writes during reconciliation. When IdentityScribe prepares a new entry type, GIN trigram indexes are built at runtime.

Settings are parameterized by server RAM and CPU cores.

SettingFormula16 GB32 GB64 GBRationale
shared_buffersRAM × 0.254 GB8 GB16 GBMust hold hot partition data and GIN indexes for query serving. Undersizing causes buffer churn — every query evicts pages needed by the next
effective_cache_sizeRAM × 0.7512 GB24 GB48 GBReflects OS page cache availability so PostgreSQL can favor index-backed reads when hot data fits in cache
work_memRAM / (connections × 4)16 MB32 MB64 MBPer-sort-node per-connection. Too high risks OOM under load; too low forces disk sorts on sorted pagination queries
maintenance_work_memmin(RAM × 0.05, 2 GB)800 MB1.6 GB2 GBGIN trigram index builds are memory-intensive. Directly affects how fast new entry types become available

The table above recommends server-level work_mem for postgresql.conf. IdentityScribe also sets work_mem = 16MB per query connection via database.connection-hints.session-flags.work-mem. This connection-level setting overrides the server default for IdentityScribe's connections.

16MB works for most deployments. If query performance degrades over time or under load, insufficient work_mem can cause PostgreSQL to spill sort and hash operations to disk — which is orders of magnitude slower than in-memory processing.

Observe detects spills automatically. When it finds disk spilling, it recommends a work_mem value and provides a ready-to-use config snippet. You can also calculate manually:

A safe starting point for work_mem given your server's total RAM and connection count:

work_mem = max(16MB, min(512MB, total_ram_mb / max_connections / 4))

For a server with 32GB RAM and 100 connections:

work_mem = max(16MB, min(512MB, 32768 / 100 / 4)) ≈ 82MB → round to 64MB or 96MB

Start at the next standard size above the formula result (16, 32, 64, 96, 128, 256, 512 MB). Monitor temp file counts after each increase — if they stop growing, you have enough.

Override per-channel or globally in IdentityScribe config:

database.connection-hints.session-flags {
work-mem = "64MB"
}

Or per-channel (e.g., only for REST API queries):

channels.rest.connection-hints.session-flags {
work-mem = "128MB"
}

The value must match <number><unit> where unit is B, kB, MB, GB, or TB (e.g., "256MB", "1GB").

Raising work_mem increases memory pressure. A single query can use work_mem multiple times — once per sort or hash operation, and once per parallel worker. On a server running many concurrent queries, doubling work_mem across the board can trigger out-of-memory errors. Raise gradually and verify in Observe that temporary-file pressure falls before moving higher.

SettingValueRationale
random_page_cost1.1SSD random reads are nearly as fast as sequential. Helps PostgreSQL favor index-backed reads, which is critical for partition-pruned queries
effective_io_concurrency200SSD can handle many concurrent read requests. Benefits bitmap heap scans on GIN trigram results

For HDD storage, use random_page_cost = 2.0 and effective_io_concurrency = 4.

SettingFormula4 cores8 cores16 cores
max_parallel_workers_per_gathercores / 4124
max_parallel_maintenance_workerscores / 2244
max_parallel_workerscores / 2248

IdentityScribe uses parallel query execution for equality lookups at scale — each equality filter is translated to a direct join predicate that PostgreSQL can split across worker processes via Gather Merge.

Maintenance parallelism is higher to speed up index creation when new entry types are prepared.

SettingValueRationale
enable_partitionwise_joinonIdentityScribe's schema is partitioned by entry type and attribute. Without this, PostgreSQL joins full parent tables and prunes afterward. With it, joins target individual partitions directly
enable_partitionwise_aggregateonEnables per-partition GROUP BY aggregation before merging results. Benefits sort and cursor queries

These settings are off by default in PostgreSQL because they add query-preparation overhead for non-partitioned schemas. IdentityScribe enables both per-session automatically on every query connection, so they work out of the box without server configuration.

Setting them server-level in postgresql.conf is still recommended — it avoids the per-connection SET overhead and ensures all connections (including ad-hoc psql sessions and monitoring queries) benefit.

SettingValueRationale
wal_levelreplicaRequired — event sourcing requires WAL for crash recovery and potential replication
synchronous_commitonRequired — event sourcing demands durable writes. Losing committed events breaks sync state
max_wal_sizeRAM × 0.125 (2–8 GB)Larger WAL before forced checkpoint. Sync writes are bursty during reconciliation
checkpoint_timeout10–15 minReduces checkpoint frequency. Default 5 min causes excessive I/O during sustained writes
checkpoint_completion_target0.9Spread checkpoint writes over 90% of the interval (default; keep as-is)
SettingFormulaRationale
max_connectionspool total + 15Sum of all connection pools, plus headroom for admin, monitoring, and migrations. Default pool total is ~43, so 60 is a safe starting point
SettingValueRationale
autovacuumonRequired — partitioned tables with frequent updates need regular vacuum to prevent bloat and maintain visibility maps
autovacuum_vacuum_scale_factor0.05More aggressive than default (0.2) — partitions are smaller, so the default leaves too many dead tuples proportionally
autovacuum_analyze_scale_factor0.02Keeps statistics fresh. Stale partition stats reduce pruning efficiency and can slow filter-heavy searches

Settings that must not be used in production

Section titled “Settings that must not be used in production”

For a 32 GB RAM / 8-core SSD server with default IdentityScribe pool sizes:

# Memory
shared_buffers = 8GB
effective_cache_size = 24GB
work_mem = 32MB
maintenance_work_mem = 1536MB
# Storage (SSD)
random_page_cost = 1.1
effective_io_concurrency = 200
# Parallelism
max_parallel_workers_per_gather = 2
max_parallel_maintenance_workers = 4
max_parallel_workers = 4
# Partition-wise operations (required for IdentityScribe)
enable_partitionwise_join = on
enable_partitionwise_aggregate = on
# WAL and Checkpoints
max_wal_size = 4GB
checkpoint_timeout = 10min
# Connections
max_connections = 60
# Autovacuum (tuned for partitioned tables)
autovacuum_vacuum_scale_factor = 0.05
autovacuum_analyze_scale_factor = 0.02

After restarting PostgreSQL, confirm buffer allocation with SHOW shared_buffers and monitor query performance on the Health and Monitoring dashboard.

IdentityScribe manages three separate HikariCP connection pools to isolate workloads. These are configured in the IdentityScribe config (not postgresql.conf).

PoolUsed byDefault sizeTuning guidance
BatchTranscription tasks (main write workload)concurrency + 5, clamped to concurrency × 1.5Increase if scribe_db_connections_pending is consistently high and traces point at write work; decrease on memory-constrained hosts
SystemMigrations, maintenance, health checks, DDLmax(transcribeCount + 4, concurrency / 4), clamped [2, max-pool-size / 2]Increase if maintenance windows overlap with heavy write load
ChannelREST, GraphQL, LDAP query servingconcurrency, minimum 2Increase if channel latency is dominated by connection acquisition waits (scribe_db_connections_pending with high query permit pressure)

The total connection count is the sum of all three pools. Ensure max_connections in postgresql.conf can accommodate the total (see the Connections table above).

A semaphore (default: channel pool size) caps concurrent query connections across all channels. HTTP and GraphQL queries that exceed this wait up to query-http-acquisition-timeout (default: 5s) and return 503 with a Retry-After header. LDAP queries block up to their query time limit instead.

ConfigEnv varDefault
database.max-pool-sizeSCRIBE_DATABASE_MAX_POOL_SIZEconcurrency + 5
database.system-pool-sizeSCRIBE_DATABASE_SYSTEM_POOL_SIZEmax(transcribeCount + 4, concurrency / 4)
database.channel-pool-sizeSCRIBE_DATABASE_CHANNEL_POOL_SIZEconcurrency
database.query-http-acquisition-timeoutSCRIBE_DATABASE_QUERY_HTTP_ACQUISITION_TIMEOUT5s

For filtered, sorted, prefix, and substring searches, IdentityScribe adjusts execution automatically based on live directory size, attribute distribution, and recent production traffic.

  • Narrow-match searches finish quickly when only a small share of rows match.
  • Sorted searches that narrow to a small set of entries finish quickly even over large directories — a sorted or virtual-list-view browse selected by entry distinguished names (a single one or a long list), a unique identifier (uuid/uoid), or a selective attribute value applies that narrowing before sorting, instead of ordering the whole result set first.
  • Broad-match searches stay predictable when a large share of rows match.
  • Prefix searches reuse recent measurements for repeated patterns.
  • Range searches use maintained coverage data when that is expected to reduce work.

No manual tuning is required for standard deployments. The default behaviour is conservative during startup and becomes more specific as normal traffic provides evidence.

Leave adaptive search tuning enabled unless you see a repeatable regression on a specific attribute or search pattern. If that happens:

  1. Capture a Query Diagnostic Report for the affected request.
  2. Compare the affected request against the Health and Monitoring dashboard for database load, queueing, and timeout signals.
  3. Contact Kenoxa support with the report and the time window. Support can provide deployment-specific overrides when a temporary safety valve is needed.

Do not tune the advanced search thresholds from symptoms alone. The same latency symptom can come from stale database statistics, undersized memory, connection pressure, or an attribute distribution that changed after a bulk import.

IdentityScribe dynamically computes the PostgreSQL statistics target before each maintenance refresh, adjusting it based on directory size. This keeps filter and sort performance more consistent at scale without the overhead of a blanket high target.

The computed target is logged at the start of each maintenance window.

ConfigEnv varDefault
database.maintenance.statistics-target-ratioSCRIBE_DATABASE_MAINTENANCE_STATISTICS_TARGET_RATIO0.00005

Set to 0 to disable dynamic tuning and use PostgreSQL's default.

Sort indexes store a compact min/max range per entry for each sortable attribute. These indexes are built in the background at startup; the service remains fully operational during the build window.

Backfill concurrency auto-scales from available system pool resources, reserving enough connections for maintenance and health checks.

ConfigEnv varDefaultWhen to change
database.sort-index-backfill.max-concurrentSCRIBE_DATABASE_SORT_INDEX_BACKFILL_MAX_CONCURRENTAuto-scaledIncrease to shorten backfill window on high-spec systems; decrease to reduce read pressure during initial sync

Common IdentityScribe database configuration environment variables:

Env varDefault
SCRIBE_DATABASE_MAX_POOL_SIZEconcurrency + 5
SCRIBE_DATABASE_SYSTEM_POOL_SIZEmax(transcribeCount + 4, concurrency / 4)
SCRIBE_DATABASE_CHANNEL_POOL_SIZEconcurrency
SCRIBE_DATABASE_QUERY_HTTP_ACQUISITION_TIMEOUT5s
SCRIBE_DATABASE_MAINTENANCE_STATISTICS_TARGET_RATIO0.00005
SCRIBE_DATABASE_SORT_INDEX_BACKFILL_MAX_CONCURRENTAuto-scaled