Search This Blog

Monday, January 13, 2025

SQL Server Listener Names: 15-Char Hard Limit

SQL Server Listener Names: 15-Char Hard Limit

There is a hard limit on how long your AlwaysOn listener name can be. That is expected; what may not be expected is that the limit is a mere 15 characters or less.

Yes, even though the maximum length allowed for an AlwaysOn listener DNS name is 63 characters, in practice you can only specify up to 15 characters. This limit comes from the NetBIOS computer name restriction, which is 15 characters, even in the latest versions of Windows 2025. Technically, the maximum length for a NetBIOS name is 16 characters, but the 16th character (NetBIOS Suffix) is reserved for identifying the network device functionality, effectively limiting it to 15 characters.

The limit is for the name part, not the entire Fully Qualified Domain Name (FQDN), which can be up to 255 characters long.

So if you are using SSMS to create an AlwaysOn Listener, it won't even let you type in more than 15 characters:






When I first encountered this issue a few years ago, that should have been my first clue. Did I pay attention and think for a second? Of course not! I spent close to two hours before zeroing in on what the main issue was.

If you bypass the GUI as I did back then and resort to TSQL (or PowerShell), you will receive error similar to this:

Msg 41066, Level 16, State 0, Line 3

Cannot bring the Windows Server Failover Clustering (WSFC) resource (ID '70ce3804-e3ed-4db6-ad42-6936daeeb5aa') online (Error code 5942). 
The WSFC service may not be running or may not be accessible in its current state, or the WSFC resource may not be in a state that could
accept the request.  For information about this error code, see "System Error Codes" in the Windows Development documentation.

Msg 19476, Level 16, State 4, Line 3

The attempt to create the network name and IP address for the listener failed.

The WSFC service may not be running or may be inaccessible in its current state, or
the values provided for the network name and IP address may be incorrect. Check the state of the WSFC cluster and
validate the network name and IP address with the network administrator.














The error is ugly and not intuitive! SQL Server and FCS logs are not very helpful either. 

As a last resort, you could bypass the SQL Server tools completely and create the listener through the Failover Cluster Manager tool, by adding a new resource to your Availability Group of type 'Client Access Point':








On the surface, this will seem to work and complete successfully:









In reality, the newly added Client Access Point will not come online, and your AlwaysOn resource will show as partially failed











Is there a solution or workaround that works?

Sadly, no. Though justifiably annoying, in the Microsoft Windows world, there is no way to get around this limit. The catch-all reason given is of course - "For backward compatibility with legacy Windows Computers and Applications that may still need compatibility with  NetBIOS". Even if you do not have any legacy systems, this requirement is still enforced. The Windows Failover Clustering still requires that the cluster name (the virtual computer name that you can connect to) must strictly adhere to NetBIOS naming convention. And this requirement extends to related services, such as SQL listener names for Always On Availability Groups. Bottom line: If the Windows Failover Cluster (WFC) service cannot successfully register your chosen name in the network (per NetBIOS standards), the resource will be unable to come online, even if the initial resource creation appears successful.

A potential alternative could be to install your SQL Servers in the Linux world without Windows Failover Clustering and Active Directory. I am told you could have a longer name for the AlwaysOn Listener in this environment. However, I have not tested this myself and therefore would not count it as a definitive solution. Even if it works, isn't that too much effort just to name your listener with more than 15 characters?

I ended up making a slight change in my naming convention so that the listener name would not exceed 15 characters.


Resources:


Configure a listener for an Always On availability group

Friday, January 10, 2025

How to clear the SSMS cache?

Clear SSMS Cache - Safe Backup And Reset Tool
Using SSMS 21 or later? This article was written for SSMS 20 and earlier. SSMS 21 is now a native 64-bit app built on Visual Studio 2022, and some of the cache folders and files described here have moved. For the updated guide, see How to Clear the SSMS Cache (SSMS 21 and Later).

If you are reading this, it's probably because you too have this rare need to clear the SSMS cache for some kind of troubleshooting. 

Generally, when I am troubleshooting a connection issue that I wonder is due to a cached or stale connection, the first things that come to mind are clearing the DNS client cache and, occasionally, the ARP cache. Rarely, if ever, do I consider the SSMS cache.  Of course, there are other reasons why you may want to clear the cache.

