Indexes are the first line of defense against slow queries. Most teams invest heavily in index design: covering indexes, filtered indexes, column order optimization. Yet queries still crawl. The problem isn't the index—it's what the query does to the index. At bitlox, we've analyzed hundreds of performance incidents where indexes existed but were effectively useless. This article uncovers the three most common hidden query killers that undermine even the best indexing strategy. If you've ever added an index and seen no improvement, read on.
Why Indexes Fail: The Mechanism Behind Hidden Killers
An index works by organizing data in a B-tree or similar structure, allowing the database to seek directly to relevant rows rather than scanning the entire table. For an index seek to occur, the query predicate must be sargable—Search ARGument ABLE. That means the database can apply the predicate directly to the index key without transformation. Any operation that wraps, converts, or splits the indexed column breaks the seek and often triggers a scan. The three killers we cover all violate sargability in different ways.
What Makes a Predicate Sargable?
A sargable predicate keeps the indexed column alone on one side of a comparison operator, with no functions, calculations, or type conversions. For example, WHERE order_date = '2025-01-15' is sargable; WHERE CAST(order_date AS DATE) = '2025-01-15' is not, if order_date is a datetime column with a time component. The database must strip the time from every row before comparing, forcing a full index or table scan.
The catch is that many developers write non-sargable queries without realizing it, because the syntax is so common. In the following sections, we'll dissect each killer, show how to detect it, and provide rewrites that restore index effectiveness.
Killer #1: Non-Sargable Predicates
Non-sargable predicates are the most widespread hidden killer. They come in three flavors: functions on columns, implicit type conversions, and leading wildcard searches. Each forces the database to evaluate the predicate for every row, negating the index's primary benefit.
Functions on Indexed Columns
Wrapping an indexed column in a function—WHERE YEAR(order_date) = 2025, WHERE UPPER(last_name) = 'SMITH'—makes the index unusable for seeks. The database must compute the function for each row and then filter. The fix is to rewrite the predicate to match the column's raw format: WHERE order_date >= '2025-01-01' AND order_date < '2026-01-01' or use a computed column with a persisted index for case-insensitive searches.
Implicit Conversions
When a column's data type differs from the literal's type, the database may convert the column to match the literal—again, a function on the column. Example: WHERE varchar_col = 123 converts every row's varchar to an integer before comparison. The solution is to use the correct literal type: WHERE varchar_col = '123'. Detecting these requires checking execution plans for CONVERT_IMPLICIT warnings or scanning query texts for mismatched types.
Leading Wildcard Searches
WHERE product_name LIKE '%widget' cannot use an index because the wildcard is at the start. While this is well-known, many teams overlook it in dynamic reporting queries. Consider a reverse index or full-text search if leading wildcards are unavoidable. For most cases, rewriting to avoid the leading wildcard—or using a separate lookup field—is the better path.
To find non-sargable predicates, query your execution plan cache for scans on large tables where you expect seeks. Tools like sys.dm_exec_query_stats in SQL Server or pg_stat_statements in PostgreSQL can help identify high-cost queries. Once found, rewrite the predicate to keep the column bare.
Killer #2: Implicit Conversions at Scale
Implicit conversions deserve their own spotlight because they are insidious and often invisible until load spikes. A single implicit conversion might cost a few extra milliseconds per query, but multiply that by millions of executions and you get a system-wide slowdown. Worse, the conversion may happen on the wrong side of a join, causing a full scan of a large table.
How Implicit Conversions Arise
They occur when the database automatically converts data types to satisfy a comparison. Common culprits: comparing NVARCHAR to VARCHAR, INT to VARCHAR, or DATETIME to DATE. The conversion rules vary by database engine, but the result is the same: the index on the converted column is not used for seeks.
For example, a table with CustomerID INT and a query WHERE CustomerID = '123' (string literal) may cause SQL Server to convert the column CustomerID to VARCHAR for comparison, breaking sargability. The execution plan shows an Index Scan instead of an Index Seek. The fix is trivial: use WHERE CustomerID = 123 (integer literal). Yet many ORMs generate string parameters by default, introducing this killer at scale.
Detecting Implicit Conversions at Scale
Monitor your slow query log for queries that have high logical reads but low actual rows returned. Those are candidates. In SQL Server, look for CONVERT_IMPLICIT in the plan XML. In PostgreSQL, check for Seq Scan on indexed columns with a filter that includes a type cast. Once identified, update the parameter types in your application code or stored procedures.
A common mistake is assuming that the database will handle conversions efficiently. It won't at scale. The conversion cost is per row, and when the table has millions of rows, that cost accumulates into seconds or minutes of extra I/O. Fixing implicit conversions is often the highest-ROI performance change a team can make.
Killer #3: OR Logic That Defeats Index Merging
The third hidden killer is poorly written OR logic that prevents the database from using multiple indexes efficiently. While a single OR predicate can sometimes be optimized via index union, complex OR conditions often force a full scan because the optimizer cannot find a single index that covers all branches.
How OR Breaks Index Usage
Consider WHERE status = 'ACTIVE' OR priority = 'HIGH'. If you have separate indexes on status and priority, the database might use both via an index union (bitmap OR in Oracle, index merge in MySQL). But if the OR condition involves three or more columns, or if the predicates are not simple equalities, the optimizer often gives up and scans. Even when it does use multiple indexes, the union operation itself has overhead.
Better Alternatives to OR
One common fix is to rewrite OR as a UNION ALL of two separate queries, each using its own index. For example:
SELECT * FROM orders WHERE status = 'ACTIVE'
UNION ALL
SELECT * FROM orders WHERE status != 'ACTIVE' AND priority = 'HIGH';
This guarantees an index seek on status for the first branch and an index seek on priority for the second (with a residual check). The UNION ALL adds a small overhead for the combine step but is almost always faster than a full scan.
IN Lists vs. OR
When OR conditions involve the same column, use an IN list: WHERE status IN ('ACTIVE', 'PENDING'). Most databases optimize IN lists as multiple seeks on the same index, which is far more efficient than multiple OR clauses. However, beware of very long IN lists (thousands of values), which may cause the optimizer to switch to a scan anyway.
To detect OR-related scans, look for execution plans that show a single index scan with a heavy predicate filter (the filter percentage is high). The fix often requires restructuring the query logic, sometimes by introducing a computed column or a filtered index that covers the OR condition.
How to Diagnose These Killers in Your Environment
Now that you know the three killers, the next step is to find them in your own database. This section provides a practical diagnostic workflow that works across major database engines.
Step 1: Capture Top Queries by Total Wait Time
Use your database's wait statistics or query store to identify the top queries consuming resources. Focus on queries that have high average duration but low execution count—they are likely scan-heavy. Also look for queries with high logical reads per execution.
Step 2: Examine Execution Plans for Scans
For each candidate query, obtain the actual execution plan. Look for Index Scan (or Table Scan) operators on large tables. If the scan has a small number of output rows, it's a strong indicator of a sargability issue. Check for warnings like 'CONVERT_IMPLICIT' or 'Type Conversion' in the plan.
Step 3: Test Rewrites in a Non-Production Environment
Before applying changes to production, rewrite the query to eliminate the non-sargable predicate, fix the implicit conversion, or restructure the OR logic. Compare the estimated and actual execution plans. Measure the reduction in logical reads and duration. A successful rewrite will change an Index Scan to an Index Seek and reduce reads by at least 90%.
Step 4: Monitor for Regressions
After deploying the fix, monitor the query's performance over a full business cycle. Sometimes a rewrite that works for one data distribution fails for another (e.g., parameter sniffing). Be prepared to use query hints or plan guides if needed, but prefer robust rewrites that are independent of parameter values.
Common Mistakes When Fixing Hidden Killers
Even with the best intentions, teams often make mistakes when addressing these issues. Here are the pitfalls to avoid.
Over-Indexing as a Workaround
When a query has a non-sargable predicate, some teams add more indexes hoping to cover the scan. But no index can fix a function on a column—the scan is inevitable. The only solution is to rewrite the query. Adding indexes only increases maintenance overhead and slows down writes.
Ignoring Parameter Sniffing
Rewriting a query to be sargable may still suffer from parameter sniffing if the query uses variables that cause the optimizer to choose different plans for different values. Use local variables or the OPTIMIZE FOR UNKNOWN hint (SQL Server) to generate a plan that works for typical values.
Applying Fixes Without Testing
A rewrite that looks correct might introduce a different performance problem, such as a missing join condition or an unintended Cartesian product. Always test with realistic data volumes and concurrent load before deploying to production.
Neglecting Application-Level Changes
Sometimes the query is generated by an ORM or reporting tool. Fixing the query in the database might be temporary—the application may regenerate the bad query. Work with the development team to change the ORM mapping or parameter types at the source.
Frequently Asked Questions
Can a non-sargable predicate ever be faster than a sargable one?
In rare edge cases, a function-based predicate might allow the optimizer to use a different access path that is faster overall, such as a partial scan of a clustered index. But these cases are exceptions, not the rule. Always test both versions with your data.
Do all databases handle implicit conversions the same way?
No. SQL Server converts the column to the literal's type, while Oracle converts the literal to the column's type. PostgreSQL follows strict type rules and often raises an error rather than performing an implicit conversion. Understand your database's behavior to write correct queries.
How do I find queries with implicit conversions at scale?
Use dynamic management views (DMVs) or query store to filter for queries with high CPU and logical reads. In SQL Server, the sys.dm_exec_query_plan XML can be searched for CONVERT_IMPLICIT. Third-party monitoring tools also highlight these issues.
What if the OR logic is unavoidable and UNION ALL is not feasible?
Consider using a filtered index or a covering index that includes all columns used in the OR condition. Another approach is to rewrite the query as a single scan with a complex predicate, relying on a clustered index scan if the table is small enough. For large tables, consider partitioning or using an index on a computed column that combines the conditions.
Should I always use IN instead of OR?
Yes, when the OR conditions are on the same column and the list is reasonably short (fewer than a few hundred values). For multiple columns, UNION ALL is usually better. For very long IN lists, test to see if the optimizer switches to a scan; if so, consider a temporary table or table-valued parameter.
These three hidden killers—non-sargable predicates, implicit conversions, and poorly structured OR logic—account for the majority of index underutilization cases we see at bitlox. By systematically detecting and rewriting them, you can unlock the full potential of your indexes and deliver faster queries without adding hardware. Start with your top five slowest queries today, examine their execution plans, and apply the fixes outlined here. Your users will notice the difference.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!