|
1 | 1 | # Search Performance Optimization |
2 | 2 |
|
3 | | -Laravel Restify includes an optional search performance optimization that replaces inefficient subqueries with JOIN-based searches for related fields. |
| 3 | +Laravel Restify includes an optional JOIN-based search optimization for better performance when searching through BelongsTo relationship fields. |
4 | 4 |
|
5 | 5 | ## Overview |
6 | 6 |
|
7 | | -By default, when searching through BelongsTo relationship fields, Laravel Restify uses subqueries which can be slow for large datasets: |
8 | | - |
| 7 | +**Default behavior (subqueries):** |
9 | 8 | ```sql |
10 | | --- Default behavior (subqueries) |
11 | 9 | SELECT * FROM `invoices` WHERE ( |
12 | 10 | UPPER(`invoices`.`gross_amount`) LIKE '%CSC%' |
13 | 11 | OR (SELECT `vendors`.`code` FROM `vendors` WHERE `vendors`.`id` = `invoices`.`vendor_id` LIMIT 1) LIKE '%csc%' |
14 | | - OR (SELECT `vendors`.`name` FROM `vendors` WHERE `vendors`.`id` = `invoices`.`vendor_id` LIMIT 1) LIKE '%csc%' |
15 | 12 | ) |
16 | 13 | ``` |
17 | 14 |
|
18 | | -With JOIN optimization enabled, these become efficient JOIN-based queries: |
19 | | - |
| 15 | +**Optimized behavior (JOINs):** |
20 | 16 | ```sql |
21 | | --- Optimized behavior (JOINs) |
22 | 17 | SELECT invoices.* FROM `invoices` |
23 | 18 | LEFT JOIN `vendors` AS vendors_for_vendor ON invoices.vendor_id = vendors_for_vendor.id |
24 | 19 | WHERE ( |
25 | 20 | UPPER(invoices.gross_amount) LIKE '%CSC%' |
26 | 21 | OR UPPER(vendors_for_vendor.code) LIKE '%CSC%' |
27 | | - OR UPPER(vendors_for_vendor.name) LIKE '%CSC%' |
28 | 22 | ) |
29 | 23 | ``` |
30 | 24 |
|
31 | 25 | ## Configuration |
32 | 26 |
|
33 | | -### Enabling JOIN Optimization |
34 | | - |
35 | | -Add the following to your `.env` file: |
36 | | - |
| 27 | +Enable in your `.env` file: |
37 | 28 | ```env |
38 | 29 | RESTIFY_SEARCH_USE_JOINS=true |
39 | 30 | ``` |
40 | 31 |
|
41 | | -Or configure it directly in `config/restify.php`: |
42 | | - |
| 32 | +Or in `config/restify.php`: |
43 | 33 | ```php |
44 | 34 | 'search' => [ |
45 | | - 'case_sensitive' => true, |
46 | | - 'use_joins' => true, // Enable JOIN optimization |
| 35 | + 'use_joins' => true, |
47 | 36 | ], |
48 | 37 | ``` |
49 | 38 |
|
50 | | -### Default Behavior |
51 | | - |
52 | | -**By default, JOIN optimization is disabled** to maintain backward compatibility. The system will continue using subqueries until explicitly enabled. |
| 39 | +**Default: `false`** (backward compatible) |
53 | 40 |
|
54 | 41 | ## When JOINs Are Used |
55 | 42 |
|
56 | | -JOIN optimization is **only applied when**: |
| 43 | +✅ **Applied when:** |
| 44 | +- Config `restify.search.use_joins` is `true` |
| 45 | +- Searching BelongsTo relationship fields marked as searchable |
57 | 46 |
|
58 | | -1. ✅ The `restify.search.use_joins` config is set to `true` |
59 | | -2. ✅ You're searching through a **BelongsTo relationship** field |
60 | | -3. ✅ The relationship field is marked as **searchable** |
| 47 | +❌ **NOT applied for:** |
| 48 | +- Direct field searches on main model |
| 49 | +- HasMany or other relationship types |
| 50 | +- When config is disabled (default) |
61 | 51 |
|
62 | | -JOIN optimization is **NOT applied for**: |
| 52 | +## Usage |
63 | 53 |
|
64 | | -- ❌ Direct field searches on the main model |
65 | | -- ❌ HasMany or other relationship types |
66 | | -- ❌ When the config flag is disabled (default) |
67 | | - |
68 | | -## Repository Configuration |
69 | | - |
70 | | -No changes are needed to your existing repository configuration. The optimization works with your current setup: |
| 54 | +No repository changes needed: |
71 | 55 |
|
72 | 56 | ```php |
73 | 57 | class InvoiceRepository extends Repository |
74 | 58 | { |
75 | 59 | public static function searchables(): array |
76 | 60 | { |
77 | | - return [ |
78 | | - 'gross_amount', // Direct field - no JOIN needed |
79 | | - 'number', // Direct field - no JOIN needed |
80 | | - ]; |
| 61 | + return ['gross_amount', 'number']; // Direct fields - no JOINs |
81 | 62 | } |
82 | 63 |
|
83 | 64 | public static function related(): array |
84 | 65 | { |
85 | 66 | return [ |
86 | 67 | 'vendor' => BelongsTo::make('vendor', VendorRepository::class)->searchable([ |
87 | | - 'vendors.code', // Will use JOIN when optimization enabled |
88 | | - 'vendors.name', // Will use JOIN when optimization enabled |
| 68 | + 'vendors.code', // Will use JOIN when enabled |
| 69 | + 'vendors.name', // Will use JOIN when enabled |
89 | 70 | ]), |
90 | 71 | ]; |
91 | 72 | } |
92 | 73 | } |
93 | 74 | ``` |
94 | 75 |
|
95 | | -## Performance Benefits |
96 | | - |
97 | | -### Query Efficiency |
98 | | -- **Eliminates N+1 subquery problems** - Each related field search no longer creates separate subqueries |
99 | | -- **Improved query performance** - JOINs are typically much faster than correlated subqueries |
100 | | -- **Reduced database load** - Single query instead of multiple subqueries per search term |
101 | | - |
102 | | -### Eager Loading Optimization |
103 | | -When JOIN optimization is enabled, the system automatically: |
104 | | -- **Detects already-joined relationships** |
105 | | -- **Excludes them from eager loading** to prevent duplicate queries |
106 | | -- **Maintains data integrity** while improving performance |
107 | | - |
108 | | -### Example Performance Impact |
109 | | - |
110 | | -**Before Optimization (3 separate subqueries)**: |
111 | | -```sql |
112 | | --- Main query + 2 subqueries for each search term |
113 | | -SELECT * FROM invoices WHERE ( |
114 | | - gross_amount LIKE '%term%' OR |
115 | | - (SELECT code FROM vendors WHERE...) LIKE '%term%' OR |
116 | | - (SELECT name FROM vendors WHERE...) LIKE '%term%' |
117 | | -) |
118 | | -``` |
119 | | - |
120 | | -**After Optimization (1 JOIN query)**: |
121 | | -```sql |
122 | | --- Single query with JOIN |
123 | | -SELECT invoices.* FROM invoices |
124 | | -LEFT JOIN vendors AS vendors_for_vendor ON invoices.vendor_id = vendors_for_vendor.id |
125 | | -WHERE ( |
126 | | - gross_amount LIKE '%term%' OR |
127 | | - vendors_for_vendor.code LIKE '%term%' OR |
128 | | - vendors_for_vendor.name LIKE '%term%' |
129 | | -) |
130 | | -``` |
| 76 | +## Benefits |
131 | 77 |
|
132 | | -## Backward Compatibility |
| 78 | +- **Better performance** - JOINs instead of subqueries |
| 79 | +- **Fewer queries** - Eliminates N+1 subquery problems |
| 80 | +- **Automatic eager loading optimization** - Skips already-joined relationships |
133 | 81 |
|
134 | | -This feature is **100% backward compatible**: |
135 | | - |
136 | | -- **Default behavior unchanged** - Existing applications continue working without any changes |
137 | | -- **Opt-in optimization** - You must explicitly enable JOIN optimization |
138 | | -- **No breaking changes** - All existing repository configurations work as before |
139 | | -- **Gradual adoption** - Enable per environment (dev/staging first, then production) |
140 | | - |
141 | | -## Best Practices |
142 | | - |
143 | | -### When to Enable |
144 | | -- ✅ **Large datasets** with frequent relationship searches |
145 | | -- ✅ **Performance-critical applications** where search speed matters |
146 | | -- ✅ **Well-tested environments** where you can validate the behavior |
147 | | - |
148 | | -### When to Keep Disabled |
149 | | -- ❌ **Small datasets** where performance difference is negligible |
150 | | -- ❌ **Legacy systems** where you want to maintain exact query behavior |
151 | | -- ❌ **Complex relationship setups** that need specific subquery behavior |
152 | | - |
153 | | -### Testing Strategy |
154 | | -1. **Enable in development/staging first** |
155 | | -2. **Run your existing test suite** to ensure no regressions |
156 | | -3. **Monitor query performance** before and after |
157 | | -4. **Gradually roll out to production** |
158 | | - |
159 | | -## Troubleshooting |
160 | | - |
161 | | -### Common Issues |
162 | | - |
163 | | -**Q: JOINs aren't being used even though the config is enabled** |
164 | | -A: Verify that: |
165 | | -- The searchable fields are on BelongsTo relationships (not direct model fields) |
166 | | -- The relationship is properly configured with `->searchable([...])` |
167 | | -- Cache has been cleared after config changes |
168 | | - |
169 | | -**Q: Getting different search results after enabling JOINs** |
170 | | -A: This could indicate: |
171 | | -- Multi-tenant constraints that were handled differently in subqueries |
172 | | -- Complex relationship setups that need adjustment |
173 | | -- Consider disabling the optimization for that specific use case |
174 | | - |
175 | | -**Q: Performance didn't improve as expected** |
176 | | -A: Check that: |
177 | | -- You have proper database indexes on JOIN columns |
178 | | -- The dataset is large enough to see meaningful performance differences |
179 | | -- Other query bottlenecks aren't masking the improvement |
180 | | - |
181 | | -### Debugging |
182 | | - |
183 | | -Enable query logging to see the generated SQL: |
| 82 | +## Testing |
184 | 83 |
|
| 84 | +Test in development first: |
185 | 85 | ```php |
| 86 | +// Enable query logging to verify behavior |
186 | 87 | DB::enableQueryLog(); |
187 | | -// Perform your search |
| 88 | +// Perform search |
188 | 89 | $queries = DB::getQueryLog(); |
189 | | -dd($queries); |
190 | | -``` |
191 | | - |
192 | | -Look for `LEFT JOIN` statements in the search queries when optimization is enabled. |
193 | | - |
194 | | -## Migration Guide |
195 | | - |
196 | | -### Step 1: Test in Development |
197 | | -```env |
198 | | -# In .env |
199 | | -RESTIFY_SEARCH_USE_JOINS=true |
200 | 90 | ``` |
201 | 91 |
|
202 | | -### Step 2: Validate Query Behavior |
203 | | -Run your test suite and verify search results remain consistent. |
204 | | - |
205 | | -### Step 3: Performance Testing |
206 | | -Measure query performance before and after enabling the optimization. |
207 | | - |
208 | | -### Step 4: Production Rollout |
209 | | -Enable in production after thorough testing in staging environments. |
210 | | - |
211 | | -### Step 5: Monitor |
212 | | -Watch for any performance regressions or unexpected query behavior. |
| 92 | +Look for `LEFT JOIN` statements when optimization is enabled. |
0 commit comments