AR
AgentRuss
ECS query diagram showing entities filtered by component combinations such as Transform, Velocity, Health, and PlayerControlled.
AgentRuss Guide

ECS Queries Explained: Finding Entities by Components

Written by
AgentRuss
Published

Queries are one of the most important ideas in an Entity Component System.

Entities give us identity.

Components give us data.

Systems give us behaviour.

But systems still need a way to answer one critical question:

Which entities should this system process?

That is the job of a query.

Instead of asking whether an entity is a Player, Enemy, or Projectile, an ECS query asks whether the entity has the components required for a particular behaviour.

For example:

JavaScript
1Transform + Velocity

can mean:

Find every entity that can participate in movement.

In this guide, we'll look at how ECS queries work, why they are so useful, how component combinations define capabilities, and how a simple query system can evolve into a cached and more efficient implementation.


What Is an ECS Query?

An ECS query searches the world for entities matching a particular component combination.

For example:

JavaScript
1world.query([
2 Transform,
3 Velocity,
4])

means:

Return every entity that has both Transform and Velocity.

Imagine our world contains:

JavaScript
1Entity 1
2 Transform
3 Velocity
4
5Entity 2
6 Transform
7 Health
8
9Entity 3
10 Transform
11 Velocity
12 Health
13
14Entity 4
15 Inventory

A query for:

JavaScript
1Transform + Velocity

returns:

JavaScript
1Entity 1
2Entity 3

Entity 2 does not match because it has no Velocity.

Entity 4 does not match either because it has neither required component.


Queries Connect Components to Systems

A system usually defines the data it requires.

For example, a movement system needs:

  • position
  • velocity

So its query might be:

JavaScript
1const entities = world.query([
2 Transform,
3 Velocity,
4])

Then:

JavaScript
1for (const entity of entities) {
2 const transform =
3 world.getComponent(entity, Transform)
4
5 const velocity =
6 world.getComponent(entity, Velocity)
7
8 transform.position.x +=
9 velocity.x * deltaTime
10
11 transform.position.y +=
12 velocity.y * deltaTime
13
14 transform.position.z +=
15 velocity.z * deltaTime
16}

The system does not care what those entities represent.

They could be:

  • players
  • enemies
  • NPCs
  • projectiles
  • moving platforms

If they contain the required data, they match the query.


Think in Capabilities, Not Types

This is one of the biggest mental shifts when moving from object-oriented game architecture to ECS.

Instead of asking:

Is this a Player?

we ask:

Does this entity have the data required for this behaviour?

For example:

JavaScript
1Transform + Velocity

means the entity can move.

JavaScript
1Health

means the entity can participate in health-related gameplay.

JavaScript
1Inventory + Equipment

means the entity can participate in equipment logic.

JavaScript
1PlayerControlled + Input + Velocity

means the entity can respond to player movement input.

The component combination describes the capability.


One Entity Can Match Many Queries

An entity does not belong to only one system.

Imagine a player entity has:

JavaScript
1Transform
2Velocity
3Health
4Inventory
5Equipment
6PlayerControlled
7Input
8BaseStats
9FinalStats

That same entity may match all of these queries:

JavaScript
1Transform + Velocity

for movement.

JavaScript
1PlayerControlled + Input + Velocity

for player control.

JavaScript
1Health

for health-related systems.

JavaScript
1BaseStats + FinalStats + Equipment

for stat calculation.

JavaScript
1Inventory + Equipment

for equipment behaviour.

The entity participates in many systems because it has many capabilities.

That is how behaviour emerges through composition.


A Simple Query Implementation

In the basic ECS we built previously, a query can be implemented by scanning all entities.

For example:

JavaScript
1query(componentTypes) {
2 const results = []
3
4 for (const entity of this.entities) {
5 const matches = componentTypes.every(
6 (componentType) =>
7 this.hasComponent(
8 entity,
9 componentType,
10 ),
11 )
12
13 if (matches) {
14 results.push(entity)
15 }
16 }
17
18 return results
19}

The logic is straightforward:

  1. loop over every entity
  2. check every required component
  3. if all required components exist, include the entity

