Skip to content

fix(usage): add entity_usage index for compute.percentile lookup - #30032

Open
sonika-shah wants to merge 2 commits into
mainfrom
fix/entity-usage-percentile-index
Open

fix(usage): add entity_usage index for compute.percentile lookup#30032
sonika-shah wants to merge 2 commits into
mainfrom
fix/entity-usage-percentile-index

Conversation

@sonika-shah

@sonika-shah sonika-shah commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Problem

compute.percentile (POST /api/v1/usage/compute.percentile/{entityType}/{date}) times out consistently on large catalogs after 1.13. The client sees a SocketTimeoutException; the server log shows the real cause — Lock wait timeout exceeded on the UsageDAO.computePercentile UPDATE.

Root cause

The 1.13 migration in #25751 reordered the entity_usage unique key from (usageDate, id)(id, usageDate) to reduce usage-upsert deadlocks. That table has a single index, shared by two different access patterns:

  • Usage upsert (insertOrUpdateCount/insertOrReplaceCount) filters by id → the new (id, usageDate) index serves it well (this path is healthy).
  • computePercentile filters by usageDate + entityType with no id → after the reorder there is no usable index, so it full-scans entity_usage. The scan grows with usage history, exceeds the client read timeout, and the client's retry then collides with the still-running statement holding row locks → Lock wait timeout exceeded.

Only computePercentile is affected; the write path is unchanged.

Fix

Add a supplementary, non-unique (usageDate, entityType) index on entity_usage in the 1.13.2 native migration, so the percentile query seeks its day/type slice again instead of scanning the whole table.

Audited all statements in UsageDAO that touch entity_usage:

  • The id-based statements (upserts, deletes, getUsageById, getLatestUsage, getLatestUsageBatch) remain served by the existing (id, usageDate) unique key — the optimizer ignores the new index for them.
  • The new index excludes the mutable count1/count7/count30 columns, so UPDATEs (which only change counts) never touch it — the count-update path optimized by Fix Metrics collection; reduce no.of metrics; improve slow request lo… #25751 is untouched and the upsert-deadlock fix is preserved.

Migrations are guarded/idempotent (MySQL prepared-statement check on information_schema.statistics; Postgres CREATE INDEX IF NOT EXISTS). Please add the To release label so this cherry-picks into 1.13.x.

Follow-up (separate PR)

computePercentile is still O(n²) (correlated COUNT(*) subqueries). This index restores the pre-1.13 working behavior; the durable fix is to rewrite it with PERCENT_RANK() window functions (the MySQL 5.7 constraint noted in the DAO comment is stale — tests run on MySQL 8).

Greptile Summary

This PR adds a usage lookup index for the percentile endpoint.

  • Adds a guarded MySQL index on entity_usage (usageDate, entityType).
  • Adds the same PostgreSQL index with concurrent drop and create statements.
  • Keeps the existing id-based usage index path unchanged.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Important Files Changed

Filename Overview
bootstrap/sql/migrations/native/1.13.2/mysql/schemaChanges.sql Adds an idempotent MySQL schema change for the new entity_usage percentile lookup index.
bootstrap/sql/migrations/native/1.13.2/postgres/schemaChanges.sql Adds the PostgreSQL percentile lookup index using concurrent index operations for the large usage table.

Reviews (3): Last reviewed commit: "fix(usage): build entity_usage percentil..." | Re-trigger Greptile

Context used (3)

  • Context used - CLAUDE.md (source)
  • Context used - openmetadata-ui-core-components/CLAUDE.md (source)
  • Context used - AGENTS.md (source)

The 1.13 unique-key reorder to (id, usageDate) sped up usage upserts but
left the compute.percentile UPDATE -- which filters by (usageDate,
entityType) with no id -- without a usable index. On large catalogs the
resulting full-table scan grows with usage history and blows past the
client read timeout; the client retry then collides with the still-running
statement, surfacing as 'Lock wait timeout exceeded'.

Add a supplementary non-unique (usageDate, entityType) index so the
percentile query seeks its day/type slice again. It excludes the mutable
count columns, so the count-update path optimized by the unique-key reorder
is untouched and the upsert-deadlock fix is preserved.
Copilot AI review requested due to automatic review settings July 14, 2026 09:49

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@github-actions

Copy link
Copy Markdown
Contributor

❌ PR checklist incomplete

