Skip to main content

sqlserver-query-optimization

Fix SQL Server stored procedures, views, and T-SQL anti-patterns. Covers non-SARGable predicates, implicit conversions, cursor replacement, DELETE+INSERT anti-pattern, CTE vs temp table decisions, and query hints.

Informações da origem

Repositório
vince-winkintel/sql-server-skills
Última atividade na origem
30 de outubro de 2026 às 19:28
Idioma detectado do SKILL.md
inglês
Estrelas
2
Forks
0

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Explorador de arquivos
2 arquivos

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
name
sqlserver-query-optimization
description
Fix SQL Server stored procedures, views, and T-SQL anti-patterns. Covers non-SARGable predicates, implicit conversions, cursor replacement, DELETE+INSERT anti-pattern, CTE vs temp table decisions, and query hints.
# SQL Server Query Optimization Use this skill when you have a specific stored procedure, view, or T-SQL batch that needs to be rewritten or improved. This is about fixing the code — for hardware/index problems, start with `sqlserver-diagnostics` and `sqlserver-indexes`. --- ## Stored Procedure Anti-Patterns ### 1. DELETE + INSERT Per Request (Most Common) **Problem:** A stored procedure deletes and re-inserts the same row every time it runs, even when nothing changed. ```sql -- ❌ ANTI-PATTERN CREATE PROCEDURE usp_UpsertUserPreference @UserID INT, @Key NVARCHAR(50), @Value NVARCHAR(500) AS DELETE FROM UserPreferences WHERE UserID = @UserID AND PreferenceKey = @Key; INSERT INTO UserPreferences (UserID, PreferenceKey, PreferenceValue) VALUES (@UserID, @Key, @Value); GO ``` **Problems:** - Two DML statements = two log writes, two index operations - Triggers fire on DELETE even when nothing really changed - Breaks foreign keys that point to this row - Causes unnecessary index fragmentation **Fix — MERGE:** ```sql -- ✅ CORRECT — MERGE (upsert) CREATE PROCEDURE usp_UpsertUserPreference @UserID INT, @Key NVARCHAR(50), @Value NVARCHAR(500) AS MERGE UserPreferences AS target USING (VALUES (@UserID, @Key, @Value)) AS source (UserID, PreferenceKey, PreferenceValue) ON target.UserID = source.UserID AND target.PreferenceKey = source.PreferenceKey WHEN MATCHED AND target.PreferenceValue != source.PreferenceValue THEN UPDATE SET target.PreferenceValue = source.PreferenceValue, target.UpdatedAt = GETDATE() WHEN NOT MATCHED THEN INSERT (UserID, PreferenceKey, PreferenceValue, CreatedAt) VALUES (source.UserID, source.PreferenceKey, source.PreferenceValue, GETDATE()); GO ``` **Alternative — staleness check:** ```sql -- ✅ CORRECT — Only update if value actually changed IF EXISTS (SELECT 1 FROM UserPreferences WHERE UserID = @UserID AND PreferenceKey = @Key) BEGIN UPDATE UserPreferences SET PreferenceValue = @Value, UpdatedAt = GETDATE() WHERE UserID = @UserID AND PreferenceKey = @Key AND PreferenceValue != @Value; -- Skip if no change END ELSE BEGIN INSERT INTO UserPreferences (UserID, PreferenceKey, PreferenceValue, CreatedAt) VALUES (@UserID, @Key, @Value, GETDATE()); END ``` --- ### 2. Cursor Loops (Replace with Set-Based Operations) ```sql -- ❌ ANTI-PATTERN — processing rows one at a time DECLARE @OrderID INT; DECLARE cur CURSOR FOR SELECT OrderID FROM Orders WHERE Status = 'Pending'; OPEN cur; FETCH NEXT FROM cur INTO @OrderID; WHILE @@FETCH_STATUS = 0 BEGIN EXEC usp_ProcessOrder @OrderID; FETCH NEXT FROM cur INTO @OrderID; END CLOSE cur; DEALLOCATE cur; ``` **Fix — set-based:** ```sql -- ✅ CORRECT — process all rows at once UPDATE Orders SET ProcessedAt = GETDATE(), Status = 'Processing' WHERE Status = 'Pending'; ``` If `usp_ProcessOrder` has complex logic that can't be inlined, consider: - Rewriting the proc to accept a table-valued parameter (TVP) - Using a temp table as a queue and processing via a single bulk statement - Using `STRING_AGG` / `FOR XML PATH` for string concatenation (not cursors) --- ### 3. Non-SARGable Predicates A **SARGable** predicate (Search ARGument able) allows SQL Server to use an index seek. Non-SARGable predicates force scans. ```sql -- ❌ NON-SARGable — function wraps the column, index can't be used WHERE YEAR(OrderDate) = 2024 WHERE CONVERT(VARCHAR, OrderDate, 101) = '01/15/2024' WHERE LEN(CustomerName) > 10 WHERE OrderTotal + 100 > 5000 -- ✅ SARGable equivalents WHERE OrderDate >= '2024-01-01' AND OrderDate < '2025-01-01' WHERE OrderDate = '2024-01-15' WHERE CustomerName LIKE 'A%' -- LIKE with suffix wildcard is SARGable WHERE OrderTotal > 4900 -- ❌ NON-SARGable — leading wildcard, can't use index WHERE CustomerName LIKE '%Smith%' -- ✅ If full-text search needed, use SQL Server Full-Text or a search table ``` --- ### 4. Implicit Conversions When a column and a parameter have different data types, SQL Server converts the entire column — forcing an index scan. ```sql -- Table column: CustomerCode NVARCHAR(20) -- ❌ Passing VARCHAR causes implicit conversion of the whole column WHERE CustomerCode = 'ACME123' -- VARCHAR literal, column is NVARCHAR -- ✅ Use N'' prefix for NVARCHAR columns WHERE CustomerCode = N'ACME123' -- Table column: Status TINYINT -- ❌ Passing string causes type conversion WHERE Status = '1' -- ✅ Pass the correct type WHERE Status = 1 ``` **Detect implicit conversions in execution plans:** - Look for a `CONVERT_IMPLICIT` function in the predicate tooltip - Or query: `sys.dm_exec_query_stats` + `sys.dm_exec_sql_text` and filter on `CONVERT_IMPLICIT` --- ### 5. SELECT * in Production Code ```sql -- ❌ Never use SELECT * in stored procedures or views SELECT * FROM Orders JOIN Customers ON Orders.CustomerID = Customers.CustomerID; -- ✅ Always name columns explicitly SELECT o.OrderID, o.OrderDate, o.TotalAmount, o.Status, c.CompanyName, c.ContactEmail FROM Orders o JOIN Customers c ON o.CustomerID = c.CustomerID; ``` **Why it matters:** - Breaks if a column is added, renamed, or reordered in the base table - Fetches columns the application doesn't need (extra I/O, network) - Prevents SQL Server from using covering index optimization --- ## View Optimization ### When Views Expand Inline By default, SQL Server expands a view definition inline during query compilation. The optimizer sees the full base table query — this is usually fine. ```sql -- This view is expanded inline — optimizer sees the base tables CREATE VIEW vw_ActiveOrders AS SELECT o.*, c.CompanyName FROM Orders o JOIN Customers c ON o.CustomerID = c.CustomerID WHERE o.Status = 'Active'; ``` ### Indexed Views (Materialized) An indexed view stores the result set physically. Dramatically speeds up aggregation queries. ```sql -- Requirements for indexed views: -- 1. SCHEMABINDING -- 2. UNIQUE CLUSTERED INDEX on the view -- 3. Strict SET options (ANSI_NULLS ON, QUOTED_IDENTIFIER ON, etc.) CREATE VIEW vw_SalesByRegion WITH SCHEMABINDING AS SELECT RegionID, COUNT_BIG(*) AS OrderCount, SUM(TotalAmount) AS TotalSales FROM dbo.Orders GROUP BY RegionID; GO CREATE UNIQUE CLUSTERED INDEX IX_vw_SalesByRegion ON vw_SalesByRegion (RegionID); ``` **On Enterprise Edition**, SQL Server automatically uses the indexed view even when the query references the base table. On Standard Edition, use `WITH (NOEXPAND)` hint: ```sql SELECT * FROM vw_SalesByRegion WITH (NOEXPAND) WHERE RegionID = 5; ``` --- ## CTE vs Temp Table vs Table Variable | Scenario | Use | Why | |----------|-----|-----| | Readability / recursive queries / used once | **CTE** | Inline, no physical storage, optimizer sees through it | | Large result set used multiple times in same batch | **Temp Table** | Has statistics, can be indexed, persists for the session | | Small result set (< few hundred rows) | **Table Variable** | Low overhead; no statistics (can be a problem if used in joins) | | Need an index on intermediate results | **Temp Table** | Table variables can only have a PK constraint | | Need statistics for optimizer accuracy | **Temp Table** | Table variables have no statistics — optimizer assumes 1 row | ```sql -- CTE (single-use, readability) WITH RankedOrders AS ( SELECT *, ROW_NUMBER() OVER (PARTITION BY CustomerID ORDER BY OrderDate DESC) AS rn FROM Orders ) SELECT * FROM RankedOrders WHERE rn = 1; -- Temp Table (multiple uses, large set) SELECT OrderID, CustomerID, TotalAmount INTO #LargeOrderSet FROM Orders WHERE TotalAmount > 10000; CREATE INDEX IX_Temp_CustomerID ON #LargeOrderSet (CustomerID); -- Can add index! SELECT o.*, c.CompanyName FROM #LargeOrderSet o JOIN Customers c ON o.CustomerID = c.CustomerID; DROP TABLE #LargeOrderSet; -- Table Variable (small set) DECLARE @SmallSet TABLE (OrderID INT, CustomerID INT); INSERT INTO @SmallSet SELECT TOP 10 OrderID, CustomerID FROM Orders; ``` --- ## Statistics Management Start with read-only inspection in the confirmed database. Review metadata visibility and the permissions for these diagnostics; missing output is not approval to change statistics. Confirm the table and statistic exist before inspecting them. ```sql -- Read-only: inspect automatic statistics settings, normally enabled. SELECT name, is_auto_update_stats_on, is_auto_create_stats_on FROM sys.databases WHERE name = DB_NAME(); -- Read-only: inspect this existing statistic's age, sampling and histogram. DBCC SHOW_STATISTICS ('dbo.Orders', 'IX_Orders_CustomerID'); ``` Bulk loads, index changes or inaccurate row estimates are reasons to inspect statistics and the workload, not automatic triggers for maintenance. Automatic update thresholds depend on SQL Server version, compatibility level and table size; do not assume a universal 20% threshold. Estimate errors alone do not justify FULLSCAN. Require **exact-target DBA approval** of the database, schema, table and statistic before any update. Prefer a reviewed named statistic (or explicitly reviewed table scope); confirm the database context and require **ALTER on the target table**. Monitoring permission alone does not authorize changes. Review the existing sampling and justify any FULLSCAN: it reads all rows and costs I/O/CPU; statistics maintenance can use tempdb and cause recompilation, changing estimates and execution plans. Rehearse on representative data in staging, schedule a maintenance window, capture statistics/histogram and query-performance baselines, and agree on monitoring, stop criteria and a DBA-owned recovery plan before execution. If errors or regressions occur, stop further maintenance and consult the DBA. There is no universal rollback: these examples cannot automatically undo statistic/histogram changes or restore the plan cache and previous plans. Recovery needs separate workload-specific review. ```sql -- Review-only mutation: replace with the approved statistic name and confirm context. -- FULLSCAN is optional and needs its own workload justification. -- UPDATE STATISTICS dbo.Orders [ReviewedStatisticName] WITH FULLSCAN; ``` `sp_updatestats` is database-wide maintenance, **not a default** troubleshooting step. It requires separate DBA approval of the entire database scope and the same staging, workload, maintenance and recovery gates above. On SQL Server, the caller must be the database owner (dbo) or a member of the sysadmin fixed server role; db_owner membership alone is not sufficient. Do not escalate a monitoring account to run it. It can affect many statistics and cause recompilation across the database. ```sql -- Review-only mutation, after the separate database-wide approval: -- EXEC sys.sp_updatestats; ``` References: [UPDATE STATISTICS](https://learn.microsoft.com/en-us/sql/t-sql/statements/update-statistics-transact-sql), [sp_updatestats](https://learn.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-updatestats-transact-sql). --- ## Query Hints — When Justified Query hints override optimizer decisions. Use sparingly — they lock in a behavior regardless of data changes. ```sql -- NOLOCK — dirty reads (accepts uncommitted data). Use for reporting only; NEVER for financial/transactional data SELECT * FROM Orders WITH (NOLOCK) WHERE CustomerID = 42; -- OPTION (RECOMPILE) — recompile this statement every execution; solves parameter sniffing SELECT * FROM Orders WHERE CustomerID = @CustomerID OPTION (RECOMPILE); -- OPTION (OPTIMIZE FOR UNKNOWN) — use average statistics, not sniffed parameter value SELECT * FROM Orders WHERE CustomerID = @CustomerID OPTION (OPTIMIZE FOR (@CustomerID UNKNOWN)); -- MAXDOP — limit parallelism for this query SELECT SUM(TotalAmount) FROM Orders OPTION (MAXDOP 2); -- FORCE ORDER — force join order as written (rarely needed; usually means missing statistics) SELECT * FROM Orders o JOIN Customers c ON o.CustomerID = c.CustomerID OPTION (FORCE ORDER); ``` --- ## Reference - [Optimization Patterns Reference](references/optimization-patterns.md) — Before/after SQL examples for all major anti-patterns
Ver no GitHub