This is a great implementation for understanding the architecture.


Why every() Works Well Here

The important part is:

JavaScript
1componentTypes.every(
2 (componentType) =>
3 this.hasComponent(
4 entity,
5 componentType,
6 ),
7)

every() only returns true when every required component exists.

For a query containing:

JavaScript
1[
2 Transform,
3 Velocity,
4 Health,
5]

the entity must contain all three:

JavaScript
1Transform
2Velocity
3Health

If even one is missing:

JavaScript
1Transform
2Velocity
3Health

the entity does not match.


Queries Usually Mean AND

A normal ECS query typically means:

JavaScript
1Component A
2AND
3Component B
4AND
5Component C

So:

JavaScript
1world.query([
2 Transform,
3 Velocity,
4])

means:

JavaScript
1Transform AND Velocity

not:

JavaScript
1Transform OR Velocity

This is important because systems usually require a specific set of data before they can safely run.

MovementSystem cannot update velocity-based movement if an entity has no Velocity.


Queries Make Systems Generic

Imagine we wrote a movement system like this:

JavaScript
1if (entity === player) {
2 // move player
3}

That system only understands one specific entity.

Or perhaps:

JavaScript
1if (
2 entity.type === 'player' ||
3 entity.type === 'enemy' ||
4 entity.type === 'projectile'
5) {
6 // move it
7}

Now the system needs to know every kind of thing that can move.

With ECS, the system simply asks:

JavaScript
1Transform + Velocity

Any future entity with those components automatically works.

No movement-system change is required.


Adding a New Entity Type Without Changing the System

Suppose we already have a player:

JavaScript
1const player = world.createEntity()
2
3world.addComponent(
4 player,
5 new Transform(),
6)
7
8world.addComponent(
9 player,
10 new Velocity(),
11)

Later we add a projectile:

JavaScript
1const projectile = world.createEntity()
2
3world.addComponent(
4 projectile,
5 new Transform(),
6)
7
8world.addComponent(
9 projectile,
10 new Velocity(10, 0, 0),
11)

We do not need to modify MovementSystem.

The projectile automatically matches:

JavaScript
1Transform + Velocity

and begins moving.

That is a major benefit of query-driven systems.


Marker Components Make Queries More Specific

Sometimes a system needs more than raw data.

We may want to distinguish between entities that have similar components but different roles.

This is where marker components are useful.

For example:

JavaScript
1export class PlayerControlled extends Component {}

Then a player controller can query:

JavaScript
1world.query([
2 PlayerControlled,
3 Input,
4 Velocity,
5])

Now an enemy with Velocity will not match unless it also has PlayerControlled.

The marker adds meaning through its presence.


Querying Player-Controlled Entities

Imagine:

JavaScript
1Entity 1
2 PlayerControlled
3 Input
4 Velocity
5 Transform
6
7Entity 2
8 AIControlled
9 Velocity
10 Transform

A player-control query:

JavaScript
1PlayerControlled + Input + Velocity

returns Entity 1 only.

A movement query:

JavaScript
1Transform + Velocity

returns both Entity 1 and Entity 2.

This demonstrates how different systems can view the same world through different queries.


Queries Define System Requirements

A query can act almost like a system's data contract.

For example:

JavaScript
1MovementSystem
2requires:
3Transform + Velocity
JavaScript
1StatsSystem
2requires:
3BaseStats + FinalStats + Equipment
JavaScript
1TargetingSystem
2requires:
3Transform + Targetable
JavaScript
1PlayerControllerSystem
2requires:
3PlayerControlled + Input + Velocity

Looking at a system's query often tells you immediately what that system expects.

This makes the architecture easier to understand.


Query Composition Can Change at Runtime

One of the powerful parts of ECS is that entities can start or stop matching a query when components are added or removed.

Suppose an entity has:

JavaScript
1Transform
2Velocity

It matches MovementSystem.

Now remove Velocity:

JavaScript
1world.removeComponent(
2 entity,
3 Velocity,
4)

It no longer matches the movement query.

The entity still exists.

