CHECKPOINT and Simple Recovery Model

This article shows how checkpoint and simple recovery model works. For databases configured with the “simple” recovery model, it’s important to understand how checkpoints work1 under simple recovery model to avoid running out of log space. Checkpointing is the process of ensuring changes recorded in memory have been committed to the data file(s) in the affected database. Microsoft defines the checkpoint process like this:

… the Database Engine performs modifications to database pages in memory-in the buffer cache-and does not write these pages to disk after every change. Rather, the Database Engine periodically issues a checkpoint on each database. A checkpoint writes the current in-memory modified pages (known as dirty pages) and transaction log information from memory to disk…

The CHECKPOINT command can be used to manually force a checkpoint, which can be useful when rolling back a transaction if you run out of log space.

Adriaen Van Ostade - Winter Landscape with Ice Skaters - so much more fun than checkpoint and simple recovery!

Adriaen Van Ostade – Winter Landscape with Ice Skaters – so much more fun than checkpoint and simple recovery!

The code in this post shows how rolling back a transaction may not actually free the used log space, even under the simple recovery model. This is true even for indirect checkpoints issued by SQL Server 2016 and newer.

Monitoring for Checkpoints

Since we’re interested in the checkpoint process, you might want to create an Extended Events session to track checkpoints as detailed in this code.

Once you setup the checkpoint tracking extended events session, you can use the built-in GUI interface to watch the live session data. Alternately, use the code in this post to query the ring buffer for events.

The test-bed

We need to create a new, empty database for use with our minimal, complete, and verifiable example code:

In order to ensure the database is actually running in simple recovery model, we need to set the recovery model and perform a database backup. Since this is a test database that we don’t actually care about, I’m going to perform the backup to the NUL device. Don’t do this on a production database!

Let’s create a table, and add some data:

The code above adds one million rows to the table. Since each row is 8,004 bytes (plus a small amount of overhead), the table is going to consume 1,000,000 * 8192 bytes, or approximately 7,812.5 MB of space. This will consume 80% of our data file. However, the log will not see substantial growth since we are using the WITH (TABLOCKX) option. This option allows SQL Server to only record the page numbers touched by the DML operation, instead of capturing all the individual rows. If a rollback is required, those pages would simply be marked unallocated, as if they were never touched.

Updating One Million Rows

The second piece of code below updates each row in the table. This operation will require a large amount of log space, since every row will be touched. Before the update, we’re going to show the size of the data and log files, then execute the update statement inside a TRY...CATCH block, mimicking the way an update is typically coded.

The output from the above two queries look like:

╔══════════════╦══════════════════╦══════╦═════════╦═════════════════════╦═════════════════════╗
║     name     ║       name       ║ Size ║ MaxSize ║ recovery_model_desc ║ log_reuse_wait_desc ║
╠══════════════╬══════════════════╬══════╬═════════╬═════════════════════╬═════════════════════╣
║ LogSpaceTest ║ LogSpaceTest_Log ║ 8000 ║    8000 ║ SIMPLE              ║ NOTHING             ║
╚══════════════╩══════════════════╩══════╩═════════╩═════════════════════╩═════════════════════╝

╔══════════════╦═══════════╦═══════════╦════════════════╗
║ DatabaseName ║ LogSizeMB ║ LogUsedMB ║ LogUsedPercent ║
╠══════════════╬═══════════╬═══════════╬════════════════╣
║ LogSpaceTest ║      7999 ║        35 ║           0.44 ║
╚══════════════╩═══════════╩═══════════╩════════════════╝

The code that fills the log:

The message printed by the code above:

rolling back - The transaction log for database 'LogSpaceTest' is full due to 'ACTIVE_TRANSACTION'.

Check the Transaction Log

If we check the status of the log, we’ll see it is very close to full, and sys.databases reports there is an active transaction, even though there are no transactions ongoing:

Output looks like:

╔══════════════╦══════════════════╦══════╦═════════╦═════════════════════╦═════════════════════╗
║     name     ║       name       ║ Size ║ MaxSize ║ recovery_model_desc ║ log_reuse_wait_desc ║
╠══════════════╬══════════════════╬══════╬═════════╬═════════════════════╬═════════════════════╣
║ LogSpaceTest ║ LogSpaceTest_Log ║ 8000 ║    8000 ║ SIMPLE              ║ ACTIVE_TRANSACTION  ║
╚══════════════╩══════════════════╩══════╩═════════╩═════════════════════╩═════════════════════╝

╔══════════════╦═══════════╦═══════════╦════════════════╗
║ DatabaseName ║ LogSizeMB ║ LogUsedMB ║ LogUsedPercent ║
╠══════════════╬═══════════╬═══════════╬════════════════╣
║ LogSpaceTest ║      7999 ║      7698 ║          96.24 ║
╚══════════════╩═══════════╩═══════════╩════════════════╝

Even though the transaction has been rolled back, the log records will not be cleared until a checkpoint occurs. An automatic checkpoint could be triggered by other ongoing transactions being written to the log, or a manual CHECKPOINT statement could be executed. However, for a database that is not seeing frequent transactions, the log may stay nearly full for an extended period of time. This scenario might be seen often during development where there are a very limited number of transactions being generated. Being aware of the behavior of automatic checkpoints is important, and is documented in Microsoft Docs as such:

  1. An automatic checkpoint occurs each time the number of log records reaches the number the Database Engine estimates it can process during the time specified in the recovery interval server configuration option.

  2. The Database Engine estimates the maximum number of log records it can process within the recovery interval. When a database using automatic checkpoints reaches this maximum number of log records, the Database Engine issues a checkpoint on the database.

  3. Under the simple recovery model, an automatic checkpoint is also queued if the log becomes 70 percent full.

Note, in point 3 above, the documentation states a single checkpoint will be issued when the log becomes 70 % full. Once this checkpoint has been issued, further checkpoints may not be issued even thought the log is over 70% full, even if the automatic checkpoint resulted in no log truncation activity. However, if further transactions are generated, and the database engine believes the recovery time will be longer than the configured recovery interval, SQL Server will issue automatic checkpoints frequently as long as transactions are being generated. If no transactions are being generated, no automatic checkpoints will be issued.

Watching the Extended Events session we setup at the beginning of the post can be instrumental in showing when automatic checkpoints are occurring. They show up in the output with the is_system column set to True.

Summary and Follow-up

As shown in this post, the log_reuse_wait_desc column in sys.databases may not be as up-to-date as you’d like. If you see ACTIVE_TRANSACTION with a very full log, try running a CHECKPOINT command to see if that clears the log and resets the log reuse status to NOTHING.

Let’s run a CHECKPOINT, and see:

Results!

╔══════════════╦══════════════════╦══════╦═════════╦═════════════════════╦═════════════════════╗
║     name     ║       name       ║ Size ║ MaxSize ║ recovery_model_desc ║ log_reuse_wait_desc ║
╠══════════════╬══════════════════╬══════╬═════════╬═════════════════════╬═════════════════════╣
║ LogSpaceTest ║ LogSpaceTest_Log ║ 8000 ║    8000 ║ SIMPLE              ║ NOTHING             ║
╚══════════════╩══════════════════╩══════╩═════════╩═════════════════════╩═════════════════════╝

╔══════════════╦═══════════╦═══════════╦════════════════╗
║ DatabaseName ║ LogSizeMB ║ LogUsedMB ║ LogUsedPercent ║
╠══════════════╬═══════════╬═══════════╬════════════════╣
║ LogSpaceTest ║      7999 ║        35 ║           0.44 ║
╚══════════════╩═══════════╩═══════════╩════════════════╝

For the curious out there, I’ve left the various configuration settings for the checkpoint process at the defaults in order to mimic what is likely to be found “in the wild”. However, running the tests above with the following setting changes makes no observable change to the outcome.

  1. EXEC sp_configure 'recovery interval','seconds';
  2. ALTER DATABASE ... SET TARGET_RECOVERY_TIME = target_recovery_time { SECONDS | MINUTES };

Let me know if you enjoyed this post, or if you think it could be improved. Perhaps you might like the rest of our posts on troubleshooting.


1 – CHECKPOINT is of course important for the FULL and BULK LOGGED recovery models, too!