Identifier et régler les problèmes de journaux de transactions

Pourquoi le disque des journaux de transaction est-il plein ?

Les journaux de transactions des bases SQL Server conservent l’historique transactionnel des écritures dans les bases.

  • Si le mode de récupération (recovery model) d’une base de données est COMPLET (FULL), le journal de transaction conserve toutes les informations pour qu’une sauvegarde de journal de transactions puisse les archiver.
  • Si le mode de récupération (recovery model) d’une base de données est SIMPLE (SIMPLE), le journal de transaction est régulièrement vidé.

Problèmes possibles :

  • Le mode de récupération d’une base nouvellement créée est COMPLET par défaut.
  • Une base de données de production en mode COMPLET va retenir ce mode si elle est restaurée sur un serveur de développement ou de recette.
  • Le journal de transactions d’une base en mode COMPLET ne se vide pas automatiquement.

Si une base est en mode COMPLET et qu’aucune sauvegarde de journaux (BACKUP LOG) n’est planifiée, le journal grandira à l’infini.

Si c’est le cas, il n’y a que deux options :

  1. planifier des sauvegardes de journaux, sur un serveur de production.
  2. passer la base en mode SIMPLE, sur un serveur hors production.

Taille des journaux de transactions

Pour voir la taille de vos journaux de transactions et l’espace utilisé :

DBCC SQLPERF(LOGSPACE);

Raison pour laquelle le journal de transactions ne se vide pas

Si vous êtes en mode de récupération COMPLET et que le journal est plein, vérifiez pourquoi :

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;

La colonne log_reuse_wait de cette requête peut indiquer :

  • LOG_BACKUP : Vérifier les travaux des sauvegardes de journaux.
  • ACTIVE_TRANSACTION : il y a une transaction active qui empêche le vidage du journal. Vous devez trouver et terminer cette transaction (voir section suivante).
  • REPLICATION : la base est configurée pour la réplication ou le CDC. Vérifier que cela fonctionne correctement.
  • AVAILABILITY_REPLICA : la base est configurée pour un groupe de disponibilité Always On. Vérifier que le secondaire est bien en synchronisation.

Le log_reuse_wait est ACTIVE_TRANSACTION

Si le résultat est ACTIVE_TRANSACTION, il y a une transaction active qui empêche le vidage du journal de transactions. Pour identifier les sessions qui maintiennent une transaction active :

-- sessions ouvertes qui maintiennent une transaction
SELECT *
FROM sys.dm_exec_sessions des
WHERE des.is_user_process = 1
AND des.open_transaction_count > 0;

Ou plus en détail avec :

Active Transactions
----------------------------------------------------------
-- 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);

Tester le mode de récupération

  • Vous pouvez utiliser la requête suivante pour voir la liste des bases et leur mode de récupération (colonne recovery).
  • Ma procédure stockée sp_databases donne la même information.
  • Vous pouvez voir les journaux et leur pourcentage de remplissages à l’aide de la commande DBCC SQLPERF (LOGSPACE)
  • Vous pouvez utiliser ma procédure stockée sp_logspace qui remplace avantageusement DBCC SQLPERF (LOGSPACE)

Sinon, la requête suivante suffit

SELECT
    name as [db],
    recovery_model_desc as [recovery]
FROM sys.databases
WHERE database_id > 4
ORDER BY name;

Changer le mode de récupération

La requête suivante change le mode de récupération d’une base de données en SIMPLE :

use [master];
GO

ALTER DATABASE [<nom de la base de données>] SET RECOVERY SIMPLE WITH NO_WAIT

Vérifiez ensuite avec la commande DBCC SQLPERF (LOGSPACE) que le pourcentage de remplissages du journal a baissé.

Récupérer l’espace disque

La taille physique du journal ne diminue pas automatiquement. C’est seulement le remplissage du fichier qui est ajusté.

Pour diminuer la taille du fichier de journal, vous devez effectuer un shrink. Par exemple avec la commande suivante.

USE [<nom de la base de données>]
GO

DBCC SHRINKFILE (N'<nom logique du fichier de journal>' , 200)

Cette commande essaie de diminuer la taille du fichier à 200 Mo. Cela peut ne pas réussir tout de suite, si la portion active du journal est à la fin du fichier. Réessayez plus tard si la taille n’a pas diminué.