Thursday, August 22, 2019

Troubleshooting "TempDB Log file Full due Active Transaction" error (9002)

If your SQL Server is barely responding, you are getting the following error message and you do not know what to do, then you are in the right place!
Error: 9002, Severity: 17, State: 4. 
The transaction log for database 'tempdb' is full due to 'ACTIVE_TRANSACTION'

Troubleshooting of that error is very easy.
At first, verify that you have exactly this problem:
SELECT log_reuse_wait, log_reuse_wait_desc
FROM master.sys.databases

WHERE name = N'tempdb';
You are supposed to get something like this:

If that is the case you need to figure out which transaction is impacting your TempDB:

SELECT DISTINCT s.session_id
       , a.transaction_id
       , a.name
       , c.client_net_address
       , se.login_name
       , se.host_name
       , se.program_name
       , Duration = CASE
                     WHEN DATEDIFF(SECOND,a.transaction_begin_time,GETDATE()) >= 3600  THEN
                           CAST(CAST(DATEDIFF(SECOND,a.transaction_begin_time,GETDATE()) /3600. as Decimal(7,2)) as VARCHAR) + ' Hr'
                     WHEN DATEDIFF(SECOND,a.transaction_begin_time,GETDATE()) >= 60  THEN
                           CAST(CAST(DATEDIFF(SECOND,a.transaction_begin_time,GETDATE()) /60. as Decimal(7,2)) as VARCHAR) + ' Min'
                     ELSE
                           CAST(CAST(DATEDIFF(MILLISECOND,a.transaction_begin_time,GETDATE()) /1000. as Decimal(7,2)) as VARCHAR) + ' Sec'
              END
       , a.transaction_begin_time
       , c.last_read
       , c.last_write
       , Delay_Min = CAST(DATEDIFF(SECOND,CASE WHEN c.last_read > c.last_write THEN c.last_write ELSE c.last_read END,GETDATE()) /60. as Decimal(7,2))
       , transaction_type = CASE a.transaction_type
                     WHEN 1 THEN 'Read/write transaction'
                     WHEN 2 THEN 'Read-only transaction'
                     WHEN 3 THEN 'System transaction'
                     WHEN 4 THEN 'Distributed transaction'
              END
       , transaction_state = CASE a.transaction_state
                     WHEN 0 THEN 'The transaction has not been completely initialized yet.'
                     WHEN 1 THEN 'The transaction has been initialized but has not started.'
                     WHEN 2 THEN 'transaction is active.'
                     WHEN 3 THEN 'The transaction has ended. This is used for read-only transactions.'
                     WHEN 4 THEN 'The commit process has been initiated on the distributed transaction. The distributed transaction is still active but further processing cannot take place.'
                     WHEN 5 THEN 'The transaction is in a prepared state and waiting resolution.'
                     WHEN 6 THEN 'The transaction has been committed.'
                     WHEN 7 THEN 'The transaction is being rolled back.'
                     WHEN 8 THEN 'The transaction has been rolled back.'
              END
       , dtc_state = CASE a.dtc_state WHEN 1 THEN 'ACTIVE' WHEN 2 THEN 'PREPARED' WHEN 3 THEN 'COMMITTED' WHEN 4 THEN 'ABORTED' WHEN 5 THEN 'RECOVERED' END
       , [Database ID] = dt.database_id
       , [Database Name] = DB_Name(dt.database_id)
       , dt.database_transaction_begin_time
       , database_transaction_type = CASE dt.database_transaction_type WHEN 1 THEN 'Read/write transaction' WHEN 2 THEN 'Read-only transaction' WHEN 3 THEN 'System transaction' END
       , database_transaction_state = CASE dt.database_transaction_state
                     WHEN 1 THEN 'The transaction has not been initialized.'
                     WHEN 3 THEN 'The transaction has been initialized but has not generated any log records.'
                     WHEN 4 THEN 'The transaction has generated log records.'
                     WHEN 5 THEN 'The transaction has been prepared.'
                     WHEN 10 THEN 'The transaction has been committed.'
                     WHEN 11 THEN 'The transaction has been rolled back.'
                     WHEN 12 THEN 'The transaction is being committed. In this state the log record is being generated, but it has not been materialized or persisted.'
              END
       , [Initiator] = CASE s.is_user_transaction WHEN 0 THEN 'System' ELSE 'User' END
       , [Is_Local] = CASE s.is_local WHEN 0 THEN 'No' ELSE 'Yes' END
       , cnn_reads = c.num_reads
       , cnn_writes = c.num_writes
       , dt.database_transaction_log_record_count
       , dt.database_transaction_log_bytes_used
       , dt.database_transaction_log_bytes_reserved
       , dt.database_transaction_log_bytes_used_system
       , dt.database_transaction_log_bytes_reserved_system
       , dt.database_transaction_begin_lsn
       , dt.database_transaction_last_lsn
       , [Transaction_Text] = IsNull((SELECT text FROM sys.dm_exec_sql_text(sp.[sql_handle])),'')
