|
| 1 | +# **Optimizing Queries in Magic IndexedDB** |
| 2 | + |
| 3 | +## **1. Understanding Query Partitioning in IndexedDB** |
| 4 | + |
| 5 | +### **What is Query Partitioning?** |
| 6 | + |
| 7 | +Partitioning is the process of analyzing your LINQ query and **breaking it down** into optimized IndexedDB queries. |
| 8 | + |
| 9 | +Magic IndexedDB **examines your predicates** and **determines** if they can: |
| 10 | + |
| 11 | +1. **Use an Indexed Query** (Best performance) |
| 12 | +2. **Leverage a Compound Index Query** (Optimized for multi-column lookups) |
| 13 | +3. **Fallback to a Cursor Query** (Last resort when indexes can’t be used) |
| 14 | + |
| 15 | +### **How Partitioning Works:** |
| 16 | + |
| 17 | +When you write a LINQ expression, **Magic IndexedDB scans for OR (`||`) operations** and **breaks them into separate queries**. |
| 18 | + |
| 19 | +#### **Example Query:** |
| 20 | + |
| 21 | +```csharp |
| 22 | +var results = await _MagicDb.Query<Person>() |
| 23 | + . Where( x => x. Email == "[email protected]" && x. Age > 30 || x. IsReallyCool) |
| 24 | + .ToListAsync(); |
| 25 | +``` |
| 26 | + |
| 27 | +This gets **partitioned** into: |
| 28 | + |
| 29 | +1. **Query A: ** `x.Email == "[email protected]" && x.Age > 30` |
| 30 | +2. **Query B:** `x.IsReallyCool == true` |
| 31 | + |
| 32 | +Each **AND (`&&`) condition inside an OR group** is analyzed **to determine**: |
| 33 | + |
| 34 | +- Can it be an **Indexed Query**? |
| 35 | +- Can it be a **Compound Index Query**? |
| 36 | +- If not, **it falls back to a Cursor Query.** |
| 37 | + |
| 38 | +--- |
| 39 | + |
| 40 | +## **2. The Three Types of Queries in IndexedDB** |
| 41 | + |
| 42 | +### **✅ Indexed Queries (Best Performance)** |
| 43 | + |
| 44 | +An **indexed query** happens when **all conditions** in an AND (`&&`) group **match a single field index**. |
| 45 | + |
| 46 | +#### **Example:** |
| 47 | + |
| 48 | +```csharp |
| 49 | +await _MagicDb. Query< Person>(). Where( x => x. Email == "[email protected]"). ToListAsync(); |
| 50 | +``` |
| 51 | + |
| 52 | +✅ **Uses an Indexed Query** → Fast lookup using IndexedDB’s native `.where()` method. |
| 53 | + |
| 54 | +> **Tip:** **Simple equality (`==`) operations on indexed fields** perform best. |
| 55 | +
|
| 56 | +--- |
| 57 | + |
| 58 | +### **🔗 Compound Index Queries (Optimized Multi-Column Lookups)** |
| 59 | + |
| 60 | +A **compound index** lets you **query multiple fields efficiently**—but **you must query all fields in the correct order**. |
| 61 | + |
| 62 | +#### **Example:** |
| 63 | + |
| 64 | +```csharp |
| 65 | +await _MagicDb.Query<Person>().Where(x => x.LastName == "Smith" && x.FirstName == "John").ToListAsync(); |
| 66 | +``` |
| 67 | + |
| 68 | +✅ **Uses a Compound Index Query** → If **LastName** and **FirstName** are indexed together. |
| 69 | + |
| 70 | +> **Tip:** To benefit from **compound indexes**, always query **all indexed fields in the correct order**. |
| 71 | +
|
| 72 | +> **⚠️ If you query only one part of a compound index, IndexedDB will not optimize it!** |
| 73 | +
|
| 74 | +--- |
| 75 | + |
| 76 | +### **🚨 Cursor Queries (Last Resort, Worst Performance)** |
| 77 | + |
| 78 | +A **cursor query** is used when a condition **cannot be optimized with an index**. |
| 79 | + |
| 80 | +#### **Example:** |
| 81 | + |
| 82 | +```csharp |
| 83 | +await _MagicDb.Query<Person>().Where(x => x.Name.Contains("John")).ToListAsync(); |
| 84 | +``` |
| 85 | + |
| 86 | +❌ **Requires a Cursor Query** → IndexedDB **does not support** `.Contains()`, so the system **must scan every record manually**. |
| 87 | + |
| 88 | +> **Avoid cursor queries whenever possible!** |
| 89 | +> Use **indexed fields and structured queries** to improve performance. |
| 90 | +
|
| 91 | +--- |
| 92 | + |
| 93 | +## **3. The Optimization Process** |
| 94 | + |
| 95 | +### **Step 1: Partitioning the Query** |
| 96 | + |
| 97 | +1. **Break apart the query by OR (`||`) conditions**. |
| 98 | +2. **Process each AND (`&&`) group separately**. |
| 99 | + |
| 100 | +### **Step 2: Checking for Compound Index Queries** |
| 101 | + |
| 102 | +1. **Check if all AND conditions match a known compound index.** |
| 103 | +2. **If yes, execute a Compound Query.** |
| 104 | +3. **If no, continue to Step 3.** |
| 105 | + |
| 106 | +### **Step 3: Checking for Indexed Queries** |
| 107 | + |
| 108 | +1. **Check if all AND conditions can be optimized as indexed queries.** |
| 109 | +2. **If yes, execute an Indexed Query.** |
| 110 | +3. **If no, continue to Step 4.** |
| 111 | + |
| 112 | +### **Step 4: Fallback to Cursor Query** |
| 113 | + |
| 114 | +1. **If the query cannot be optimized, execute a Cursor Query.** |
| 115 | +2. **A cursor query scans all records manually—this should be avoided.** |
| 116 | + |
| 117 | +--- |
| 118 | + |
| 119 | +## **4. How Query Additions Affect Optimization** |
| 120 | + |
| 121 | +### **Understanding Query Additions** |
| 122 | + |
| 123 | +Query additions **modify your query structure** by introducing **sorting, pagination, or limits**. While some additions **fully utilize IndexedDB’s indexing**, others **require transformations or force a cursor fallback**. |
| 124 | + |
| 125 | +Magic IndexedDB **intelligently optimizes query additions** to: |
| 126 | + |
| 127 | +- **Keep queries indexed whenever possible** |
| 128 | +- **Leverage compound indexes and order optimizations** |
| 129 | +- **Only force a cursor when necessary** |
| 130 | + |
| 131 | +--- |
| 132 | + |
| 133 | +### **🚨 When Does a Query Become a Cursor Query?** |
| 134 | + |
| 135 | +A **query must be executed using a cursor when**: |
| 136 | + |
| 137 | +1. **At least one AND (`&&`) group cannot be expressed as a single indexed or compound index query.** |
| 138 | +2. **A non-indexed field is used in sorting (`OrderBy`) or filtering.** |
| 139 | +3. **A query addition (like `Skip`) is used on an unindexed query.** |
| 140 | +4. **A complex OR (`||`) operation cannot be compressed into fewer indexed queries.** |
| 141 | + |
| 142 | +--- |
| 143 | + |
| 144 | +### **🚀 Query Addition Rules & IndexedDB Optimizations** |
| 145 | + |
| 146 | +|**Addition**|**Effect**| |
| 147 | +|---|---| |
| 148 | +|`.OrderBy(x => x.Age)`|✅ **Optimized if `Age` is indexed**| |
| 149 | +|`.OrderBy(x => x.NonIndexedField)`|❌ **Forces a cursor query** (Ordering requires an index)| |
| 150 | +|`.Skip(10).Take(5)`|✅ **Optimized when query is indexed**| |
| 151 | +|`.Take(10)`|✅ **Optimized if query is indexed**| |
| 152 | +|`.TakeLast(10)`|✅ **Optimized via smart transformation**| |
| 153 | +|`.FirstOrDefaultAsync()`|✅ **Indexed if ordering is indexed**| |
| 154 | +|`.LastOrDefaultAsync()`|✅ **Optimized via reverse query transformation**| |
| 155 | + |
| 156 | +--- |
| 157 | + |
| 158 | +### **💡 How TakeLast() is Indexed** |
| 159 | + |
| 160 | +Normally, IndexedDB **does not natively support `TakeLast()`**, but Magic IndexedDB **transforms the query** to **achieve the same effect** using indexed operations: |
| 161 | + |
| 162 | +1. **Reverses sorting order (`OrderByDescending`)** |
| 163 | +2. **Applies `Take(n)` to retrieve the last `n` elements efficiently** |
| 164 | +3. **Returns results in the correct order after retrieval** |
| 165 | + |
| 166 | +✅ **Optimized Example (Indexed Query)** |
| 167 | + |
| 168 | +```csharp |
| 169 | +await _MagicDb.Query<Person>() |
| 170 | + .OrderBy(x => x.Age) |
| 171 | + .TakeLast(5) |
| 172 | + .ToListAsync(); |
| 173 | +``` |
| 174 | + |
| 175 | +🔹 **Translates into:** |
| 176 | + |
| 177 | +```javascript |
| 178 | +table.where("Age").above(0).reverse().limit(5) |
| 179 | +``` |
| 180 | + |
| 181 | +🚀 **Efficient and fully indexed!** |
| 182 | + |
| 183 | +--- |
| 184 | + |
| 185 | +### **⚠️ When a Query Becomes a Cursor** |
| 186 | + |
| 187 | +Queries only **fall back to a cursor** if **they cannot be fully executed using indexed queries**. |
| 188 | + |
| 189 | +#### **🚨 Cursor Example (Due to Non-Indexable Condition)** |
| 190 | + |
| 191 | +```csharp |
| 192 | +await _MagicDb.Query<Person>() |
| 193 | + . Where( x => x. Email == "[email protected]" || x. Age > 30 || x. Name. Contains( "John")) |
| 194 | + .Take(5) |
| 195 | + .ToListAsync(); |
| 196 | +``` |
| 197 | + |
| 198 | +🚨 **Forces a cursor query** because: |
| 199 | + |
| 200 | +1. `Email == "[email protected]"` → ✅ **Indexed ** |
| 201 | +2. `Age > 30` → ✅ **Indexed** |
| 202 | +3. `Name.Contains("John")` → ❌ **Not indexed (Requires full scan)** |
| 203 | +4. **Since one condition is unindexable, the entire query must be executed as a cursor.** |
| 204 | + |
| 205 | +--- |
| 206 | + |
| 207 | +### **✅ Summary: Optimizing Queries with Additions** |
| 208 | + |
| 209 | +To **keep queries optimized**: |
| 210 | + |
| 211 | +- ✅ **Use indexed fields whenever possible** |
| 212 | +- ✅ **Leverage `.TakeLast()` only when an indexed field is used for ordering** |
| 213 | +- ✅ **Always use `OrderBy()` on an indexed field** |
| 214 | +- 🚨 **Avoid `Contains()` unless absolutely necessary** |
| 215 | + |
| 216 | +> **💡 Remember:** |
| 217 | +> Magic IndexedDB **pushes IndexedDB to the limit** by transforming queries intelligently, ensuring **maximum performance** while preserving **accurate intent**. 🚀 |
| 218 | +
|
| 219 | +--- |
| 220 | + |
| 221 | +## **5. Best Practices for Writing Optimized Queries** |
| 222 | + |
| 223 | +### **✅ Key Takeaways to Maximize Performance** |
| 224 | + |
| 225 | +- **Use indexed fields** as much as possible. |
| 226 | +- **Leverage compound indexes** for multi-field lookups. |
| 227 | +- **Minimize OR (`||`) operations**—they create multiple queries. |
| 228 | +- **Avoid `.Contains()` and `.StartsWith()` (unless case-sensitive is off).** |
| 229 | +- **Never use `.OrderBy()` on a non-indexed field.** |
| 230 | +- **Avoid `TakeLast()` if possible—it always forces a cursor.** |
| 231 | + |
| 232 | +--- |
| 233 | + |
| 234 | +## **6. Deep Dive: The Magic IndexedDB Optimization Layer** |
| 235 | + |
| 236 | +### **How Does Optimization Work?** |
| 237 | + |
| 238 | +Magic IndexedDB includes an **advanced optimization layer** that: |
| 239 | + |
| 240 | +1. **Compresses queries** into fewer IndexedDB requests. |
| 241 | +2. **Rearranges conditions** to maximize index usage. |
| 242 | +3. **Combines multiple queries** into a single efficient query when possible. |
| 243 | + |
| 244 | +--- |
| 245 | + |
| 246 | +### **🔍 How Query Compression Works** |
| 247 | + |
| 248 | +**For each `||` operation, the query will always result in the same or fewer queries.** |
| 249 | + |
| 250 | +✅ **Optimized Example:** |
| 251 | + |
| 252 | +```csharp |
| 253 | +await _MagicDb.Query<Person>() |
| 254 | + .Where(x => x.Age > 30 || x.Age < 20 || x.Age == 25) |
| 255 | + .ToListAsync(); |
| 256 | +``` |
| 257 | + |
| 258 | +**🔹 This will be optimized into a single query:** |
| 259 | + |
| 260 | +```javascript |
| 261 | +table.where("Age").anyOf([25]).or(table.where("Age").above(30)).or(table.where("Age").below(20)) |
| 262 | +``` |
| 263 | + |
| 264 | +This **combines multiple queries** into a **single efficient IndexedDB query**. |
| 265 | + |
| 266 | +--- |
| 267 | + |
| 268 | +## **⚡ Query Compression Techniques Used** |
| 269 | + |
| 270 | +The optimization layer **automatically applies** advanced compression techniques: |
| 271 | + |
| 272 | +|**Optimization**|**How It Works**|**Example Transformation**| |
| 273 | +|---|---|---| |
| 274 | +|**Merges Equality Conditions**|`x.Age == 30|| |
| 275 | +|**Converts Ranges to BETWEEN**|`x.Age > 30 && x.Age < 40` → Uses `between(30, 40)`|`WHERE Age BETWEEN 30 AND 40`| |
| 276 | +|**Combines Queries for Efficiency**|`x.Name == "John"|| |
| 277 | +|**Avoids Redundant Queries**|**Removes unnecessary conditions**|`x.Age > 30| |
| 278 | + |
| 279 | +> **🚀 These optimizations make queries up to 10x faster!** |
| 280 | +
|
| 281 | +--- |
| 282 | + |
| 283 | +## **🎯 How IndexedDB Limitations Affect Optimization** |
| 284 | + |
| 285 | +IndexedDB **is powerful but has some limitations** that impact query optimization. |
| 286 | + |
| 287 | +### **🚀 Things IndexedDB is Good At** |
| 288 | + |
| 289 | +✅ **Fast Indexed Lookups** (e.g., `.where("ID").equals(5)`) |
| 290 | +✅ **Efficient Range Queries** (e.g., `.where("Age").between(20, 30)`) |
| 291 | +✅ **Compound Indexes** (e.g., `.where(["LastName", "FirstName"]).equals(["Smith", "John"])`) |
| 292 | +✅ **Sorting & Pagination (when indexed)** |
| 293 | + |
| 294 | +### **⚠️ IndexedDB Limitations** |
| 295 | + |
| 296 | +❌ **No Native `LIKE` or `Contains()` Queries** |
| 297 | +❌ **No `ORDER BY` on non-indexed fields** |
| 298 | +❌ **No `TakeLast()` or Reverse Pagination** - Well normally... We got you covered though! |
| 299 | +❌ **No Joins or Complex Aggregates** |
| 300 | + |
| 301 | +> **Magic IndexedDB works around these limitations** by: |
| 302 | +> |
| 303 | +> - **Using cursors where needed.** |
| 304 | +> - **Rewriting queries for optimal execution.** |
| 305 | +> - **Applying query compression techniques.** |
| 306 | +
|
| 307 | +--- |
| 308 | + |
| 309 | +# **The Cursor Query: How Magic IndexedDB Handles Non-Indexed Queries** |
| 310 | + |
| 311 | +## **🧐 What is a Cursor?** |
| 312 | + |
| 313 | +A **cursor** in IndexedDB is similar to how SQL processes row-by-row searches when no index is available. It **scans the entire dataset**, checking each record to see if it meets your query conditions. Since IndexedDB does not support **complex filtering** (like case-insensitive searches or `Contains()` on non-indexed fields), a cursor **must be used** to process those queries. |
| 314 | + |
| 315 | +In **Magic IndexedDB**, the cursor **only runs when absolutely necessary**, and when it does, it **does so in the most efficient way possible**. By leveraging **meta-data partitioning** and **batching optimizations**, Magic IndexedDB makes cursor queries **as performant as possible** while maintaining **low memory overhead**. |
| 316 | + |
| 317 | +--- |
| 318 | + |
| 319 | +## **🚀 How the Cursor Works in Magic IndexedDB** |
| 320 | + |
| 321 | +When a query **contains non-indexable operations**, Magic IndexedDB **translates** it into a **single cursor query** that **efficiently** finds and processes the required data. Here's how it works: |
| 322 | + |
| 323 | +### **🔍 Step 1: Partitioning the Query** |
| 324 | + |
| 325 | +- First, your query is **broken into multiple AND (`&&`) and OR (`||`) groups**. |
| 326 | +- Any **AND group that cannot be expressed as an indexed or compound indexed query must go into the cursor**. |
| 327 | +- If **any part of an OR group** contains a non-indexable condition, **the entire OR group must be processed by the cursor**. |
| 328 | + |
| 329 | +### **🧠 Step 2: Collecting Only Meta-Data** |
| 330 | + |
| 331 | +Instead of **loading full database records into memory**, Magic IndexedDB **only collects meta-data** during the cursor scan: ✅ **Primary Keys** (for fetching actual data later) |
| 332 | +✅ **Indexed Fields** (to preserve ordering & filtering intent) |
| 333 | +✅ **Fields involved in sorting or pagination** |
| 334 | + |
| 335 | +🚨 **No full data is loaded yet!** This **minimizes memory usage** and keeps things efficient. |
| 336 | + |
| 337 | +### **📑 Step 3: Processing the Meta-Data** |
| 338 | + |
| 339 | +Once the **cursor has finished scanning**, the collected **meta-data** undergoes **memory-based filtering**: |
| 340 | + |
| 341 | +- **Filters out unnecessary records immediately** |
| 342 | +- **Applies sorting (`OrderBy`, `OrderByDescending`)** |
| 343 | +- **Handles pagination (`Take`, `TakeLast`, `Skip`)** |
| 344 | +- **Only retains the necessary primary keys** |
| 345 | + |
| 346 | +At this stage, **Magic IndexedDB knows exactly what records need to be fetched**. |
| 347 | + |
| 348 | +### **📦 Step 4: Bulk Fetching in Batches** |
| 349 | + |
| 350 | +Once the required **primary keys** have been determined, **Magic IndexedDB sends out bulk queries in batches of 500 records per request**: |
| 351 | + |
| 352 | +- **Avoids overwhelming IndexedDB with massive single queries** |
| 353 | +- **Optimizes `anyOf()` performance when dealing with OR (`||`) conditions** |
| 354 | +- **Efficiently pulls the remaining required data for final processing** |
| 355 | + |
| 356 | +### **⏳ Step 5: Yielding Data Efficiently** |
| 357 | + |
| 358 | +Once the **bulk queries start returning results**, **Magic IndexedDB immediately starts yielding results**: |
| 359 | + |
| 360 | +- ✅ **No need to wait for the full query to finish** |
| 361 | +- ✅ **Each batch is processed and returned in real-time** |
| 362 | +- ✅ **Keeps memory footprint low by never loading unnecessary data** |
| 363 | + |
| 364 | +--- |
| 365 | + |
| 366 | +## **🛠️ Why the Cursor is Powerful in Magic IndexedDB** |
| 367 | + |
| 368 | +Unlike traditional IndexedDB cursors, **Magic IndexedDB transforms how cursors work**: ✔️ **Supports case-insensitive searches (e.g., `StringComparison.OrdinalIgnoreCase`)** |
| 369 | +✔️ **Handles unsupported IndexedDB operations (e.g., `Contains()`)** |
| 370 | +✔️ **Ensures that even cursor-based queries follow LINQ-style ordering & pagination rules** |
| 371 | +✔️ **Optimized for memory efficiency using meta-data filtering** |
| 372 | +✔️ **Smart batching prevents IndexedDB from slowing down under heavy OR queries** |
| 373 | + |
| 374 | +--- |
| 375 | + |
| 376 | +## **💡 Cursor Performance: What You Need to Know** |
| 377 | + |
| 378 | +While **Magic IndexedDB optimizes cursor queries**, **they are still slower than indexed queries**. **Your goal should always be to write queries that take full advantage of indexing** whenever possible. |
| 379 | + |
| 380 | +### **🔹 Best Practices for Faster Queries** |
| 381 | + |
| 382 | +✅ **Use indexed fields whenever possible** |
| 383 | +✅ **Leverage compound indexes for multi-condition queries** |
| 384 | +✅ **Avoid `Contains()` on large datasets unless necessary** |
| 385 | +✅ **Minimize OR (`||`) operations, as each OR condition can trigger separate queries** |
| 386 | + |
| 387 | +> **🚀 Remember:** Magic IndexedDB **gives you maximum flexibility**, but **indexed queries are always faster than cursor queries**. The more you optimize your query structure, the **faster your queries will run**. |
| 388 | +
|
| 389 | +--- |
| 390 | + |
| 391 | +## **💡 Magic IndexedDB is Evolving—Help Make it Even Better!** |
| 392 | + |
| 393 | +Magic IndexedDB **pushes IndexedDB to its absolute limits**, but **there’s always room for improvement**! Want to see **even more optimizations?** Have an idea for **new features**? **Join the project** and help make IndexedDB the **powerful database it should be!** 🚀 |
0 commit comments