Skip to content

ESQL: Remove O(n^2) from DROP foo* - #154716

Merged
nik9000 merged 4 commits into
elastic:mainfrom
nik9000:fix_drop_wildcard_n_squared
Jul 22, 2026
Merged

nik9000 merged 4 commits into
elastic:mainfrom
nik9000:fix_drop_wildcard_n_squared

Conversation

@nik9000

@nik9000 nik9000 commented Jul 22, 2026 •

Copy link
Copy Markdown
Contributor

Removes an O(n^2) from DROP when it applies to a pattern. A friend of mine has an index pattern with 700,000 fields. It's a mistake. He shouldn't have 700,000 fields. But we shouldn't O(n^2) anyway. One mistake shouldn't cause double suffering.

nik9000 added 3 commits July 22, 2026 03:41
Analyzer.dropResolver resolved DROP wildcard.* patterns into an
ArrayList and then called resolvedProjections.removeIf(resolved::contains),
which is O(n) per lookup times O(n) elements to scan: O(n^2) overall.
Against a real CCS query with `| DROP lost.*` over a merged field-caps
schema of ~700,000 fields (OTel attribute explosion across 7,606
indices), this made the analysis phase take ~3.73 hours.

Fix: copy `resolved` into a HashSet before the removeIf call.
Attribute equality/hashing is keyed by NameId, so this is safe, and it
turns the inner lookup into O(1), making the whole operation O(n).

Also added benchmarks/src/main/java/org/elasticsearch/benchmark/esql/
AnalysisBenchmark.java (analysis / logicalOptimization / fullPipeline
benchmarks parameterized by fieldCount and plan=keep|drop) to make the
quadratic-vs-linear scaling reproducible and to guard against
regression.
@nik9000 nik9000 changed the title Sever the O(n^2) tumor in dropResolver ESQL: Remove O(n^2) from DROP foo* Jul 22, 2026
@nik9000
nik9000 marked this pull request as ready for review July 22, 2026 14:15
@elasticsearchmachine elasticsearchmachine added the Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) label Jul 22, 2026
@elasticsearchmachine

Copy link
Copy Markdown
Collaborator

Pinging @elastic/es-analytical-engine (Team:Analytics)

@nik9000 nik9000 added the >bug label Jul 22, 2026
@elasticsearchmachine

Copy link
Copy Markdown
Collaborator

Hi @nik9000, I've created a changelog YAML for you.

@github-actions

Copy link
Copy Markdown
Contributor

🔍 Preview links for changed docs

⏳ Building and deploying preview... View progress

This comment will be updated with preview links when the build is complete.

@github-actions

Copy link
Copy Markdown
Contributor

ℹ️ Important: Docs version tagging

👋 Thanks for updating the docs! Just a friendly reminder that our docs are now cumulative. This means all 9.x versions are documented on the same page and published off of the main branch, instead of creating separate pages for each minor version.

We use applies_to tags to mark version-specific features and changes.

Expand for a quick overview

When to use applies_to tags:

✅ At the page level to indicate which products/deployments the content applies to (mandatory)
✅ When features change state (e.g. preview, ga) in a specific version
✅ When availability differs across deployments and environments

What NOT to do:

❌ Don't remove or replace information that applies to an older version
❌ Don't add new information that applies to a specific version without an applies_to tag
❌ Don't forget that applies_to tags can be used at the page, section, and inline level

🤔 Need help?

// so, remove things that are in common
resolvedProjections.removeIf(resolved::contains);
Set<? extends NamedExpression> resolvedSet = new HashSet<>(resolved);
resolvedProjections.removeIf(resolvedSet::contains);

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.

Should we collect to a LinkedHashSet in first place instead of copying?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

👍

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It's actually a huge change to do LinkedHashSet. At least, to do it in any way that'd help.

