Hello! Welcome to the final lesson in our "Database Performance & Optimization" module.
In our last session, you learned how to process massive datasets without running out of memory using chunkById and cursor. This was a crucial application-level optimization technique. Today, we'll tackle the final, and often most impactful, optimization strategy: preventing the database query from running in the first place.
This lesson focuses on query caching, a powerful technique to reduce database load and dramatically speed up your application by storing and reusing the results of frequently executed queries. By the end of this lesson, you will be able to implement query caching to significantly improve your application's performance.
What is Caching?
At its core, caching is the process of storing the result of an expensive operation and reusing that result for subsequent requests. Instead of hitting your MSSQL database every time a user loads the homepage to fetch the list of blog posts, you can fetch it once, store it in a fast, temporary storage, and then serve it from there for the next hour.
This significantly reduces the load on your database and makes your application feel much faster to the end-user.
Let's begin by understanding why caching is so important in modern web applications and get an overview of the tools Laravel provides.
Everything You Need to Know About Laravel Caching
The following video from Kinsta provides an excellent introduction to why caching is essential and the different caching 'backends' or 'drivers' that Laravel supports, such as file, database, and high-performance in-memory stores like Redis.
Watch the video from the beginning until the 4:55 mark. Pay attention to the different types of cache drivers and their use cases. For high-performance applications, drivers like Redis or Memcached are standard.
As you saw, Laravel provides a unified API through the Cache facade, allowing you to switch between drivers like file, database, or redis just by changing a line in your .env file. For our purposes, we'll focus on the caching logic itself, which remains the same regardless of the driver you choose.
The Core Technique: Cache::remember()
Laravel makes implementing query caching incredibly simple with the Cache::remember() method. This method is the workhorse of query caching.
The remember method accepts three arguments:
- A unique key: A string that identifies the cached item (e.g.,
'homepage:posts'). - A duration (TTL - Time To Live): How long the item should be stored in the cache, typically in seconds or as a
DateTimeinstance. - A closure: The code that will be executed to get the data if it's not found in the cache. The return value of this closure is what gets cached.
Here's how it works:
- Laravel checks the cache for the given key.
- If the item exists and hasn't expired, it returns the cached data immediately.
- If the item does not exist or has expired, it executes the closure, stores the result in the cache with the given key and duration, and then returns the result.
Let's see this powerful method in action.
Let's Talk About Caching Dos and Don'ts
The official Laravel channel has a great video that explains the 'dos and don'ts' of caching and provides a very clear demonstration of the Cache::remember() method.
Watch the segment from 2:32 to 5:13. It clearly demonstrates how Cache::remember() simplifies the logic of checking, retrieving, and storing cached data.
Practical Example: Caching a Query
Let's apply this to a real-world scenario. Imagine you have a query to get all featured products for your e-commerce homepage.
Before Caching:
This query runs every single time a user visits the homepage.
// In your ProductController
public function home() {
$featuredProducts = Product::where('featured', true)
->with('categories')
->take(10)
->get();
return view('home', ['products' => $featuredProducts]);
}
After Caching:
This query runs only once every 10 minutes (600 seconds). Subsequent visits within that window get the data almost instantly from the cache.
use Illuminate\Support\Facades\Cache;
// In your ProductController
public function home() {
$featuredProducts = Cache::remember('home.featured_products', 600, function () {
return Product::where('featured', true)
->with('categories')
->take(10)
->get();
});
return view('home', ['products' => $featuredProducts]);
}
This single change can have a massive impact on performance, especially for pages with high traffic. For a deeper look at this pattern, the following article provides another clear example.
Advanced Laravel Caching Techniques with Redis | by Zulfikar Ditya
The article 'Advanced Laravel Caching Techniques with Redis' has a section that directly addresses caching database queries with excellent examples.
Read the section titled 'Using Database Caching with Redis for Frequently Accessed Queries'. Focus on the 'Basic query caching' example, which reinforces the Cache::remember pattern we just discussed.
The Hardest Problem: Cache Invalidation
Caching is powerful, but it introduces a new challenge: what happens when the underlying data changes? If you add a new featured product, your cache is now "stale" and needs to be updated or removed. This is famously known as one of the hard problems in computer science.
If you don't have a good invalidation strategy, your users could see outdated information.
Let's Talk About Caching Dos and Don'ts
Let's revisit the Laravel video, which discusses the importance of being intentional about when to 'break' or 'forget' your cache.
Watch the clip from 10:52 to 12:21. This part emphasizes the need to actively manage your cache when data changes.
Manual Invalidation with Cache::forget()
The most straightforward way to invalidate a cache is to manually remove it using Cache::forget('your-key'). You would typically do this after any action that makes the cached data obsolete.
For example, when an admin adds or updates a product, you should clear the home.featured_products cache.
// In your ProductController's update method
public function update(Request $request, Product $product) {
// ... validation and update logic ...
$product->update($request->all());
// Invalidate the cache because the data has changed
Cache::forget('home.featured_products');
return redirect()->route('products.index');
}
This is simple and effective for basic cases. However, what if that product data is used in multiple cached items (e.g., homepage, category page, "top products" widget)? Forgetting each key manually becomes cumbersome and error-prone. This is where cache tags come in.
Grouped Invalidation with Cache Tags
Cache tags are one of Laravel's most powerful caching features. They allow you to "tag" related cache items and then flush all items with a specific tag at once.
Note: Cache tags are not supported by the file or database cache drivers. You need to use a driver like redis or memcached to use them.
Here's how you can refactor the previous example using tags:
-
Store with tags:
$featuredProducts = Cache::tags(['products'])->remember('home.featured_products', 600, function () { return Product::where('featured', true)->get(); }); $allProducts = Cache::tags(['products'])->remember('products.all.page.1', 600, function () { return Product::paginate(15); }); -
Invalidate by tag:
Now, in yourupdatemethod, you only need to flush theproductstag, and Laravel will remove both cached items.public function update(Request $request, Product $product) { // ... update logic ... // Invalidate all caches tagged with 'products' Cache::tags(['products'])->flush(); return redirect()->route('products.index'); }
This makes your invalidation logic much cleaner and more robust.
Advanced Laravel Caching Techniques with Redis | by Zulfikar Ditya
The 'Advanced Laravel Caching' article provides a fantastic walkthrough of implementing tag-based caching and even shows how to automate invalidation using model observers for a truly clean architecture.
Read the part of the article under the heading 'Implementing Tag-based Caching'. This will show you the syntax for using tags and an excellent pattern for triggering invalidation automatically when a model is updated or deleted.
Caching Best Practices
Now that you know the mechanics, let's cover some essential rules of thumb.
- What to Cache (and What Not to): Caching isn't a silver bullet. Applying it incorrectly can cause more harm than good.
The following resource provides a very clear, step-by-step guide on caching patterns. We'll use it to define some hard rules.
Read 'Step 1 — Confirm This Is a Query'. It gives a concise list of what is good to cache (read-heavy queries, heavy computations) and what you should never cache (commands, transactional writes).
- Cache Key Design: A good key is descriptive and unique. If a query depends on parameters (like a page number or category ID), include them in the key.
- Bad key:
'products'(if you have pagination) - Good key:
'products:page:' . request('page', 1)
- Bad key:
Let's refer back to the pattern guide for rules on designing cache keys.
Read 'Step 5 — Cache Key Design Rules' for a clear format and important guidelines, like never using raw request input directly in a key.
- Choose a Sensible TTL: How long should data live in the cache? This is a trade-off between performance and data freshness.
- Frequently changing data: Short TTL (1-10 minutes).
- Mostly static data: Long TTL (several hours or a day).
- Configuration/settings: Can be cached "forever" with
Cache::rememberForeverand invalidated only when changed.
Conclusion
You've now learned how to complete the performance optimization trifecta: optimizing the database query itself, handling large result sets efficiently in the application, and now, avoiding the query altogether with caching. This layered approach is key to building high-performance, scalable Laravel applications.
Key Takeaways:
- Query caching stores the results of expensive queries in a fast, temporary storage to reduce database load and improve response times.
Cache::remember('key', $ttl, $closure)is the primary tool for implementing query caching in Laravel.- Cache invalidation is critical for ensuring data freshness. You can do this manually with
Cache::forget()or manage groups of related items withCache::tags([...])->flush(). - A good caching strategy involves choosing what to cache, designing unique keys, and setting an appropriate Time To Live (TTL).
Up Next:
This lesson concludes the "Database Performance & Optimization" module. You've journeyed from schema design and advanced MSSQL queries to application-level optimizations.
In the next module, "API Development: Resources & Versioning," we will shift our focus to building professional, robust, and well-structured APIs. You'll learn how to control the "shape" of your JSON data, a skill that pairs perfectly with your newfound mastery of efficient data retrieval.