Fading Coder

One Final Commit for the Last Sprint

Home > Notes > Content

Collecting Disk Capacity and Free Space Metrics in SQL Server

Notes 1

Monitoring storage consumption is a routine operational task. SQL Server exposes several way to inspect disk space, ranging from quick checks to more complete inventories that include total capacity.

Quicck check: xp_fixeddrives (free space only)

xp_fixeddrives shows the current free space (in MB) for fixed drives visible to SQL Server.

-- Minimal view of free space by drive letter (MB)
EXEC master.sys.xp_fixeddrives;

A formatted view in GB:

DECLARE @drives TABLE (
    drive     char(1),
    free_mb   int
);

INSERT @drives
EXEC master.sys.xp_fixeddrives;

SELECT
    drive                        AS Drive,
    CONVERT(decimal(12,2), free_mb / 1024.0) AS Free_GB
FROM @drives
ORDER BY drive;

Limitations:

  • Does not return total disk capacity, only free space.
  • Only reports drives that SQL Server can access.

Total and free space via sys.dm_os_volume_stats

For SQL Server 2008 R2 SP1 and newer, sys.dm_os_volume_stats exposes total and available bytes for volumes that host SQL Server database files.

-- Aggregate total/used/free space per volume hosting SQL Server files
WITH vols AS (
    SELECT
        REPLACE(vs.volume_mount_point, ':\', '') AS drive_key,
        vs.total_bytes                           AS total_bytes,
        vs.available_bytes                       AS free_bytes
    FROM sys.master_files AS mf
    CROSS APPLY sys.dm_os_volume_stats(mf.database_id, mf.file_id) AS vs
)
SELECT
    drive_key                                    AS Drive,
    CONVERT(decimal(18,2), SUM(total_bytes) / 1024.0 / 1024 / 1024)            AS Total_GB,
    CONVERT(decimal(18,2), SUM(total_bytes - free_bytes) / 1024.0 / 1024 / 1024) AS Used_GB,
    CONVERT(decimal(18,2), SUM(free_bytes) / 1024.0 / 1024 / 1024)              AS Free_GB,
    CONVERT(decimal(18,2), 100.0 * SUM(free_bytes) / NULLIF(SUM(total_bytes), 0)) AS Free_Pct
FROM vols
GROUP BY drive_key
ORDER BY drive_key;

Notes:

  • Requires SQL Server 2008 R2 SP1+.
  • Only shows volumes that contain SQL Server database files; drives with no SQL files are not included.

Persisted inventory for older versions: combine xp_fixeddrives and xp_cmdshell

On versions where sys.dm_os_volume_stats is unavailable or when you want a complete drive inventory (including drives without SQL files), you can persist results and compute total size using fsutil via xp_cmdshell.

The following scripts create a table and three stored procedures:

  • usp_refresh_drive_free_mb: capture free space using xp_fixeddrives
  • usp_populate_drive_capacity_mb: compute total capacity via fsutil volume diskfree
  • usp_report_drive_space: refresh and return a combined view
USE [monitor];
GO

-- Inventory table
IF OBJECT_ID('dbo.host_drive_inventory') IS NULL
BEGIN
    CREATE TABLE dbo.host_drive_inventory
    (
        drive_letter  nvarchar(10)  NOT NULL CONSTRAINT PK_host_drive_inventory PRIMARY KEY,
        capacity_mb   bigint        NOT NULL CONSTRAINT DF_host_drive_inventory_capacity_mb DEFAULT (0),
        free_mb       bigint        NOT NULL CONSTRAINT DF_host_drive_inventory_free_mb     DEFAULT (0),
        notes         nvarchar(400) NOT NULL CONSTRAINT DF_host_drive_inventory_notes       DEFAULT (N''),
        last_checked  datetime2     NOT NULL CONSTRAINT DF_host_drive_inventory_checked     DEFAULT (sysdatetime())
    );
END
GO

-- 1) Refresh free space using xp_fixeddrives
CREATE OR ALTER PROCEDURE dbo.usp_refresh_drive_free_mb
AS
BEGIN
    SET NOCOUNT ON;
    SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

    DECLARE @free TABLE (letter nvarchar(10), free_mb int);
    INSERT @free
    EXEC master.sys.xp_fixeddrives;

    MERGE dbo.host_drive_inventory AS t
    USING (SELECT letter, free_mb FROM @free) AS s
      ON t.drive_letter = s.letter
    WHEN MATCHED THEN
        UPDATE
            SET t.free_mb = s.free_mb,
                t.last_checked = sysdatetime()
    WHEN NOT MATCHED THEN
        INSERT (drive_letter, free_mb)
        VALUES (s.letter, s.free_mb);
END
GO

-- 2) For drives with unknown capacity, enable xp_cmdshell temporarily and query fsutil
CREATE OR ALTER PROCEDURE dbo.usp_populate_drive_capacity_mb
AS
BEGIN
    SET NOCOUNT ON;
    SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

    IF NOT EXISTS (SELECT 1 FROM dbo.host_drive_inventory WHERE capacity_mb = 0)
        RETURN;

    -- Enable xp_cmdshell (temporarily)
    EXEC sys.sp_configure N'show advanced options', 1;
    RECONFIGURE WITH OVERRIDE;
    EXEC sys.sp_configure N'xp_cmdshell', 1;
    RECONFIGURE WITH OVERRIDE;

    DECLARE @drv  nvarchar(10),
            @cmd  nvarchar(4000);

    DECLARE c CURSOR LOCAL FAST_FORWARD FOR
        SELECT drive_letter
        FROM dbo.host_drive_inventory
        WHERE capacity_mb = 0;

    OPEN c;
    FETCH NEXT FROM c INTO @drv;
    WHILE @@FETCH_STATUS = 0
    BEGIN
        SET @cmd = N'fsutil volume diskfree ' + @drv + N':';

        CREATE TABLE #fsutil_out (line nvarchar(4000));
        INSERT #fsutil_out
        EXEC master.sys.xp_cmdshell @cmd;

        DECLARE @cap_mb bigint;
        -- Parse the "Total # of bytes" line from fsutil output
        SELECT TOP (1)
               @cap_mb = TRY_CONVERT(bigint,
                         REPLACE(RIGHT(line, LEN(line) - CHARINDEX(':', line)), ' ', '')) / 1024 / 1024
        FROM #fsutil_out
        WHERE line LIKE '%Total # of bytes%';

        IF @cap_mb IS NOT NULL
        BEGIN
            UPDATE dbo.host_drive_inventory
            SET capacity_mb = @cap_mb
            WHERE drive_letter = @drv;
        END

        DROP TABLE #fsutil_out;
        FETCH NEXT FROM c INTO @drv;
    END

    CLOSE c;
    DEALLOCATE c;

    -- Disable xp_cmdshell again
    EXEC sys.sp_configure N'xp_cmdshell', 0;
    RECONFIGURE WITH OVERRIDE;
    EXEC sys.sp_configure N'show advanced options', 0;
    RECONFIGURE WITH OVERRIDE;
END
GO

-- 3) Unified view: refresh and return totals/used/free with percentages
CREATE OR ALTER PROCEDURE dbo.usp_report_drive_space
AS
BEGIN
    SET NOCOUNT ON;

    EXEC dbo.usp_refresh_drive_free_mb;
    EXEC dbo.usp_populate_drive_capacity_mb;

    SELECT
        drive_letter AS Drive,
        CONVERT(decimal(18,2), capacity_mb / 1024.0)                   AS Total_GB,
        CONVERT(decimal(18,2), (capacity_mb - free_mb) / 1024.0)       AS Used_GB,
        CONVERT(decimal(18,2), free_mb / 1024.0)                       AS Free_GB,
        CONVERT(decimal(18,2), 100.0 * free_mb / NULLIF(capacity_mb,0)) AS Free_Pct,
        last_checked
    FROM dbo.host_drive_inventory
    ORDER BY drive_letter;
END
GO

-- Example usage
EXEC dbo.usp_report_drive_space;

Behavioral notes:

  • The total capacity is only fetched for drives with capacity_mb = 0 to minimize the time xp_cmdshell is enabled.
  • This can be scheduled via SQL Server Agent to keep the invantory fresh.

Avoiding RECONFIGURE errors inside user transactions

If you enable xp_cmdshell programmatically while a user transaction is open, RECONFIGURE fails with error 574 (CONFIG statements cannot be used in user transactions). One approach is to run the configuration in dynamic SQL that starts with a COMMIT; capture and ignore the "no corresponding BEGIN TRANSACTION" error if no transaction is open.

DECLARE @cfg nvarchar(2000) = N'
COMMIT;
EXEC sys.sp_configure ''show advanced options'', 1;
RECONFIGURE WITH OVERRIDE;
EXEC sys.sp_configure ''xp_cmdshell'', 1;
RECONFIGURE WITH OVERRIDE;';

BEGIN TRY
    EXEC (@cfg);
END TRY
BEGIN CATCH
    IF ERROR_NUMBER() NOT IN (3902)  -- 3902: COMMIT without active transaction
        THROW;
END CATCH;

Alternatively, execute configuration on a separate connection that is guaranteed to be autocommit.

Querying additional disk/NTFS details via xp_cmdshell

Once xp_cmdshell is enibled, you can retrieve filesystem characteristics such as sector and cluster size.

-- Enable xp_cmdshell
EXEC sys.sp_configure 'show advanced options', 1;
RECONFIGURE;
EXEC sys.sp_configure 'xp_cmdshell', 1;
RECONFIGURE;

-- Example: sector and cluster size for drive D:
EXEC master.sys.xp_cmdshell 'fsutil fsinfo ntfsinfo D: | findstr /C:"Bytes Per Sector" /C:"Bytes Per Cluster"';

-- Disable xp_cmdshell
EXEC sys.sp_configure 'xp_cmdshell', 0;
RECONFIGURE;
EXEC sys.sp_configure 'show advanced options', 0;
RECONFIGURE;

On non-English systems, adjust the filter strings passed to find/findstr based on localized output from fsutil.

Related Articles

Designing Alertmanager Templates for Prometheus Notifications

This guide explains how to craft Alertmanager templates to format alert messages, improving clarity and presentation. Alertmanager uses Go’s text/template engine with additional helper functions. Ale...

Deploying a Maven Web Application to Tomcat 9 Using the Tomcat Manager

Tomcat 9 does not provide a dedicated Maven plugin. The Tomcat Manager interface, however, is backward-compatible, so the Tomcat 7 Maven Plugin can be used to deploy to Tomcat 9. This guide shows two...

Skipping Errors in MySQL Asynchronous Replication

When a replica halts because the SQL thread encounters an error, you can resume replication by skipping the problematic event(s). Two common approaches are available. Methods to Skip Errors 1) Skip a...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.