Say, you have a query having performance issues and you decide to look it's graphical execution plan in SSMS (or other tools). If you are lucky, right off the bat SQL Server reveals a quick and easy fix:
Its missing a crucial index! You add the missing index and the problem solved, you are the hero of the hour!!!!
Sooner than later, you will come across queries that don't have such obvious fixes aka low hanging fruits. And, you will realize, analyzing execution plans is very tedious, time consuming and its probably impossible to read every single property of every single node of every single query. You decide to be smarter about it so you apply the pareto principle 1) focus your energies on 20% queries causing 80% of performance issues (or maybe there is no performance issue to solve per se but you just want to make things run better) and 2) Within execution plans for those 20% of queries, you devote your attention to the highest impact or informative operators of the execution plans. For example:
Plan metadata:
Start by first reviewing the plan metadata. It is the very first operator listed on the far left of the plan, it contains the metadata about the plan and as you can probably see in this screen shot, the information displayed is at least, informative:
At the bottom you see the “Warnings” that are associated with this plan, all in one place.
Warnings:
Warnings are indicators of a possible issue with the query that could affect the performance. This doesn't necessarily mean that its a real problem. It's just an indicator of a possible problem and should be investigated.
The tooltip will show the reason for the warning:
In that particular case if I am not using this column in a where clause, then this is most likely a false warning as it cannot then affect the row count estimates. Whereas in the following example both queries show warning in the execution plan due to the use of the CAST function. However its the second query where it's negatively affecting the performance because the war due to the use of CAST function in the WHERE CONDITION, making it essentially unsargable. First you will notice that the second query is switched to doing scan vs seek operator being used by the first query.
And secondly, the second query requires significantly (relatively speaking) more I/O to get the same results.
Even though the costs listed are estimates only, optimizer does rely on those estimates and therefore we must too pay more attention to them.
Fat Pipes:
Pipes, are the arrows that connect one operator to the next and represents data movement. Width of the pipes indicate amount of data being processed so a thick pipe would indicate lots of data and thin would mean less data. Another thing you would want to pay attention to is the change in pipes width from thin to fat or from fat to thin or, a fat pipe getting even fatter.
Scans:
Relatively speaking, seeks are better than scans. It’s not that Seeks are always good or that scans are always bad but scans do indicate I/O which is often an issue for query performance. Some times optimizer will choose scan even if a seek option is available and often it's right. In a simplistic example, if my query SELECT * FROM MYTABLE; that contains no WHERE clause or any kind of filtering condition at all, in that case the optimizer can only choose a scan, In fact for such queries a seek will run slower than a scan.
In short, look at the scan operators more closely than say seek operators.
Huge discrepancies between estimated and actual counts
For example, in the below screenshots the estimated rows is only 8 but actual plan returned 5.1 million rows, that's a huge discrepancy.
2) Parameter sniffing 3) a bad query plan getting cached (use WITH RECOMPILE to get rid of it) or sometimes combination of more than one factors.
Another discrepancy to pay attention is to look if there is a very high “Estimated Number Of Rows to be Read” compared to very low “Estimated Rows Per Execution”. This maybe indication that your WHERE clause is unsargable. A very simple example it would be to use aggregate functions (sum, count etc.) against all rows.
Final thoughts:
Often this approach works to quickly identify query performance problems. Even if it doesn't eventually, its a good starting strategy. And I would admit this has its limits and won't always be enough or "good enough" and you do then have to put in real hard work in understanding and optimizing your query.
At that point its good idea to start paying attention to the the NodeIDs of each operation. NodeIDs are integer values starting with 0...n, from left-to-right. It lets you know order in which your query is being processed by the SQL Server... Some operators even refer back to other operators.
Next, keep track of output of each operator, some operators add or change columns to the query results so knowing where each column came from can be very helpful.
And finally, to get a complete picture, get into habit of reviewing properties of each operator and not just look at the information in the tooltip.
Find out how fast is your SQL database growing using the backup metadata from msdb
Often, we need to determine the rate of growth of a database or identify which database is expanding the fastest among all those on a SQL instance. Currently, I do not have a central repository where I regularly poll and store the database size or other data. Nor do I have a CMDB tool at my current job.
What I present here is an ad-hoc, indirect method to gauge database growth using backup history information from the MSDB database. This script should be used primarily for ad-hoc solutions, though I find it a convenient and quick method when I need to quickly assess growth.
It’s important to note that this script will not be helpful for databases that are not backed up (there may be justifiable reasons for this) or if you only keep backup history in MSDB for a very short period of time (e.g., purging them after 7 days).
THIS IS A SCRIPT SO PLEASE REMEMBER TO MODIFY THE VALUES FOR
-- default is current date/time declare@number_of_days int=30-- ignored if the @backup_start_date is given
/*GET THE DATABASE SIZE GROWTH INDIRECTLY BY COMPARING CHANGE IN
DATABASE BACKUP SIZEYou can either specify a date range or a number of days to go
back to compare with.For example, you can specify a date range
if you want to know how much your databasegrew during a specific
period in the past. This info can be useful for comparisonbetween
two different time periods.And specify a number of days if you want to know much the database
grew in the past week, month, year, etc.Uses the backup metadata from the msdb to get the backup size.
If you are like me,you probably keep the msdb size in check by
purging metadata regularly. So, you can only go as far back as
you have the backup metadata for.Limitations:In the case of an AG cluster, you must run the query on the
replica where the fullbackups are performed or run it as a
multi-server query against all nodessimultaneously (recommended).Only looks for the full/complete backups (backup type=D).
In other words, does notconsider differential and/or log backups.*/-- Check if the temp table already exists and drop if it does
if OBJECT_ID('tempdb..#db_backup_metadata') isnotnulldroptable#db_backup_metadata
go-- LET'S FIRST GET THE DATA FROM msdb..backupset INTO A TEMP TABLEdeclare@backup_start_date datetime -- Example: '7/1/2022'declare@backup_end_date datetime -- Example: '8/1/2022' -- default is current date/time
-- ignored if the @backup_start_date is given
declare@number_of_days int=90-- Error handling for date input
if @backup_start_date isnulland@number_of_days isnullbegin
raiserror('Error: Either a start date or number of days to go back
to must be provided.', 16,1)
returnend-- Set default values for start and end datesset@backup_start_date =isnull(@backup_start_date, GETDATE() -
@number_of_days)
set@backup_end_date =isnull(@backup_end_date, GETDATE())
-- Printing the start and end dates
print '-- BEGIN AND START DATES:'
print @backup_start_date
print @backup_end_date
print '--'-- Error handling for start and end date logic
if @backup_end_date <@backup_start_date
begin
raiserror('Error: Start date cannot be after the end date.', 16,1)
returnend-- Select and insert backup data into temp tableSELECT
bs.backup_set_id,
bs.database_name,
bs.database_creation_date db_create_date,
bs.backup_start_date,
cast(bs.backup_size /1024/1024/1024asnumeric(19,3))
backup_size_gb
INTO#db_backup_metadata
FROM msdb..backupset bs
WHERE
bs.type='D'and bs.backup_start_date >=@backup_start_date
and bs.backup_finish_date <=@backup_end_date
ORDERBY database_name, backup_start_date DESC-- USE THE CTEs TO GET THE DESIRED DATA
;WITH c1 AS
(
SELECT database_name,
db_create_date,
backup_start_date,
backup_size_gb
FROM#db_backup_metadata a
WHERE backup_set_id =
(SELECTMIN(backup_set_id)
FROM#db_backup_metadata
WHERE database_name = a.database_name
GROUPBY database_name)
),
c2 AS
(
SELECT database_name,
backup_start_date,
backup_size_gb
FROM#db_backup_metadata a
WHERE backup_set_id =
(SELECTMAX(backup_set_id)
FROM#db_backup_metadata
WHERE database_name = a.database_name
GROUPBY database_name)
)
-- Final select statement to output the growth dataSELECT
dbs.name db_name,
c1.db_create_date,
c1.backup_start_date backup_date1, -- earliest backup
c2.backup_start_date backup_date2, -- most recent backup
datediff(D, c1.backup_start_date, c2.backup_start_date) [days],
c1.backup_size_gb backup_size1, -- size of the earliest backup
c2.backup_size_gb backup_size2, -- size of the most recent backup
(c1.backup_size_gb - c2.backup_size_gb) *-1 growth_gb,
try_cast(casewhen c1.backup_size_gb >0then
((c1.backup_size_gb - c2.backup_size_gb) *-1) *100/ c1.backup_size_gb
else0endasnumeric(10,2)) growth_rate
FROM sys.databases dbs
LEFTJOIN c1 ON dbs.name = c1.database_name
LEFTJOIN c2 ON c1.database_name = c2.database_name
WHERE dbs.name !='tempdb'ORDERBY growth_gb DESC
Now, I'm curious to hear from you! How do you currently track your SQL database growth, and how effective has it been? Or, if you've tried out the method described in this post, I’d love to hear about your experiences. Did it offer new insights into your database management?
Monitor SQL replication subscriptions synchronization status
In your current role if you are managing a SQL server replication setup, you are probably well aware that if any of the subscriber is not getting synchronized within a reasonable time (which is subjective and variable, I may add), it has serious consequences not only for the subscribers but also for the publisher.
So I had written this little script to see if any of the subscriber had not been synchronized in last @hrs. I used 24 hours as a threshold as some of the subscriptions we only synchronized after hours. So adjust that value according to your needs.
Caveat: I wrote this script long time ago to monitor and get alert if any of the replication subscribers were falling behind or had completely stopped getting synchronized with the publisher. So its not up to date with any new features etc. that newer versions of SQL Server may have added to the replication technology. For example there maybe now built in alerts you could leverage to achieve the same results. The built alerts were probably available even back then as well but I had found my little custom script much more suitable for our needs at that time. So I am hoping you may find this just as useful or give a starting point for your own solution.
RUN THIS SCRIPT ON THE DISTRIBUTOR SERVER
/* Execute this script on the distributor serverDescription: Return subscriptions that have not been synchronized in last @hrs hours.The script loops through each publisher served by the local distributor server. */SET NOCOUNT ON-- declare variablesDECLARE@SQL NVARCHAR(4000), @SERVERNAME VARCHAR(500), @mypublisher nvarchar(500), @hrs intDECLARE@rcpts nvarchar(1000), @msg nvarchar(1000), @sub nvarchar(150), @query2 nvarchar(2000)
SET@hrs =24-- return subscription that has not synchronized in last @hrsSET@rcpts ='<YOUR EMAIL ADDRESS>'-- make sure the distribution server is configured for the data access
if notexists (select*from sysservers where srvid =0and dataaccess =1)
EXEC('exec sp_serveroption @server = '''+@@SERVERNAME +''', @optname = ''DATA ACCESS'', @optvalue = ''TRUE''')
-- uses a cursor to loop through each publisherDECLARE C1 CURSORFORSELECT NAME FROM MSDB..MSdistpublishers ORDERBY1OPEN C1
FETCH C1 INTO@mypublisher
WHILE @@FETCH_STATUS =0BEGIN
IF OBJECT_ID('TEMPDB..##Temp_sp_replmonitorhelpsubscription') ISNOTNULLDROPTABLE##Temp_sp_replmonitorhelpsubscription
SET@SQL='select * INTO ##Temp_sp_replmonitorhelpsubscription from openquery(['+@@SERVERNAME +'], ''SET FMTONLY OFF exec [distribution].sys.sp_replmonitorhelpsubscription @publisher = '''''+@mypublisher +''''', @publication_type = 0, @mode = 0, @exclude_anonymous = 0, @refreshpolicy = N''''0'''''')'
PRINT @SQLEXEC (@SQL)
IF EXISTS (SELECT@mypublisher Publisher, *FROM##Temp_sp_replmonitorhelpsubscription WHERE datepart(hh, getdate()-last_distsync) >@hrs)
BEGINSELECT@mypublisher Publisher, *FROM##Temp_sp_replmonitorhelpsubscription WHERE datepart(hh, getdate()-last_distsync) >@hrs
-- SELECT @mypublisher Publisher,Subscriber, Publication FROM ##Temp_sp_replmonitorhelpsubscription WHERE datepart(hh, getdate()-last_distsync) > @hrsSET@sub =@mypublisher +': Subscribers found with no data sync in last '+cast(@hrs asvarchar(15)) +' hours'SET@msg =''SET@query2 ='SET NOCOUNT ON; SELECT '''+LEFT(@mypublisher, 25) +''' Publisher,LEFT(Subscriber, 15) Subscriber, LEFT(Publication, 35) Publication, last_distsync [LastDistSync] FROM ##Temp_sp_replmonitorhelpsubscription WHERE datepart(hh, getdate()-last_distsync) > '+cast(@hrs asvarchar(15))
EXEC dba..sp_sendmail
@recipients =@rcpts
,@message =@msg
,@subject =@sub
-- ,@body_format = 'HTML'
,@query =@query2
ENDFETCH C1 INTO@mypublisher
ENDCLOSE C1
DEALLOCATE C1
Truncating very large transaction log in full recovery mode
While I always configure transaction log backups for every database in non-simple recovery model, and to compliment it I also have monitoring in place if transaction log doesn't get backed up, which does happen on rare occasions. For instance in my current organization where all applications/databases are third party apps with no in-house developed apps, once in a while an app/user may run a huge transaction, it could be that they decided to purge almost all historical data from a 500TB database and delete 450TB data from it all in a single transaction, leaving a huge transaction log and not enough available disk space on the backup drive (separate issue/topic).
Per best practice and for almost any conceivable argument, its preferred that you take a transaction log backup and then shrink the log file/s. But in an emergency situation you could use the following SQL script to quickly transaction the transaction log. I believe the script is self-explanatory but I missed anything or have questions, please feel free to post a comment here and I will go my best to get to back you.
/* CAUTION:USE THIS ONLY IN EMERGENCY WHERE THE TRANSACTION LOG HAS NOT BEEN BACKED
IN A VERY LONG TIME AND HAS GROWN TO BE TOO BIG
The script does the following in order to shrink the transaction log file:- Ensure the db is not running in Simple recovery mode- Store the database recovery mode into a variable- Change recovery mode to SIMPLE- Issues checkpoint command- Wait 2 seconds- Shrink the log file- Wait 2 seconds- Change recovery mode back to original value
IMPORTANT: This script does not take a full or differential database backup so its crucial that you take a full or differential backup to re-set the transaction log chain. Otherwise any attempt to take a transaction log backup will fail, creating more problems.
*/
declare@db nvarchar(100) ='MyDB' -- Specify name of the database here declare@target_log_size_mb nvarchar(20) =64-- if DB has multiple log files, each log file will be of this sizedeclare@recovery_mode nvarchar(100)
declare@sql nvarchar(4000)
select@recovery_mode = recovery_model_desc from sys.databases where name =@db
if @recovery_mode='SIMPLE'
raiserror('Error: Use this script only on database in Full recovery mode.', 16, 1)
else if databasepropertyex(@db, 'Updateability')='READ_ONLY'
raiserror('Error: Database is not updatable. Please ensure that the database is not read only, mirrored or otherwise inaccessible.', 16, 1)
elseBEGINset@sql=' USE [master] ALTER DATABASE ['+@db +'] SET RECOVERY SIMPLE WITH NO_WAIT USE ['+@db +'] WAITFOR DELAY ''00:00:02'' checkpoint WAITFOR DELAY ''00:00:02'' -- open a cursor so we can iterate through and shrink all log files declare @log_file_name sysname declare c1 cursor for select name from sys.database_files where type=1 open c1 fetch c1 into @log_file_name while @@FETCH_STATUS = 0 begin print @log_file_name DBCC SHRINKFILE (@log_file_name, '+@target_log_size_mb +') fetch c1 into @log_file_name end close c1 deallocate c1 WAITFOR DELAY ''00:00:02'' USE [master] ALTER DATABASE ['+@db +'] SET RECOVERY '+@recovery_mode +' WITH NO_WAIT '
print @sqlexec sp_executesql @sqlEND
For database participating in an AlwaysOn Availability Group:
/* CAUTION:USE THIS ONLY IN EMERGENCY WHERE THE TRANSACTION LOG HAS NOT BEEN BACKED IN A VERY LONG TIME AND HAS GROWN TO BE TOO BIGThis script generates a TSQL script that does the following in order to shrink the transaction log file:- Ensure the db is not running in Simple recovery mode- If the DB is participating in an Availablity Group then ensure that we are on the primary replica server- Store the database recovery mode into a variable- Store the Availability Group name into a variable- Change recovery mode to SIMPLE- Issues checkpoint command- Wait x number of seconds- Shrink the log file/s- Wait x number of seconds- Change recovery mode back to original value- AG settings, If applicable Backup the database Add the database back to the AG Provide TSQL to drop the database on secondary replicasWHAT IT DOESN'T DO:- It won't delete/drop the database on the secondary replicas*/
-- Specify the database name here
DECLARE@db nvarchar(100) ='VLDB';
-- If DB has multiple log files, each log file will be of this sizeDECLARE@target_log_size_mb nvarchar(20) =64;
DECLARE@recovery_mode nvarchar(100);
DECLARE@sql nvarchar(4000);
DECLARE@ag_name nvarchar(100);
DECLARE@is_primary_replica int;
SELECT@recovery_mode = recovery_model_desc,
@is_primary_replica = agrs.is_primary_replica,
@ag_name = ag.name
FROM sys.databases d
leftjoin sys.dm_hadr_database_replica_states agrs
ON d.database_id = agrs.database_id and agrs.is_local =1leftjoin sys.availability_groups ag
ON ag.group_id = agrs.group_id
WHERE d.name =@db;
IF DB_ID(@db) ISNULL
RAISERROR('No database found with name Database %s.', 16, 1, @db);
ELSE IF @recovery_mode='SIMPLE'
RAISERROR('Error: Use this script only on database in Full recovery mode.', 16, 1);
ELSE IF databasepropertyex(@db, 'Updateability')='READ_ONLY'
RAISERROR('Error: Database is not updatable. Please ensure that the database is not read only, mirrored or otherwise inaccessible.', 16, 1);
ELSE IF @ag_name isnotnulland@is_primary_replica =0BEGIN
RAISERROR('Database %s is participating in an AlwaysOn Availablity Group.', 16, 1, @db);
RAISERROR('This must be run on the primary replica server.', 16, 1);
ENDELSE IF @ag_name isnullBEGIN
RAISERROR('Database %s is not part of an AlwaysOn Availablity Group.', 16, 1, @db);
ENDELSEBEGINSET@sql='';
IF @ag_name isnotnulland@is_primary_replica =1SET@sql=@sql+' /* EXECUTE ON THE PRIMARY REPLICA SERVER */ RAISERROR(''INFO: Removing database [%s] from Availability Group [%s]...'', 10, 1, '''+@db +''', '''+@ag_name +''') WITH NOWAIT; USE [master] ALTER AVAILABILITY GROUP ['+@ag_name +'] REMOVE DATABASE ['+@db +'];'SET@sql=@sql+' RAISERROR(''INFO: Change database recovery model to SIMPLE....'', 10, 1) WITH NOWAIT USE [master]; ALTER DATABASE ['+@db +'] SET RECOVERY SIMPLE WITH NO_WAIT; USE ['+@db +']; WAITFOR DELAY ''00:00:01'' CHECKPOINT; WAITFOR DELAY ''00:00:01''; -- open a cursor so we can iterate through and shrink all log files RAISERROR(''INFO: Truncate all transaction log files.....'', 10, 1) WITH NOWAIT; DECLARE @log_file_name sysname; DECLARE c1 cursor for SELECT name from sys.database_files where type=1; OPEN c1; FETCH c1 into @log_file_name; WHILE @@FETCH_STATUS = 0 BEGIN---- PRINT @log_file_name; RAISERROR(''INFO: Truncating transaction log file %s....'', 10, 1, @log_file_name); DBCC SHRINKFILE (@log_file_name, '+@target_log_size_mb +'); FETCH c1 into @log_file_name; end CLOSE c1; DEALLOCATE c1; WAITFOR DELAY ''00:00:01''; RAISERROR(''INFO: Change back the database recovery model to %s...'', 10, 1, '''+@recovery_mode +''') WITH NOWAIT USE [master]; ALTER DATABASE ['+@db +'] SET RECOVERY '+@recovery_mode +' WITH NO_WAIT; 'SET@sql=@sql+' RAISERROR(''INFO: Backup the database...'', 10, 1, '''+''') WITH NOWAIT BACKUP DATABASE ['+@db +'] TO DISK = N''O:\'+@db +'_'+CONVERT(nvarchar(10), getdate(), 112) +'.bak'' WITH NOFORMAT, INIT, COMPRESSION, STATS = 10; RAISERROR(''INFO: Add the database back to the availability group....'', 10, 1) WITH NOWAIT; ALTER AVAILABILITY GROUP ['+@ag_name +'] ADD DATABASE ['+@db +']; /* EXECUTE ON THE SECONDARY REPLICAS -- DROP THE DATABASE ON THE SECONDARY REPLICAS USE MASTER; ALTER AVAILABILITY GROUP ['+@ag_name +'] GRANT CREATE ANY DATABASE; IF DB_ID('''+@db +''') IS NULL RAISERROR(''No database found with name Database %s.'', 16, 1, '''+@db +'''); ELSE IF EXISTS (SELECT * FROM SYS. DATABASES WHERE NAME = '''+@db +''' AND STATE_DESC = ''RESTORING'') BEGIN PRINT ''-- Dropping database: '+@db +''' DROP DATABASE ['+@db +'] END ELSE RAISERROR(''Error: Cannot drop database %s. Ensure that it exists and is not in RESTORING state.'', 16, 1, '''+@db +'''); */ '
PRINT @sql;
END;
Obviously, if a DBA or a more experienced Database Developer is the one running a gigantic transaction (e.g. delete 450TB worth of data from a 500TB database), they would most likely switch the recovery mode to SIMPLE and disable the transaction log backups BEFORE running such a transaction, then change it back to FULL/Bulk-Load mode, take a full backup and re-enable transaction log backup job.
They would also likely to break such operations into a much smaller, manageable batches/transactions.
Monitor SQL Server Transaction Log Backups Across Always On Replicas
This post applies to SQL Server 2016 and later, where newer DMVs make it easier to inspect transaction log activity across databases and replicas.
For years I relied on the backup history tables in msdb to check whether backups were running on schedule and staying within SLAs. There are, of course, other ways to monitor backups, but I prefer having a simple, query‑driven view that I can trust.
I don’t like taking chances with database backups, not just for disaster recovery but also for day‑to‑day availability. If transaction log backups are skipped due to oversight or repeated job failures, the log can grow until it fills the underlying disk, potentially taking not only that database offline but any others sharing the same volume.
To avoid that, I use a script that checks when the last successful transaction log backup completed and compares it to a threshold (for example, more than 24 hours). If that threshold is exceeded, I get an automatic email alert.
Things get even more interesting once you add Availability Groups into the mix, especially if your organization uses third-party backup tools. SQL Server allows transaction log backups to run on any eligible replica rather than forcing them onto the primary. That flexibility is great for offloading work, but it also means you need to know which replica can back up which database types.
Here is a quick summary of which backup types are allowed on each replica type.
Backup Type
Primary Replica
Secondary Replica (Synchronous and Asynchronous)
Full
Yes
Yes but with Copy_Only option)
Log
Yes
Yes (we cannot take log backup with COPY_ONLY on secondary replica)
Differential
Yes
No
Backup events are recorded only on the replica where the backup actually occurs, and that data doesn't automatically sync across replicas.
That makes checking for the last successful backup more complicated than it first appears.
If you always run backups from one specific replica, you can usually rely on msdb's backup history tables. But even then, you'll eventually face some unpleasant surprises (are there any other kind?).
To cover all the bases, I needed a more reliable approach. Luckily, Microsoft introduced the sys.dm_db_log_stats function in SQL Server 2016 and later. This DMV has fields that get updated regardless of whether you're on the primary or secondary replica, including log_backup_time, which shows the timestamp of the last successful log backup.
Here's the query I use to monitor transaction log backups across all replicas.
SELECT
d.name AS [Database],
dbl.recovery_model,
dbl.log_backup_time,
dbl.total_log_size_mb,
dbl.log_since_last_log_backup_mb,
dbl.log_since_last_checkpoint_mb,
dbl.total_vlf_count,
dbl.active_vlf_count,
dbl.active_log_size_mb,
dbl.log_truncation_holdup_reason,
dbl.log_state
FROM
sys.databases d
CROSS APPLY
sys.dm_db_log_stats(d.database_id) dbl;
Partial result:
Here is an example of one of its use cases:
--===============================================================================-- TRANSACTION LOG BACKUP CHECK AND EXECUTION SCRIPT-- Purpose: Automatically check database transaction log backup status and trigger-- backup if older than 24 hours. Designed for automated maintenance jobs.-- Author: DBA Team-- Last Updated: Jan 16, 2026--===============================================================================DECLARE@DBNamesysname='msdb';-- Target databaseDECLARE@DBIDint=DB_ID(@DBName);-- Get database ID for DMV query efficiency-- Validate database existsIF@DBIDISNULLBEGINRAISERROR('ERROR: Database %s does not exist or DB_ID lookup failed.',16,1,@DBName);
RETURN;
END;
-- Check last transaction log backup time using dynamic management viewDECLARE@LogBackupTimedatetime=(
SELECTlog_backup_timeFROMsys.dm_db_log_stats(@DBID)-- Returns last log backup timestamp
);
-- Main logic: Trigger backup if log backup is older than 24 hoursIF@LogBackupTime<DATEADD(DAY,-1,GETDATE())-- Explicit 24hr thresholdBEGINDECLARE@SQLNVARCHAR(4000);
SET@SQL=N'BACKUP LOG '+QUOTENAME(@DBName)+N' TO DISK = N''C:\'+@DBName+N'.TRN'''+N' WITH INIT, COMPRESSION';
PRINT'Executing: '+@SQL;
-- TODO: Uncomment for production execution-- EXEC sp_executesql @SQL;ENDELSEBEGINPRINT'Log backup current: '+CONVERT(varchar(23),@LogBackupTime,121);
END;
-- Cleanup and return status
PRINT'Log backup check completed for '+@DBName+' at '+CONVERT(varchar(23),GETDATE(),121);
It gives you a reliable, replica-agnostic view of transaction log backup health across your SQL Server environment. Drop it into a scheduled job with your 24-hour threshold, pipe the results to an email alert. Simple, effective monitoring that catches problems before they become outages.