Identify and fix transaction log problems
See also my YouTube video: Comprendre les problèmes du journal de transaction dans SQL Server (in French)
The transaction log of a SQL Server database keeps the transactional history of the writes made to that database.
- If the recovery model of a database is
FULL, the transaction log keeps everything, so that a transaction log backup can archive it. - If the recovery model of a database is
SIMPLE, the transaction log is emptied regularly.
The problems this creates:
- The recovery model of a newly created database is
FULLby default. - A production database in
FULLkeeps that model when it is restored onto a development or staging server. - The transaction log of a database in
FULLdoes not empty itself.
If a database is in FULL and no log backup (BACKUP LOG) is scheduled, the log grows without limit.
When that happens, there are only two options:
- schedule log backups, on a production server;
- switch the database to
SIMPLE, on a non-production server.
Transaction log size
To see the size of your transaction logs and how much of them is used:
DBCC SQLPERF(LOGSPACE);
Why the transaction log does not empty
If you are in the FULL recovery model and the log is full, find out why:
SELECT
name as [db],
recovery_model_desc as [recovery],
ISNULL(NULLIF(log_reuse_wait_desc, N'NOTHING'), '') as log_reuse_wait
FROM sys.databases
WHERE database_id > 4
ORDER BY name;
The log_reuse_wait column can report:
LOG_BACKUP— check the log backup jobs.ACTIVE_TRANSACTION— an active transaction is preventing the log from being emptied. You need to find and end that transaction, see the next section.REPLICATION— the database is configured for replication or CDC. Check that it is working properly.AVAILABILITY_REPLICA— the database belongs to an Always On availability group. Check that the secondary is synchronizing.
When log_reuse_wait is ACTIVE_TRANSACTION
An active transaction is holding the transaction log. To identify the sessions holding one open:
-- open sessions holding a transaction
SELECT *
FROM sys.dm_exec_sessions des
WHERE des.is_user_process = 1
AND des.open_transaction_count > 0;
Or in more detail with:
----------------------------------------------------------
-- list all opened transactions with detail
-- rudi@babaluga.com, go ahead license
----------------------------------------------------------
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SELECT
t.transaction_id,
t.name,
CAST(t.transaction_begin_time as datetime2(0)) as begin_time,
DATEDIFF(SECOND, t.transaction_begin_time, CURRENT_TIMESTAMP) as tran_elapsed_time_seconds,
CASE t.transaction_type
WHEN 1 THEN 'Read/Write'
WHEN 2 THEN 'Read-Only'
WHEN 3 THEN 'System'
WHEN 4 THEN 'Distributed'
ELSE CONCAT('Unknown - ', transaction_type)
END AS [type],
CASE t.transaction_state
WHEN 0 THEN 'Uninitialized'
WHEN 1 THEN 'Not Yet Started'
WHEN 2 THEN 'Active'
WHEN 3 THEN 'Ended (Read-Only)'
WHEN 4 THEN 'Committing'
WHEN 5 THEN 'Prepared'
WHEN 6 THEN 'Committed'
WHEN 7 THEN 'Rolling Back'
when 8 THEN 'Rolled Back'
ELSE CONCAT('Unknown - ', transaction_state)
END AS [state],
case t.dtc_state
WHEN 0 THEN NULL
WHEN 1 THEN 'Active'
WHEN 2 THEN 'Prepared'
WHEN 3 THEN 'Committed'
WHEN 4 THEN 'Aborted'
WHEN 5 THEN 'Recovered'
ELSE CONCAT('Unknown - ', dtc_state)
END AS [dtc state],
db.name as db,
db.log_reuse_wait_desc as log_reuse_wait,
db.is_read_committed_snapshot_on as rcsi,
dt.database_transaction_log_bytes_reserved as log_bytes_reserved,
dt.database_transaction_log_bytes_used as log_bytes_used,
dt.database_transaction_log_record_count as log_record_count,
CAST(logSize.cntr_value / 1000.0 as numeric(20, 2)) as [log size MB],
logPercent.cntr_value as [log %],
st.session_id,
st.transaction_descriptor as [tran descr],
st.is_user_transaction as [user tran],
st.open_transaction_count as [tran cnt],
st.enlist_count as [stmt nb],
se.login_time,
se.host_name,
se.program_name,
se.login_name,
se.status,
inputbuffer.text as inputbuffer,
CASE se.transaction_isolation_level
WHEN 0 THEN 'Unspecified'
WHEN 1 THEN 'Read Uncommitted'
WHEN 2 THEN 'Read Committed'
WHEN 3 THEN 'Repeatable Read'
WHEN 4 THEN 'Serializable'
WHEN 5 THEN 'Snapshot'
ELSE CAST(se.transaction_isolation_level as varchar(50))
END as isolation_level
FROM sys.dm_tran_active_transactions t
JOIN sys.dm_tran_database_transactions dt ON t.transaction_id = dt.transaction_id
JOIN sys.databases db ON dt.database_id = db.database_id
JOIN sys.dm_os_performance_counters logSize ON db.name = logSize.instance_name
AND logSize.object_name = 'SQLServer:Databases' AND logSize.counter_name = 'Log File(s) Size (KB)'
JOIN sys.dm_os_performance_counters logPercent ON db.name = logPercent.instance_name
AND logPercent.object_name = 'SQLServer:Databases' AND logPercent.counter_name = 'Percent Log Used'
JOIN sys.dm_tran_session_transactions st ON t.transaction_id = st.transaction_id
LEFT JOIN sys.dm_exec_sessions se ON st.session_id = se.session_id
LEFT JOIN sys.dm_exec_connections cn ON cn.session_id = se.session_id
OUTER APPLY sys.dm_exec_sql_text(cn.most_recent_sql_handle) AS inputbuffer
ORDER BY t.transaction_begin_time
OPTION (RECOMPILE, MAXDOP 1);Checking the recovery model
- You can use this query to list the databases and their recovery model, in the
recoverycolumn. - My sp_databases stored procedure gives the same information.
DBCC SQLPERF (LOGSPACE)shows the logs and how full they are.- My sp_logspace stored procedure is a more useful replacement for
DBCC SQLPERF (LOGSPACE).
Otherwise, this query is enough:
SELECT
name as [db],
recovery_model_desc as [recovery]
FROM sys.databases
WHERE database_id > 4
ORDER BY name;
Changing the recovery model
This statement switches a database to the SIMPLE recovery model:
use [master];
GO
ALTER DATABASE [<database name>] SET RECOVERY SIMPLE WITH NO_WAIT
Then check with DBCC SQLPERF (LOGSPACE) that the log fill percentage has dropped.
Reclaiming the disk space
The physical size of the log does not shrink on its own. Only the used portion of the file changes.
To reduce the size of the log file you have to shrink it, for instance with:
USE [<database name>]
GO
DBCC SHRINKFILE (N'<logical name of the log file>' , 200)
This tries to reduce the file to 200 MB. It may not succeed straight away, if the active portion of the log sits at the end of the file. Try again later if the size has not gone down.