
ECS Queries Explained: Finding Entities by Components
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:
1Transform + Velocitycan 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:
1world.query([2 Transform,3 Velocity,4])means:
Return every entity that has both Transform and Velocity.
Imagine our world contains:
1Entity 12 Transform3 Velocity4
5Entity 26 Transform7 Health8
9Entity 310 Transform11 Velocity12 Health13
14Entity 415 InventoryA query for:
1Transform + Velocityreturns:
1Entity 12Entity 3Entity 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:
1const entities = world.query([2 Transform,3 Velocity,4])Then:
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 * deltaTime10
11 transform.position.y +=12 velocity.y * deltaTime13
14 transform.position.z +=15 velocity.z * deltaTime16}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:
1Transform + Velocitymeans the entity can move.
1Healthmeans the entity can participate in health-related gameplay.
1Inventory + Equipmentmeans the entity can participate in equipment logic.
1PlayerControlled + Input + Velocitymeans 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:
1Transform2Velocity3Health4Inventory5Equipment6PlayerControlled7Input8BaseStats9FinalStatsThat same entity may match all of these queries:
1Transform + Velocityfor movement.
1PlayerControlled + Input + Velocityfor player control.
1Healthfor health-related systems.
1BaseStats + FinalStats + Equipmentfor stat calculation.
1Inventory + Equipmentfor 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:
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 results19}The logic is straightforward:
- loop over every entity
- check every required component
- 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:
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:
1[2 Transform,3 Velocity,4 Health,5]the entity must contain all three:
1Transform ✓2Velocity ✓3Health ✓If even one is missing:
1Transform ✓2Velocity ✓3Health ✗the entity does not match.
Queries Usually Mean AND
A normal ECS query typically means:
1Component A2AND3Component B4AND5Component CSo:
1world.query([2 Transform,3 Velocity,4])means:
1Transform AND Velocitynot:
1Transform OR VelocityThis 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:
1if (entity === player) {2 // move player3}That system only understands one specific entity.
Or perhaps:
1if (2 entity.type === 'player' ||3 entity.type === 'enemy' ||4 entity.type === 'projectile'5) {6 // move it7}Now the system needs to know every kind of thing that can move.
With ECS, the system simply asks:
1Transform + VelocityAny 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:
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:
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:
1Transform + Velocityand 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:
1export class PlayerControlled extends Component {}Then a player controller can query:
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:
1Entity 12 PlayerControlled3 Input4 Velocity5 Transform6
7Entity 28 AIControlled9 Velocity10 TransformA player-control query:
1PlayerControlled + Input + Velocityreturns Entity 1 only.
A movement query:
1Transform + Velocityreturns 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:
1MovementSystem2requires:3Transform + Velocity1StatsSystem2requires:3BaseStats + FinalStats + Equipment1TargetingSystem2requires:3Transform + Targetable1PlayerControllerSystem2requires:3PlayerControlled + Input + VelocityLooking 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:
1Transform2VelocityIt matches MovementSystem.
Now remove Velocity:
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:
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:
1Transformmight represent something positioned in the world.
Add:
1Velocityand it can move.
Add:
1Healthand it can receive damage.
Add:
1Interactableand interaction systems can detect it.
Add:
1PlayerControlledand 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:
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:
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:
160 query scans per secondfor 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:
1Query:2Transform + Velocity3
4Matching entities:516377812918The 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:
1export class Query {2 constructor(3 world,4 componentTypes,5 ) {6 this.world = world7 this.componentTypes =8 componentTypes9
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:
1world.addComponent(2 entity,3 new Velocity(),4)After that operation, the world can ask relevant queries:
Does this entity match now?
Conceptually:
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:
1Transform + VelocityA 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:
1Find matches every frameto:
1Maintain matches when composition changesThat 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:
1this.query =2 world.createQuery([3 Transform,4 Velocity,5 ])Then inside update():
1for (2 const entity3 of this.query.entities4) {5 // movement logic6}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:
1export class MovementSystem2 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 entity16 of this.query.entities17 ) {18 // movement19 }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:
1PlayerControlled + Transformfor the player.
And:
1Interactable + Transformfor objects the player can interact with.
Conceptually:
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:
1Transform + Meshbut optionally inspect:
1HitFlashif present.
The required query remains:
1world.query([2 Transform,3 Mesh,4])Inside the loop:
1const hitFlash =2 world.getComponent(3 entity,4 HitFlash,5 )6
7if (hitFlash) {8 // apply optional behaviour9}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:
1Transform + Velocity2WITHOUT DeadThat might mean:
Move every entity with Transform and Velocity unless it is dead.
Conceptually:
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:
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:
1Transform + Velocitythat 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:
1DeathSystem2adds DeadA 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:
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:
1During systems:2queue component changes3
4↓5
6End of frame:7apply component changes8update queriesDeferred 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:
1for (2 const entity3 of this.query.entities4) {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:
1Query:2Health + Transformfinds entities currently possessing those components.
An event:
1EVT_DAMAGE_RESOLVEDdescribes 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:
1StatusEffects + HealthWe 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:
1Health2StatusEffectsthe 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:
1MovementSystem2Transform + Velocity1PlayerControllerSystem2PlayerControlled + Input + Velocity1GravitySystem2Velocity + Grounded1StatsSystem2BaseStats + FinalStats + Equipment1TargetingSystem2Transform + Targetable1FloatingHealthBarSystem2Transform + Health + HealthBarVisibility1InteractionSystem2Transform + InteractableEach 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:
1Transform + Velocitythen don't make it query:
1Transform2Velocity3Health4Inventory5Equipment6PlayerControlledThat 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:
1if (entity === player) {2 // ...3}or:
1if (2 entity.type === 'enemy'3) {4 // ...5}or:
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:
1Transform + Velocityevery frame has a very different access pattern from a system handling:
1Inventory + Equipmentonly 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:
1Simple queries2↓3Build real gameplay4↓5Measure performance6↓7Identify bottlenecks8↓9Optimize the hot pathsA 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:
1Archetype A2Transform + Velocity3
4Entities:51677121Archetype B2Transform + Velocity + Health3
4Entities:5368715A query for:
1Transform + Velocitycan 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:
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:
1Entity 12Entity 23Entity 34Entity 45Entity 56...Each entity has a component composition.
A system declares:
1I require:2
3Transform4VelocityThe query filters the world:
1World2 ↓3Transform?4 ↓5Velocity?6 ↓7Matching entities8 ↓9MovementSystemThat is the relationship between queries and systems.
The Bigger ECS Picture
Queries fit into the larger architecture like this:
1ENTITY2Identity3
4↓5
6COMPONENTS7State8
9↓10
11QUERY12Find matching entities13
14↓15
16SYSTEM17Apply behaviour18
19↓20
21EVENTS22Communicate meaningful outcomesFor example:
1Entity 72
3Transform4Velocity5Health6
7↓8
9Movement Query10Transform + Velocity11
12↓13
14MovementSystem15
16↓17
18Transform changesAt the same time:
1Entity 72
3Health4
5↓6
7DamageSystem8
9↓10
11EVT_DAMAGE_RESOLVEDQueries 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:
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:
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.

Build a simple Entity Component System in JavaScript from scratch with entities, data-only components, queries, systems, and a working update loop.

Learn how to decide whether game logic belongs in an Entity, Component, or System when building an Entity Component System in JavaScript.

Learn why ECS system order matters and how to structure an update pipeline for input, movement, physics, combat, UI, cameras, and rendering.