perf(core): avoid rewriting all NULL-ecosystem rows during database maintenance - #8736
Conversation
…aintenance The UPDATE_ECOSYSTEM statement updated every cpeEntry row with a NULL ecosystem on every maintenance run. Rows whose vendor/product has no usable cpeEcosystemCache entry (unknown or 'MULTIPLE') can never receive a value, yet the statement rewrote them with NULL on each run - roughly 145k wasted row writes per update on H2. - Add an EXISTS guard to UPDATE_ECOSYSTEM so only rows that will actually receive an ecosystem value are touched. - Rewrite the H2 CLEANUP_ORPHANS as NOT EXISTS so it probes the idxSoftwareCpe index instead of materializing a full LEFT JOIN. - Log the elapsed time of each maintenance statement. Measured on a full NVD rebuild (H2): database maintenance dropped from 189,482 ms to 1,732 ms. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Hello, first time contributor here! Our team at KOR Financial (https://www.korfinancial.com) are actively using this plugin on a daily basis for every build on Let me know if you want me to adjust something in the commit. Also, not sure whether you prefer (or dislike) the Claude co-author in the commit message. Let me know and I can adjust. |
|
@Turbots I had Claude take a look and... Gaps and improvements
None of these should block the merge — item 1 is the only one I'd ask the author to change in-PR; the rest are follow-ups. |
@Turbots I do prefer Claude to be there |
There was a problem hiding this comment.
Pull request overview
Optimizes H2 database maintenance and adds statement-level timing.
Changes:
- Avoids no-op ecosystem updates.
- Uses indexed orphan checks.
- Adds maintenance timing logs.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
dbStatements.properties |
Guards ecosystem updates with EXISTS. |
dbStatements_h2.properties |
Replaces orphan cleanup with NOT EXISTS. |
CveDB.java |
Records maintenance statement durations. |
Suppressed comments (2)
core/src/main/java/org/owasp/dependencycheck/data/nvdcve/CveDB.java:1797
- Orphan cleanup commonly has nothing to delete, but the query can still regress and become slow. Because this log remains behind
count > 0, those executions have no per-statement duration; log the zero count and timing as well.
LOGGER.info("Cleaned up {} orphaned NVD records ({} ms)", count,
System.currentTimeMillis() - orphanStart);
core/src/main/java/org/owasp/dependencycheck/data/nvdcve/CveDB.java:1789
- This timing also disappears when the statement changes zero rows, so a costly no-op
UPDATE_ECOSYSTEM2cannot be identified from the new per-statement logs. Log its count and elapsed time unconditionally.
LOGGER.info("Removed the CPE ecosystem on {} NVD records ({} ms)", count,
System.currentTimeMillis() - ecosystemStart);
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Address Copilot review feedback: a slow statement that updates zero rows is exactly the regression the timing instrumentation should expose, so log count and elapsed time even when count is zero.
|
@chadlwilson I've addressed the comments from Copilot Would be cool to get this merged and released, our team will be very happy with the speed improvements that this brings 👍🏼 |
|
I do not have merge permissions, only triage. I can at best offer an opinion or help with testing. It looks “in principle” reasonable/desirable but without deep knowledge into the DB queries (I’ve not looked there much). I’m not sure I’ve seen it take 3 minutes to update an existing DB when processing just the delta from a day or two of updates, but 2 minutes off data feed files with a restored DB and other caches after 24 hours, yes. Not sure the scenario is accurately described in the description - but practically speaking is this only relevant for the data feed usages, where there is a lot of potentially redundant updates since an entire file changes rather than a handful of records? Or also for NVD? Since you have raised a PR-without-issue, I’d suggest you step back and edit your description to note the actual observable problem and circumstances, with your environment/usage style and how to reproduce - rather than solely a solution/diagnosis. (this is the normal Claude/LLM problem, starting with solution rather than problem) And please mark the PR comments resolved as you either reject them (as irrelevant/stupid) or resolve them, thx 🙏🏻 |
|
Did some analysis on this and this will improve performance. Thanks! |
|
Thanks @jeremylong appreciate it 🙏🏼 |
Just for future reference, I have updated the summary of the problem and clarified that this impacts mostly first-time users with the default H2 database setting, or people doing full restores of the NVD database. |
Summary
Database maintenance after a full NVD update rewrites every
cpeEntryrow with a NULL ecosystem on every run.This causes for a lot of unnecessary updates when using the default H2 database implementation.
Everyone using this plugin for the first time with the default settings is impacted by this and might give the wrong impression that the plugin is
sloworinefficient. This change improves that first experience considerably.Technical Details
The
'MULTIPLE'filter ofUPDATE_ECOSYSTEMsits inside the correlated subquery, while the outerWHEREclause only checkse.ecosystem IS NULL. Rows whose vendor/product pair is unknown or mapped toMULTIPLEcan never receive a value, yet H2 rewrites them with NULL-to-NULL updates each time — ~145k wasted row writes per maintenance run on a typical database.Since the insert path (
H2Functions.insertSoftware/CveDB.updateVulnerabilityInsertSoftware) already sets the ecosystem from theCpeEcosystemCacheat insert time, the repair pass has very little real work to do.Changes
dbStatements.properties: add anEXISTSguard toUPDATE_ECOSYSTEMso only rows that will actually receive an ecosystem value are updated (mirrors the approach already used by the MS SQL Server variant).dbStatements_h2.properties: rewriteCLEANUP_ORPHANSasNOT EXISTSso H2 probes theidxSoftwareCpeindex per row instead of materializing a fullLEFT JOINofcpeEntryagainstsoftwareinside anINsubquery.CveDB.cleanupDatabase(): log the elapsed time of each maintenance statement to make future regressions visible.Measurements
Full NVD cache rebuild (data feed mirror, H2 database, all years + modified):
Verification
MULTIPLErows NULL, and the orphan delete removes exactly the orphaned rows.mvn -pl maven -am installbuilds successfully; ran a full rebuild plus scan through the built plugin against a real project.🤖 Generated with Claude Code