FROM sys.dm_tran_active_transactions a
LEFT JOIN sys.dm_tran_session_transactions s ON a.transaction_id=s.transaction_id
LEFT JOIN sys.[dm_exec_connections] c ON s.session_id  = c.session_id
LEFT JOIN sys.dm_exec_sessions se on c.session_id = se.session_id
LEFT JOIN sys.dm_tran_database_transactions dt
       ON a.transaction_id = dt.transaction_id
LEFT JOIN sys.sysprocesses as sp ON sp.spid = s.session_id
WHERE s.session_id is Not Null
ORDER BY a.transaction_begin_time, s.session_id

OPTION (RECOMPILE);

This is pretty long query, but provides a lot of information about "WHO", "WHEN", "WHAT", "HOW". You can store that info to analyze it later.
If you are not interested in FULL investigation or that query is also blocked by other processes you can run its smaller version with only the basic info, such as Session ID and starting time of active transaction(s), which has some footprints in TempDB:

SELECT s.session_id, dt.database_transaction_begin_time
FROM sys.dm_tran_session_transactions s
INNER JOIN sys.dm_tran_database_transactions dt
       ON s.transaction_id = dt.transaction_id
WHERE dt.database_id = 2 and database_transaction_log_record_count > 0
OPTION (RECOMPILE);

At this point you might think you caught the troublemaker, but do not rush. Before any action check if that transaction is blocked by any other transaction using following query:
;WITH DS AS (
SELECT c.session_id
       , Blk_Id = r.blocking_session_id
       , r.wait_type
       , r.last_wait_type
       , r.wait_time
       , r.wait_resource
       , [DB_Name] = DB_Name(r.database_id)
       , s.login_name
       , r.command
       , Query_Text = SUBSTRING(t.text
                     , r.statement_start_offset/2 + CASE r.statement_start_offset WHEN 0 THEN 0 ELSE 1 END
                     , ABS(
                           CASE r.statement_end_offset
                                  WHEN -1 THEN DATALENGTH(t.text) ELSE r.statement_end_offset
                           END - r.statement_start_offset
                     )/2
              )
       , Query_Plan=tqp.query_plan
       , Batch_Plan=qp.query_plan
       , qmg.query_cost
       , total_time_Min = (2 * (CAST(r.total_elapsed_time AS BIGINT) & 0x80000000) + r.total_elapsed_time) / 60000.
       , Memory_Request = qmg.request_time
       , qmg.requested_memory_kb
       , qmg.granted_memory_kb
       , qmg.required_memory_kb
       , qmg.used_memory_kb
       , qmg.max_used_memory_kb
       , r.status
       , r.cpu_time
       , total_elapsed_time = 2 * (CAST(r.total_elapsed_time AS BIGINT) & 0x80000000) + r.total_elapsed_time
       , Memory = r.granted_query_memory
       , cnn_reads = c.num_reads
       , c.last_read
       , cnn_writes = c.num_writes
       , c.last_write
       , Batch_Text = t.text
       , request_reads = r.reads
       , request_writes = r.writes
       , r.logical_reads
       , s.host_name
       , s.program_name
       , c.client_net_address
       FROM sys.[dm_exec_connections] AS c
       CROSS APPLY sys.dm_exec_sql_text(c.[most_recent_sql_handle]) AS t
       INNER JOIN sys.dm_exec_sessions AS s on c.session_id = s.session_id
       LEFT JOIN sys.dm_exec_query_memory_grants qmg on c.session_id = qmg.session_id
       LEFT JOIN sys.dm_exec_requests AS r on c.session_id = r.session_id
       OUTER APPLY sys.dm_exec_query_plan(r.[plan_handle]) AS qp
       OUTER APPLY sys.dm_exec_text_query_plan(r.[plan_handle],r.statement_start_offset,r.statement_end_offset) AS tqp
       WHERE s.is_user_process = 1
)
SELECT s1.* FROM DS as s1 WHERE IsNull(s1.Blk_Id,0) > 0
UNION ALL
SELECT s1.* FROM DS as s1
WHERE EXISTS (SELECT TOP 1 1 FROM DS as s2 WHERE s2.Blk_Id = s1.session_id)
ORDER BY Blk_Id DESC, s1.session_id