Since almost everything in the computer world uses some kind of cache, it's no surprise that SQL Server Management Studio (SSMS) also has its own cache. This cache makes our lives easier by remembering things like most recently used (MRU) connections and objects, and enabling features like IntelliSense, auto-completion, syntax highlighting, object names, our preferences, and even idiosyncrasies.  To elaborate on how the cache aids the IntelliSense feature, it stores local copies of database schema metadata, such as tables, columns, and functions, which allows IntelliSense to quickly provide accurate auto-completion suggestions as you type. This enables context-aware recommendations, tracks object dependencies, and improves performance by avoiding repeated database queries. In large, active environments with many DBAs and developers, this can significantly enhance both performance and team productivity.

However, during active database design and development phases where schema changes occur frequently (such as adding new tables or columns), IntelliSense may not immediately recognize these changes until the cache is refreshed.

The cache is stored in what Microsoft refers to as a cache file located in your user profile directory. The cache files are not human-readable per se (it would be a security risk if anyone got hold of those cache files), nor do I currently have any desire to read them. And I'm not concerned right now with the size of the cache or the exact algorithm it uses to decide when to automatically refresh it. However, you can use the keyboard shortcut CTRL+SHIFT+R to manually refresh it. This ability to manually refresh the cache comes in handy if you are part of a team that is actively working and creating new database objects.

You can use SQL Profiler or Extended Events to see which queries are used to populate the cache. These queries will appear as being executed by 'Microsoft SQL Server Management Studio - Transact-SQL IntelliSense' under your login.












*SSMS does not currently update the IntelliSense cache aggressively, nor is it as instantaneous or automatic as one might expect. Therefore, after creating, altering, or dropping database objects, a manual refresh using the shortcut Ctrl+Shift+R is required. Otherwise, SSMS may display outdated schema information in the query window.


And depending on the version of SQL Server Management Studio (SSMS), the cache folder also contains one or more settings and configuration files. For example, in the case of SSMS 20.x, one such file is NewSettings.vssettings (first introduced in SSMS 17.x), which stores your customized preferences and environment settings,  basically all the options and settings you see here:
Click on Tools menu on top then Options:












The file is created or saved (if it already exists) whenever you close SQL Server Management Studio (SSMS).

If you have customized your SSMS appearance and experience significantly, you should consider backing up this file. It is located at "%USERPROFILE%\AppData\Local\Microsoft\SQL Server Management Studio\%Version%\Settings\SQL Server Management Studio".

There are some other configuration files you may want to save, such as Ssms.exe.config, UserSettings.xml, and RegSrvr*.xml. Please scroll down to the end of this article for a brief description

Clearing the cache per se is simple. First, if you have SQL Server Management Studio (SSMS) running, make sure to close it. Copy the following folders somewhere safe in case you need them, then delete them entirely. Alternatively, you could also move (cut and paste) these folders to another location.
  • "%USERPROFILE%\AppData\Local\Microsoft\SQL Server Management Studio"
  • %USERPROFILE%\AppData\Roaming\Microsoft\SQL Server Management Studio"

There is a Microsoft article on clearing everything from SSMS cache:

Clear SSMS Cache Files

Then when you relaunch the SSMS after clearing the cache, you will see the following warning, which you could ignore by clicking OK if you want to start SSMS with a blank slate, or as I just did a bit ago, save the settings files somewhere before deleting the cache files, then copy NewSettings.vssettings back to its original location.










Here is a PowerShell script I use to backup then clear the SSMS cache:

<#
.SYNOPSIS
    Backs up AND CLEARS SQL Server Management Studio (SSMS) cache/configuration folders.

.DESCRIPTION
    This script verifies SSMS (Ssms.exe) is not running, then MOVES SSMS cache and 
    configuration folders from AppData to a timestamped backup directory under 
    Documents. This effectively CLEARS the SSMS cache while preserving a backup.
    
    By default, runs in DryRun mode to preview actions. Use -NoDryRun to execute.

.PARAMETER NoDryRun
    Execute the actual backup/clear operation (default is DryRun simulation).

.NOTES
    Author: [Your Name]
    Updated: 2026-02-02  
    Version: 1.5
    Effect: MOVES folders from AppData --> Backup (clears SSMS cache)

.EXAMPLE
    .\Backup-SsmsCache.ps1
    PREVIEWS the cache clearing operation (DryRun default).

.EXAMPLE
    .\Backup-SsmsCache.ps1 -NoDryRun  
    BACKS UP and CLEARS SSMS cache folders from AppData.
#>

param(
    [switch]$NoDryRun
)

# Determine if DryRun is active (default: $true)
$DryRun = -not $NoDryRun
if ($DryRun) {
    Write-Output "[DRY-RUN MODE] Previewing SSMS cache backup + clear operation."
} else {
    Write-Output "[LIVE MODE] Will backup AND CLEAR SSMS cache folders."
}

# --- Check if SSMS is running -------------------------------------------------
$ssmsProcess = Get-Process -Name "Ssms" -ErrorAction SilentlyContinue
if ($ssmsProcess) {
    Write-Warning "SSMS is running. Close SSMS before clearing cache."
    Throw "SSMS is currently running. Please close SSMS before running this script."
}

# --- Setup backup paths -------------------------------------------------------
$ProfilePath = $env:USERPROFILE
Write-Output "User Profile: $ProfilePath"

$DateTimeSuffix = Get-Date -Format 'yyyyMMdd_HHmmss'
$BackupRoot = Join-Path $ProfilePath 'Documents\SQL Server Management Studio - Backup' 
$BackupPath = Join-Path $BackupRoot $DateTimeSuffix
Write-Output "Backup + Clear Target: $BackupPath"

if ($DryRun) {
    Write-Output "[DryRun] Would create: $BackupPath"
} else {
    New-Item -Path $BackupPath -ItemType Directory -Force | Out-Null
}

# --- Locate and process SSMS cache folders ------------------------------------
$SSMSRoamingPath = Join-Path $env:APPDATA 'Microsoft\SQL Server Management Studio'
$SSMSLocalPath = Join-Path $env:LOCALAPPDATA 'Microsoft\SQL Server Management Studio'

$SSMSPaths = @($SSMSRoamingPath, $SSMSLocalPath) | Where-Object { Test-Path $_ }

if ($SSMSPaths.Count -eq 0) {
    Write-Warning "No SSMS cache folders found in AppData (nothing to clear)."
} else {
    Write-Output "Found $($SSMSPaths.Count) SSMS cache folder(s) to process:"
    foreach ($Path in $SSMSPaths) {
        $RelativePath = $Path.Substring($ProfilePath.Length + 1)
        $DestPath = Join-Path $BackupPath $RelativePath

        Write-Output "  → Source:      $Path"
        Write-Output "  → Backup Dest: $DestPath"
        Write-Output "  → Result:      SSMS cache CLEARED from AppData"

        if ($DryRun) {
            Write-Output "  [DryRun] Would MOVE folder to backup (clearing cache)"
        } else {
            New-Item -Path (Split-Path $DestPath) -ItemType Directory -Force | Out-Null
            Move-Item -Path $Path -Destination $DestPath -Force
            Write-Output "  Cache folder MOVED (cleared from AppData)"
        }
        Write-Output ""
    }
}

if ($DryRun) {
    Write-Output "`n[DRY-RUN COMPLETE] SSMS cache would be cleared. Use -NoDryRun to execute."
} else {
    Write-Output "`nSSMS CACHE SUCCESSFULLY BACKED UP AND CLEARED to: $BackupPath"
}


Finally, here is a brief description of some of the configuration files used file by SSMS:

  • NewSettings.vssettings: Stores user environment and IDE settings such as fonts, colors, window layouts, and editor preferences.
  • Ssms.exe.config: This is the main configuration file for SSMS and  stores runtime settings, assembly bindings, and other .NET framework configuration directives needed for SSMS to run.
  • UserSettings.xml*: Stores specific customization and preferences for SSMS UI behavior, including tool windows, recent files, and other user-level options.
  • RegSrvr.xml*: Stores local registered SQL connection information, such as saved server names, authentication types, and connection properties for servers previously registered in SSMS.

*An important difference to keep in mind between UserSettings.xml and RegSrvr.xml: UserSettings.xml caches your recent ad-hoc connections automatically, while RegSrvr.xml holds your explicitly saved registered server groups. In other words, connections stored inside UserSettings.xml are maintained/cached automatically by SSMS, whereas RegSrvr.xml is for manually maintained registered server lists.


See also


Thursday, June 13, 2024

DBCC CHECKDB Last Run Query for Availability Group CHECKDB Monitoring

DBCC CHECKDB Last Run Query for Availability Group CHECKDB Monitoring

DBCC CHECKDB checks the logical and physical integrity of a database, including allocation structures, tables, indexed views, and catalog consistency. It is one of the core preventive maintenance tasks, yet it often gets less attention than index and statistics maintenance. Many DBAs run index maintenance nightly, albeit with dynamic thresholds, but run CHECKDB only once a week. I won't debate the optimal frequency here; Brent Ozar's DBCC CHECKDB FAQ covers it well, noting that weekly is common and daily is often chosen when availability matters.

My purpose is to answer one simple question:

When did this database last complete a successful integrity check?


Starting with SQL Server 2016 SP2 and SQL Server 2017 CU9, DATABASEPROPERTYEX exposes the LastGoodCheckDbTime property. It is a convenient monitoring signal for finding databases that may be overdue for a successful DBCC CHECKDB.

Keep a few limitations in mind from the start:

  • It is only a timestamp. It doesn't tell you the CHECKDB options, output, duration, or where it ran. If those details matter, keep job history and centralized logging.
  • In an Availability Group, it reflects the primary replica. The value shows the last successful CHECKDB on the primary, regardless of which replica you query. Checks run on a secondary aren't reflected, and each replica has its own storage to verify.
  • 1900-01-01 means never checked. If CHECKDB has never run on the database, the property returns 1900-01-01 00:00:00.000.
  • NULL means investigate. DATABASEPROPERTYEX returns NULL for invalid input or when a database hasn't started, such as one that is offline or closed by AUTO_CLOSE. A NULL value is a signal to look further, not evidence about CHECKDB history.



What updates LastGoodCheckDbTime?


The value is the start time of the last DBCC CHECKDB that completed without finding corruption. Community testing shows a few behaviors worth knowing:

Updates the value:

  • DBCC CHECKDB that completes with no corruption errors
  • DBCC CHECKDB WITH PHYSICAL_ONLY, even though logical checks are skipped
  • DBCC CHECKFILEGROUP, even on a single filegroup

Does not update the value:

  • DBCC CHECKTABLE, CHECKALLOC, or CHECKCATALOG, even if you run all of them to split up a CHECKDB
  • DBCC CHECKDB that reports corruption errors
  • Checks on READ_ONLY databases
  • Checks run against a manually created database snapshot
  • Checks run on an Availability Group secondary replica
  • WITH ESTIMATEONLY, which estimates tempdb space without running the checks


Because PHYSICAL_ONLY and single-filegroup checks also update the timestamp, a recent value doesn't prove a full logical check covered the whole database. These behaviors are based on community testing, not Microsoft documentation, so verify them on your SQL Server version.



Here is an example SQL statement to demo it:

SELECT DATABASEPROPERTYEX('model', 'LastGoodCheckDbTime') AS [LastGoodCheckDbTime]







This time  lets get LastGoodCheckDbTime for all databases: 

Monday, April 8, 2024

Data Search within SQL Server Databases

Search and Export Script for SQL Server Databases

Plainly speaking, this article is about searching for a specific value inside all tables and their columns of a SQL database and return list of tables and columns that contains that value. 

That being said, third-party search tools have been around for a long time, as well as custom SQL scripts that you can find online for free, for the purpose of searching for specific values within a database. And even if they don’t meet all your requirements, they at least offer a starting point. However, I realized these solutions weren't sufficient for my specific needs, and I found it easier and quicker to write my own script.

This script uses the dynamic SQL queries to search across all user tables and columns. It does allow you to specify filter criteria to search within a particular schema, table, or column or columns of specific data type groups, for example search inside numeric data only. It also includes commands for exporting table data using PowerShell and BCP utilities, which was one of my requirements.


/* 
-- CREATE A TEST TABLE FOR DEMO 
IF OBJECT_ID('my_data_search_table', 'U') IS NOT NULL
    DROP TABLE my_data_search_table;

CREATE TABLE my_data_search_table (
    id INT,
    name VARCHAR(100)
);

INSERT INTO my_data_search_table (id, name)
VALUES (1996, 'dummy');

*/
SET NOCOUNT ON;
USE <Your DB Name Here>;
GO

-- Drop temp tables if exists
IF OBJECT_ID('tempdb..#t_table_columns') IS NOT NULL DROP TABLE #t_table_columns;
IF OBJECT_ID('tempdb..#results') IS NOT NULL DROP TABLE #results;
GO

-- Search string
DECLARE @search_value NVARCHAR(4000) = N'dummy'; -- Example search value

/* Limit what column data types to search into 
Here, you can specify whether to search within numeric data types, 
other data types, or both. This helps in narrowing down the search 
to relevant fields, potentially speeding up the search 
process and reducing the load on the database.
*/
-- valid values are numeric, other or both
DECLARE @search_datatype   VARCHAR(10)  
-- SET @search_datatype   = 'numeric'

-- Search mode
DECLARE @exact_match       BIT = 1; 

-- EXECUTION OPTIONS
DECLARE @execute           BIT = 1; -- 1 to execute the search queries
DECLARE @debug             BIT = 0; -- 1 to print only
DECLARE @show_progress     BIT = 1; -- 1 to print progress messages
DECLARE @progress_interval INT = 100;

-- Schema, Table, and Column Filtering (NULL means no filter)
DECLARE @search_column_name NVARCHAR(1000);
DECLARE @search_table_name  NVARCHAR(1000);
DECLARE @search_schema_name NVARCHAR(1000);

-- Dynamic SQL query
DECLARE @SQL NVARCHAR(4000);

-- If wild card searfch is requested i.e. @exact_match = 0 
IF @exact_match = 0 SET @search_value = N'%' + @search_value + N'%';

-- Temp table to store columns metadata
SELECT s.name AS [schema_name],
       t.name AS [table_name], 
       c.name AS [column_name],
       TYPE_NAME(c.system_type_id) AS [column_type]
INTO #t_table_columns 
FROM sys.tables t
JOIN sys.schemas s ON t.schema_id = s.schema_id
JOIN sys.columns c ON t.object_id = c.object_id
WHERE t.is_ms_shipped = 0
  AND TYPE_NAME(c.system_type_id) NOT IN ('image', 'varbinary');


-- Store list of numeric data types into a table variable
DECLARE @numeric_types TABLE (name VARCHAR(100));

INSERT INTO @numeric_types (name)
VALUES
    ('bigint'),
    ('bit'),
    ('decimal'),
    ('int'),
    ('money'),
    ('numeric'),
    ('smallint'),
    ('smallmoney'),
    ('tinyint'),
    ('float'),
    ('real');


IF (@search_datatype = 'numeric' AND ISNUMERIC(@search_value) = 0)
BEGIN
   RAISERROR('Error: Search value (%s) invalid for numeric search.', 16, 1, @search_value)
   GOTO QUIT
END

-- Apply filters
IF @search_schema_name IS NOT NULL AND @search_schema_name <> ''
    DELETE FROM #t_table_columns WHERE [schema_name] <> @search_schema_name;
IF @search_table_name IS NOT NULL AND @search_table_name <> ''
    DELETE FROM #t_table_columns WHERE [table_name] <> @search_table_name;
IF @search_column_name IS NOT NULL AND @search_column_name <> ''
    DELETE FROM #t_table_columns WHERE [column_name] <> @search_column_name;

IF @search_datatype IS NOT NULL AND @search_datatype <> ''
BEGIN
	IF @search_datatype NOT IN ('numeric', 'other', 'both')
	BEGIN
		RAISERROR('Error: Invalid value %s for @search_datatype.', 16, 1, @search_datatype) WITH NOWAIT
		RAISERROR('Valid values are 1) numeric 2) other 3) both.', 16, 1) WITH NOWAIT
		GOTO QUIT
	END
	ELSE IF @search_datatype = 'numeric'
	    DELETE #t_table_columns FROM #t_table_columns t
			WHERE NOT EXISTS (SELECT * FROM @numeric_types v WHERE v.name = t.[column_type])
	ELSE IF @search_datatype = 'other'
	    DELETE #t_table_columns FROM #t_table_columns t
			INNER JOIN @numeric_types v ON v.name = t.[column_type]
END

-- Placeholder for results
SELECT TOP 0 * INTO #results FROM #t_table_columns;

-- Progress tracking
DECLARE @total_columns INT;
DECLARE @counter INT = 0;

-- Variables for cursor
DECLARE @schema_name SYSNAME,
        @table_name  SYSNAME,
        @column_name SYSNAME,
        @column_type NVARCHAR(500);

-- Declare cursor
DECLARE c1 CURSOR STATIC FOR 
    SELECT * FROM #t_table_columns 
    ORDER BY [schema_name], [table_name], [column_name];

OPEN c1;
SELECT @total_columns = @@CURSOR_ROWS; 


FETCH NEXT FROM c1 INTO 
    @schema_name, @table_name, @column_name, @column_type;

WHILE @@FETCH_STATUS = 0
BEGIN
    SET @counter = @counter + 1;

    -- Progress message
    IF @counter % @progress_interval = 0 AND @show_progress = 1
        RAISERROR('%i columns of %i processed', 10, 1, @counter, @total_columns) WITH NOWAIT;

    -- Build and execute the search query
    SET @SQL = N'SELECT TOP 1 ''' + @schema_name + N''' AS [schema_name], ''' 
               + @table_name + N''' AS [table_name], ''' +  @column_name 
               + N''' AS [column_name], ''' +  @column_type 
               + N''' AS [column_type] FROM ' + QUOTENAME(@schema_name) 
               + N'.' + QUOTENAME(@table_name) + N' WHERE TRY_CAST(' 
               + QUOTENAME(@column_name) + N' AS VARCHAR(8000)) LIKE ''' 
               + @search_value + N''';';

    IF @debug = 1 PRINT @SQL;

    -- Insert into results if match is found
    IF @execute = 1 BEGIN
        INSERT INTO #results EXEC (@SQL);
        IF @@ROWCOUNT > 0 BEGIN
            PRINT '';
            RAISERROR('*** match found ***', 10, 1) WITH NOWAIT;
            PRINT @SQL;
            PRINT '';
        END
    END

    FETCH NEXT FROM c1 INTO 
        @schema_name, @table_name, @column_name, @column_type;
END

-- Cleanup
CLOSE c1;
DEALLOCATE c1;

-- Display results with export commands
SELECT *,
       'SELECT COUNT(*) [' + [schema_name] + '.' + [table_name] 
       + '] FROM '  + QUOTENAME([schema_name]) + '.' 
       + QUOTENAME([table_name]) + ' WHERE TRY_CAST(' 
       + QUOTENAME([column_name])  + ' AS VARCHAR(8000)) LIKE ''' 
       + @search_value + N''';' AS [SQL],
       'Send-SQLDataToExcel -Connection "Server=' + @@SERVERNAME 
       + ';Trusted_Connection=True;" -MsSQLserver -DataBase "' 
       + DB_NAME() + '" -SQL "select * from ' 
       + QUOTENAME([schema_name]) + '.' + QUOTENAME([table_name]) 
       + '" -Path "$env:USERPROFILE\Documents\' + DB_NAME() +  '.' 
       + [schema_name] + '.' + [table_name] + '.xlsx"' AS [PS Export],
       'BCP ' + QUOTENAME(DB_NAME()) + '.' +  QUOTENAME([schema_name]) 
       + '.' + QUOTENAME([table_name]) + ' out %USERPROFILE%\Documents\' 
       + DB_NAME() + '_' +  [schema_name] + '_' + [table_name] 
       + '.txt -c -t, -T -S' + @@SERVERNAME AS [BCP Export]
FROM #results 
ORDER BY [schema_name], [table_name], [column_name];

QUIT:


Gotchas and work arounds:


The script does not search within columns of certain data types, notably 'IMAGE' and 'VARBINARY'. These data types often store binary data (e.g., files, images, binary large objects (BLOBs)) that require different methods to search and interpret. Searching within such data types would necessitate additional logic for conversion or binary pattern matching, which is not covered by this script. This is by design.

And for columns with data type VARCHAR(MAX) and NVARCHAR(MAX) that store large amounts of text, the script's default method of casting the column to `VARCHAR(8000)` may truncate data in those columns, possibly missing matches in the truncated portion. You can however modify it to VARCHAR(MAX).

There are also couple performance considerations to keep in mind.  The script uses a cursor to iterate over potentially thousands of columns across many tables and executes a dynamic SQL query for each column. This can be resource-intensive and slow, especially in large databases with many tables or columns. As a mitigating action, you can set some filters (e.g., specific schemas or tables, numeric data only etc.).

Similarly, for very huge tables (e.g. billions of rows), the script understandly performs very poorly, especially if the table has no matching records at all but the script will still have to scan all rows for each and every column in it to find out that it doesn't have any matching records after all. You can however limit the performance hit by applying some filters mentioned above.

And because it uses dynamic SQL, it also poses a risk of SQL injection which is why I do not recommend to build a UI around it where users can supply any input values to it. You can however convert the script into a stored procedure and add code to validate and sanitize any inputs passed to it.

I believe the script is highly customizable and moderately detailed enough method for searching data inside a SQL Server database and, designed to accommodate some of the more typical search scenarios and preferences. Please ensure you modify and adapt it to suit your specific requirements.



Friday, March 29, 2024

Finding the Latest Backup Timestamps for Your Databases

Finding the Latest Backup Timestamps for Your Databases

I wanted to find out the most recent date and time each database was last backed up, focusing specifically on full, differential, and transaction log backups, all displayed on a single line for each database for easier viewing and reading. For example:







This provides a high-level overview of when my databases were last backed up, including differential and transaction log backups, if applicable.  The query results also show the duration of each backup (not shown in the above screenshot). Here is the query:

USE msdb
GO

;WITH cte_latest_backup AS (
    SELECT 
        sd.Name AS DatabaseName,
        bs.type AS backup_type,
        MAX(bs.backup_start_date) AS backup_start_time,
        MAX(bs.backup_finish_date) AS backup_finish_time
    FROM sys.sysdatabases sd
    INNER JOIN msdb.dbo.backupset bs 
       ON bs.database_name = sd.name
    GROUP BY sd.Name, bs.type
),
cte_full_backup AS 
(
    SELECT * FROM cte_latest_backup 
    WHERE backup_type = 'D'
),
cte_diff_backup AS 
(
    SELECT * FROM cte_latest_backup 
    WHERE backup_type = 'I'
),
cte_tlog_backup AS 
(
    SELECT * FROM cte_latest_backup
    WHERE backup_type = 'L'
)

SELECT
    d.name AS [Database], 
    d.recovery_model_desc,
    full_backup.backup_start_time AS full_backup_start,
    diff_backup.backup_start_time AS diff_backup_start,
    tlog_backup.backup_start_time AS tlog_backup_start,
    DATEDIFF(MINUTE, full_backup.backup_start_time, 
             full_backup.backup_finish_time) AS full_backup_elapsed_min,
    DATEDIFF(MINUTE, diff_backup.backup_start_time, 
             diff_backup.backup_finish_time) AS diff_backup_elapsed_min,
    DATEDIFF(SECOND, tlog_backup.backup_start_time, 
             tlog_backup.backup_finish_time) AS tlog_backup_elapsed_sec
FROM master.sys.databases d
LEFT JOIN cte_full_backup full_backup 
  ON full_backup.DatabaseName = d.name
LEFT JOIN cte_diff_backup diff_backup 
  ON diff_backup.DatabaseName = d.name
LEFT JOIN cte_tlog_backup tlog_backup 
  ON tlog_backup.DatabaseName = d.name
WHERE d.name NOT IN ('tempdb')
ORDER BY [Database]



Gotchas:

  • The query doesn't capture other available types of backups like file backup, file group backups, partial backups etc. If your backup strategy includes any of those backup types, please consider adapting the query to meet your needs. 
  • The query doesn't also differentiate whether the full backup was done in copy-only mode or not.
  • In AlwaysOn AG servers, some of your database backups maybe offloaded or distributed across primary and secondary replicas. This query doesn't capture backups performed on other replicas and therefore the most recent backup date/times displayed by the query may not be accurate for databases that are in an Availability Group.