But the behaviour associated with that query disappears.

Add Velocity again:

JavaScript
1world.addComponent(
2 entity,
3 new Velocity(),
4)

and it automatically becomes eligible for movement again.


Behaviour Emerges From Composition

This leads to an important ECS principle:

Adding or removing components can add or remove behaviour without changing the entity's class.

For example:

JavaScript
1Transform

might represent something positioned in the world.

Add:

JavaScript
1Velocity

and it can move.

Add:

JavaScript
1Health

and it can receive damage.

Add:

JavaScript
1Interactable

and interaction systems can detect it.

Add:

JavaScript
1PlayerControlled

and player-control systems can operate on it.

Queries are what make this composition useful.


The Cost of Scanning Every Entity

Our simple implementation checks every entity each time a query runs.

Imagine:

JavaScript
1world.query([
2 Transform,
3 Velocity,
4])

runs every frame.

If the world contains 50 entities, this is trivial.

If the world contains 500 entities, it may still be perfectly reasonable.

But imagine:

  • 50,000 entities
  • dozens of systems
  • several queries per system
  • every query scanning every entity

Now the cost becomes much larger.

This is where more advanced ECS implementations start optimizing queries.


The Same Query May Run Every Frame

Consider:

JavaScript
1world.query([
2 Transform,
3 Velocity,
4])

inside MovementSystem.

The required component combination rarely changes.

But our simple query performs the matching process again every update.

At 60 FPS:

JavaScript
160 query scans per second

for just that one system.

If 20 systems do the same thing, we may perform many unnecessary component checks.

One common optimization is query caching.


What Is a Cached Query?

Instead of finding matches from scratch every frame, we can create a query object that remembers them.

Conceptually:

JavaScript
1Query:
2Transform + Velocity
3
4Matching entities:
51
63
77
812
918

The query only changes when entity composition changes.

For example:

  • an entity is created
  • an entity is destroyed
  • a component is added
  • a component is removed

If nothing changes, the cached result remains valid.


A Simple Query Class

A basic Query might look like this:

JavaScript
1export class Query {
2 constructor(
3 world,
4 componentTypes,
5 ) {
6 this.world = world
7 this.componentTypes =
8 componentTypes
9
10 this.entities = new Set()
11 }
12
13 matches(entity) {
14 return this.componentTypes.every(
15 (componentType) =>
16 this.world.hasComponent(
17 entity,
18 componentType,
19 ),
20 )
21 }
22}

The query knows:

  • which world it belongs to
  • which component types it requires
  • which entities currently match

We can then update the cached entity set when composition changes.


Updating Queries When Components Change

Suppose we add a component:

JavaScript
1world.addComponent(
2 entity,
3 new Velocity(),
4)

After that operation, the world can ask relevant queries:

Does this entity match now?

Conceptually:

JavaScript
1for (const query of this.queries) {
2 if (query.matches(entity)) {
3 query.entities.add(entity)
4 } else {
5 query.entities.delete(entity)
6 }
7}

The same logic can run when:

  • components are added
  • components are removed
  • entities are created
  • entities are destroyed

Now systems can iterate cached results instead of scanning the entire world.


Why Cached Queries Can Be Faster

Imagine a world with 10,000 entities.

Only 300 have:

JavaScript
1Transform + Velocity

A full-scan query checks all 10,000 entities every frame.

A cached query lets MovementSystem iterate only those 300 matching entities.

The work shifts from:

JavaScript
1Find matches every frame

to:

JavaScript
1Maintain matches when composition changes

That can be a major improvement when composition changes less frequently than systems update.


Optimization Has a Cost

Cached queries are faster in many situations, but they make the ECS more complicated.

Now the world must correctly update queries whenever:

  • an entity is created
  • an entity is destroyed
  • a component is added
  • a component is removed

If one of those paths forgets to notify the query system, cached results can become incorrect.

This is why starting with a simple implementation is often a good idea.

Build the architecture first.

Optimize once you actually need the performance.


Query Registration

A larger ECS might allow systems to register queries once.

For example:

JavaScript
1this.query =
2 world.createQuery([
3 Transform,
4 Velocity,
5 ])

Then inside update():

JavaScript
1for (
2 const entity
3 of this.query.entities
4) {
5 // movement logic
6}

The system no longer constructs the same query every frame.

Its data requirement is declared once.


Query Objects Improve Readability Too

Performance is not the only benefit.

A dedicated query also makes a system easier to read.

For example:

JavaScript
1export class MovementSystem
2 extends System {
3 constructor(world) {
4 super(world)
5
6 this.query =
7 world.createQuery([
8 Transform,
9 Velocity,
10 ])
11 }
12
13 update(deltaTime) {
14 for (
15 const entity
16 of this.query.entities
17 ) {
18 // movement
19 }
20 }
21}

The constructor clearly communicates:

MovementSystem operates on Transform + Velocity.

Then update() focuses on behaviour rather than entity discovery.


Multiple Queries in One System

A system can also need more than one group of entities.

Imagine an interaction system.

It might need:

JavaScript
1PlayerControlled + Transform

for the player.

And:

JavaScript
1Interactable + Transform

for objects the player can interact with.

Conceptually:

JavaScript
1this.players =
2 world.createQuery([
3 PlayerControlled,
4 Transform,
5 ])
6
7this.interactables =
8 world.createQuery([
9 Interactable,
10 Transform,
11 ])

The system can then compare those groups.

Queries are not limited to one per system.


Optional Components

Sometimes a system requires certain components but can make use of additional optional ones.

For example, a rendering system might require:

JavaScript
1Transform + Mesh

but optionally inspect:

JavaScript
1HitFlash

if present.

The required query remains:

JavaScript
1world.query([
2 Transform,
3 Mesh,
4])

Inside the loop:

JavaScript
1const hitFlash =
2 world.getComponent(
3 entity,
4 HitFlash,
5 )
6
7if (hitFlash) {
8 // apply optional behaviour
9}

Not every possible component needs to be part of the query.

Only include components required for the system to function.


Excluding Components

More advanced query systems often support exclusion.

For example:

JavaScript
1Transform + Velocity
2WITHOUT Dead

That might mean:

Move every entity with Transform and Velocity unless it is dead.

Conceptually:

JavaScript
1world.query({
2 all: [
3 Transform,
4 Velocity,
5 ],
6
7 none: [
8 Dead,
9 ],
10})

This can make certain system rules more expressive.


Required, Optional, and Excluded Components

A more advanced query syntax might support:

JavaScript
1world.query({
2 all: [
3 Transform,
4 Health,
5 ],
6
7 any: [
8 PlayerControlled,
9 AIControlled,
10 ],
11
12 none: [
13 Dead,
14 ],
15})

This could mean:

  • must have Transform
  • must have Health
  • must have either player or AI control
  • must not have Dead

Not every ECS needs this complexity.

But it shows how query systems can evolve.


Don't Put Gameplay Logic Inside Queries

Queries should generally answer:

Which entities match?

They should not become gameplay systems themselves.

For example, avoid making a query responsible for:

  • applying damage
  • moving entities
  • changing inventory
  • calculating stats

A query identifies entities.

A system decides what happens to them.

Keeping that boundary clear makes the ECS easier to reason about.


Query Results Should Not Define Identity

If an entity matches:

JavaScript
1Transform + Velocity

that does not mean:

This is a Player.

It only means:

This entity has Transform and Velocity.

This distinction is important.

The same query can match unrelated types of things.

That's a feature, not a problem.


Queries and System Order

Queries also interact with system order.

Suppose one system adds a component during the frame.

For example:

JavaScript
1DeathSystem
2adds Dead

A later system has a query excluding Dead.

If query membership updates immediately, that later system will stop seeing the entity during the same frame.

If membership updates at frame boundaries instead, the entity may remain visible until the next frame.

Neither approach is universally correct.

But the behaviour should be intentional.


Immediate vs Deferred Composition Changes

Some ECS designs apply component changes immediately.

For example:

JavaScript
1world.addComponent(
2 entity,
3 new Dead(),
4)

updates query membership straight away.

