One DBA's Ongoing Search for Clarity in the Middle of Nowhere


*or*

Yet Another Andy Writing About SQL Server

Wednesday, April 18, 2018

Why Don't I have a Preferred Replica?


The client reported that the log file on their main database was growing unusually large, and when they checked the backup target the database didn't have any LOG backups!

Oh, and the database was part of an Availability Group.

https://imgflip.com/i/28n8j8
--

After some investigation we found that while the Availability Group was configured to prefer the primary replica for backups, it wasn't evaluating as the preferred backup via the system function sys.fn_hadr_backup_is_preferred_replica!

What we found in the output logs for the LOG backup (Hallengren) job looked like this:

--

Availability group: DATABASE1_AG1
Availability group role: PRIMARY
Availability group backup preference: PRIMARY
Is preferred backup replica: No

--

Even though the preferred backup replica is the primary replica, and we are on the primary replica, the is_preferred check says No.

A test showed that a manual BACKUP LOG statement worked, but both the Hallengren DatabaseBackup *and* a "regular" maintenance plan backup ignored Database1.

To check the preferred replica status we manually ran the command in a query window:

--

SELECT sys.fn_hadr_backup_is_preferred_replica(‘Database1’)

--

...it came back as 0 (false) on both nodes – SQL1 and SQL2.


As long as the preferred backup replica is 0, no intelligent backups will run against that replica, which is why backups weren’t happening on SQL1 or SQL2.

--

Now what?  Back to the old standby:

https://imgflip.com/i/28n92w

As described in the solution post, one possible answer is if the internal server names don't match - we checked the servers and sure enough:

--

SELECT SERVERPROPERTY('ServerName') as [InstanceName]
, SERVERPROPERTY('ComputerNamePhysicalNetBIOS') as [ComputerNamePhysicalNetBIOS]
, @@SERVERNAME as ServerName

--


InstanceName
ComputerNamePhysicalNetBIOS
@@SERVERNAME
SQL1
SQL1
123-223



InstanceName
ComputerNamePhysicalNetBIOS
@@SERVERNAME
SQL2
SQL2
437-326

--

The SERVERPROPERTY names reflect the friendly SQL1 and SQL2 names while the @@SERVERNAME reflects the client's standard infrastructure team ###-### names.

This often means that the Windows Server was renamed after SQL was installed but the final step of renaming SQL wasn’t taken (more on that at the end of the email).

--

Renaming the servers (taking a downtime) was not immediately acceptable so after some consideration we tried manually editing the code of the master.dbo.DatabaseBackup procedure to hard-code the fact that the primary replica is truly the preferred backup replica:

-- 
Original code: 
IF @Version >= 11 AND @Cluster IS NOT NULL AND @CurrentAvailabilityGroup IS NOT NULL
BEGIN
SELECT @CurrentIsPreferredBackupReplica = sys.fn_hadr_backup_is_preferred_replica(@CurrentDatabaseName)
END


Modified code: 
IF @Version >= 11 AND @Cluster IS NOT NULL AND @CurrentAvailabilityGroup IS NOT NULL AND @CurrentAvailabilityGroupRole = 'PRIMARY'
BEGIN
SELECT @CurrentIsPreferredBackupReplica = 1
END
--
               
…and that allowed the backups to run successfully by setting the procedure to true as long as the replica is primary.

https://blogger.googleusercontent.com/img/b/R29vZ2xl/AVvXsEgXvs_E_2M3yuNsiycQus0yJq5Nh_Wb7A5V6iTo4clDhL_4CPhXPWU7O9uHUg-06Bw3vEkW23znYcE_ejypyVGyX9f5zZtTx2SMTba_crycTE3ZmcACc-KG1_HklbmbCbrxcTz0b721VOI/s1600/kool-aid.jpg

--

One other catch that came up after that also relates directly back to the mismatched names…after making the above change, the LOG backup of Database1 was succeeding but the overall LOG backup job was still failing.  Looking in the job output we found an error:

--

Msg 50000, Level 16, State 1, Server SQL1, Procedure DatabaseBackup, Line 1041

--

The author of DatabaseBackup (Ola Hallengren) is ready for the fact that mismatched names can cause issues and has a custom error built into his procedure to throw the Error 50000 if the names don’t match.  The procedure proceeds to run backups if it can, but still ultimately fails overall with the 50000 message.

To mitigate this we further edited the code of DatabaseBackup to comment out the code block that generates this specific error:

--

