Skip to content

[ESQL] Adds wriring for compute side of LIMIT BY command#143458

Merged
ncordon merged 17 commits intoelastic:mainfrom
ncordon:esql-limit-by
Mar 5, 2026
Merged

[ESQL] Adds wriring for compute side of LIMIT BY command#143458
ncordon merged 17 commits intoelastic:mainfrom
ncordon:esql-limit-by

Conversation

@ncordon
Copy link
Copy Markdown
Member

@ncordon ncordon commented Mar 3, 2026

Adds a compute engine operator GroupedLimitOperator, for a new LIMIT BY ESQL command. LIMIT N BY expr1, expr2,... retains at most N documents per group, where groups are defined by one or more grouping key expressions.

FROM test
| SORT salary DESC
| LIMIT 5 BY languages
| KEEP first_name, last_name, salary, languages

This should be a no-op because it's not used anywhere in the language yet.

Part of #112918, https://github.com/elastic/esql-planning/issues/238

@elasticsearchmachine elasticsearchmachine added needs:triage Requires assignment of a team area label v9.4.0 labels Mar 3, 2026
@ncordon ncordon added >feature Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) :Analytics/ES|QL AKA ESQL and removed needs:triage Requires assignment of a team area label labels Mar 3, 2026
@elasticsearchmachine
Copy link
Copy Markdown
Collaborator

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

@ncordon ncordon requested a review from Copilot March 3, 2026 12:14
@ncordon ncordon review requested due to automatic review settings March 3, 2026 12:22
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds compute-engine support for the ESQL LIMIT BY plan shape by threading grouping expressions through logical/physical Limit nodes and introducing a new compute operator to enforce per-group row limits, including transport-version gated serialization.

Changes:

  • Extend Limit (logical) and LimitExec (physical) with groupings and add mixed-version serialization behavior for LIMIT BY.
  • Wire planning/mapping to pass groupings through and execute GroupedLimitOperator when groupings are present.
  • Introduce GroupedLimitOperator + PositionKeyEncoder with accompanying unit tests and register the new operator status writeable.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/physical/LimitExecSerializationTests.java Updates physical plan serialization tests for new groupings field + mixed-version behavior.
x-pack/plugin/esql/src/test/java/org/elasticsearch/xpack/esql/plan/logical/LimitSerializationTests.java Updates logical plan serialization tests for new groupings field + mixed-version behavior.
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plugin/EsqlPlugin.java Registers GroupedLimitOperator.Status in named writeables.
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/mapper/Mapper.java Passes Limit.groupings() into LimitExec.
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/mapper/LocalMapper.java Passes Limit.groupings() into LimitExec for local planning.
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/planner/LocalExecutionPlanner.java Plans LimitExec into LimitOperator vs GroupedLimitOperator based on groupings.
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/physical/LimitExec.java Adds groupings to physical node and gates serialization by transport version.
x-pack/plugin/esql/src/main/java/org/elasticsearch/xpack/esql/plan/logical/Limit.java Adds groupings to logical node and gates serialization by transport version.
x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/PositionKeyEncoderTests.java Adds coverage for composite key encoding semantics.
x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/GroupedLimitOperatorTests.java Adds behavioral tests for grouped limiting and status fields.
x-pack/plugin/esql/compute/src/test/java/org/elasticsearch/compute/operator/GroupedLimitOperatorStatusTests.java Adds wire/xcontent tests for GroupedLimitOperator.Status.
x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/PositionKeyEncoder.java Implements grouping-key encoding across channels/values.
x-pack/plugin/esql/compute/src/main/java/org/elasticsearch/compute/operator/GroupedLimitOperator.java Implements per-group limiting operator + status.
server/src/main/resources/transport/upper_bounds/9.4.csv Updates the 9.4 transport upper bound to include esql_limit_by.
server/src/main/resources/transport/definitions/referable/esql_limit_by.csv Adds referable transport version definition for esql_limit_by.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

private final int limitPerGroup;
private final PositionKeyEncoder keyEncoder;
private final BigArrays bigArrays;
private final BytesRefHashTable seenKeys;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

It's worth a comment saying that you want this and not the a BlockHash implementation because those expand multivalues and this doesn't. I could see a world where one day we have other implementations of BlockHash that don't expand keys, but today is not that day. This is a good way to do it.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could you make the comment javadoc? It'll read nicer if you mouseover the item.