This PR cannot be merged until the following are addressed on its linked issue:

  • No GitHub issue is linked. Link an issue in the Development section of the PR (or add Fixes #12345 to the description). For a same-org cross-repo issue, add Fixes open-metadata/<repo>#123 to the description.

The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically.

Maintainers can bypass this check by adding the skip-pr-checks label.

@github-actions github-actions Bot added backend safe to test Add this label to run secure Github workflows on PRs labels Jul 14, 2026
Comment thread bootstrap/sql/migrations/native/1.13.2/postgres/schemaChanges.sql Outdated
Plain CREATE INDEX takes a write-blocking lock on entity_usage for the full
build. Match the CONCURRENTLY pattern used by the other large-table index
builds (1.13.0) so usage writes and percentile reads aren't blocked during
upgrade; DROP ... IF EXISTS first clears any INVALID index left by an
interrupted build. MySQL is unchanged -- ADD INDEX is online by default on
MySQL 8.
Copilot AI review requested due to automatic review settings July 14, 2026 09:54

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Comment on lines +13 to +16
-- actively-written table) for the duration of the build. The DROP ... IF EXISTS first clears any
-- INVALID leftover from a previously interrupted CONCURRENTLY build so this re-runs cleanly.
DROP INDEX CONCURRENTLY IF EXISTS entity_usage_percentile_idx;
CREATE INDEX CONCURRENTLY IF NOT EXISTS entity_usage_percentile_idx ON entity_usage (usageDate, entityType);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Bug: DROP-first cleanup defeated by per-statement migration tracking

The comment claims the leading DROP INDEX CONCURRENTLY IF EXISTS clears an INVALID leftover so the migration "re-runs cleanly", but the migration framework tracks each statement individually in SERVER_MIGRATION_SQL_LOGS and skips any already-recorded statement (MigrationFile.parseSQLFiles line 83 filters via checkIfQueryPreviouslyRan; MigrationProcessImpl also re-checks). On a first run the DROP succeeds (no-op) and is logged; if the subsequent CREATE INDEX CONCURRENTLY is interrupted it leaves an INVALID index and is NOT logged. On re-run the DROP is skipped (already logged) and CREATE INDEX CONCURRENTLY IF NOT EXISTS sees the index name already exists (validity is not checked by IF NOT EXISTS), so it silently no-ops. The INVALID index is never rebuilt, the planner ignores it, and computePercentile keeps full-scanning — the fix silently fails with no error. Consider handling cleanup outside the tracked statement (e.g. via the Java Migration class that can DROP INVALID indexes before the CREATE), or reindex/validate as part of the same statement.

Was this helpful? React with 👍 / 👎

Comment on lines +15 to +16
DROP INDEX CONCURRENTLY IF EXISTS entity_usage_percentile_idx;
CREATE INDEX CONCURRENTLY IF NOT EXISTS entity_usage_percentile_idx ON entity_usage (usageDate, entityType);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Bug: CREATE INDEX CONCURRENTLY requires autocommit / no open transaction

Postgres CREATE INDEX CONCURRENTLY and DROP INDEX CONCURRENTLY throw "cannot run inside a transaction block" if executed within a transaction. This works only because the server jdbi connection pool defaults to autoCommit=true (HikariCPDataSourceFactory) and statements run via handle.execute without an explicit transaction. If migrations are ever run through a path/pool that disables autocommit or wraps DDL in a transaction, both statements will fail. Worth confirming the ops/CLI migrate entrypoint uses the same autocommit-enabled connection, since the earlier non-CONCURRENTLY form did not have this constraint.

Was this helpful? React with 👍 / 👎

@gitar-bot

gitar-bot Bot commented Jul 14, 2026

Copy link
Copy Markdown
Code Review ⚠️ Changes requested 0 resolved / 2 findings

Adds a non-unique index to entity_usage to optimize percentile lookups, but the migration scripts will fail due to invalid usage of CONCURRENTLY within transaction blocks and reliance on a DROP statement that the migration framework tracks as already executed.

⚠️ Bug: DROP-first cleanup defeated by per-statement migration tracking

📄 bootstrap/sql/migrations/native/1.13.2/postgres/schemaChanges.sql:13-16

The comment claims the leading DROP INDEX CONCURRENTLY IF EXISTS clears an INVALID leftover so the migration "re-runs cleanly", but the migration framework tracks each statement individually in SERVER_MIGRATION_SQL_LOGS and skips any already-recorded statement (MigrationFile.parseSQLFiles line 83 filters via checkIfQueryPreviouslyRan; MigrationProcessImpl also re-checks). On a first run the DROP succeeds (no-op) and is logged; if the subsequent CREATE INDEX CONCURRENTLY is interrupted it leaves an INVALID index and is NOT logged. On re-run the DROP is skipped (already logged) and CREATE INDEX CONCURRENTLY IF NOT EXISTS sees the index name already exists (validity is not checked by IF NOT EXISTS), so it silently no-ops. The INVALID index is never rebuilt, the planner ignores it, and computePercentile keeps full-scanning — the fix silently fails with no error. Consider handling cleanup outside the tracked statement (e.g. via the Java Migration class that can DROP INVALID indexes before the CREATE), or reindex/validate as part of the same statement.

💡 Bug: CREATE INDEX CONCURRENTLY requires autocommit / no open transaction

📄 bootstrap/sql/migrations/native/1.13.2/postgres/schemaChanges.sql:15-16

Postgres CREATE INDEX CONCURRENTLY and DROP INDEX CONCURRENTLY throw "cannot run inside a transaction block" if executed within a transaction. This works only because the server jdbi connection pool defaults to autoCommit=true (HikariCPDataSourceFactory) and statements run via handle.execute without an explicit transaction. If migrations are ever run through a path/pool that disables autocommit or wraps DDL in a transaction, both statements will fail. Worth confirming the ops/CLI migrate entrypoint uses the same autocommit-enabled connection, since the earlier non-CONCURRENTLY form did not have this constraint.

🤖 Prompt for agents
Code Review: Adds a non-unique index to `entity_usage` to optimize percentile lookups, but the migration scripts will fail due to invalid usage of `CONCURRENTLY` within transaction blocks and reliance on a `DROP` statement that the migration framework tracks as already executed.

1. ⚠️ Bug: DROP-first cleanup defeated by per-statement migration tracking
   Files: bootstrap/sql/migrations/native/1.13.2/postgres/schemaChanges.sql:13-16

   The comment claims the leading `DROP INDEX CONCURRENTLY IF EXISTS` clears an INVALID leftover so the migration "re-runs cleanly", but the migration framework tracks each statement individually in SERVER_MIGRATION_SQL_LOGS and skips any already-recorded statement (MigrationFile.parseSQLFiles line 83 filters via checkIfQueryPreviouslyRan; MigrationProcessImpl also re-checks). On a first run the DROP succeeds (no-op) and is logged; if the subsequent CREATE INDEX CONCURRENTLY is interrupted it leaves an INVALID index and is NOT logged. On re-run the DROP is skipped (already logged) and `CREATE INDEX CONCURRENTLY IF NOT EXISTS` sees the index name already exists (validity is not checked by IF NOT EXISTS), so it silently no-ops. The INVALID index is never rebuilt, the planner ignores it, and computePercentile keeps full-scanning — the fix silently fails with no error. Consider handling cleanup outside the tracked statement (e.g. via the Java Migration class that can DROP INVALID indexes before the CREATE), or reindex/validate as part of the same statement.

2. 💡 Bug: CREATE INDEX CONCURRENTLY requires autocommit / no open transaction
   Files: bootstrap/sql/migrations/native/1.13.2/postgres/schemaChanges.sql:15-16

   Postgres `CREATE INDEX CONCURRENTLY` and `DROP INDEX CONCURRENTLY` throw "cannot run inside a transaction block" if executed within a transaction. This works only because the server jdbi connection pool defaults to autoCommit=true (HikariCPDataSourceFactory) and statements run via handle.execute without an explicit transaction. If migrations are ever run through a path/pool that disables autocommit or wraps DDL in a transaction, both statements will fail. Worth confirming the ops/CLI migrate entrypoint uses the same autocommit-enabled connection, since the earlier non-CONCURRENTLY form did not have this constraint.

Options

Display: compact → Showing less information.

Comment with these commands to change the behavior for this request:

Compact
gitar display:verbose         

Was this helpful? React with 👍 / 👎 | Gitar

@github-actions

Copy link
Copy Markdown
Contributor

🟡 Playwright Results — all passed (30 flaky)

✅ 4524 passed · ❌ 0 failed · 🟡 30 flaky · ⏭️ 96 skipped