/*
  2018-04-18 - Commented out to prevent 50000 error since @@SERVERNAME does not match ServerName

  AndyG, Ntirety


  IF @@SERVERNAME <> SERVERPROPERTY('ServerName') AND SERVERPROPERTY('IsHadrEnabled') = 1
  BEGIN
    SET @ErrorMessage = 'The @@SERVERNAME does not match SERVERPROPERTY(''ServerName''). See ' + CASE WHEN SERVERPROPERTY('IsClustered') = 0 THEN 'https://docs.microsoft.com/en-us/sql/database-engine/install-windows/rename-a-computer-that-hosts-a-stand-alone-instance-of-sql-server' WHEN SERVERPROPERTY('IsClustered') = 1 THEN 'https://docs.microsoft.com/en-us/sql/sql-server/failover-clusters/install/rename-a-sql-server-failover-cluster-instance' END + '.' + CHAR(13) + CHAR(10) + ' '
    RAISERROR(@ErrorMessage,16,1) WITH NOWAIT
    SET @Error = @@ERROR
  END
*/

--

This directly bypasses the error check and prevents the 50000 error for the name mismatch.

--

As an aside I want to comment on the code editing we were doing - when modifying code of any type consider the source and their licensing/intellectual property rights.  Ola's licensing is very short and sweet:

--

License 
The SQL Server Maintenance Solution is licensed under the MIT license, a popular and widely used open source license.
Copyright (c) 2018 Ola Hallengren 
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
https://ola.hallengren.com/license.html 

--

At the end of the day as long as you don't remove his copyright notices you have the right to modify his code.

This is very common with code online - ALWAYS ATTRIBUTE YOUR SOURCES - but if you are looking at any type of commercial code be extremely careful ; not only may you be violating the letter (or spirit) of the law, you may even prevent the code's creator from supporting you in the future!

--