OPTION (RECOMPILE);


You Might get something like this:

As you can see from this blocking chain, the initial troublemaker is Session #56. That is the one you need to KILL. That KILL should release other transaction and your problem will go away, but you might have to repeat the research and kill couple of times until your TempDB Log will be released.

To verify that you are OK now you can run following query
DBCC LOGINFO ('Tempdb');

You are supposed to get only "0" in the "Status" column, which would mean TempDB Log file's VLFs are inactive now.

At this point you are fine and can start analyzing results of researching queries to figure out what was the original cause of your problem and maybe take an action to prevent it in the future.

Thursday, August 15, 2019

Defining Data Type

Have you ever had a problem to sort values in a VARCHAR column by a different data types?
All of them are stored as a text, how to check their data types?

Here is a simple query to solve that problem:

DECLARE @table TABLE (Unknown VARCHAR(100));

INSERT INTO @table VALUES
('0'),('9'),('-27'),('257'),('   20190815'),('7855567554354')
,('0.00000000000000000000000000000000000063')
,('34.349732949394273943654684095609489063')
,('2349347923437438976567456687685.1'),('6.3E-37')
,('2019-08-15'),('23:45:17'),('20190815 23:45:09')
,('62FD6025-7775-4F28-88EB-BCF9DE33D831')
,('0x5B612D7A412D5A5D5B612D7A412D5A5D25');

SELECT [Value] = Unknown
       , [BIT] = IIF(LEN(Unknown) = 1 and TRY_CAST(Unknown AS TINYINT) in (0,1),'x',Null)
       , [TINYINT] = IIF(TRY_CAST(Unknown AS TINYINT) Is Not Null,'x',Null)
       , [SMALLINT] = IIF(TRY_CAST(Unknown AS SMALLINT) Is Not Null,'x',Null)
       , [INT] = IIF(TRY_CAST(Unknown AS INT) Is Not Null,'x',Null)
       , [BIGINT] = IIF(TRY_CAST(Unknown AS BIGINT) Is Not Null,'x',Null)
       , [DECIMAL] = IIF(TRY_CAST(Unknown AS DECIMAL(38,0)) Is NOT Null,'x',Null)
       , [FLOAT] = IIF(TRY_CAST(Unknown AS FLOAT) Is NOT Null,'x',Null)
       , [BINARY] = CASE WHEN Unknown not like '0x%' OR LEN(Unknown) % 2 = 1 THEN NULL
                    ELSE IIF(Unknown LIKE '0x%[g-zG-Z]%', NULL, 'x' ) END
       , [DATETIME] = IIF(TRY_CAST(Unknown AS DATETIME) Is Not Null,'x',Null)
       , [UNIQUEIDENTIFIER] = IIF(TRY_CAST(Unknown AS UNIQUEIDENTIFIER) Is Not Null,'x',Null)
FROM @table;

GO

Here is the result:


Tuesday, August 13, 2019

File Auto Grow history using default trace

Why it is important?

Unpredictable "File Auto Grow" can be real bottleneck in your highly performing system.
At first, if file growing chunks are too small the create unnecessary fragmentation on you physical drive and for the log file it generates humongous number of VLFs. The fragmentation slows down all your I/O operations and huge number of VLFs can really slow down your server startup procedure.
At second, when your chunks are too big it might generate unpredictable few seconds delay during busy business hours.
So, tracking these events and fight with them is one of DBA's priorities.