Other ECS designs queue structural changes until the end of the current update.

Conceptually:

JavaScript
1During systems:
2queue component changes
3
4
5
6End of frame:
7apply component changes
8update queries

Deferred structural changes can make iteration safer because query contents do not suddenly change while a system is looping over them.


Be Careful When Modifying Query Membership During Iteration

Imagine:

JavaScript
1for (
2 const entity
3 of this.query.entities
4) {
5 world.removeComponent(
6 entity,
7 Velocity,
8 )
9}

If removing Velocity immediately removes the entity from this.query.entities, we are modifying the collection while iterating it.

Depending on the data structure and implementation, this can produce surprising behaviour.

Possible solutions include:

  • copying the results before iteration
  • deferring structural changes
  • using data structures designed for safe removal
  • processing removal queues after system execution

This is one reason mature ECS frameworks often separate structural changes from ordinary component data updates.


Queries and Events Solve Different Problems

Queries and events are both ways systems interact with the world, but they serve different purposes.

A query asks:

What currently exists with this state?

An event says:

Something happened.

For example:

JavaScript
1Query:
2Health + Transform

finds entities currently possessing those components.

An event:

JavaScript
1EVT_DAMAGE_RESOLVED

describes a specific gameplay occurrence.

Queries are about current composition.

Events are about changes or actions over time.


Queries Make New Features Easier to Add

Suppose we add a poison system.

It might query:

JavaScript
1StatusEffects + Health

We do not need to modify:

  • the Player class
  • the Enemy class
  • the DamageSystem
  • the MovementSystem

Any entity with those required components automatically participates.

Later, if a destructible object gains:

JavaScript
1Health
2StatusEffects

the poison system can process it too.

This is the power of capability-based design.


Realistic Query Examples

As an ECS game grows, queries might look like:

JavaScript
1MovementSystem
2Transform + Velocity
JavaScript
1PlayerControllerSystem
2PlayerControlled + Input + Velocity
JavaScript
1GravitySystem
2Velocity + Grounded
JavaScript
1StatsSystem
2BaseStats + FinalStats + Equipment
JavaScript
1TargetingSystem
2Transform + Targetable
JavaScript
1FloatingHealthBarSystem
2Transform + Health + HealthBarVisibility
JavaScript
1InteractionSystem
2Transform + Interactable

Each one expresses a system's minimum data requirements.


Keep Queries Focused

Avoid adding components to a query simply because they happen to exist.

If MovementSystem only requires:

JavaScript
1Transform + Velocity

then don't make it query:

JavaScript
1Transform
2Velocity
3Health
4Inventory
5Equipment
6PlayerControlled

That would unnecessarily restrict which entities can move.

The query should describe what the system actually needs.


Queries Help Prevent Hard-Coded Entity Checks

Without queries, code can slowly accumulate checks like:

JavaScript
1if (entity === player) {
2 // ...
3}

or:

JavaScript
1if (
2 entity.type === 'enemy'
3) {
4 // ...
5}

or:

JavaScript
1if (
2 entity.name === 'boss'
3) {
4 // ...
5}

Those checks tie behaviour to specific identities.

Queries encourage us to ask:

What data or capability actually makes this behaviour relevant?

That usually leads to a more reusable design.


Query Performance Is About Access Patterns

ECS performance is not just about raw entity count.

It is also about how systems access data.

A system processing:

JavaScript
1Transform + Velocity

every frame has a very different access pattern from a system handling:

JavaScript
1Inventory + Equipment

only when equipment changes.

Frequently used queries may deserve more optimization.

Rarely used queries may be perfectly fine with simple scanning.

Performance decisions should follow actual usage.


Start Simple, Then Measure

It can be tempting to build:

  • archetypes
  • bitmasks
  • sparse sets
  • query caches
  • structural-change queues
  • chunk storage

before the game even has ten entities.

Those techniques can be valuable.

But they also add complexity.

A better learning path is often:

JavaScript
1Simple queries
2
3Build real gameplay
4
5Measure performance
6
7Identify bottlenecks
8
9Optimize the hot paths