@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@State(Scope.Benchmark)
public class AnalysisBenchmark {

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.

Out of curiosity, how much faster did this become?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I can find it for you. O(n^2) left so.... depend on the input size

@idegtiarenko idegtiarenko Jul 22, 2026 •

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.

I also wonder if it is worth moving this example to org.elasticsearch.benchmark._nightly.esql.QueryPlanningBenchmark?
It runs periodically and it already have some infra setting up many attributes.

@nik9000 nik9000 Jul 22, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

analysis                100       from      0.026 ->   0.025  ms/op
analysis                100       sort      0.348 ->   0.329  ms/op
analysis                100  drop_sort      0.357 ->   0.362  ms/op
fullPipeline            100       from      0.035 ->   0.034  ms/op
fullPipeline            100       sort      0.423 ->   0.415  ms/op
fullPipeline            100  drop_sort      0.485 ->   0.457  ms/op
logicalOptimization     100       from      0.009 ->   0.008  ms/op
logicalOptimization     100       sort      0.038 ->   0.041  ms/op
logicalOptimization     100  drop_sort      0.059 ->   0.059  ms/op
analysis             100000       from     37.661 ->  36.913  ms/op
analysis             100000       sort     47.561 ->  46.790  ms/op
analysis             100000  drop_sort  26960.619 ->  78.841  ms/op
fullPipeline         100000       from     49.466 ->  48.058  ms/op
fullPipeline         100000       sort     61.719 ->  70.408  ms/op
fullPipeline         100000  drop_sort  25543.589 -> 109.739  ms/op
logicalOptimization  100000       from     15.170 ->  17.946  ms/op
logicalOptimization  100000       sort     22.083 ->  19.261  ms/op
logicalOptimization  100000  drop_sort     19.955 ->  14.472  ms/op

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

rest of the query execution.

In the example I bumped into last night planning the drop command took 3.8 hours and the query execution took a few milliseconds.

@astefan astefan 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.

LGTM

@nik9000 nik9000 added the auto-backport Automatically create backport pull requests when merged label Jul 22, 2026
@nik9000
nik9000 merged commit 52a2c7c into elastic:main Jul 22, 2026
43 checks passed
@elasticsearchmachine

Copy link
Copy Markdown
Collaborator

💚 Backport successful

Status Branch Result
✅ 9.5
✅ 8.19

elasticsearchmachine pushed a commit that referenced this pull request Jul 22, 2026
Removes an `O(n^2)` from `DROP` when it applies to a pattern. A friend of mine has an index pattern with 700,000 fields. It's a mistake. He shouldn't have 700,000 fields. But we shouldn't `O(n^2)` anyway. One mistake shouldn't cause double suffering.
benchaplin pushed a commit to benchaplin/elasticsearch that referenced this pull request Jul 28, 2026
Removes an `O(n^2)` from `DROP` when it applies to a pattern. A friend of mine has an index pattern with 700,000 fields. It's a mistake. He shouldn't have 700,000 fields. But we shouldn't `O(n^2)` anyway. One mistake shouldn't cause double suffering.
elasticsearchmachine pushed a commit that referenced this pull request Aug 3, 2026
* ESQL: Remove `O(n^2)` from `DROP foo*` (#154716)

Removes an `O(n^2)` from `DROP` when it applies to a pattern. A friend of mine has an index pattern with 700,000 fields. It's a mistake. He shouldn't have 700,000 fields. But we shouldn't `O(n^2)` anyway. One mistake shouldn't cause double suffering.

* Remove AnalysisBenchmark from 8.19 backport

The benchmark references APIs that don't exist on 8.19
(Utils, PromqlFunctionRegistry, QuerySettings, etc.).
The actual fix in Analyzer.java is self-contained.
elasticsearchmachine pushed a commit that referenced this pull request Sep 9, 2026
…g attributes (#154818)

This is a follow up from
#154716. It's targeting
mainly queries like `DROP f1,f2,f3,....fn`  and `KEEP f1,f2,f3,....fn`
at resolution (analysis) time by building once per resolving tree node
and only when the child output exceeds 128 attributes a `Map` of
[attribute name, list of attributes with that name] of that node's
children output. Then, as the tree scan visits each
`UnresolvedAttribute`, it resolves via a single `Map.get(name)` instead
of an `Objects.equals(name, attr.name())` **scan over all attributes**.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

:Analytics/ES|QL AKA ESQL auto-backport Automatically create backport pull requests when merged >bug Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) v8.19.20 v9.5.1 v9.6.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants