Skip to content

Commit 0e9482a

Browse files
more documentation
1 parent 52722ff commit 0e9482a

File tree

3 files changed

+667
-1
lines changed

3 files changed

+667
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,393 @@
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

Comments
 (0)