Pages Already Recorded as Bad, and a Backup Check Before DBCC CHECKDB
An 823 or 824 just landed in the error log, or DBCC CHECKDB is due and you want to know whether anything is already recorded against this server. Get-SuspectPages lists the pages SQL Server has already flagged as bad, and any row with event_type 1, 2 or 3 is the one to check first. Get-DatabaseIntegrityChecks shows each database’s last backups, recovery model and page verify setting, so you know you have a restore to fall back on before the check runs.
What a Row in suspect_pages Means
- A row with
event_type1, 2 or 3 is a page to check now. 4, 5 and 7 are history. SQL Server writes the row when it hits an 823 or 824 on a page: a bad checksum, a torn page or an I/O error. MS Docs notes some rows come from transient errors, so a row means check the page, not proof it is still bad. - A restore does not clear the row. It changes
event_typeto 4, a repair sets 5 or 7, and the row stays until someone deletes it. Readevent_typebefore you panic over an old row.
When to Run These Scripts
- Get-SuspectPages, the moment an 823 or 824 shows up in the error log, and as a routine health check (it’s part of the standard health-check suite)
- Get-DatabaseIntegrityChecks, before a manual
DBCC CHECKDB, to confirm there is a backup and the recovery model you expect - A Get-SuspectPages row with
event_type1, 2 or 3 meansDBCC CHECKDBon that database now - Alongside Last DBCC CHECKDB and the rest of the Backups and Recovery scripts
The Scripts
1. Get-SuspectPages: Recorded Corruption Evidence
- Tested on: SQL Server 2025 (RTM CU8) 17.0.4075.5, Windows lab instance
- Last verified: 2026-10-05 (both scripts run on the lab instance; the Example Output is a real run)
- Permissions: any login that can connect, because msdb grants SELECT on
dbo.suspect_pagesanddbo.backupsetto public; database names need VIEW ANY DATABASE, which public also has by default - Safety: read-only, impact low
/*
Script Name : Get-SuspectPages
Category : maintenance-and-reliability
Purpose : Show any pages recorded in msdb.dbo.suspect_pages — evidence of I/O or corruption errors.
Author : Peter Whyte (https://sqldba.blog/dba-scripts-get-suspect-pages-and-integrity-checks/)
Requires : db_datareader on msdb
Notes : Any rows here indicate a serious integrity concern. Cross-reference with
the error log and run DBCC CHECKDB immediately on the affected database.
Entries persist until manually cleared or the database is restored clean.
HealthCheck : Yes
*/
-- SAFE:ReadOnly
-- IMPACT:Low
SET NOCOUNT ON;
SELECT
DB_NAME(sp.database_id) AS database_name,
sp.file_id,
sp.page_id,
CASE sp.event_type
WHEN 1 THEN '823/824 hard I/O error'
WHEN 2 THEN 'Bad checksum'
WHEN 3 THEN 'Torn page'
WHEN 4 THEN 'Restored (no longer suspect)'
WHEN 5 THEN 'Repaired by DBCC'
WHEN 7 THEN 'Deallocated by DBCC CHECKDB'
ELSE CAST(sp.event_type AS VARCHAR(10))
END AS event_type,
sp.error_count,
sp.last_update_date
FROM msdb.dbo.suspect_pages AS sp
ORDER BY sp.last_update_date DESC;
2. Get-DatabaseIntegrityChecks: Pre-Check Before Running DBCC CHECKDB
/*
Script Name : Get-DatabaseIntegrityChecks
Category : maintenance-and-reliability
Purpose : Pre-check database readiness and configuration for integrity validation runs.
Author : Peter Whyte (https://sqldba.blog/dba-scripts-get-suspect-pages-and-integrity-checks/)
Requires : VIEW ANY DATABASE, db_datareader on msdb
*/
-- SAFE:ReadOnly
-- IMPACT:Low
SET NOCOUNT ON;
-- Use as a pre-check before running DBCC CHECKDB.
SELECT
d.name AS database_name,
d.state_desc,
d.recovery_model_desc,
d.user_access_desc,
d.is_read_only,
d.is_auto_close_on,
d.is_auto_shrink_on,
d.page_verify_option_desc,
MAX(CASE WHEN bs.type = 'D' THEN bs.backup_finish_date END) AS last_full_backup,
MAX(CASE WHEN bs.type = 'I' THEN bs.backup_finish_date END) AS last_differential_backup,
MAX(CASE WHEN bs.type = 'L' THEN bs.backup_finish_date END) AS last_log_backup
FROM sys.databases AS d
LEFT JOIN msdb.dbo.backupset AS bs
ON d.name = bs.database_name
GROUP BY
d.name,
d.state_desc,
d.recovery_model_desc,
d.user_access_desc,
d.is_read_only,
d.is_auto_close_on,
d.is_auto_shrink_on,
d.page_verify_option_desc
ORDER BY d.name;
/*
Example manual integrity run in SSMS:
DBCC CHECKDB ('YourDatabaseName') WITH NO_INFOMSGS, ALL_ERRORMSGS;
For a faster targeted check on one database:
DBCC CHECKDB ('YourDatabaseName') WITH TABLOCK, PHYSICAL_ONLY, NO_INFOMSGS;
*/
How To Run From The Repo
Clone DBA Tools, initialize and run whichever script answers your question:
# Clone dba-tools repo:
git clone https://github.com/peterwhyte-lgtm/dba-tools
# Initialize environment:
cd dba-tools
.\Initialize-Environment.ps1
# Check for recorded corruption evidence:
.\run.ps1 Get-SuspectPages
# Pre-check database state and backup coverage before DBCC CHECKDB:
.\run.ps1 Get-DatabaseIntegrityChecks
# Either against a remote sql server:
.\run.ps1 Get-DatabaseIntegrityChecks -ServerInstance SQLSERVER01
These scripts live in the repo at:
sql/monitoring/databases/Get-SuspectPages.sqlsql/monitoring/databases/Get-DatabaseIntegrityChecks.sql
Example Output
Get-SuspectPages
I inserted this row by hand against a real data page of a lab table, rather than corrupting a file. MS Docs allows a DBA to add rows to msdb.dbo.suspect_pages.
A full restore of Lab1005_sp_db afterwards turned this row into event_type 4, Restored, and kept it. It is history now, and it stays until someone deletes it.
Get-DatabaseIntegrityChecks
5 of the 64 rows, trimmed to the columns that differ.
Lab1005_sp_reuseis the trap. It was dropped and created again, has never been backed up since, and still shows a full backup: the dropped database’s. Backup history is matched by name.Lab1005_sp_oldneeds 2 fixes before a check: no backup to fall back on, andTORN_PAGE_DETECTIONinstead ofCHECKSUM.Lab1005_sp_rstis a restored copy, so it shows no backups here even though its source has them.
Understanding the Results
event_typeDBCC CHECKDB on that database now, find the matching 823 or 824 in the error log, and plan a restore from backup before a repair. Errors 823 and 824 walks it.error_countlast_update_datelast_full_backuplast_log_backupDBCC CHECKDB, and if the database was recreated or restored recently, check the date against its create_date.page_verify_option_descCHECKSUM stores a checksum when a page is written and checks it on read; TORN_PAGE_DETECTION and NONE catch far less.Act when it is anything but CHECKSUM. ALTER DATABASE [name] SET PAGE_VERIFY CHECKSUM covers pages from their next write, not retroactively.Best Practices
- Treat any row with
event_type1, 2 or 3 as urgent. Delete old 4, 5 and 7 rows so the table never reaches its 1,000-row limit - Run Get-DatabaseIntegrityChecks before every manual
DBCC CHECKDB. A recent backup is cheap insurance against a check that finds something serious - Set
PAGE_VERIFY CHECKSUMon every database that isn’t already using it - Pair it with Last DBCC CHECKDB: this page tells you what has been recorded, that one tells you when the database was last actually checked
MS Docs covers DBCC CHECKDB, sys.databases, the suspect_pages table and managing it in full.
Related Scripts
You may also find these scripts useful:
- Backups and Recovery (hub)
- Last DBCC CHECKDB
- Errors 823 and 824, the incident page for a row with event_type 1, 2 or 3
- DBCC CHECKDB Found Corruption, what to do once the check confirms it
- Backup Coverage
- DBA Scripts: Get Database Health
- DBA Scripts: The Complete Guide, the map across every script on this site
Frequently Asked Questions
Does 0 rows from Get-SuspectPages guarantee no corruption exists?
No. It means nothing has been detected and recorded yet. msdb.dbo.suspect_pages only gets a row when SQL Server reads a bad page, in normal work or during a DBCC CHECKDB run. A database that has never been checked, and never happened to read a bad page, could still be corrupt. Last DBCC CHECKDB tells you whether anything has actually looked.
Why check backups before DBCC CHECKDB, if it is read-only?
DBCC CHECKDB itself is safe. The risk is what comes after: if it finds corruption, the repair may need REPAIR_ALLOW_DATA_LOSS, and a recent backup is the alternative to losing whatever that repair drops.
Does a restore clear the rows from suspect_pages?
No. A full, file or page restore changes the row’s event_type to 4, Restored, and a repair sets 5 or 7. In the lab, a full RESTORE ... WITH REPLACE turned both rows to 4 and kept them. Only DROP DATABASE, ALTER DATABASE ... REMOVE FILE or your own DELETE removes them.
Who can run these scripts?
Any login that can connect. msdb grants SELECT on dbo.suspect_pages and dbo.backupset to public, and a login with no msdb user at all returned the same rows as sysadmin.
The exception is a login denied VIEW ANY DATABASE: no error, but wrong answers. Get-SuspectPages showed its row with a NULL database_name, and Get-DatabaseIntegrityChecks returned only master and tempdb.
Inserting or deleting rows needs db_owner on msdb or sysadmin. Anyone else gets Msg 229.
Does this show when DBCC CHECKDB last succeeded?
No, that is Last DBCC CHECKDB. Two things to know when you read it: a database that has never been checked reports 1900-01-01, not NULL, and the date travels with a backup, so a restored copy shows its source’s last good check, not one run on the copy.
Summary
msdb.dbo.suspect_pages tells you what SQL Server has already caught. The pre-check tells you whether you have a backup in hand before DBCC CHECKDB looks for more.
Run Get-SuspectPages as a routine health check and the moment an 823 or 824 appears. Run Get-DatabaseIntegrityChecks before every manual DBCC CHECKDB.
Leave a Reply