DOTS//CORE local · not synced

m01 · Entities Core · reading · 8 min

Queries — how iteration finds its chunks

A query is a description, resolved against archetypes

A query names a set of component requirements — must have these, must not have those, optionally has these:

// must have LocalTransform and Velocity, must not have Frozen
var query = SystemAPI.QueryBuilder()
    .WithAll<LocalTransform, Velocity>()
    .WithNone<Frozen>()
    .Build();

The critical thing about how this executes: the query is not a scan over every entity checking whether it matches. It is resolved against the set of archetypes. An archetype either satisfies the requirements or it doesn’t — that’s a property of its component set, decided once — and the runtime caches which archetypes match. Iterating the query then means visiting the chunks of exactly those matching archetypes, in order, and nothing else. Entities that don’t match are never examined because their archetypes were excluded up front.

This is why WithAll/WithNone/WithAny are cheap regardless of how many entities exist: the matching decision is per-archetype (there are usually tens to low hundreds of archetypes) not per-entity (there may be millions). The tag lesson’s claim — “filtering by tag is a chunk-level selection” — is just this mechanism: WithAll<Enemy>() narrows the set of matching archetypes to those containing Enemy, and you iterate only their chunks.

Iterating a chunk is the fast path made real

Once you’re inside a matching chunk, you get contiguous SoA arrays for each requested component and you walk them in lockstep. This is the exact access pattern the memory wall demanded: one component’s array is a dense linear stream, the prefetcher’s bargain is intact, and the loop body sees each entity’s LocalTransform and Velocity already resident by the time it needs them.

cache line · 64 B fetched 64 B useful · 100% of the bandwidth you paid for inside a matched chunk: LocalTransform advances as a dense stream, Velocity as another — the query delivers the memory wall's ideal pattern automatically

Everything upstream — archetypes, chunks, the 16 KB size, SoA — exists so that this innermost loop is cache-optimal without the author doing anything special. You wrote a component-set requirement; you got a prefetcher-friendly linear walk. That’s the whole architecture paying off in one place.

Change filtering: skipping chunks you don’t need to read

Every chunk stamps a change version on each component type whenever that component is written through a system. A query can filter on it:

query.SetChangedVersionFilter(typeof(LocalTransform));

Now iteration visits only the chunks whose LocalTransform was written since this system last ran. The comparison is a single integer check per chunk, made against the chunk header — no entity data is touched to decide whether to skip. A system that reacts to movement can ignore thousands of stationary entities by skipping their chunks wholesale, paying only one version comparison per skipped chunk instead of reading any of their transforms.

The granularity is the thing to understand: change versions are per-chunk, per-type, not per-entity. Writing one entity’s LocalTransform bumps the version for the whole chunk, so change filtering says “something in this chunk changed,” not “these specific entities changed.” It’s a coarse filter that’s nearly free — exactly the right tradeoff for skipping large idle populations, and the wrong tool if you need per-entity change precision.

Order is the runtime’s, and that’s the point

You do not control the order in which a query yields entities. It’s archetype by archetype, chunk by chunk, in whatever arrangement the runtime holds them — and that arrangement can shift as entities are created, destroyed, or moved between archetypes by structural changes. Code that assumes a stable iteration order, or that entity A is processed before entity B, is relying on something the system never promised.

This is not a limitation to work around; it’s a precondition for the performance. Because you don’t demand a particular order, the runtime is free to store entities in the layout that’s fastest to iterate, and to hand independent chunks to independent worker threads without reconciling a global order. Order-independence is what makes the parallelism in the Jobs module legal. If you need entities processed in a specific sequence, that ordering has to be expressed as data — a sort key you read — not assumed from iteration.

What this buys you

You can now trace a query from a component-set description down to a cache-optimal loop over exactly the right chunks, and you know the two levers — component requirements and change versions — that decide which chunks get visited at all. That completes the storage-and-iteration picture: archetypes group by set, chunks hold 16 KB of SoA data, tags and shared components shape the partitioning, and queries turn all of it into a linear walk. The lab makes you generate the archetypes yourself and watch the chunk counts move as you change component representations.

m01.l05