A clear ECS that is slightly slower is often easier to improve than a highly optimized ECS nobody understands.


How Production ECS Libraries Optimize Queries

Different ECS implementations use different storage strategies.

Common approaches include:

  • component maps
  • sparse sets
  • archetypes
  • bitmasks
  • signature matching
  • cached entity sets
  • contiguous component arrays

For example, an entity's component composition could be represented as a bitmask.

Then query matching becomes a fast bitwise operation.

Or entities with identical component combinations can be grouped into archetypes.

These approaches can make queries extremely efficient.

But they are optimizations of the same core concept:

Find entities whose component composition satisfies the system's requirements.

Query Caching and Archetypes

An archetype-based ECS groups entities with identical component sets.

For example:

JavaScript
1Archetype A
2Transform + Velocity
3
4Entities:
51
67
712
JavaScript
1Archetype B
2Transform + Velocity + Health
3
4Entities:
53
68
715

A query for:

JavaScript
1Transform + Velocity

can match both archetypes.

Instead of examining every entity individually, the ECS can operate on groups known to contain the required components.

This is one reason archetype-based ECS implementations can achieve strong performance.


You Don't Need Archetypes to Understand Queries

Archetypes are useful, but they are not required to understand ECS architecture.

The fundamental question remains:

JavaScript
1Does this entity contain the components this system requires?

A simple Map-based implementation and a highly optimized archetype ECS answer the same conceptual question.

They simply use different data structures to get there.


A Useful Query Checklist

When designing a query, ask:

Which components are absolutely required?

Those belong in the query.

Are some components only optional?

Check those inside the system instead.

Should certain entities be excluded?

Consider a marker such as Dead and exclusion support if your query system provides it.

Does this query run every frame?

It may become a candidate for caching.

Does composition change frequently?

Think carefully about the cost of maintaining cached membership.

Am I querying based on capability or hard-coded identity?

Prefer capabilities whenever possible.


A Practical Mental Model

Think of a query as a filter over the ECS world.

The world contains:

JavaScript
1Entity 1
2Entity 2
3Entity 3
4Entity 4
5Entity 5
6...

Each entity has a component composition.

A system declares:

JavaScript
1I require:
2
3Transform
4Velocity

The query filters the world:

JavaScript
1World
2
3Transform?
4
5Velocity?
6
7Matching entities
8
9MovementSystem

That is the relationship between queries and systems.


The Bigger ECS Picture

Queries fit into the larger architecture like this:

JavaScript
1ENTITY
2Identity
3
4
5
6COMPONENTS
7State
8
9
10
11QUERY
12Find matching entities
13
14
15
16SYSTEM
17Apply behaviour
18
19
20
21EVENTS
22Communicate meaningful outcomes

For example:

JavaScript
1Entity 7
2
3Transform
4Velocity
5Health
6
7
8
9Movement Query
10Transform + Velocity
11
12
13
14MovementSystem
15
16
17
18Transform changes

At the same time:

JavaScript
1Entity 7
2
3Health
4
5
6
7DamageSystem
8
9
10
11EVT_DAMAGE_RESOLVED

Queries and events serve different purposes but work together within the same ECS architecture.


Conclusion

ECS queries are the bridge between component data and system behaviour.

A system does not need to know whether an entity is a player, enemy, projectile, NPC, or destructible object.

It only needs to know:

Does this entity have the components I require?

A simple query might look like:

JavaScript
1world.query([
2 Transform,
3 Velocity,
4])

That small idea gives us an extremely flexible architecture.

New entities can participate in existing behaviour simply by receiving the right components.

Removing components can remove behaviour.

Marker components can refine capabilities.

And systems remain reusable because they operate on data requirements rather than rigid object types.

A simple ECS can find matches by scanning every entity.

As the game grows, queries can evolve into cached sets, signature matching, sparse-set storage, or archetype-based systems.

But the underlying idea remains the same:

JavaScript
1Components describe capabilities.
2
3Queries find those capabilities.
4
5Systems act on them.

Once that model becomes familiar, ECS architecture becomes much easier to reason about.