Benchmark 1 · 10M rows
Collation & Implicit Data Type Conversion
When .NET / Entity Framework passes parameters as Unicode (nvarchar) against a varchar column:
Matching
3 Reads · 0 ms CPU
nvarchar
27,469 Reads · 3,011 ms CPU
Root cause: Under SQL Server Data Type Precedence, nvarchar outranks varchar. The engine cannot convert the parameter; it must convert every single table row on the fly (Plan warning: CONVERT_IMPLICIT). Index seeks become impossible; CPU burns for 2.4 seconds.
Benchmark 2 · 10M rows
Set Passing (IN Lists) & Plan Cache Bloat
Comparing dynamic concatenated strings with Table-Valued Parameters (TVP) for 2,000 IDs:
TVP
222 ms · 0 ms Compile
IN String
589 ms · 185 ms Compile
Root cause: Dynamically constructed IN (1, 2, 3...) queries flood the plan cache with single-use plans. Across 50,000 queries, ad-hoc execution seized 781 MB of RAM in the plan cache, while parameterized TVP required only 0.1 MB.
Benchmark 3 · 1M Child / 50k Parent rows
Natural Composite Key vs. Surrogate int Key
A 3-part natural text key (56 characters) compared to a slim int surrogate key:
int Key
65 MB · 471 ms Join
Text Key
287 MB · 1,142 ms Join
Root cause: The natural composite key duplicates into every foreign key and secondary index across all child tables. Storage expands by 4.4×, exerting persistent buffer pool pressure.
Benchmark 4 · 10M rows
SARGability: Leading Wildcards (LIKE '%berg')
Suffix search on company names: Standard scan vs. computed column with REVERSE():
REVERSE
43 Reads · 6.3 ms
LIKE '%...'
49,022 Reads · 3,890 ms
Root cause: LIKE '%berg' cannot seek B-trees. Adding a computed column FirmaRev AS REVERSE(Firma) with an index converts a non-SARGable suffix search into a high-speed prefix seek LIKE 'greb%': ×617 faster.
Benchmark 5 · 1M rows
Database boundaries: collation mismatch
Cross-database queries:
Matching
66 Reads · Index Seek
Mismatch
7,882 Reads (Factor ×119)
Root cause: If the collation of a remote database differs, SQL Server pulls all rows locally and compares them row-by-row. Remote index seeks become impossible.
Benchmark 6 · 1M rows
Normalisation (3NF) & Data Quality
Raw string columns in transactional tables vs. indexed lookup tables:
Normalised
33 ms · Validation: 4.3 ms
Plain Text
1,518 ms · Validation: 521 ms
Root cause: Filtering on a compact lookup table (5,000 cities) executes in near-zero time before joining via a lean int key – 46.4× faster, and the model prevents differing spellings of the same city.
Benchmark 7 · 10M customers, 40M line items
Stored procedure vs. view: where the filter is applied
We look for the revenue of the 100 customers in one city, with suitable indexes. The logic sits once in a view and once in a procedure without parameters whose result is filtered afterwards (INSERT … EXEC):
View, outer WHERE
52 ms · 3,408 reads
Multi-statement function
53,095 ms (factor ×1,029)
Procedure, filtered outside
134,172 ms · 44,110,987 reads (factor ×2,600)
Root cause: The view is expanded into the query, so the city filter moves inside and uses the index. The procedure computes all 10 million customers first and is filtered only afterwards. The procedure itself is not the problem: with a parameter it takes 54 ms, as fast as the view. Client times with a cold cache.
Benchmark 8 · 100M bookings
Removing one month: DELETE vs. truncating a partition
The same bookings over 120 months, stored by booking number, by date, and partitioned by month. One month (833,333 rows) is removed:
Truncate partition
8.5 ms (TRUNCATE … WITH (PARTITIONS))
DELETE, by date
14,082 ms (factor ×1,657)
DELETE, no date index
67,298 ms (factor ×7,917)
Root cause: DELETE logs every row and maintains every index. Truncating a partition only deallocates its pages. Prerequisite: the month lies in exactly one partition and all indexes are aligned. Only the delete statement was measured, inside a transaction that was rolled back afterwards.
Benchmark 9 · 100M bookings
Partitioning does not make queries faster by itself
The same three tables as in benchmark 8, this time two queries: the total of one month and the total of one account across all years:
Month, partitioned
244 ms (no date index: 11,077 ms)
Account, not partitioned
45 ms
Account, partitioned
285 ms
Root cause: For the monthly total an index by date is enough; in this measurement partitioning was even slightly slower. The account query does not know the month and has to search all 120 partitions. Partitioning is a tool for data maintenance (benchmark 8), not a substitute for the right index.
Benchmark 10 · 100M customers, 400M line items
Three-table join at 100 million customers
Customer, order and line item; we look for the 400 line items of the customers in one city. No index on the foreign keys vs. indexes on city and foreign keys (with INCLUDE):
With indexes
66 ms · 3,144 reads
Primary keys only
41,982 ms · 6,843,764 reads (factor ×632)
Root cause: Without an index SQL Server reads all 400 million line items to find 400 – 2,177 times as many pages. Three subqueries per customer instead of the join: 50,737 ms vs. 232 ms (factor ×219). Forced nested loops without an index were not measured at this size.