Skip to main content

Title: “How I Optimized My Mongoose Schema and Aggregation for Better Performance”


When working with databases, it’s easy to overlook the impact of schema design on query performance. I learned this lesson the hard way. Here’s my story of how I optimized my Mongoose schema and aggregation logic to handle thousands of records efficiently.


The Problem: Slow Queries and Lookup Overhead

In my application, I needed to fetch CollectionOrder documents, which are linked to several other collections: RetailStore, DeliveryMan, and CollectionCompany. Each CollectionOrder had fields referencing these collections, which required multiple $lookup stages to fetch related data.

Initially, my schema design looked like this:

@Prop({
required: true,
type: mongoose.Schema.Types.ObjectId,
ref: 'RetailStore',
})
retailStore: mongoose.Schema.Types.ObjectId;

@Prop({
required: true,
type: mongoose.Schema.Types.ObjectId,
ref: 'DeliveryMan',
})
deliveryMan: mongoose.Schema.Types.ObjectId;

This design seemed fine at first, but as my database grew, the queries became unbearably slow. Why? Because each $lookup operation introduced significant overhead, especially with over 10,000 RetailStores and CollectionCompanies.


The Realization: Embedding Subschemas

It was clear that relying on $lookup for every query wasn’t scalable. I needed a more efficient approach, one that would eliminate the need for additional lookups. That’s when I decided to embed the related data directly into the CollectionOrder schema as subschemas.

Here’s how the updated schema looks:

@Prop({ type: RetailStoreSchema })
retailStore: RetailStore;

@Prop({ type: DeliveryManSchema })
deliveryMan: DeliveryMan;
@Prop({ type: CollectionCompanySchema })
collectionCompany?: CollectionCompany;

Instead of storing just the IDs, I now store the entire RetailStore, DeliveryMan, and CollectionCompany objects. This means the data is denormalized, but the performance gains make it worthwhile.


The Benefits: Faster Queries, Simpler Aggregations

With the updated schema, I no longer need $lookup stages in my aggregations. When fetching CollectionOrder, all the related data is already embedded, making the queries significantly faster.

Here’s an example query before and after the change:

Before:

[
{ "$lookup": { "from": "retailstores", "localField": "retailStore", "foreignField": "_id", "as": "retailStore" } },
{ "$unwind": "$retailStore" }
]

After:

[
{ "$match": { "status": "active" } }
]

By embedding the data, the query complexity reduced dramatically. This change alone shaved seconds off my query execution time.


Challenges: Keeping Data Up-to-Date

Of course, this approach isn’t without its challenges. One major drawback of embedding data is keeping it up-to-date. For instance, if the name of a RetailStore changes, I need to ensure that all related CollectionOrder documents are updated.

To handle this, I implemented update hooks in my RetailStore and DeliveryMan services. These hooks propagate changes to all embedded documents whenever a related field is updated.


My Takeaway

This experience taught me the importance of designing schemas with performance in mind. While normalization is often the default choice, it’s not always the best solution — especially for queries that require joining large collections.

Switching to embedded schemas transformed how my application handles data. Queries that used to take several seconds now return results in milliseconds. This optimization not only improved performance but also simplified my codebase.


Final Thoughts

If you’re facing similar challenges with slow queries in Mongoose, consider revisiting your schema design. While embedding data isn’t a one-size-fits-all solution, it can be a game-changer in scenarios like mine. And remember, always benchmark your changes to measure the impact.

Happy coding!

    Popular posts from this blog

    Xcode and iOS Version Mismatch: Troubleshooting "Incompatible Build Number" Errors

    Have you ever encountered a frustrating error while trying to run your iOS app in Xcode, leaving you scratching your head? A common issue arises when your device's iOS version is too new for the Xcode version you're using. This often manifests as an "incompatible build number" error, and looks like this: DVTDeviceOperation: Encountered a build number "" that is incompatible with DVTBuildVersion. This usually happens when you are testing with beta versions of either iOS or Xcode, and can prevent Xcode from properly compiling your storyboards. Let's explore why this occurs and what you can do to resolve it. Why This Error Occurs The core problem lies in the mismatch between the iOS version on your test device and the Software Development Kit (SDK) supported by your Xcode installation. Xcode uses the SDK to understand how to build and run apps for specific iOS versions. When your device runs a newer iOS version than Xcode anticipates, Xcode mi...

    How to Fix the “Invariant Violation: TurboModuleRegistry.getEnforcing(…): ‘RNCWebView’ Could Not Be Found” Error in React Native

    When working with React Native, especially when integrating additional libraries like react-native-signature-canvas , encountering errors can be frustrating. One such error is: Invariant Violation: TurboModuleRegistry. getEnforcing (...): 'RNCWebView' could not be found This error often occurs when the necessary dependencies for a module are not properly linked or when the environment you’re using doesn’t support the required native modules. Here’s a breakdown of how I encountered and resolved this issue. The Problem I was working on a React Native project where I needed to add the react-native-signature-canvas library to capture user signatures. The installation process seemed straightforward: Installed the package: npm install react-native-signature- canvas 2. Since react-native-signature-canvas depends on react-native-webview , I also installed the WebView package: npm install react- native -webview 3. I navigated to the iOS directory and ran: cd ios pod install Everythi...

    Fixing FirebaseMessagingError: Requested entity was not found.

    If you’re working with Firebase Cloud Messaging (FCM) and encounter the error: FirebaseMessagingError: Requested entity was not found. with the error code: messaging/registration-token-not-registered this means that the FCM registration token is invalid, expired, or unregistered . This issue can prevent push notifications from being delivered to users. ๐Ÿ” Possible Causes & Solutions 1️⃣ Invalid or Expired FCM Token FCM tokens are not permanent and may expire over time. If you’re storing tokens in your database, some might be outdated. ✅ Solution: Remove invalid tokens from your database when sending push notifications. Refresh and store the latest FCM token when the app starts. Example: Automatically Refresh Token firebase. messaging (). onTokenRefresh ( ( newToken ) => { // Send newToken to your backend and update the stored token }); 2️⃣ Token Unregistered on Client Device A token might become unregistered if: The app is uninstalled on the user’s device. ...