The edits to DatabaseBackup are allowing the jobs to run successfully for now, but it is truly a bandage rather than a real fix.  The ultimate fix is to rename the SQL Server internally (as described at the link in Ola’s 5000 error https://docs.microsoft.com/en-us/sql/database-engine/install-windows/rename-a-computer-that-hosts-a-stand-alone-instance-of-sql-server)

The document title references a “stand-alone instance of SQL Server” but that is what this is since it is two SQL Servers with an availability group rather than a failover cluster (shared) instance.

*BUT*

It is unknown from the information here whether changing the @@SERVERNAME from 123-223 to SQL1 would impact any other business or application processes – this is something the client's teams would need to determine before making any changes to the name.

Also changing the server name would require a SQL downtime (service restart, not Windows reboot) for the name change to take effect (as described in the Microsoft doc) so even if you are comfortable with the name change it needs to be scheduled for a downtime window.

--

Hope this helps!


Wednesday, April 4, 2018

Toolbox - Do You Have Current Backups?

On our managed servers we run a job each day to check that either a FULL or DIFF has run in the last 24 hours, but all-too-frequently I find myself on a server that we don't manage, trying to determine the current situation (aka the "health check").

An important thing to check with the health check - or even when responding to a 911-SAVEME call - is whether there are current backups.

https://stephcalvertart.com/wp-content/uploads/2014/07/funny-memes-wordpress-maintenance-backups-updates-hearts-and-laserbeams-star-wars-obi-wan-kenobi.jpg
Similar to my previous Toolbox post,  I wanted a script I could just run without worrying whether the server was 2005 or 2016, and it also needed to handle availability groups.  This second requirement made the first even more important as their are new DMV's for availability groups that are needed in SQL 2012+ but *don't exist* in 2005-2008.

I used the same SQLVersion logic as the previous post, CONVERTing the SERVERPROPERTY('ProductVersion') into a four character value:
9.00
10.0
10.5
11.0
12.0
13.0
...which I then used to branch the code.

Like most of my code, I started with several general queries (cited in the code) and then modified them significantly for my needs. #AlwaysGiveCreditWhereDue

--


https://memegenerator.net/instance/53270254/yeah-thatd-be-great-yeah-if-you-could-just-show-me-some-code-thatd-be-great
Without further ado, the code:

--
/*
Most Recent Backups

There is a code branch to allow for availability groups so that it is visible
whether the given database is the preferred backup replica or not
since backups may appear “missing” on the non-preferred replicas

Guts of availability group backup query modified from:
http://www.centinosystems.com/blog/sql/when-was-your-last-backup/

Modified to exclude AG-only fields for pre-2012
*/


DECLARE @SQLVersion as NVARCHAR(4)

SET @SQLVersion = LEFT(CONVERT(VARCHAR, SERVERPROPERTY('ProductVersion')),4)

/*
PRINT @SQLVersion
*/


IF @SQLVersion in ('9.00', '10.0', '10.5')
BEGIN
SELECT @@SERVERNAME as [InstanceName]
, db.[name] as [DatabaseName]
, db.[recovery_model_desc] as [RecoveryModelDescription]
, 'Not AG Database' as [IsAGDatabase] /* Placeholder since no AGs before 2012 */
, 'N/A' as [AGName]
, 'N/A' as [IsPreferredReplica] /* Placeholder since no AGs before 2012 */
,  CASE
WHEN LastFullBackup is NULL
THEN 'NONE'
ELSE 
CONVERT(varchar(10),t.[LastFullBackup], 111) + ' ' /* Format YYYY/MM/DD */
+ CONVERT(varchar(10),t.[LastFULLBackup], 108) /* Format HH:MM:SS */
  END  as [LastFullBackupDate]
, CASE
WHEN LastDiffBackup is NULL
THEN 'NONE'
ELSE 
CONVERT(varchar(10),t.[LastDiffBackup], 111) + ' '  /* Format YYYY/MM/DD */
+ CONVERT(varchar(10),t.[LastDiffBackup], 108) /* Format HH:MM:SS */
  END  as [LastDiffBackupDate]
, CASE
WHEN db.[recovery_model_desc] = 'SIMPLE'
THEN 'N/A - SIMPLE RECOVERY' /* No LOG backups in SIMPLE */
WHEN LastLogBackup is NULL
THEN 'NONE'
ELSE 
CONVERT(varchar(10),t.[LastLogBackup], 111) + ' ' /* Format YYYY/MM/DD */ 
+ CONVERT(varchar(10),t.[LastLogBackup], 108) /* Format HH:MM:SS */
  END as [LastLogBackupDate]
, CASE
WHEN LastFullBackup is NULL
THEN 'N/A - NO FULLS'
ELSE  cast(t.[DaysSinceLastFullBackup] as varchar(100))
  END as [DaysSinceLastFullBackup]
, CASE
WHEN LastDiffBackup is NULL
THEN 'N/A - NO DIFFS'
ELSE  cast(t.[DaysSinceLastDiffBackup] as varchar(100))
  END as [DaysSinceLastDiffBackup]
, CASE
WHEN db.[recovery_model_desc] = 'SIMPLE'
THEN 'N/A - SIMPLE RECOVERY' /* No LOG backups in SIMPLE */
WHEN LastLogBackup is NULL
THEN 'N/A - NO LOGS'ELSE
cast (t.[MinutesSinceLastLogBackup]  as varchar(100))
  END as [MinutesSinceLastLogBackup]
FROM sys.databases db
LEFT OUTER JOIN
(
SELECT p.[database_name] as [DatabaseName]
, MAX(p.[D]) as [LastFullBackup]
, MAX(p.[I]) as [LastDiffBackup]
, MAX(p.[L]) as [LastLogBackup]
, DATEDIFF(DAY, MAX(p.[D]), GETDATE()) as [DaysSinceLastFullBackup]
, DATEDIFF(DAY, MAX(p.[I]), GETDATE()) as [DaysSinceLastDiffBackup]
, DATEDIFF(MINUTE, MAX(p.[L]), GETDATE()) as [MinutesSinceLastLogBackup]
FROM msdb.dbo.backupset bs
PIVOT (MAX(bs.backup_finish_date) FOR [type] IN ([D],[L],[I])) as p
GROUP BY p.[database_name]
) t
ON db.[name] = t.[DatabaseName]
/*
-- Commented Out since no AG's before 2012
LEFT OUTER JOIN sys.dm_hadr_database_replica_states agdb
ON agdb.[database_id] = db.[database_id]
AND agdb.[is_local] = 1
LEFT OUTER JOIN sys.dm_hadr_name_id_map agmap
ON agdb.group_id = agmap.ag_id
*/
WHERE db.[name]<>'tempdb'
ORDER BY LastFullBackupDate DESC
END

ELSE /* SQL 2012+ */

BEGIN
SELECT @@SERVERNAME as [InstanceName]
, db.[name] as [DatabaseName]
, db.[recovery_model_desc] as [RecoveryModelDescription]
, CASE
  WHEN agdb.[database_id] IS NOT NULL
  THEN 'AG Database'
  ELSE 'Not AG Database'
  END as [IsAGDatabase]
, ISNULL(agmap.ag_name,'N/A') as [AGName]
, CASE
  WHEN
  sys.fn_hadr_backup_is_preferred_replica(db.[name]) = 1
  AND agdb.[database_id] IS NOT NULL
  THEN 'YES'
  WHEN sys.fn_hadr_backup_is_preferred_replica(db.[name]) = 1
  AND agdb.[database_id] IS NULL
  THEN 'N/A'
  ELSE 'NO'
  END as [IsPreferredReplica]
, CASE
WHEN LastFullBackup is NULL
THEN 'NONE'
ELSE 
CONVERT(varchar(10),t.[LastFullBackup], 111) + ' ' /* Format YYYY/MM/DD */ 
+ CONVERT(varchar(10),t.[LastFULLBackup], 108) /* Format HH:MM:SS */
  END  as [LastFullBackupDate]
, CASE
WHEN LastDiffBackup is NULL
THEN 'NONE'
ELSE 
CONVERT(varchar(10),t.[LastDiffBackup], 111) + ' ' /* Format YYYY/MM/DD */ 
+ CONVERT(varchar(10),t.[LastDiffBackup], 108) /* Format HH:MM:SS */
  END  as [LastDiffBackupDate]
, CASE
WHEN db.[recovery_model_desc] = 'SIMPLE'
THEN 'N/A - SIMPLE RECOVERY' /* No LOG backups in SIMPLE */
WHEN LastLogBackup is NULL
THEN 'NONE'
ELSE 
CONVERT(varchar(10),t.[LastLogBackup], 111) + ' ' /* Format YYYY/MM/DD */ 
+ CONVERT(varchar(10),t.[LastLogBackup], 108) /* Format HH:MM:SS */
  END as [LastLogBackupDate]
, CASE
WHEN LastFullBackup is NULL
THEN 'N/A - NO FULLS'
ELSE  cast(t.[DaysSinceLastFullBackup] as varchar(100))
  END as [DaysSinceLastFullBackup]
, CASE
WHEN LastDiffBackup is NULL
THEN 'N/A - NO DIFFS'
ELSE  cast(t.[DaysSinceLastDiffBackup] as varchar(100))
  END as [DaysSinceLastDiffBackup]
, CASE
WHEN db.[recovery_model_desc] = 'SIMPLE'
THEN 'N/A - SIMPLE RECOVERY' /* No LOG backups in SIMPLE */
WHEN LastLogBackup is NULL
THEN 'N/A - NO LOGS'ELSE
cast (t.[MinutesSinceLastLogBackup]  as varchar(100))
  END as [MinutesSinceLastLogBackup]
FROM sys.databases db
LEFT OUTER JOIN
(
SELECT p.[database_name] as [DatabaseName]
, MAX(p.[D]) as [LastFullBackup]
, MAX(p.[I]) as [LastDiffBackup]
, MAX(p.[L]) as [LastLogBackup]
, DATEDIFF(DAY, MAX(p.[D]), GETDATE()) as [DaysSinceLastFullBackup]
, DATEDIFF(DAY, MAX(p.[I]), GETDATE()) as [DaysSinceLastDiffBackup]
, DATEDIFF(MINUTE, MAX(p.[L]), GETDATE()) as [MinutesSinceLastLogBackup]
FROM msdb.dbo.backupset bs
PIVOT (MAX(bs.backup_finish_date) FOR [type] IN ([D],[L],[I])) as p
GROUP BY p.[database_name]
) t ON db.[name] = t.[DatabaseName]
LEFT OUTER JOIN sys.dm_hadr_database_replica_states agdb
ON agdb.[database_id] = db.[database_id]
AND agdb.[is_local] = 1
LEFT OUTER JOIN sys.dm_hadr_name_id_map agmap
ON agdb.group_id = agmap.ag_id
WHERE db.[name]<>'tempdb'
ORDER BY LastFullBackupDate DESC
END
--

The results look like this (split for readability):

--

InstanceName DatabaseName Recovery
Model
Description
IsAGDatabase AGName IsPreferredReplica
Instance01 Database01 FULL Not AG Database N/A N/A
Instance01 Database02 FULL AG Database AvailGroup99 YES
Instance01 Database03 FULL AG Database AvailGroup99 YES
Instance01 Database04 FULL AG Database AvailGroup99 YES
Instance01 Database05 FULL AG Database AvailGroup99 YES
Instance01 Database06 FULL AG Database AvailGroup99 YES
Instance01 msdb SIMPLE Not AG Database N/A N/A
Instance01 model FULL Not AG Database N/A N/A
Instance01 master SIMPLE Not AG Database N/A N/A

InstanceName DatabaseName LastFullBackupDate LastDiffBackupDate LastLogBackupDate
Instance01 Database01 NONE NONE NONE
Instance01 Database02 2018/04/03 22:30:39 NONE 2018/04/04 13:01:09
Instance01 Database03 2018/04/03 22:30:38 NONE 2018/04/04 13:01:08
Instance01 Database04 2018/04/03 22:30:38 NONE 2018/04/04 13:01:09
Instance01 Database05 2018/04/03 22:29:40 NONE 2018/04/04 13:01:02
Instance01 Database06 2018/04/03 22:28:29 NONE 2018/04/04 13:01:01
Instance01 msdb 2018/04/03 22:00:05 NONE N/A - SIMPLE RECOVERY
Instance01 model 2018/04/03 22:00:04 NONE NONE
Instance01 master 2018/04/03 22:00:02 NONE N/A - SIMPLE RECOVERY

InstanceName DatabaseName DaysSinceLastFullBackup DaysSinceLastDiffBackup MinutesSinceLastLogBackup
Instance01 Database01 N/A - NO FULLS N/A - NO DIFFS N/A - NO LOGS
Instance01 Database02 1 N/A - NO DIFFS 23
Instance01 Database03 1 N/A - NO DIFFS 23
Instance01 Database04 1 N/A - NO DIFFS 23
Instance01 Database05 1 N/A - NO DIFFS 23
Instance01 Database06 1 N/A - NO DIFFS 23
Instance01 msdb 1 N/A - NO DIFFS N/A - SIMPLE RECOVERY
Instance01 model 1 N/A - NO DIFFS N/A - NO LOGS
Instance01 master 1 N/A - NO DIFFS N/A - SIMPLE RECOVERY

--

The code flags whether each database is in an AG, and if so whether it is currently the primary replica and the name of its availability group.

It calculates the age of the most recent backup of each type and then sorts by the FULL value desc.

--

I use this query all the time - hope this helps!


Thursday, March 29, 2018

Toolbox - Just the Basics

There are many query sets out there to collect varying groups of configuration data - the ones I most frequently use come from Glenn Berry of SQLskills (blog/@GlennAlanBerry) but I have found that there is more data than his basic set that I like to pull, and I wanted a single query that I could run across multiple versions of SQL Server.  Glenn's basic setup is version-specific - one script for 2005, a different for 2008, etc. - and I wanted to be able to just copy-paste my script in and press Execute.



I ended up starting with some base DMV info and ran it trial-and-error against different versions until I found the fields and formats that are different.  The biggest branch is between SQL 2005-2008R2 and 2012+; there were lots of changes to the dynamic management views in SQL 2012 - so many that I ended up creating two separate branches of code with a version check at the top to determine which branch the execution goes down.

There are multiple ways to determine the SQL Server version, but the one I found that I liked the best in this case is the SERVERPROPERTY('ProductVersion'):

SET @SQLVersion = LEFT(CONVERT(VARCHAR, SERVERPROPERTY('ProductVersion')),4)PRINT @SQLVersion

This returned a nice four character value that was easy to use in the code (it's converted to a char so that I can use the LEFT function to trim it):

9.00
10.0
10.5
11.0
12.0
13.0
--

The biggest change between the versions is a new DMV added in SQL 2008R2 SP1, sys.dm_os_windows_info.  I very much like having a DMV with some basic Windows information:
SELECT *
FROM sys.dm_os_windows_info 
windows_release windows_service_pack_level windows_sku os_language_version
6.3 7 1033

...but what I don't like is that it was added in SP1 - SQL 2008R2 RTM doesn't have this view.  What that meant to my code was that I reference this view in the 2012+ branch but not in the 2005-2008R2 branch since I would have to get more specific (to the SP level) to determine whether I could use it in 2008R2.

http://s2.quickmeme.com/img/cc/cc63896fead20f99f3381a5c389587c48263f258d755be752624b60b62769bab.jpg

--

To return RAM info, I ended up using a temp table (not my favorite but it worked here) - there is a deferred name resolution issue with returning the data due to changes in SQL 2012+.  Even with the IF...ELSE code branch I was using to split my code, the changes in this particular query broke without using an up-front temp table to hold the RAM info (which I then added into the data return query).

--

So...the query:

/*
Physical RAM data comes from the sys.dm_os_sys_info DMV 
The catch is that in SQL 2012 the field changed from being measured in bytes to KB and the field name changed. 
I am collecting the data in a Temp Table because it was the only way I could find to handle deferred name resolution - by having the
DMV inline in a regular query the name resolution errors out even inside an IF ELSE statement.
The temp table is then selected from in the actual data return query below.
IF...ELSE...EXEC Code slightly modified from:
https://social.msdn.microsoft.com/Forums/sqlserver/en-US/442ce31a-9f08-4fa2-90ce-aaba23e05fac/gathering-physical-memory-for-sql-2005sql-2014?forum=transactsql
*/
IF OBJECT_ID('tempdb..#RAM') IS NOT NULL
DROP TABLE #RAM
CREATE TABLE #RAM
(
InstanceName SYSNAME,
Physical_Mem_MB INT
)
IF LEFT(CAST(SERVERPROPERTY('ResourceVersion') as VARCHAR(20)),2) in ('11','12','13','14')
EXEC ('INSERT INTO #RAM SELECT @@SERVERNAME, (physical_memory_KB/1024) as Physical_Mem_MB FROM sys.dm_os_sys_info')
ELSE
EXEC ('INSERT INTO #RAM SELECT @@SERVERNAME, (physical_memory_in_bytes/1024/1024) as Physical_Mem_MB FROM sys.dm_os_sys_info')
--
/*
The actual data return query for the Instance settings
There is a major branch for SQL 2005-2008R2 and a separate branch for 2012+, again because of differing field names and new DMVs between versions
Much of the data is selected as inline queries to prevent the need to tack on the Instance Name and then create a huge multi-way join
*/
DECLARE @SQLVersion as NVARCHAR(4)
SET @SQLVersion = LEFT(CONVERT(VARCHAR, SERVERPROPERTY('ProductVersion')),4)
/*
PRINT @SQLVersion
*/
IF @SQLVersion in ('9.00', '10.0', '10.5')
SELECT SERVERPROPERTY('ServerName') as [InstanceName]
, SERVERPROPERTY('ComputerNamePhysicalNetBIOS') as [ComputerNamePhysicalNetBIOS]
, SERVERPROPERTY('ProductVersion') as [SQLProductVersion]
, @SQLVersion as [SQLMajorVersion]
, CASE @SQLVersion
WHEN '9.00' THEN 'SQL Server 2005'
WHEN '10.0' THEN 'SQL Server 2008'
WHEN '10.5' THEN 'SQL Server 2008 R2'
END as [SQLVersionBuild]
, SERVERPROPERTY('ProductLevel') as [SQLServicePack]
, SERVERPROPERTY('Edition') as [SQLEdition]
, (
SELECT RIGHT(SUBSTRING(@@VERSION, CHARINDEX('Windows NT', @@VERSION), 14), 4)
) as [WindowsVersionNumber]
, (
SELECT CASE LTRIM(RIGHT(SUBSTRING(@@VERSION, CHARINDEX('Windows NT', @@VERSION), 14), 4))
/* LTRIM needed to trim leading space in three character version numbers such as 5.0 */
/* No DMV for this in SQL 2005, 2008, or 2008R2 RTM */
WHEN '5.0' THEN 'Windows 2000'
WHEN '5.1' THEN 'Windows XP'
WHEN '5.2' THEN 'Windows Server 2003/2003 R2'
WHEN '6.0' THEN 'Windows Server 2008/Windows Vista'
WHEN '6.1' THEN 'Windows Server 2008 R2/Windows 7'
WHEN '6.2' THEN 'Windows Server 2012/Windows 8'
WHEN '6.3' THEN 'Windows Server 2012 R2'
WHEN '10.0' THEN 'Windows Server 2016'
ELSE 'Windows vNext'
END
) as [WindowsVersionBuild]
, (
SELECT [Physical_Mem_MB]
FROM #RAM
) as [PhysicalMemMB]
, (
SELECT [value_in_use]
FROM sys.configurations
WHERE name LIKE '%min server memory%'
) as [MinServerMemoryMB]
, (
SELECT [value_in_use]
FROM sys.configurations
WHERE name LIKE '%max server memory%'
) as [MaxServerMemoryMB]
, (
SELECT [cpu_count]
FROM sys.dm_os_sys_info
) as [LogicalCPUCount]
, (
SELECT [hyperthread_ratio]
FROM sys.dm_os_sys_info
) as [HyperthreadRatio]
, (
SELECT [cpu_count]/[hyperthread_ratio]
FROM sys.dm_os_sys_info
) as [PhysicalCPUCount]
, (
SELECT [value_in_use]
FROM sys.configurations
WHERE name LIKE '%max degree of parallelism%'
) as [MAXDOP]
, (
SELECT [value_in_use]
FROM sys.configurations
WHERE name LIKE '%cost threshold for parallelism%'
) as [CTOP]
ELSE /* SQL 2012+ */
SELECT SERVERPROPERTY('ServerName') as [InstanceName]
, SERVERPROPERTY('ComputerNamePhysicalNetBIOS') as [ComputerNamePhysicalNetBIOS]
, SERVERPROPERTY('ProductVersion') as [SQLProductVersion]
, @SQLVersion as [SQLMajorVersion]
, CASE @SQLVersion
WHEN '11.0' THEN 'SQL Server 2012'
WHEN '12.0' THEN 'SQL Server 2014'
WHEN '13.0' THEN 'SQL Server 2016'
ELSE 'SQL Server vNext'
END as [SQLVersionBuild]
, SERVERPROPERTY('ProductLevel') as [SQLServicePack]
, SERVERPROPERTY('Edition') as [SQLEdition]
, (
SELECT [windows_release]
FROM sys.dm_os_windows_info
) as [WindowsVersionNumber]
, (
SELECT CASE windows_release
WHEN '5.0' THEN 'Windows 2000'
WHEN '5.1' THEN 'Windows XP'
WHEN '5.2' THEN 'Windows Server 2003/2003 R2'
WHEN '6.0' THEN 'Windows Server 2008/Windows Vista'
WHEN '6.1' THEN 'Windows Server 2008 R2/Windows 7'
WHEN '6.2' THEN 'Windows Server 2012/Windows 8'
WHEN '6.3' THEN 'Windows Server 2012 R2'
WHEN '10.0' THEN 'Windows Server 2016'
ELSE 'Windows vNext'
END
FROM sys.dm_os_windows_info /* New DMV in SQL 2008 R2 SP1*/
) as [WindowsVersionBuild]
, (
SELECT [Physical_Mem_MB]
FROM #RAM
) as [PhysicalMemMB]
, (
SELECT [value_in_use]
FROM sys.configurations
WHERE name LIKE '%min server memory%'
) as [MinServerMemoryMB]
, (
SELECT [value_in_use]
FROM sys.configurations
WHERE name LIKE '%max server memory%'
) as [MaxServerMemoryMB]
, (
SELECT [cpu_count]
FROM sys.dm_os_sys_info
) as [LogicalCPUCount]
, (
SELECT [hyperthread_ratio]
FROM sys.dm_os_sys_info
) as [HyperthreadRatio]
, (
SELECT [cpu_count]/[hyperthread_ratio]
FROM sys.dm_os_sys_info
) as [PhysicalCPUCount]
, (
SELECT [value_in_use]
FROM sys.configurations
WHERE name LIKE '%max degree of parallelism%'
) as [MAXDOP]
, (
SELECT [value_in_use]
FROM sys.configurations
WHERE name LIKE '%cost threshold for parallelism%'
) as [CTOP]
/*
Clean-Up
*/
IF OBJECT_ID('tempdb..#RAM') IS NOT NULL
DROP TABLE [#RAM]
--

The resultset looks like this:


InstanceName ComputerName
PhysicalNetBIOS
SQLProductVersion SQLMajorVersion
Instance1\I1 Server55 12.0.5000.0 12

SQLVersionBuild SQLServicePack SQLEdition
SQL Server 2014 SP2 Standard Edition (64-bit)


Windows
Version
Number
Windows
Version
Build
Physical
MemMB
MinServer
MemoryMB
MaxServer
MemoryMB
6.3 Windows Server 2012 R2 81918 64 66560


LogicalCPUCount HyperthreadRatio PhysicalCPUCount MAXDOP CTOP
12 1 12 1 5

--

I like this a lot as it is a quick way to pull basic SQL, Windows, RAM, and CPU from any SQL 2005+, which is something I need all the time when I touch a new server.

Hope this helps!



Wednesday, March 7, 2018

Which Blogs Do I Recommend?

One of the most important parts of continuing learning as a DBA (or probably an IT Professional) is reading blogs.  The world used to run on professional magazines and journals (and there are a few good ones still out there) but the current equivalent of a weekly or quarterly periodical is a daily/weekly blog produced by anyone from a corporate presence like Microsoft or SentryOne to individual DBA's chronicling their IT journey (like me)!

I used to rely on the Blogger Rankings from Tom LaRock as a good list of general SQL Server blogs, but recently I noticed the rankings list has been removed from his site.  I don't know when it happened but since I frequently referenced new blog readers to his list I figured I should try to recreate a version of it myself.

These are the blogs I most frequently read and reference, although there are many, many, more out there.

https://nebraskasql.blogspot.com/p/blog-list.html

I use feedly as my tool of choice, adding the individual links to my feedly so that all of the blog posts aggregate in one place.

As with any blog posts, read at your own risk and your mileage may vary (and remember, #ItDepends) but these are sources I find to be generally reliable, sorted by their employers for ease of reference.

Even though the blogs are broken out by employer, most of the posts (by far) that you see in these blogs are general SQL Server knowledge and *not* company product marketing (although those sometimes appear as well - for example you will see references to the new version of Plan Explorer Pro on some of the SentryOne blogs, etc.)

--

Hope this helps!

Monday, March 5, 2018

Error 33206 - SQL Server Audit Failed to Create the Audit File

One of the joys of working with SQL Server is the descriptive and meaningful error messages, right?

https://vignette.wikia.nocookie.net/batmantheanimatedseries/images/a/a7/DiD_39_-_Joker_Vision.jpg/revision/latest?cb=20160610021326

Exhibit A - the 33206:
Error: 33206, Severity: 17, State: 1.
SQL Server Audit failed to create the audit file 'T:\MSSQL\Audit Files\PLAND_Objects_DML_Audit_Events_A2B00B57-4B43-4570-93C8-B30EE77CC8C9_0_131645629182310000.sqlaudit'. Make sure that the disk is not full and that the SQL Server service account has the required permissions to create and write to the file.
At first glance, this error tells us one of two things is wrong - the disk is full or the SQL service account doesn't have the required permissions to create and write to a file at the given location.

Simple to troubleshoot, right?

Except when the service account already has Full Control to each folder and sub-folder in the path...


(Note that it is important to check every folder along the path - I have seen situations where the account has permissions to the direct sub-folder (in this example T:\MSSQL\Audit Files) or even the direct sub-folder and the root of the drive (T:) and yet a security check fails due to the intermediate folder not having the correct permissions.  To me it shouldn't work this way, but sometimes it does...)

What about the drive being full?


Maybe not.

Having exhausted the obvious paths (at least obvious from the error text), I went back to my default next step:

https://memegenerator.net/img/instances/52632802/i-see-your-google-fu-is-as-strong-as-mine.jpg

A base search for a "SQL Error 33206" actually brought back a bunch of results about Error 33204 - "SQL Server Audit could not write to the security log" (FYI - the common fix for a 33204 is to grant the SQL service account rights to a related registry key as described here)

A better Google contains a piece of the error message: 
SQL 33206 "SQL Server Audit failed to create the audit file"
The second result in the resultset for this search is a recent blog post from someone I know to be a reliable source, Microsoft Certified Master Jason Brimhall (blog/@sqlrnnr).  

In his post Jason describes the same process I describe above - eliminate the obvious - but then he shows what the real problem was for my situation, the file configuration in the SQL Server Audit configuration:


When I checked the T:\MSSQL\Audit Files folder, sure enough, there were fifteen audit files reaching back over thirteen months' worth of service restarts.

To mitigate the problem I deleted the oldest of the fifteen files, and the Audit resumed.

WOOHOO!

The real fix to this situation is to configure the files as "rollover" files - setting the Audit File Maximum Limit to fifteen "Maximum Rollover Files" instead of fifteen"Maximum Files" would allow the audit to overwrite the oldest file once it reaches to configured max rather than crashing the audit as happened here.

Realize you can only do this if you can handle the oldest files being overwritten; if you have to persist the files you need to create a separate archiving process to handle that.

--

If nothing else, the real silver lining in my particular situation was that the creator of this audit at the client configured the audit to "Continue" rather than "Shut Down Server" (completely shut down Windows - not just stop SQL, but shut down the whole server) or "Fail Operation" (allow SQL to run but fail any operation that would meet the audit specification - not as catastrophic as "Shut Down Server" but still very impactful).

https://www.thesun.co.uk/wp-content/uploads/2016/11/nintchdbpict000273355923.jpg?w=960
There are definitely situations that call for "Shut Down Server" or "Fail Operation" - if your Audit is in place to satisfy a legal/regulatory/moral requirement, then definitely consider these options - but this is often not the case.

--

Hope this helps!