Overview
Part 1: Is setting up SQL Database Engine to suggested setting over the stock default values.
Part 2: Missing Indexes - Removing Unused Indexes and Adding Missing Indexes.
Part 3: Additional Tools for Performance Monitoring.
Where Possible upgrade the client to SQL2019 Express (Latest SQL) due to the way the Query Planner creates execution plans.
Part 1:
Configure TEMPDB
Should be on the fastest Harddrive and away from the production database drive and OS if possible if system resources are limited at least split the TEMPDB from the Production Database. (TEMPDB on the OS drive and Production DB on its own Drive)
Suggested would be to Start from 1 GB for all TempDB files and 500MB for the LOG file.
This script can be used if you want to move the TempDB from a Drive to its own Drive
Configure Database Resizing
The Log file (LDF) should always be 25% of the MDF File
Example 10GB Database the Log should be 2.5GB
The Autogrowth should also be set to Megabytes and not %
Configure the max degree of parallelism
Run the following query to get a suggested value for MAXDOP
DECLARE @CoreCount int;
DECLARE @NumaNodes int;
SET @CoreCount = (SELECT i.cpu_count from sys.dm_os_sys_info i);
SET @NumaNodes = (
SELECT MAX(c.memory_node_id) + 1
FROM sys.dm_os_memory_clerks c
WHERE memory_node_id < 64
);
IF @CoreCount > 4 /* If less than 5 cores, don't bother. */
BEGIN
DECLARE @MaxDOP int;
/* 3/4 of Total Cores in Machine */
SET @MaxDOP = @CoreCount * 0.75;
/* if @MaxDOP is greater than the per NUMA node
Core Count, set @MaxDOP = per NUMA node core count
*/
IF @MaxDOP > (@CoreCount / @NumaNodes)
SET @MaxDOP = (@CoreCount / @NumaNodes) * 0.75;
/*
Reduce @MaxDOP to an even number
*/
SET @MaxDOP = @MaxDOP - (@MaxDOP % 2);
/* Cap MAXDOP at 8, according to Microsoft */
IF @MaxDOP > 8 SET @MaxDOP = 8;
PRINT 'Suggested MAXDOP = ' + CAST(@MaxDOP as varchar(max));
END
ELSE
BEGIN
PRINT 'Suggested MAXDOP = 0 since you have less than 4 cores total.';
PRINT 'This is the default setting, you likely do not need to do';
PRINT 'anything.';
END
USE CustomerDatabase;
GO
EXEC sp_configure 'show advanced options', 1;
GO
RECONFIGURE WITH OVERRIDE;
GO
EXEC sp_configure 'max degree of parallelism', 8;
GO
RECONFIGURE WITH OVERRIDE;
GO
USE CustomerDatabase;
GO
EXEC sp_configure 'show advanced options', 1 ;
GO
RECONFIGURE
GO
EXEC sp_configure 'cost threshold for parallelism', 50 ;
GO
RECONFIGURE
GO
Set Max Server Memory with T-SQL
GB |
MB |
Recommended Setting |
Command |
16 |
16384 |
12288 |
EXEC sys.sp_configure ‘max server memory (MB)’, ‘12288’; RECONFIGURE; |
32 |
32768 |
29491 |
EXEC sys.sp_configure ‘max server memory (MB)’, ‘29491’; RECONFIGURE; |
64 |
65536 |
58982 |
EXEC sys.sp_configure ‘max server memory (MB)’, ‘58982’; RECONFIGURE; |
128 |
131072 |
117964 |
EXEC sys.sp_configure ‘max server memory (MB)’, ‘117964’; RECONFIGURE; |
ALTER DATABASE (Transact-SQL) Compatibility Level
Sets Transact-SQL and query processing behaviours to be compatible with the specified version of the SQL engine.
Generally, allows SQL to process and create better execution plans.
Product |
Database Engine Version |
Default Compatibility Level Designation |
Supported Compatibility Level Values |
SQL Server 2019 (15.x) |
15 |
150 |
150, 140, 130, 120, 110, 100 |
SQL Server 2017 (14.x) |
14 |
140 |
140, 130, 120, 110, 100 |
Azure SQL Database |
12 |
150 |
150, 140, 130, 120, 110, 100 |
Azure SQL Database Managed Instance |
12 |
150 |
150, 140, 130, 120, 110, 100 |
SQL Server 2016 (13.x) |
13 |
130 |
130, 120, 110, 100 |
SQL Server 2014 (12.x) |
12 |
120 |
120, 110, 100 |
SQL Server 2012 (11.x) |
11 |
110 |
110, 100, 90 |
SQL Server 2008 R2 |
10.5 |
100 |
100, 90, 80 |
SQL Server 2008 |
10 |
100 |
100, 90, 80 |
SQL Server 2005 (9.x) |
9 |
90 |
90, 80 |
SQL Server 2000 (8.x) |
8 |
80 |
80 |
--Example of setting SQL compatibility to 2019
ALTER DATABASE CustomerDatabase
SET COMPATIBILITY_LEVEL = 150
TURN AUTOCLOSE OFF
USE [master]
GO
ALTER DATABASE [CustomerDatabase] SET AUTO_CLOSE OFF;
GO
Enabling Parameterization
Query to check if Parameterization is forced
SELECT name, is_parameterization_forced FROM sys.databases
--Forced
ALTER DATABASE CustomerDatabase SET PARAMETERIZATION FORCED
Enable Query Store on SQL 2016 and above
(Great for Resource/Query monitoring) with only a 3-5% overhead on system resources)
Can be left on permanently
SQL 2016
ALTER DATABASE [CustomerDatabase]
SET QUERY_STORE = ON
(
OPERATION_MODE = READ_WRITE,
CLEANUP_POLICY = ( STALE_QUERY_THRESHOLD_DAYS = 30 ),
DATA_FLUSH_INTERVAL_SECONDS = 900,
QUERY_CAPTURE_MODE = AUTO,
MAX_STORAGE_SIZE_MB = 1000,
INTERVAL_LENGTH_MINUTES = 1
);
SQL 2017
ALTER DATABASE [CustomerDatabase]
SET QUERY_STORE = ON
(
OPERATION_MODE = READ_WRITE,
CLEANUP_POLICY = ( STALE_QUERY_THRESHOLD_DAYS = 30 ),
DATA_FLUSH_INTERVAL_SECONDS = 900,
MAX_STORAGE_SIZE_MB = 1000,
INTERVAL_LENGTH_MINUTES = 1,
SIZE_BASED_CLEANUP_MODE = AUTO,
MAX_PLANS_PER_QUERY = 200,
WAIT_STATS_CAPTURE_MODE = ON,
QUERY_CAPTURE_MODE = CUSTOM,
QUERY_CAPTURE_POLICY = (
STALE_CAPTURE_POLICY_THRESHOLD = 24 HOURS,
EXECUTION_COUNT = 30,
TOTAL_COMPILE_CPU_TIME_MS = 1000,
TOTAL_EXECUTION_CPU_TIME_MS = 100
)
);
SQL 2019
ALTER DATABASE [CustomerDatabase]
SET QUERY_STORE = ON
(
OPERATION_MODE = READ_WRITE,
CLEANUP_POLICY = ( STALE_QUERY_THRESHOLD_DAYS = 30 ),
DATA_FLUSH_INTERVAL_SECONDS = 900,
MAX_STORAGE_SIZE_MB = 1000,
INTERVAL_LENGTH_MINUTES = 1,
SIZE_BASED_CLEANUP_MODE = AUTO,
MAX_PLANS_PER_QUERY = 200,
WAIT_STATS_CAPTURE_MODE = ON,
QUERY_CAPTURE_MODE = CUSTOM,
QUERY_CAPTURE_POLICY = (
STALE_CAPTURE_POLICY_THRESHOLD = 24 HOURS,
EXECUTION_COUNT = 30,
TOTAL_COMPILE_CPU_TIME_MS = 1000,
TOTAL_EXECUTION_CPU_TIME_MS = 100
)
);
Enable AutoTuning
(SQL 2017 and above (Requires Query store running) (cannot be enabled on 2016 and lower)
This will monitor any regressed Queries and if there are multiple Execution plans, SQL will Force the Best Execution Plan.
ALTER DATABASE CustomerDatabase
SET AUTOMATIC_TUNING (FORCE_LAST_GOOD_PLAN = ON );
GO
To check Query Store,
select * from sys.database_query_store_options
You're looking for actual_state_desc to be READ_WRITE.
Then check the tuning options with
select * from sys.database_automatic_tuning_options where name = 'FORCE_LAST_GOOD_PLAN'
You're looking for actual_state to be set to ON.
Part 2:
Drop Unused Indexes
This Script will highlight unused indexes from the PLAN CACHE (Should be used when this system has been running for a week without and SQL Server Restart)
This will highlight the TOP 25 Unused Indexes, which can be Dropped via SSMS.
-- Unused Index Script
-- Original Author: Pinal Dave
SELECT TOP 25
o.name AS ObjectName
, i.name AS IndexName
, i.index_id AS IndexID
, dm_ius.user_seeks AS UserSeek
, dm_ius.user_scans AS UserScans
, dm_ius.user_lookups AS UserLookups
, dm_ius.user_updates AS UserUpdates
, p.TableRows
, 'DROP INDEX ' + QUOTENAME(i.name)
+ ' ON ' + QUOTENAME(s.name) + '.'
+ QUOTENAME(OBJECT_NAME(dm_ius.OBJECT_ID)) AS 'drop statement'
FROM sys.dm_db_index_usage_stats dm_ius
INNER JOIN sys.indexes i ON i.index_id = dm_ius.index_id
AND dm_ius.OBJECT_ID = i.OBJECT_ID
INNER JOIN sys.objects o ON dm_ius.OBJECT_ID = o.OBJECT_ID
INNER JOIN sys.schemas s ON o.schema_id = s.schema_id
INNER JOIN (SELECT SUM(p.rows) TableRows, p.index_id, p.OBJECT_ID
FROM sys.partitions p GROUP BY p.index_id, p.OBJECT_ID) p
ON p.index_id = dm_ius.index_id AND dm_ius.OBJECT_ID = p.OBJECT_ID
WHERE OBJECTPROPERTY(dm_ius.OBJECT_ID,'IsUserTable') = 1
AND dm_ius.database_id = DB_ID()
AND i.type_desc = 'nonclustered'
AND i.is_primary_key = 0
AND i.is_unique_constraint = 0
ORDER BY (dm_ius.user_seeks + dm_ius.user_scans + dm_ius.user_lookups) ASC
GO
Create Missing Indexes
This script will highlight the most expensive Queries and Missing Indexes from the PLAN CACHE (Should be used when this system has been running for a week without and SQL Server Restart)
-- Missing Index Script
-- Original Author: Pinal Dave
SELECT TOP 25
dm_mid.database_id AS DatabaseID,
dm_migs.avg_user_impact*(dm_migs.user_seeks+dm_migs.user_scans) Avg_Estimated_Impact,
dm_migs.last_user_seek AS Last_User_Seek,
OBJECT_NAME(dm_mid.OBJECT_ID,dm_mid.database_id) AS [TableName],
'CREATE INDEX [IX_' + OBJECT_NAME(dm_mid.OBJECT_ID,dm_mid.database_id) + '_'
+ REPLACE(REPLACE(REPLACE(ISNULL(dm_mid.equality_columns,''),', ','_'),'[',''),']','')
+ CASE
WHEN dm_mid.equality_columns IS NOT NULL
AND dm_mid.inequality_columns IS NOT NULL THEN '_'
ELSE ''
END
+ REPLACE(REPLACE(REPLACE(ISNULL(dm_mid.inequality_columns,''),', ','_'),'[',''),']','')
+ ']'
+ ' ON ' + dm_mid.statement
+ ' (' + ISNULL (dm_mid.equality_columns,'')
+ CASE WHEN dm_mid.equality_columns IS NOT NULL AND dm_mid.inequality_columns
IS NOT NULL THEN ',' ELSE
'' END
+ ISNULL (dm_mid.inequality_columns, '')
+ ')'
+ ISNULL (' INCLUDE (' + dm_mid.included_columns + ')', '') AS Create_Statement
FROM sys.dm_db_missing_index_groups dm_mig
INNER JOIN sys.dm_db_missing_index_group_stats dm_migs
ON dm_migs.group_handle = dm_mig.index_group_handle
INNER JOIN sys.dm_db_missing_index_details dm_mid
ON dm_mig.index_handle = dm_mid.index_handle
WHERE dm_mid.database_ID = DB_ID()
ORDER BY Avg_Estimated_Impact DESC
GO
Alternative method to missing Indexes is DETA
Database engine Tuning Advisor using the query store. (SQL 2017+ with the latest SSMS) only available on Standard or Dev editions, (Express is a no go).
*Rule no more than 5 indexes per table at max, should really only create an index with careful consideration.
The DETA could recommend 100’s of index creation, which could ultimately degrade performance.
DETA with the latest SSMS can be used with the Query Store Plan Cache to highlight Expensive Queries.
Part 3:
Other Stored Procedures for looking at Database Statistics to highlight Performance issues if you want a more granular level of the Monitoring
These stored procedures can highlight various statistics to highlight potential performance issues.
Download the Stored Procedures from Brent Ozar (SQL Database Master)
Release 2021-05-30: Fixes and Improvements · BrentOzarULTD/SQL-Server-First-Responder-Kit · GitHub
WhoIsActive - Releases · amachanic/sp_whoisactive · GitHub
SP_BlitzFirst – is used to get a quick snapshot of system resources.
SP_Blitz – Priority checklist of Database backups, performance issues
SP_BlitzCache – Checks the PLAN CACHE to highlight Queries and Indexes
SP_BlitzIndex – a More In-depth Index Check per Database/ Table
sp_blitzQueryStore @databasename = 'CustomerDatabase' – This Stored Procedure is a combination of all the above Stored Procedures but utilize the Query Store Data to highlight performance issues – Great overview on Database Statistics to highlight Missing Index’s etc
Query Store Dashboard
Query Store Dashboard Report for an Overview Report.
Installation · Evdlaar/QueryStoreDashboard Wiki · GitHub