If you’ve ever dug down in the SQL Server transaction logs or had to build up restore chains, then you’ll have come across Log Sequence Numbers (LSNs). Ever wondered why they’re so large, why they all look suspiciously the same, why don’t they start from 0 and just how does SQL Server generate these LSNs? Well, here we’re going to take a look at them

Below we’ll go through examples of how to look inside the current transaction log, and backed up transaction logs. This will involve using some DBCC commands and the undocumented fn_dblog and fn_dump_dblog function. The last 2 are very handy for digging into SQL Server internals, but be wary about running them on a production system without understanding what’s going on. They can leave filehandles and processes behind that can impact on your system.

So with that warning out of the way, let’s push on

Let’s start from the beginning, We’ll be using the DBCC LOG command here to peek into the transaction. First off let’s create 2 new databases to play with. If you’re following along, you need to run this block together. It’s not that it won’t run if you don’t, but comparing values get’s trickier as other Instance level operations may skew the results from each database making it harder to compare the LSNs

DBCC LOG (model)
CREATE DATABASE LsnPost1
DBCC LOG (LsnPost1)
CREATE DATABASE LsnPost2
DBCC LOG (LsnPost2)

Let’s have a look at the results, starting with model:

Current LSN Operation Context Transaction ID LogBlockGeneration
00000022:00000044:0001 LOP_BEGIN_CKPT LCX_NULL 0000:00000000 0
00000022:00000045:0001 LOP_XACT_CKPT LCX_BOOT_PAGE_CKPT 0000:00000000 0
00000022:00000046:0001 LOP_END_CKPT LCX_NULL A0000:00000000 0

Notice that Current LSN isn’t a nice nuerical value here. We’ll come back to that later on. Now we’ll look at the output for the next 2 databases. First thing to notice is that they both have the same ‘amount’ of info returned (70 rows on my 2016 SQL Express instance). We don’t need all of that information just now, so we’ll just look at the first 8 rows for each:

LsnPost1

Current LSN Operation Context Transaction ID LogBlockGeneration
00000023:00000018:0001 LOP_BEGIN_XACT LCX_NULL 0000:00000410 0
00000023:00000018:0002 LOP_MODIFY_COLUMNS LCX_CLUSTERED 0000:00000410 0
00000023:00000019:0001 LOP_MODIFY_COLUMNS LCX_CLUSTERED 0000:00000410 0
00000023:0000001a:0001 LOP_MODIFY_ROW LCX_BOOT_PAGE 0000:00000410 0
00000023:0000001a:0002 LOP_MODIFY_ROW LCX_BOOT_PAGE 0000:00000410 0
00000023:0000001b:0001 LOP_DELETE_ROWS LCX_MARK_AS_GHOST 0000:00000410 0
00000023:0000001b:0002 LOP_MODIFY_HEADER LCX_PFS 0000:00000000 0
00000023:0000001b:0003 LOP_SET_BITS LCX_PFS 0000:00000000 0

LsnPost2

Current LSN Operation Context Transaction ID LogBlockGeneration
00000023:00000018:0001 LOP_BEGIN_XACT LCX_NULL 0000:00000410 0
00000023:00000019:0001 LOP_MODIFY_COLUMNS LCX_CLUSTERED 0000:00000410 0
00000023:00000019:0002 LOP_MODIFY_COLUMNS LCX_CLUSTERED 0000:00000410 0
00000023:0000001b:0001 LOP_MODIFY_ROW LCX_BOOT_PAGE 0000:00000410 0
00000023:0000001b:0002 LOP_MODIFY_ROW LCX_BOOT_PAGE 0000:00000410 0
00000023:0000001c:0001 LOP_DELETE_ROWS LCX_MARK_AS_GHOST 0000:00000410 0
00000023:0000001c:0002 LOP_MODIFY_HEADER LCX_PFS 0000:00000000 0
00000023:0000001c:0003 LOP_SET_BITS LCX_PFS 0000:00000000 0

Those 2 pieces of output look VERY similar, and they should do, as they’re the standard operations SQL Server performs when it create a new database as a clone from Model.

This explains why LSNs will look similar across all databases created on the same instance, they are all seeded with much the same values at creation time. If the databases workload are vaguely similar, then they’ll increase in a very roughly similar way. Note that if you move a database across instances it will continue with it’s LSN values, and not pick it up from the new instance.

So, back to that strange looking Current LSN value. Starting from the left of the Current LSN we have 00000022 for Model and 00000023 for our Test databases. The fact that they’re sequential is interesting, and using DBCC LOGINFO we can see that it’s the File Sequence Number, just represented in hexadecimal (0x22 = 34, 0x23 = 35):

DBCC LOGINFO (model)
RecoveryUnitId FileId FileSize StartOffset FSeqNo Status Parity CreateLSN
0 2 253952 8192 34 2 128 0
DBCC LOGINFO (LsnPost1)
RecoveryUnitId FileId FileSize StartOffset FSeqNo Status Parity CreateLSN
0 2 2031616 8192 35 2 64 0

Now, this being computer related, it’s not quite that simple! Let’s take a look at a database that’s been around a while:

DBCC LOG (mscrm_config)
Current LSN Operation Context Transaction ID LogBlockGeneration
00015e2f:000002b8:0002 LOP_BEGIN_CKPT LCX_NULL 0000:00000000 0
00015e2f:000002b8:0003 LOP_COUNT_DELTA LCX_CLUSTERED 0000:00000000 0
00015e2f:000002b8:0004 LOP_COUNT_DELTA LCX_CLUSTERED 0000:00000000 0
00015e2f:000002b8:0005 LOP_COUNT_DELTA LCX_CLUSTERED 0000:00000000 0
00015e2f:000002b8:0006 LOP_COUNT_DELTA LCX_CLUSTERED 0000:00000000 0
DBCC LOGINFO (mscrm_config)
RecoveryUnitId FileId FileSize StartOffset FSeqNo Status Parity CreateLSN
0 2 3866624 156041216 89647 2 64 57548000000349600505

