Goal: Treat the installation as a complete platform build, not just "install SQL Server and create databases". Every decision must support predictable performance, easy troubleshooting, workload separation, recoverability, security hardening, and operational readiness.
| Component | Recommendation |
|---|---|
| OS | Windows Server 2025 (Datacenter or Standard) |
| SQL Version | SQL Server 2025 Enterprise (mission-critical) or Standard (departmental); align edition with feature requirements (Always On, 128+ core, etc.) |
| Installation Type | Standalone instance unless Failover Cluster Instance (FCI) or Always On Availability Group is required |
| Instance | Default instance preferred (MSSQLSERVER) — easier tooling, monitoring, connectivity |
| Authentication | Windows + SQL Mixed Mode (required for legacy apps; use Windows Auth where possible) |
| Service Accounts | Dedicated domain accounts or gMSA (Group Managed Service Accounts) — never use built-in accounts |
| Storage | Separate volumes for OS, Data, Log, TempDB, Backups |
| Backup Target | Separate storage (network share, blob, or dedicated volume) — never same volume as data |
| Monitoring | Plan for monitoring from day one (DMV collection, perfmon, Query Store, Extended Events) |
Install the latest Windows cumulative updates, security updates, firmware updates, storage controller drivers, and NIC drivers. Verify with winver.
Ensure required features are installed:
Set Windows power plan to High Performance. The default Balanced plan allows the CPU to idle and throttle down cores, which causes erratic SQL Server latency — queries that should complete in milliseconds may sporadically take seconds when the CPU ramps back up.
Activate via command line (more reliable than GUI for Server Core or remote management):
powercfg /list -- list all plans with their GUIDs
powercfg /setactive <GUID> -- e.g., 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c (High Perf)
powercfg /getactivescheme -- verify the active plan
For SQL Server VMs running on Hyper-V or VMware, also verify that the hypervisor host and the physical BIOS/UEFI have power management set to High Performance / Maximum Performance, not power-saving or balanced. Windows inside the guest cannot override a host or BIOS that is capping frequency.
powercfg /setactive with GUID)
D:\SQLData)
E:\SQLLog)
F:\SQLTempDB)
G:\SQLBackup)
C:\Program Files\Microsoft SQL Server)
.mdf, .ldf, .ndf, .bak, .trn)
Critical: A production SQL Server must NOT have data, logs, or TempDB on the C: drive.
| Drive | Purpose |
|---|---|
| C: | OS + Page File |
| D: | Data files (.mdf, .ndf) |
| E: | Transaction log files (.ldf) |
| F: | TempDB (data + log) |
| G: | Backups (dedicated, not same volume as data) |
C:\Program Files\Microsoft SQL Server) — do NOT separate
Use NTFS with 64 KB allocation unit size (65536 bytes) for all SQL volumes. On Windows Server 2022/2025, consider ReFS (Resilient File System) with 64 KB cluster size for Data and Backup volumes — ReFS leverages block cloning to accelerate file creation, instant file initialization for transaction logs, and storage-level checkpoints.
fsutil fsinfo ntfsinfo D: → Bytes Per Cluster = 65536 (or ReFS equivalent)
DOMAIN\svc_sqlengine or gMSA) created in AD
DOMAIN\svc_sqlagent or gMSA) created in AD
Both permissions must be assigned via Local Security Policy (secpol.msc) → Local Policies → User Rights Assignment. The exact service account name (DOMAIN\svc_sqlengine) must be explicitly added — group membership inheritance is not sufficient.
DOMAIN\svc_sqlengine added explicitly
DOMAIN\svc_sqlengine added explicitly
permission.sql
| Feature | Status |
|---|---|
| ✔ Database Engine Services | Required |
| ✔ SQL Server Replication | If needed |
| ✔ Full-Text and Semantic Extractions for Search | If needed |
| ✘ Analysis Services (SSAS) | Separate server |
| ✘ Reporting Services (SSRS) | Separate server |
| ✘ Integration Services (SSIS) | Separate server if heavy ETL |
| ✘ Machine Learning Services (In-Database) | Rarely on prod OLTP |
| ✘ PolyBase Query Service | Only if needed |
| Service | Account | Startup Type |
|---|---|---|
| SQL Server Database Engine | DOMAIN\svc_sqlengine | Automatic |
| SQL Server Agent | DOMAIN\svc_sqlagent | Automatic |
DOMAIN\svc_sqlengine (or gMSA)
DOMAIN\svc_sqlagent
DOMAIN\DBA_Group added as SQL Server administrator
VERY IMPORTANT. Change defaults on the Data Directories tab during setup.
D:\SQLData
E:\SQLLog
G:\SQLBackup
F:\SQLTempDB
SQL Server will consume all available memory unless constrained. Always set max server memory.
| Server RAM | Reserve for OS | SQL Max Server Memory |
|---|---|---|
| 32 GB | 4 GB | 28 GB (28672 MB) |
| 64 GB | 8 GB | 56 GB (57344 MB) |
| 128 GB | 16 GB | 112 GB (114688 MB) |
| 256 GB | 20–24 GB | 236 GB (~240000 MB) |
| 512 GB+ | 5–10% of total | Remainder |
-- Example: 64 GB RAM server — reserve 8 GB for OS
EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;
EXEC sp_configure 'max server memory (MB)', 57344; -- 56 GB
RECONFIGURE;
-- Verify
SELECT name, value_in_use FROM sys.configurations
WHERE name = N'max server memory (MB)';
SELECT cpu_count FROM sys.dm_os_sys_info
Follow Microsoft guidance (KB 2806535). For NUMA systems, also consider max degree of parallelism together with cost threshold for parallelism.
-- Check logical CPUs
SELECT cpu_count, hyperthread_ratio FROM sys.dm_os_sys_info;
-- Set MAXDOP (example: 8-core server or cap at 8 for larger hosts)
EXEC sp_configure 'show advanced options', 1;
RECONFIGURE;
EXEC sp_configure 'max degree of parallelism', 8;
RECONFIGURE;
SQL Server auto-configures max worker threads based on CPU count. For high-core production servers with heavily concurrent workloads, the default can sometimes lead to thread starvation before SQL scales. Evaluate and adjust if needed.
-- Check current setting
SELECT max_workers_count FROM sys.dm_os_sys_info;
-- Current configured value
EXEC sp_configure 'max worker threads';
The default formula (SQL 2016+): 256 + ((cpu_count - 4) * 8) for x64 systems. For servers with > 64 cores, verify the calculated value matches your workload concurrency. Only adjust upward if you observe thread waits (THREADPOOL wait type) under normal load.
sys.dm_os_sys_info.max_workers_count matches workload requirements
| Logical Cores | TempDB Files |
|---|---|
| ≤ 8 | 4–8 |
| 16 | 8 |
| 32+ | 8 (scale up only if contention detected) |
-- File count, sizes, and growth uniformity
SELECT name, type_desc, size * 8 / 1024 AS size_mb,
growth, is_percent_growth, physical_name
FROM sys.master_files
WHERE database_id = 2
ORDER BY type_desc, file_id;
-- Active TempDB PAGELATCH contention (allocation pages)
SELECT session_id, wait_type, wait_duration_ms, resource_description
FROM sys.dm_os_waiting_tasks
WHERE wait_type LIKE 'PAGELATCH_%' AND resource_description LIKE '2:%';
Remediation: Add equal-sized data files (up to 8) on a dedicated fast volume. Set identical fixed MB growth on every file. Pre-size files to expected peak — autogrowth during peak load causes PAGELATCH waits. Restart is not required after adding TempDB files.
-- Example: add a TempDB data file (repeat until file count matches target)
ALTER DATABASE tempdb ADD FILE (
NAME = tempdev3,
FILENAME = N'F:\SQLTempDB\tempdev3.ndf',
SIZE = 8192MB,
FILEGROWTH = 512MB
);
sys.dm_server_services.instant_file_initialization_enabled = 1
Multi-TB databases: For databases over 500 GB, set autogrowth to at least 2048–4096 MB. A small autogrowth (e.g., 100 MB) on a large database causes excessive VLF fragmentation and frequent growth events during heavy data loads, leading to IO stalls.
| Type | Frequency | Retention |
|---|---|---|
| Full Backup | Daily (e.g., 12:00 AM) | 30 days (adjust per policy) |
| Differential Backup | Every 4–6 hours | 7–14 days |
| Transaction Log Backup | Every 5–15 minutes | Until full + diff chain break |
backup compression default enabled (set to 1)
backup checksum default enabled (set to 1) — SQL 2022+
G:\SQLBackup\FULL, DIFF, LOG, SYSTEM
After migration or bulk load, index fragmentation often spikes. Use Microsoft thresholds: 5–30% → reorganize; >30% → rebuild (online when edition supports it). Skip small indexes (<1000 pages).
-- Fragmentation report across current database (run per DB or use physical_stats_and_heaps.sql)
SELECT
s.name AS schema_name,
t.name AS table_name,
i.name AS index_name,
ps.page_count,
ps.avg_fragmentation_in_percent,
CASE
WHEN ps.avg_fragmentation_in_percent > 30 THEN 'REBUILD'
WHEN ps.avg_fragmentation_in_percent > 5 THEN 'REORGANIZE'
ELSE 'OK'
END AS recommended_action
FROM sys.dm_db_index_physical_stats(DB_ID(), NULL, NULL, NULL, N'LIMITED') AS ps
INNER JOIN sys.tables AS t ON ps.object_id = t.object_id
INNER JOIN sys.schemas AS s ON t.schema_id = s.schema_id
INNER JOIN sys.indexes AS i ON ps.object_id = i.object_id AND ps.index_id = i.index_id
WHERE ps.index_id > 0
AND ps.page_count >= 1000
AND ps.avg_fragmentation_in_percent > 5
ORDER BY ps.avg_fragmentation_in_percent DESC;
Repository script: 05_Index_Statistics/physical_stats_and_heaps.sql (read-only report). To apply maintenance online: 05_Index_Statistics/index_maintenance_online.sql — set @ExecuteMaintenance = 0 first for a dry run.
-- Reorganize (5–30% fragmentation, always online)
ALTER INDEX [IX_OrderDate] ON [dbo].[Orders]
REORGANIZE WITH (LOB_COMPACTION = ON);
-- Rebuild (>30% fragmentation — Enterprise/Developer/Azure for ONLINE)
ALTER INDEX [IX_OrderDate] ON [dbo].[Orders]
REBUILD PARTITION = ALL WITH (
ONLINE = ON,
SORT_IN_TEMPDB = ON,
WAIT_AT_LOW_PRIORITY (MAX_DURATION = 0 MINUTES, ABORT_AFTER_WAIT = BLOCKERS)
);
Stale statistics cause bad cardinality estimates and plan regressions after migration. Flag stats where modifications exceed 20–30% of row count since last update.
-- Stale statistics in current database
SELECT
OBJECT_NAME(s.object_id) AS table_name,
s.name AS statistics_name,
sp.last_updated,
sp.rows,
sp.modification_counter,
CAST(sp.modification_counter AS DECIMAL(18,2))
/ NULLIF(sp.rows, 0) * 100 AS modification_pct
FROM sys.stats AS s
CROSS APPLY sys.dm_db_stats_properties(s.object_id, s.stats_id) AS sp
WHERE OBJECTPROPERTY(s.object_id, 'IsUserTable') = 1
AND CAST(sp.modification_counter AS DECIMAL(18,2))
/ NULLIF(sp.rows, 0) * 100 >= 20
ORDER BY modification_pct DESC;
Repository script: 05_Index_Statistics/statistics_freshness.sql (all user databases).
-- Single statistic (preferred for large tables during business hours)
UPDATE STATISTICS [dbo].[Orders] ([IX_OrderDate]) WITH FULLSCAN;
-- Entire table (off-peak; can be expensive on large tables)
UPDATE STATISTICS [dbo].[Orders] WITH FULLSCAN;
-- Large table alternative — sampled update
UPDATE STATISTICS [dbo].[Orders] WITH SAMPLE 30 PERCENT;
-- All user databases: schedule via Ola Hallengren IndexOptimize
-- or run statistics_freshness.sql and update flagged objects only
With transaction log backups every 5–15 minutes, the SQL Server Error Log fills rapidly with thousands of "Backup succeeded" messages, making it difficult to find real errors during an outage.
Recommendation: Add Trace Flag 3226 as a startup parameter. It suppresses successful backup messages while still logging failures.
To add: Open SQL Server Configuration Manager → SQL Server Services → right-click SQL Server → Properties → Startup Parameters. Add -T3226 and restart.
-- Verify the trace flag is active
DBCC TRACESTATUS(3226);
ALTER LOGIN sa DISABLE) or renamed
DOMAIN\DBA_Group)
sys.traces)
READ_WRITE, 2048 MB max, AUTO capture mode
Context: Most SQL Server deployments are now virtualized — on Hyper-V, VMware vSphere, or Azure. A VM that inherits default virtualization settings will leave significant performance on the table. This section covers the additional steps required when SQL Server runs inside a virtual machine.
Critical: Disable Dynamic Memory (Hyper-V) or Memory Ballooning (VMware) for all SQL Server VMs. SQL Server manages its own buffer pool and expects a fixed amount of memory. Ballooning/dynamic memory causes SQL to be paged out, leading to severe performance degradation or even the SQL Server process being killed by the OS memory manager.
| Setting | Production Recommendation |
|---|---|
| VHDX vs VHD | VHDX (4 KB logical sector support, better alignment, larger max size) |
| VHDX Type | Fixed size for SQL data/log/TempDB volumes — never dynamically expanding |
| Pass-through Disks | Acceptable but add operational complexity; modern VHDX with fixed size performs equivalently |
| Host Caching | Disable host caching on SQL data/log volumes (write-through) — enable only on read-heavy volumes if supported |
| Storage QoS | Set minimum IOPS reservations on the host to prevent noisy-neighbor interference |
Windows Server VMs running on Hyper-V have access to "enlightenments" — optimized code paths that detect the hypervisor and use hypercalls instead of emulated instructions. These are enabled by default on modern Windows Server but should be verified:
Get-VMIntegrationService)
For VMs with more than 64 vCPUs, enable Virtual NUMA so SQL Server can properly schedule across processor groups. For VMs with fewer than 64 vCPUs, let the hypervisor and SQL Server auto-manage NUMA — no manual intervention needed beyond the MAXDOP guidance in Section 15.
For high-throughput SQL workloads (Always On synchronous replication, backup streaming), use SR-IOV (Single Root I/O Virtualization) to bypass the virtual switch and reduce latency. If SR-IOV is not available, ensure VMQ (Virtual Machine Queue) is enabled on the host for the physical NIC.
Extend antivirus exclusions to the host level as well. If the host scans VHDX files while SQL Server is writing to them, it causes IO latency spikes inside the VM. Exclude .vhdx, .vhd, .avhdx (snapshots/checkpoints), and the VM storage path from host-side real-time scanning.
.vhdx, .vhd, .avhdx files and VM storage path
When to use this section: The instance is online after migration but queries are slower than the source server — especially with high elapsed time, low CPU, and moderate logical reads. Work through these checks in order before chasing query hints or compatibility level changes.
Excessive VLFs in the transaction log slow down log backups, recovery, replication, and AG sync. Common after many small autogrowth events during migration restore.
| VLF Count | Status | Action |
|---|---|---|
| < 200 | Good | None |
| 200–500 | Warning | Monitor; fix autogrowth on next maintenance window |
| > 1000 | Critical | Rebuild log during maintenance window |
-- VLF count per database (SQL Server 2016+)
SELECT
d.name AS database_name,
COUNT(*) AS vlf_count,
CAST(SUM(vlf_size_mb) AS DECIMAL(18,2)) AS total_log_size_mb
FROM sys.databases AS d
CROSS APPLY sys.dm_db_log_info(d.database_id) AS v
WHERE d.database_id > 4 AND d.state = 0
GROUP BY d.name
ORDER BY vlf_count DESC;
Remediation (maintenance window):
DBCC SHRINKFILE (N'LogLogicalName', TRUNCATEONLY);FILEGROWTH going forward.-- Example: grow log in controlled steps after shrink
ALTER DATABASE [YourDB] MODIFY FILE (NAME = N'YourDB_log', SIZE = 8192MB, FILEGROWTH = 2048MB);
Repository script: 03_Storage_Engine/vlf_fragmentation.sql
Post-migration slowness is frequently storage-related — especially cold buffer pool, shared VM storage, or logs on slow volumes. sys.dm_io_virtual_file_stats is cumulative since instance start; note sqlserver_start_time before comparing.
| Avg I/O Stall | Status |
|---|---|
| < 5 ms | Good |
| 5–15 ms | Acceptable |
| 15–20 ms | Warning — investigate |
| > 20 ms | Critical — storage remediation required |
-- Per-file read/write latency
SELECT
DB_NAME(vfs.database_id) AS database_name,
mf.name AS logical_file,
mf.physical_name,
mf.type_desc,
CAST(vfs.io_stall_read_ms / NULLIF(vfs.num_of_reads, 0) AS DECIMAL(10,2)) AS avg_read_ms,
CAST(vfs.io_stall_write_ms / NULLIF(vfs.num_of_writes, 0) AS DECIMAL(10,2)) AS avg_write_ms,
CAST(vfs.io_stall / NULLIF(vfs.num_of_reads + vfs.num_of_writes, 0) AS DECIMAL(10,2)) AS avg_io_ms
FROM sys.dm_io_virtual_file_stats(NULL, NULL) AS vfs
INNER JOIN sys.master_files AS mf
ON vfs.database_id = mf.database_id AND vfs.file_id = mf.file_id
ORDER BY avg_io_ms DESC;
-- Pending I/O backlog
SELECT COUNT(*) AS pending_io, MAX(io_pending_ms_ticks) AS max_pending_ms
FROM sys.dm_io_pending_io_requests;
Remediation:
PAGEIOLATCH_*, WRITELOG, IO_COMPLETION.Repository scripts: 01_Server_OS/disk_latency.sql, Prod_Migration/08_Storage_OS/01_io_latency_deep_dive.sql
Percent-based autogrowth and tiny growth increments cause repeated growth events, VLF proliferation, and I/O pauses during peak load — especially on multi-TB databases after migration.
-- Autogrowth and space utilization (all databases)
SELECT
DB_NAME(database_id) AS database_name,
name AS logical_name,
type_desc,
size * 8 / 1024 AS size_mb,
CASE WHEN is_percent_growth = 1
THEN CAST(growth AS VARCHAR(10)) + '%'
ELSE CAST(growth * 8 / 1024 AS VARCHAR(10)) + ' MB'
END AS autogrowth,
is_percent_growth,
physical_name
FROM sys.master_files
WHERE database_id > 4
ORDER BY database_name, type_desc;
Remediation:
-- Fix percent-based autogrowth
ALTER DATABASE [YourDB] MODIFY FILE (NAME = N'YourDB', FILEGROWTH = 1024MB);
ALTER DATABASE [YourDB] MODIFY FILE (NAME = N'YourDB_log', FILEGROWTH = 2048MB);
Repository script: 03_Storage_Engine/database_files_growth.sql
TempDB contention (PAGELATCH on PFS/GAM/SGAM) affects every workload using sorts, hashes, and spools. After migration, TempDB often retains default file count, uneven sizes, or percent growth.
Checklist:
Repository scripts: 03_Storage_Engine/tempdb_configuration.sql, Prod_Migration/08_Storage_OS/03_tempdb_autogrowth_audit.sql
Repository playbook: Prod_Migration/01_Quick_Triage/00_RUN_FIRST_triage_playbook.sql
| # | Item | Change |
|---|---|---|
| 1 | H: drive for SQL binaries | Corrected: Binaries remain on C: drive — separating binaries is unnecessary and complicates patching. |
| 2 | Instant File Initialization verification | Corrected: Uses sys.dm_server_services.instant_file_initialization_enabled (SQL 2022+) instead of generic SELECT *. |
| 3 | MAXDOP guidance | Expanded: Added per-core count recommendations and referenced Microsoft KB 2806535. |
| 4 | TempDB file count guidance | Clarified: 8 file max unless contention observed; discourage 1:1 with cores. |
| 5 | Page File | Added: Page file sizing guidance (missing from original). |
| 6 | Auto Close / Auto Shrink | Added: Must be OFF for all production databases. |
| 7 | Page Verify CHECKSUM | Added: Verify this setting on all databases. |
| 8 | Network configuration | Added: TCP/IP enablement, Windows Firewall rules, port configuration. |
| 9 | Security hardening | Expanded: Added xp_cmdshell, Ole Automation, CLR, cross-DB chaining, ad hoc workload optimization, server audit. |
| 10 | Backup checksum default | Added: backup checksum default for SQL 2022+ as a best practice. |
| 11 | Error log retention | Added: Set to 99 files via registry. |
| 12 | Operators & Alerts | Expanded: Added sample T-SQL for alert creation and operator setup. |
| 13 | Database Mail | Expanded: Added T-SQL creation script and testing procedure. |
| 14 | Backup restore validation | Emphasized: Added note that RESTORE VERIFYONLY is insufficient — must perform actual restore test. |
| 15 | Memory reservation formula | Expanded: Added table with multiple RAM sizes and noted Lock Pages in Memory behavior. |
| 16 | ReFS support | Added: ReFS with 64 KB cluster as an option for Data/Backup volumes on Windows Server 2022+. |
| 17 | BIOS power verification | Added: BIOS/UEFI power management set to Maximum Performance — Windows cannot override a restrictive BIOS. |
| 18 | Trace Flag 3226 | Added: Startup parameter to suppress successful backup messages in error log; critical when log backups run every 5–15 min. |
| 19 | Fail-Safe Operator | Added: Ensures alert delivery even when msdb or operator table is unavailable. |
| 20 | Max Worker Threads | Added: Verification step for high-core servers to avoid THREADPOOL waits. |
| 21 | LPIM clarification | Clarified: Lock Pages in Memory must be assigned via secpol.msc with the exact account name — group membership is insufficient. |
| 22 | Autogrowth for large DBs | Added: Warning about insufficient autogrowth sizes (e.g., 100 MB) on multi-TB databases causing VLF fragmentation. |
| 23 | Post-migration debugging | Added: Section 37 — VLF, disk latency, file growth, TempDB triage with remediation scripts. |
| 24 | Index statistics scripts | Added: Fragmentation detection, online rebuild/reorganize, stale stats detection, and UPDATE STATISTICS examples in Section 25. |
| 25 | Permissions reference | Added: Repository permission.sql for SQL Server and PostgreSQL diagnostic roles. |