Collation & Data Type Precedence · 10M rows
Implicit conversion: 3 reads vs. 27,469 reads
When an application sends a Unicode parameter (@p nvarchar, default in .NET/EF) against a varchar column, the outcome depends on collation precedence:
-
Optimal:
1.0 ms
(0 ms CPU)
·
3 reads
(Index Seek)
-
Collation mismatch:
318 ms
(+2,120 ms CPU)
·
27,469 reads
(Index Scan)
-
Type mismatch (int):
287 ms
(+1,864 ms CPU)
·
27,469 reads
(Index Scan)
Under a SQL collation or with varchar = int, SQL Server must convert every single row (plan warning: CONVERT_IMPLICIT). From 3 page reads and 0 ms CPU to 27,469 reads and over 2 seconds of burned CPU time per single query.
Set passing & Plan Cache · 10M rows
Long IN lists (2,000 IDs) and ad-hoc plan bloat
How should hundreds or thousands of IDs be passed from C# to SQL Server? I benchmarked four approaches across 10 to 2,000 IDs and across 50,000 individual calls:
-
TVP (2,000 IDs):
172 ms
(0 ms compile)
·
200 KB cache
-
Literal list:
591 ms
(196 ms compile)
·
6,376 KB cache
-
50,000 ad-hoc:
781 MB plan waste vs. 0.1 MB (parameterised)
Individual parameters (@p1 … @p1000) degrade at 1,000 IDs due to Merge Interval / Concatenation to 565 ms and 7.1 MB plan size. Table-Valued Parameters (TVP) consistently deliver a reusable plan with zero compile overhead.
Data model & storage · 1M child / 50,000 parent rows
Composite text key vs. surrogate key (int)
A three-column business key (Tenant + Area + ItemNumber, averaging 56 characters) is duplicated in every child table and every foreign-key index:
-
Technical int key:
0.3 ms
·
108 MB storage
(factor ×2.83 leaner)
-
3-part text key:
18.3 ms
·
305 MB storage
(60× slower)
The technical 4-byte key saves nearly two thirds of storage and buffer pool memory (while the business key remains protected by a UNIQUE constraint on the parent table). For existing customer databases I wrote a catalog script that calculates this exact saving upfront purely from sys.dm_db_partition_stats and sys.index_columns without touching user data.
SARGability · 10M rows
Leading wildcard (LIKE '%berg'): from 4.2 s to 3.3 ms
A suffix search such as WHERE Company LIKE '%berg' is not SARGable – even with an index on Company, SQL Server scans all 49,062 index pages (21,297 ms CPU time, 4,231 ms runtime in a parallel scan).
-
REVERSE + index:
3.3 ms
(0 ms CPU)
·
45 reads
-
LIKE '%berg' (scan):
4,231 ms
(21,297 ms CPU)
·
49,062 reads
With a computed column CompanyRev AS REVERSE(Company) and an index on it, the suffix search becomes a prefix search (LIKE REVERSE(@p)). Logical reads drop from 49,062 to 45 reads (factor ×1,090 less I/O), runtime from 4,231 ms to 3.3 ms (over ×1,200 faster).
Cross-DB & Linked Server · 1M rows
Linked Server vs. OPENQUERY and the collation trap
Queries across database and server boundaries flip into completely different execution plans depending on selectivity and collation:
-
Matching collation:
13 ms
·
66 reads
(Index Seek)
-
Mismatched collation:
1,383 ms
·
7,882 reads
(factor ×119 I/O)
-
OPENQUERY (single point):
5.9 ms
·
(vs. 4-part: 210 ms · ×35 faster)
For point lookups, OPENQUERY pushes the filter to the remote server; for full joins, it forces thousands of local nested-loop seeks. And if the child database collation differs, an implicit conversion prevents the index seek (factor ×119 more reads).
Normalisation & data quality · 1M rows
Plain-text columns vs. lookup tables (3NF)
Storing values such as City and PaymentMethod as plain text in the transaction table instead of an int foreign key to a lookup table hurts both speed and data integrity:
-
Normalised:
15 ms
(432 reads)
·
Consistency check:
2 ms
(28 reads)
-
Plain text:
160 ms
(3,438 reads)
·
Consistency check:
147 ms
(4,655 reads)
On the small lookup table (5,000 cities), even a LIKE '%berg' costs almost nothing before joining via the lean CityId. At the same time, the data model prevents contradictory spellings of the same city for a given postcode.