The current LSN starts with 00015e2f which if we treat as 0x00015e2f then we get 89647, the curent File Sequence Number.

Now to get the ‘official’ numeric versions of the LSN, we’ll just drop out a backup and read back the headers. I’m taking 2 log backups, as the first 1 on the new database will contain some over

ALTER DATABASE lsnpost1 SET RECOVERY FULL;
BACKUP DATABASE lsnpost1 TO DISK = 'c:\blogpost\lsn1.bak';
BACKUP LOG lsnpost1 TO DISK = 'c:\blogpost\lsn1.trn';
BACKUP LOG lsnpost1 TO DISK = 'c:\blogpost\lsn1_2.trn';
RESTORE HEADERONLY FROM DISK='c:\blogpost\lsn1_2.trn';
FirstLSN LastLSN CheckpointLSN DatabaseBackupLSN
35000000009400001 35000000011200001 35000000009600002 35000000005800065

And now the undocumented function fn_dump_dblog is going to come in to play. This lets us peak at the contents of a log backup in the same what that fn_dblog lets us look into the live transaction log. We’re going to lock for the 2 checkpoints that booked the log backup. This is a little bit contrived as during a real prod backup you may have checkpoints occurring while the backup is running

SELECT TOP 1 * FROM
fn_dump_dblog (
NULL, NULL, N'DISK', 1, N'c:\blogpost\lsn1_2.trn',
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT,
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT,
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT,
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT,
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT,
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT,
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT,
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT,
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT);

The abridged results should look somewhat like this:

Current LSN Operation Context
00000023:0000005e:0001 LOP_BEGIN_XACT LCX_NULL

So we know that 0x23 = 35, so that’s that’s the first 2 digits of FirstLSN decoded

The next fragment is 0000005e, which is obviously another hexadecimal value, which is 94 decimal. That appears after a string of 0’s. How many 0s though? Well, as it’s a hex value of 8 digits, the highest possible value is 0xffffffff which is 4294967295 decimal with 10 digits. Our value is 2 digits and 8 0s, 10 digits

And the final fragment is 0001. This is hex again, but this one I can do without a conversion utility and it’s 1 decmial. This is padded with 4 0s (largest hex value for that fragment is 0xffff, which is 65535 decimal, 5 digits total)

So that’s how we convert these values but what do they mean? To understand the second fragment we need to know that SQL Server first writes all log records to it’s Log Buffer before flushing them to disk in multiple 512k writes (https://blogs.msdn.microsoft.com/sql_pfe_blog/2013/03/28/observing-sql-server-transaction-log-flush-sizes-using-extended-events-and-process-monitor/). The Log Buffer is flushed to disk on one of these occasions:

A Transaction is committed

A transaction is committed once the record of it’s changes (the log) has been hardened to disk, so the Log Buffer must be flushed to disk. This flushes the whole Log Buffer to disk, not just the blocks involved in the Committing transaction

The Log Buffer is full

The Log Buffer is 60k in size (https://blogs.msdn.microsoft.com/sql_pfe_blog/2013/03/28/observing-sql-server-transaction-log-flush-sizes-using-extended-events-and-process-monitor/), once it’s full it has to be flushed to disk.

A checkpoint occurs

A checkpoint causes SQL Server to write all dirty pages and Buffer Log records to disk.

If you look at the full output of

SELECT * FROM
fn_dump_dblog (
NULL, NULL, N'DISK', 1, N'c:\blogpost\lsn1_2.trn',
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT,
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT,
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT,
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT,
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT,
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT,
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT,
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT,
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT);

You’ll see that the second fragment advances after an operations such as LOP_COMMIT_XACT or LOP_BEGIN_CKPT. This is because the Log Buffer was flushed to disk by one of theses actions. You’ll also notice that the fragment doesn’t increment nicely by 1. So with my demo database I go from “00000023:0000005e” to “00000023:00000060” rather than increment by 1 to “00000023:0000005f”, and later on I jump from “00000023:00000060” to “00000023:0000006f”, an increment of 15

The reason for these jumps is the amount of data that is flushed from the Log Buffer. This can be calculated by adding up the Log Record Length column. So for my large jump above I can do:

select *  from
fn_dump_dblog (
NULL, NULL, N'DISK', 1, N'c:\dbatools\lsn1_2.trn',
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT,
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT,
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT,
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT,
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT,
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT,
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT,
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT,
DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT, DEFAULT)
where [current lsn] like '00000023:00000060%'

which gives me 7088. So divide by 512 and I get 13.84, as SQL Server only write whole block, that’s 14 512 byte blocks written out. So the jump from 0x60 to 0x6f (15) is the number of 512 byte blocks written out from the Log Buffer, in this case by the log backup finishing. SO the biggest jump in this value you should see is 120 (0x78) as 120*512b = 60kb.

The final fragment then is just a simple counter of the number of records in that ‘block’ of Log Buffer

This is one of the main reason why LSNs are not a nice continuous number line you can do meaningful arithmetic on. You can go from 00000023:00000060:0002 (35000000009600002) to 00000023:00000061:0001 (35000000009700001), a jump of 99999 ‘LSNs’ with a single checkpoint call, so without analysing the actual work done in the log you can’t tell if those LSNs just ‘jumped’ or were actually consumed doing a large insert. So, as the documentation says, they’re only good for comparison purposes. All a larger LSN tells you is that the higher numbered action occurred after the lower numbered on. There is no meaning to the gap between them other than a count of the number of FileSequence and Log Buffer flushes between them.