Last month I had a post on how to track and possibly alert a DBA about file growth event (https://slavasql.blogspot.com/2019/07/capturing-filegrowth-event-using-job.html)

That is pretty helpful, but what if you can't set it up or want to know about the past file growth events.

In that case a DBA might use a limited option to look back in the SQL Server history.

Assuming your default trace is enabled and nobody deleted its log files.
Then you will be able to run the following script to figure out ALL historical Auto file growth events:
DECLARE @FileName NVARCHAR(MAX) = (
       SELECT CAST(value as NVARCHAR(MAX))
       FROM ::fn_trace_getinfo(DEFAULT)
       WHERE traceid = 1 AND property = 2);

SELECT @FileName = LEFT(@FileName
       , LEN(@FileName)-CHARINDEX('_'
       ,REVERSE(@FileName),1))
              + RIGHT(@FileName,4);

SELECT t.StartTime
       , [Event Name] = te.name
       , t.DatabaseName 
       , t.Filename
       , [Growth, Mb] = CAST(t.IntegerData/128. as DECIMAL(18,3))
       , [Duration, ms] = t.duration/1000
       , t.Hostname
       , t.LoginName
       , t.ApplicationName
FROM ::fn_trace_gettable(@FileName, DEFAULT) as t
INNER JOIN sys.trace_events as te
       ON t.EventClass = te.trace_event_id
WHERE EventClass in (92,93)
ORDER BY t.StartTime DESC;

GO

Running this you are supposed to get similar output:

Limitations:

As you can guess, nothing is given for free. Default trace is capturing only "Log File Auto Grow" and
"Data File Auto Grow" (besides of many others), but does not capturing "Manual File Size" changes. Also, you can see only the increase in size, but not original or new file size. However, for simple troubleshooting that has to be more than enough.

Tuesday, August 6, 2019

Old and forgotten xp_availablemedia procedure

If you plan to use "xp_availablemedia" please stop right now!

It is very old and outdated procedure and it might be deprecated by Microsoft at any moment.

I am writing this post because I've spent an hour trying to decode it's output and found the method only in an ancient book "SQL Server 2000 Stored Procedures Handbook" from 2003!
You can buy it on Amazon: https://www.amazon.com/Server-Stored-Procedures-Handbook-Experts/dp/1590592875 and it still very valuable and costs more than $30.

So, giving credits to the authors Robin Dewson, Tony Bain, Chuck Hawkins & Louis Davidson for the great job.

For those who hit my post trying to figure out what "xp_availablemedia" is I'll give you the formula I've got from the book.

Here is a compare of values from xp_availablemedia output and an output from newer and recommended procedure xp_fixeddrives:
DECLARE @availablemedia TABLE (name SYSNAME, [low free] INT, [high free] INT, mediatype INT)
DECLARE @fixeddrives TABLE (drive CHAR(1), mbfree INT)
INSERT INTO @availablemedia EXEC master..xp_availablemedia 2;
INSERT INTO @fixeddrives EXEC master..xp_fixeddrives;

SELECT m.*
       , [Free Gb] = CAST(4 * (IIF(m.[low free]<0,1,0)+m.[high free]) + m.[low free] / 1073741824. AS DECIMAL(19,1))
       , [Free Mb] = CAST(4096 * (IIF(m.[low free]<0,1,0)+m.[high free]) + m.[low free] / 1048576. AS DECIMAL(19,0))
       , [Drives Free Mb] = d.mbfree
FROM @availablemedia as m
INNER JOIN @fixeddrives as d
       ON LEFT(m.name,1) = d.drive;

As you can see, the difference in numbers is very minor, but xp_fixeddrives does not have any hassles with additional calculations:

Here is the Book!!!


Monday, August 5, 2019

SQL Server String concatenation behavior.

Pretty recently hit very interesting problem of how SQL Server concatenates strings.

The very same SELECT clause in some cases produced VARCHAR(MAX), but in same cases it cut the result to VARCHAR(8000) ( or NVARCHAR(4000) )

After a research I've discovered following:
1. SQL Server is applying escalation of datatype to "MAX" ONLY in the case if at least one of the concatenated strings already has that type.
2. SQL Server does concatenation operation from left to right and treats the result as VARCHAR(8000) or NVARCHAR(4000) unless it hits the "MAX".

That means that the following query will return 8000 and not 8001:
SELECT LEN(REPLICATE('.',8000) + '1');

If we add to concatenated string "MAX" type value that will switch the result ONLY at the point of that "MAX" type value. So, the following script will return 8001:
SELECT LEN( REPLICATE('0',8000) + '1' + CAST('2' AS VARCHAR(MAX)) );

In order to get correct value for a string we need to have "MAX" type value BEFORE the string reaches size limit of 8000. For instance you can do following, which will return correct number of characters - 8002:
SELECT LEN( REPLICATE('0',8000) + CAST('1' AS VARCHAR(MAX))  + '2' );

So, in order to prevent any misbehavior in the future you can just put EMPTY "MAX" type value as the very first mamber of the concatenation:
SELECT LEN(CAST('' AS VARCHAR(MAX)) + REPLICATE('0',8000) + '1' + '2');

In case of use of variables the solution can be the same:
DECLARE @BigString VARCHAR(MAX);

SET @BigString = '';
SELECT @BigString = @BigString + REPLICATE('0',8000) + '1' + '2';

SELECT LEN(@BigString);

However, be careful with the following solution, which WON'T produce the correct result:
DECLARE @BigString VARCHAR(MAX);

SET @BigString = '';
SELECT @BigString += REPLICATE('0',8000) + '1' + '2';

SELECT LEN(@BigString);