Writing Efficient Entity Queriesfor Drupal 11 , 10 , 9 , and 8

Last updated :  

There's a piece of advice that circulates in Drupal performance discussions: "just add accessCheck(FALSE) to your entity queries for a speed boost." It's worth addressing directly, because it's misleading — accessCheck() is a security setting, not a performance switch, and treating it as the latter is exactly how permission bugs get introduced. This lesson covers what actually makes an entity query fast, and where accessCheck() genuinely belongs in that picture.

What you'll learn in this lesson

  • Why accessCheck(FALSE) is a security decision, not a performance one — and when it's actually appropriate
  • What genuinely makes an entity query fast: narrow conditions, correct indexes, and a bounded range
  • Why sorting on an unindexed field is often the real hidden cost in a "slow" query
  • How to use range() to avoid pulling back rows you'll never use

accessCheck() is about correctness, not speed

// Runs the query on behalf of the current user, filtering out
// anything they can't view. This is the correct default.
$query = \Drupal::entityQuery('node')
  ->condition('type', 'article')
  ->accessCheck(TRUE);

// Skips permission filtering entirely. Only appropriate in trusted,
// internal contexts — a cron job, an admin report, a batch operation —
// never anywhere a visitor's own permissions should apply.
$query = \Drupal::entityQuery('node')
  ->condition('type', 'article')
  ->accessCheck(FALSE);

Access checking does add a small amount of query overhead (an additional join or subquery to filter by grant), but it's rarely the dominant cost, and disabling it to "speed up" a query that a real visitor's request depends on is a genuine security regression — you'd be showing content to people who shouldn't see it. Reach for accessCheck(FALSE) only when the query is running in a context where per-user permissions were never the point in the first place, like an internal report an administrator already has full access to review.

What actually makes a query fast: narrow conditions and real indexes

$query = \Drupal::entityQuery('node')
  ->condition('type', 'article')
  ->condition('status', 1)
  ->condition('created', strtotime('-30 days'), '>=')
  ->sort('created', 'DESC')
  ->range(0, 20)
  ->accessCheck(TRUE);

Every condition here filters on a column that's part of Drupal's base table schema (type, status, created) — all indexed by default in node_field_data. That's the real lever: conditions on indexed columns let MySQL/MariaDB narrow the row set using the index itself, rather than scanning every row and checking each one. A condition on a non-indexed custom field, by contrast, forces a full scan of that field's storage table no matter how narrow the condition logically is.

Sorting is often the hidden cost, not filtering

->sort('created', 'DESC') is cheap here because created is indexed. Sorting on an unindexed field forces the database to materialize and sort the entire filtered result set in memory (or on disk, for large enough sets) before it can apply your range() — meaning the range doesn't save you the sort cost, only the cost of returning extra rows afterward. If a query feels slow despite tight conditions, check whether the sort column is the actual culprit before assuming the conditions are.

Always pair a broad time-window condition with a range(). ->condition('created', strtotime('-30 days'), '>=') alone could still match thousands of rows on a busy site. Without ->range(0, 20), you're asking the database to sort and return all of them, even though you only render 20. The range is what turns "give me everything matching this, sorted" into "give me only the first page of that."

Quick check: a colleague suggests adding accessCheck(FALSE) to a query that powers a public "recent articles" listing, claiming it'll speed up the page. What's the right response? (Answer: push back — that query runs on behalf of anonymous visitors, and disabling access checking there could surface unpublished or restricted content to people who shouldn't see it. If the query genuinely needs to be faster, the fix is tighter conditions, correct indexes, and a range — not disabling the permission filter.)

Key takeaways

  • accessCheck() controls whether a query respects the current user's permissions — it's a correctness/security setting, not a performance optimization, and disabling it on a user-facing query is a real security risk.
  • Only use accessCheck(FALSE) in trusted, internal contexts (cron, batch, admin-only reports) where per-user permission filtering was never the intent.
  • Real query speed comes from conditions on indexed columns, a bounded range(), and sorting on indexed fields.
  • Sorting on an unindexed field can dominate a query's cost even when every condition is tight — check the sort column, not just the conditions, when diagnosing a slow query.
  • A broad condition without a matching range() still forces the database to sort and return every matching row, not just the page you'll actually display.

Coming up next

Writing one fast query at a time only gets you so far when your module needs to touch hundreds or thousands of entities in a single operation — like a bulk update or a data migration. Next, we look at the Batch API pattern for spreading entity operations across multiple requests without timing out or exhausting memory.