Shard Passed Failed Flaky Skipped
🟡 Shard 1 425 0 2 16
✅ Shard 2 11 0 0 0
🟡 Shard 3 819 0 12 9
✅ Shard 4 822 0 0 18
🟡 Shard 5 837 0 2 5
🟡 Shard 6 786 0 2 46
🟡 Shard 7 824 0 12 2
🟡 30 flaky test(s) (passed on retry)
  • Pages/Lineage/LineageRightPanel.spec.ts › Verify custom properties tab IS visible for supported type: metric (shard 1, 1 retry)
  • Flow/SearchRBAC.spec.ts › a fully denied user sees neither asset type when browsing (shard 1, 1 retry)
  • Features/BulkEditEntity.spec.ts › Table (shard 3, 1 retry)
  • Features/BulkImportWithDotInName.spec.ts › CSV with quoted FQN loads correctly in import grid (shard 3, 1 retry)
  • Features/BulkImportWithDotInName.spec.ts › Full import cycle with dot in service name (shard 3, 1 retry)
  • Features/ContextCenterArchivePage.spec.ts › archive page lazy-loads more rows on scroll within its own scroll container (shard 3, 1 retry)
  • Features/ContextCenterArticles.spec.ts › Article listing search filters, clears, and shows empty state (shard 3, 1 retry)
  • Features/ContextCenterArticles.spec.ts › Article card metadata, widgets, and listing search update from UI edits (shard 3, 1 retry)
  • Features/ContextCenterArticles.spec.ts › Article edit persistence and unsaved title behavior are correct (shard 3, 1 retry)
  • Features/ContextCenterArticles.spec.ts › description: switching articles does not bleed unsaved content into next article (shard 3, 2 retries)
  • Features/ContextCenterMemories.spec.ts › clearing search restores the unfiltered list (shard 3, 2 retries)
  • Features/ContextCenterMemories.spec.ts › view modal switches to edit mode and saves changes (shard 3, 1 retry)
  • Features/ContextCenterMemories.spec.ts › cancel button in edit mode closes the modal without saving (shard 3, 1 retry)
  • Features/ContextCenterMemories.spec.ts › ArrowDown + Enter keyboard navigation selects the linked table result (shard 3, 1 retry)
  • Flow/ExploreDiscovery.spec.ts › Should display domain and owner of deleted asset in suggestions when showDeleted is on (shard 5, 1 retry)
  • Pages/CustomProperties.spec.ts › Integer (shard 5, 1 retry)
  • Pages/Entity.spec.ts › Tag Add, Update and Remove for child entities (shard 6, 1 retry)
  • Pages/ExplorePageRightPanel_KnowledgeCenter.spec.ts › Should remove user owner for knowledgeCenter (shard 6, 1 retry)
  • Pages/ExplorePageRightPanel.spec.ts › Should NOT show restricted edit buttons for Data Steward for dashboardDataModel (shard 7, 1 retry)
  • Pages/ExplorePageRightPanel.spec.ts › Should allow Data Steward to edit tier for searchIndex (shard 7, 1 retry)
  • Pages/GlossaryImportExport.spec.ts › Check for Circular Reference in Glossary Import (shard 7, 1 retry)
  • Pages/GlossaryImportExport.spec.ts › Import partial success - some terms pass, some fail (shard 7, 1 retry)
  • Pages/Lineage/DataAssetLineage.spec.ts › Column lineage for dashboard -> table (shard 7, 1 retry)
  • Pages/Lineage/LineageFilters.spec.ts › Verify Impact Analysis service filter selection (shard 7, 1 retry)
  • Pages/Lineage/LineageRightPanel.spec.ts › Verify custom properties tab is NOT visible for pipelineService in platform lineage (shard 7, 1 retry)
  • Pages/Lineage/LineageRightPanel.spec.ts › Verify custom properties tab is NOT visible for apiService in platform lineage (shard 7, 1 retry)
  • Pages/ServiceEntity.spec.ts › Inactive Announcement create & delete (shard 7, 1 retry)
  • Pages/ServiceEntity.spec.ts › User as Owner Add, Update and Remove (shard 7, 1 retry)
  • Pages/TasksUIFlow.spec.ts › Create and reject tag task for Table via UI (shard 7, 1 retry)
  • Pages/TestSuite.spec.ts › Logical TestSuite (shard 7, 1 retry)

📦 Download artifacts

How to debug locally
# Download playwright-test-results-<shard> artifact and unzip
npx playwright show-trace path/to/trace.zip    # view trace

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

Labels

backend safe to test Add this label to run secure Github workflows on PRs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants