Over the years I was in the Storage Engine team I saw a lot of concern on the various forums about the ghost cleanup task. There have been a few bugs with it in previous versions (see these KB articles - 932115 and 815594) and there's very little info available on it. For some reason I didn't get around to posting about it on my old blog but today I want to go into some depth on it.
So what is ghost cleanup? It's a background process that cleans up ghost records - usually referred to as the ghost cleanup task. What's a ghost record? As I described briefly in the Anatomy of a record post last week, a ghost record is one that's just been deleted in an index on a table (well, actually it gets more complicated if snapshot isolation of some form is enabled but for now, a record in an index is a good start). Such a delete operation never physically removes records from pages - it only marks them as having been deleted, or ghosted. This is a performance optimization that allows delete operations to complete more quickly. It also allows delete operations to rollback more quickly because all that needs to happen is to unmark the records as being deleted/ghosted, instead of having to reinsert the deleted records. The deleted record will be physically removed (well, its slot will be removed - the record data isn't actually overwritten) later by the background ghost cleanup task. The ghost cleanup task will leave a single record on the page to avoid having to deallocate empty data or index pages.
The ghost cleanup task can't physically delete the ghost records until after the delete transaction commits because the deleted records are locked and the locks aren't released until the transaction commits. As an aside, when ghost records exist on a page, even a NOLOCK or READ UNCOMMITTED scan won't return them because they are marked as ghost records.
When a record is deleted, apart from it being marked as a ghost record, the page that the record is on is also marked as having ghost records in one of the allocation maps - the PFS page (post coming soon!) - and in its page header. Marking a page as having ghost records in a PFS page also changes the database state to indicate that there are some ghost records to cleanup - somewhere. Nothing tells the ghost cleanup task to clean the specific page that the delete happened on - yet. That only happens when the next scan operation reads the page and notices that the page has ghost records.
The ghost cleanup task doesn't just start up when it's told to - it starts up in the background every 5 seconds and looks for ghost records to cleanup. Remember that it won't be told to go cleanup a specific page by a delete operation - it's a subsequent scan that does it, if a scan happens. When the ghost cleanup task starts up it checks to see if its been told to cleanup a page - if so it goes and does it. If not, it picks the next database that is marked as having some ghost records and looks through the PFS allocation map pages to see if there are any ghost records to cleanup. It will check through or cleanup a limited number of pages each time it wakes up - I remember the limit is 10 pages - to ensure it doesn't swamp the system. So - the ghost records will eventually be removed - either by the ghost cleanup task processing a database for ghost records or by it specifically being told to remove them from a page. If it processes a database and doesn't find any ghost records, it marks the database as not having any ghost records so it will be skipped next time.
How can you tell its running? On SQL Server 2005, you can use the following code to see the ghost cleanup task in sys.dm_exec_requests:
SELECT * INTO myexecrequests FROM sys.dm_exec_requests WHERE 1 = 0;
GO
SET NOCOUNT ON;
GO
DECLARE @a INT
SELECT @a = 0;
WHILE (@a < 1)
BEGIN
INSERT INTO myexecrequests SELECT * FROM sys.dm_exec_requests WHERE command LIKE '%ghost%'
SELECT @a = COUNT (*) FROM myexecrequests
END;
GO
SELECT * FROM myexecrequests;
GO
And on SQL Server 2000 you need to use sysprocesses (well, on SQL Server 2005 this works as well but its fake view derived from the DMVs):
SELECT * INTO mysysprocesses FROM master.dbo.sysprocesses WHERE 1 = 0;
GO
SET NOCOUNT ON;
GO
DECLARE @a INT
SELECT @a = 0;
WHILE (@a < 1)
BEGIN
INSERT INTO mysysprocesses SELECT * FROM master.dbo.sysprocesses WHERE cmd LIKE '%ghost%'
SELECT @a = COUNT (*) FROM mysysprocesses
END;
GO
SELECT * FROM mysysprocesses;
GO
CREATE TABLE t1 (c1 CHAR(10))
CREATE CLUSTERED INDEX t1c1 on t1 (c1)
GO
BEGIN TRAN
INSERT INTO t1 VALUES ('PAUL')
INSERT INTO t1 VALUES ('KIMBERLY')
DELETE FROM t1 WHERE c1='KIMBERLY';
GO
DBCC IND ('ghostrecordtest', 't1', 1);
GO
DBCC TRACEON (3604);
GO
DBCC PAGE ('ghostrecordtest', 1, 143, 3);
GO
<snip>
m_freeData = 130 m_reservedCnt = 0 m_lsn = (20:88:20)
m_xactReserved = 0 m_xdesId = (0:518) m_ghostRecCnt = 1
m_tornBits = 0
<snip>
Slot 0 Offset 0x71 Length 17
Record Type = GHOST_DATA_RECORD Record Attributes = NULL_BITMAP
Memory Dump @0x6256C071
00000000: 1c000e00 4b494d42 45524c59 20200200 †....KIMBERLY ..
00000010: fc†††††††††††††††††††††††††††††††††††.
UNIQUIFIER = [NULL]
Slot 0 Column 1 Offset 0x4 Length 10
c1 = KIMBERLY
Slot 1 Offset 0x60 Length 17
Record Type = PRIMARY_RECORD Record Attributes = NULL_BITMAP
Memory Dump @0x6256C060
00000000: 10000e00 5041554c 20202020 20200200 †....PAUL ..
00000010: fc†††††††††††††††††††††††††††††††††††.
UNIQUIFIER = [NULL]
Slot 1 Column 1 Offset 0x4 Length 10
c1 = PAUL