* below 128 are encoded in a single byte, making this compact for the
* small numbers typical of value counts and byte-array lengths.
*/
private void writeVInt(int value) {
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.

It would be nice to be able to share logic with DefaultUnsortableTopNEncoder in these methods, as they do basically the same

Copy link
Copy Markdown
Member Author

@ncordon ncordon Mar 4, 2026

Choose a reason for hiding this comment

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

We could have a TopNEncoder.DEFAULT_UNSORTABLE inside this class? I've done that here: d2bd9f3. Not sure if I like we are calling inside. Any thoughts?

BytesRef key = keyEncoder.encode(page, pos);
long hashOrd = seenKeys.add(key);
int count;
long ord;
Copy link
Copy Markdown
Contributor

@cimequinox cimequinox Mar 4, 2026

Choose a reason for hiding this comment

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

No need to change anything but TIL BytesRefHashTable.add returns >= 0 when a new key was added and (-1-id) which is < 0 when it was already present.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I found that quite clever when I read about it 😄

private final int limitPerGroup;
private final PositionKeyEncoder keyEncoder;
private final BigArrays bigArrays;
private final BytesRefHashTable seenKeys;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could you make the comment javadoc? It'll read nicer if you mouseover the item.

this.counts = bigArrays.newIntArray(16, false);
}

public static final class Factory implements Operator.OperatorFactory {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could you move the Factory up to the top? I get confused when the factory is below the ctor. Mostly when I'm scanning the files quickly.

}
}
return result;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Could you move this into Page?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I see we do it like 40 times in other places. It'd be lovely have it in one. But it'd be nice to dedupe in a follow-up change.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yep, Ivan pointed to me we already have a filter method in Page

@Override
public void finish() {
finished = true;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For later: there are cases where we know the hash can never grow any larger - like, we know the number of unique values we're grouping on. In that case we could early terminate - just like LimitOperator does. Say you are doing:

FROM foo
| SORT @timestamp DESC
| LIMIT 10 BY hostname

It's quite possible to know the number of unique hostnames, especially if it's less than a million. The old aggs framework uses a thing called "global ordinals" for this. We don't want those because they are very expensive at unexpected times. But their existence proves that such counts are possible/easy/good.

That is for later though.

Copy link
Copy Markdown
Member Author

@ncordon ncordon Mar 4, 2026

Choose a reason for hiding this comment

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

👍 I've added it to the issue we have tracking things to polish LIMIT BY: https://github.com/elastic/esql-planning/issues/262

@ncordon
Copy link
Copy Markdown
Member Author

ncordon commented Mar 4, 2026

I've addressed all of the feedback, just missing an opinion on this #143458 (comment) from @ivancea

public class GroupKeyEncoder {
public class GroupKeyEncoder implements Releasable {

private static final DefaultUnsortableTopNEncoder encoder = TopNEncoder.DEFAULT_UNSORTABLE;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Not sure whether I like this

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.

Looks good to me, and we reuse some code. Plus, having the concrete class declared there should help with inlining.
Is there something you don't like specifically?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

The naming perhaps (we are calling something from TopN inside limit), but it's fine, I can live with it

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.

Uhm yep, the namespacing seems odd, but I would live with it. Ideally, if we decide to change to a BlockHash or similar, that would be "fixed" (Fun, because BlockHash is in the aggregations package, so not perfect either! 😆)

public class GroupKeyEncoder {
public class GroupKeyEncoder implements Releasable {

private static final DefaultUnsortableTopNEncoder encoder = TopNEncoder.DEFAULT_UNSORTABLE;
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.

Looks good to me, and we reuse some code. Plus, having the concrete class declared there should help with inlining.
Is there something you don't like specifically?

Copy link
Copy Markdown
Contributor

@ivancea ivancea left a comment

Choose a reason for hiding this comment

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

LGTM!

@ncordon ncordon merged commit 3b090df into elastic:main Mar 5, 2026
35 checks passed
@ncordon ncordon deleted the esql-limit-by branch March 5, 2026 15:17
jfreden pushed a commit to jfreden/elasticsearch that referenced this pull request Mar 5, 2026
)

---------

Co-authored-by:  Iván Cea Fontenla <ivan.cea@elastic.co>
spinscale pushed a commit to spinscale/elasticsearch that referenced this pull request Mar 6, 2026
)

---------

Co-authored-by:  Iván Cea Fontenla <ivan.cea@elastic.co>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

:Analytics/ES|QL AKA ESQL >feature Team:Analytics Meta label for analytical engine team (ESQL/Aggs/Geo) v9.4.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants