Every previous lesson in this section worked around the query Views already builds — caching its output, trimming its templates, tuning its pager. This lesson goes one level deeper: hook_views_query_alter() lets you reach into the query itself, right before it runs, and change it directly from code — the same "intercept before it's used" pattern you met with form alter hooks, applied to SQL instead of a render array.
What you'll learn in this lesson
- What
hook_views_query_alter()receives and when it fires - How to add a condition, a range limit, or a join that Views UI can't express
- The difference between altering a query and altering rendered results
- Why this hook needs a narrow, defensive guard clause
The hook signature
/**
* Implements hook_views_query_alter().
*/
function mymodule_views_query_alter(ViewExecutable $view, QueryPluginBase $query) {
if ($view->id() !== 'recent_items' || $view->current_display !== 'block_1') {
return;
}
// Restrict to nodes updated in the last 30 days, entirely in SQL —
// not filterable this precisely through Views UI alone.
$query->addWhere(
0,
'node_field_data.changed',
\Drupal::time()->getRequestTime() - (30 * 24 * 60 * 60),
'>='
);
}
The hook runs for every view on the site right before its query executes — which is exactly why the guard clause at the top (checking $view->id() and $view->current_display) isn't optional. Skip it, and your alteration silently applies to views it was never meant to touch.
Why this beats filtering the rendered results
A tempting shortcut is to let Views fetch everything and filter the unwanted rows out in a hook_views_pre_render() instead. That's strictly worse for performance: the database still does the work of finding and returning every row, including the ones you're about to throw away, and Views' pager count becomes wrong (it reports the pre-filter total). Altering the query itself means the database only ever computes and returns rows you actually want — the entire reason this is a performance-optimization technique, not just a filtering technique.
Adding a genuinely custom join
function mymodule_views_query_alter(ViewExecutable $view, QueryPluginBase $query) {
if ($view->id() !== 'popular_content') {
return;
}
$query->addTable('node__field_popularity_score');
$query->addOrderBy(
'node__field_popularity_score',
'field_popularity_score_value',
'DESC'
);
}
Some sorting and joining logic — scoring by a computed value, joining against a table Views' UI doesn't expose as a relationship — simply isn't reachable through configuration. This hook is the escape hatch for exactly that case, while still benefiting from everything else Views already handles (pager, caching, access checks) around the query it builds.
hook_views_query_alter() runs after Views has already built its query from the UI configuration, but before it executes. Adding a condition here stacks with (doesn't replace) whatever filters were already configured through Views UI — which is usually what you want, but worth remembering if your alteration seems to have "extra" conditions applied.Quick check: why must the guard clause check both
$view->id()AND$view->current_display, not just the view's ID? If you said because the same view can have multiple displays (a page and a block, say) and your alteration might only be intended for one of them, you've understood exactly why the check needs to be that specific.
Key takeaways
hook_views_query_alter(ViewExecutable $view, QueryPluginBase $query)fires for every view, right before its query executes — always guard with an ID/display check.- Altering the query pushes filtering work into the database, which is strictly faster than filtering already-fetched rows in a later hook.
- Use it for anything Views UI genuinely can't express — custom joins, computed sort orders, precise date-range conditions.
- Your alteration stacks on top of, not instead of, whatever's already configured through Views UI for that display.
- Reach for query alteration only when UI configuration truly can't do it — it's harder for other developers to discover than a Views UI setting.
Coming up next
You've now covered the full Views optimization toolkit — caching, query cost, pagers, templates, and direct query alteration. Next, we widen the lens beyond Views specifically and look at optimizing entity loading and field rendering across the whole site.