Harnessing Bevy Queries: Parallel, Change‑Aware Data Access in ECS
Bevy’s Query type lets systems declare precise, parallel‑friendly data access. Learn how With/Without/Changed filters work, see a full example that moves entities, and understand the trade‑offs of mutable access and API instability.
09 Jul 2026, 07:52 UTC

Problem: Data‑Driven Game Loops Need Fine‑Grained Access
In a traditional object‑oriented game engine, a single update loop iterates over all game objects, mutating their state. That approach is easy to reason about but limits parallelism and can lead to cache thrashing. Bevy’s Entity‑Component‑System (ECS) promises a different model: systems are pure functions that pull the data they need from a global world. The key to that model is the Query type, which lets a system declare exactly which components it wants to read or write.
Thesis: Queries Give You Parallel, Change‑Aware Data Access
Bevy’s scheduler automatically runs systems in parallel when they don’t share mutable access to the same component types. Queries are the mechanism that expresses that contract. By combining filters such as With, Without, and Changed, you can:
- Limit the entities a system touches to only those that matter.
- Tell the scheduler that a system only cares about entities whose data changed.
- Prevent data races by declaring mutable vs. immutable access.
Understanding the Query API
A query is a type parameter on a system function. In the example below, the system reads the Position component immutably and writes to Velocity mutably:
fn move_system(
mut query: Query<(&mut Position, &Velocity), With<Movable>>,
) {
for (mut pos, vel) in query.iter_mut() {
pos.x += vel.x;
pos.y += vel.y;
}
}
Here With<Movable> is a filter that restricts the query to entities that also have a Movable component. The scheduler sees that Position is written (mutable) and Velocity is read (immutable). If no other system writes to Position, move_system can run concurrently with those systems.
Change Detection with Changed
Bevy tracks modifications to components internally. By adding the Changed<T> filter, a system will only be scheduled for entities whose T component was mutated in the previous frame. This is especially useful for expensive calculations that only need to recompute when the underlying data changes.
fn render_system(
mut query: Query<(&Transform, &Mesh), (Changed<Transform>, With<Renderable>)>,
) {
for (transform, mesh) in query.iter_mut() {
// update GPU buffers only if the transform changed
}
}
Note that Changed only works for components that implement the ChangeDetection marker trait, which is automatically derived for most structs.
Concrete Example: Moving Entities with Velocity
Below is a minimal Bevy app that demonstrates query usage, change detection, and parallel execution. The code is written for Bevy 0.11, but the concepts apply to all pre‑1.0 releases.
use bevy::prelude::*;
#[derive(Component, Clone, Copy, Default)]
struct Position { x: f32, y: f32 }
#[derive(Component, Clone, Copy, Default)]
struct Velocity { x: f32, y: f32 }
#[derive(Component)]
struct Movable;
fn setup(mut commands: Commands) {
commands.spawn((Position { x: 0.0, y: 0.0 }, Velocity { x: 1.0, y: 0.0 }, Movable));
commands.spawn((Position { x: 5.0, y: 5.0 }, Velocity { x: -0.5, y: 0.0 }, Movable));
}
fn move_system(mut query: Query<(&mut Position, &Velocity), With<Movable>>) {
for (mut pos, vel) in query.iter_mut() {
pos.x += vel.x;
pos.y += vel.y;
}
}
fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_startup_system(setup)
.add_system(move_system)
.run();
}
To verify parallelism, you can enable the --profile flag or inspect the scheduler logs: systems that don’t share mutable access to the same component types will be marked as parallel in the log.
Trade‑Offs and Limitations
- Complex Queries Increase Cognitive Load: Using many filters (e.g.,
With,Without,Changed) can make a system’s intent less obvious. Keep queries simple when possible. - Mutable Access Blocks Parallelism: If two systems both write to
Position, they must run serially. Design your system graph to minimize such conflicts. - API Instability: Bevy is still in pre‑1.0. Query syntax and filter names may change between releases. Pin your Cargo dependency or be prepared to refactor.
- Change Detection Overhead: The
Changedfilter adds bookkeeping per component. For very large worlds, this overhead can become noticeable.
Actionable Checklist for Your Next Bevy Project
- Define components as simple, plain structs. Derive
Componentand, if needed,ChangeDetection. - Write systems with explicit query signatures. Use
With/Withoutto narrow the target set. - Mark mutable components only when necessary. Immutable queries enable maximum parallelism.
- Use
Changedfor expensive per‑entity work that only needs to run when data changes. - Run
cargo run --profileor enablebevy::log::tracing::subscriberto inspect the scheduler and confirm parallel execution. - Pin your Bevy dependency (e.g.,
bevy = "0.11.1") to avoid breaking changes when updating other crates.
By mastering queries, you unlock Bevy’s full ECS potential: fine‑grained data access, automatic parallelism, and efficient change detection. Start small, profile often, and let the scheduler do the heavy lifting.
Diagram Labels
| Label |
|---|
| Entity |
| Component |
| System |
| Query |
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.