Take the 2-minute tour ×
Database Administrators Stack Exchange is a question and answer site for database professionals who wish to improve their database skills and learn from others in the community. It's 100% free, no registration required.

I am working on a customized maintenance solution using the sys.dm_db_index_physical_stats view. I currently have it being referenced from a stored procedure. Now when that stored procedure runs on one of my databases, it does what I want it to do and pulls down a listing of all records regarding any database. When I place it on a different database it pulls down a listing of all records relating to only that DB.

For example (code at bottom):

  • Query run against Database 6 shows [requested] information for databases 1-10.
  • Query run against Database 3 shows [requested] information for only database 3.

The reason I want this procedure specifically on database three is because I'd prefer to keep all maintenance objects within the same database. I'd like to have this job sit in the maintenance database and work as if it were in that application database.

Code:

ALTER PROCEDURE [dbo].[GetFragStats] 
    @databaseName   NVARCHAR(64) = NULL
    ,@tableName     NVARCHAR(64) = NULL
    ,@indexID       INT          = NULL
    ,@partNumber    INT          = NULL
    ,@Mode          NVARCHAR(64) = 'DETAILED'
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @databaseID INT, @tableID INT

    IF @databaseName IS NOT NULL
        AND @databaseName NOT IN ('tempdb','ReportServerTempDB')
    BEGIN
        SET @databaseID = DB_ID(@databaseName)
    END

    IF @tableName IS NOT NULL
    BEGIN
        SET @tableID = OBJECT_ID(@tableName)
    END

    SELECT D.name AS DatabaseName,
      T.name AS TableName,
      I.name AS IndexName,
      S.index_id AS IndexID,
      S.avg_fragmentation_in_percent AS PercentFragment,
      S.fragment_count AS TotalFrags,
      S.avg_fragment_size_in_pages AS PagesPerFrag,
      S.page_count AS NumPages,
      S.index_type_desc AS IndexType
    FROM sys.dm_db_index_physical_stats(@databaseID, @tableID, 
           @indexID, @partNumber, @Mode) AS S
    JOIN 
       sys.databases AS D ON S.database_id = D.database_id
    JOIN 
       sys.tables AS T ON S.object_id = T.object_id
    JOIN 
       sys.indexes AS I ON S.object_id = I.object_id
                        AND S.index_id = I.index_id
    WHERE 
        S.avg_fragmentation_in_percent > 10
    ORDER BY 
        DatabaseName, TableName, IndexName, PercentFragment DESC    
END
GO
share|improve this question
    
I'm not sure I see a question in there...? –  Joachim Isaksson Jun 18 '13 at 13:59
4  
@JoachimIsaksson seems the question is how to have one single copy of the procedure in their maintenance database, that references the DMV in other databases, rather than having to put a copy of the procedure in each database. –  Aaron Bertrand Jun 18 '13 at 14:03
    
Sorry I wasn't more clear, been staring at this for a few days. Aaron is spot on. I want this SP to sit in my maintenance database with the ability to grab data from across the server. As it stands, when it sits in my maintenance DB, it only pulls in fragmentation data about the maintenance DB itself. What I'm confused on is why, when I place this exact same SP in a different database and execute it identically, does it pull it fragmentation data from across the server? Is there a setting or privilege that needs changed for this SP to operate as such from the maintenance DB? –  Josh Waclawski Jun 18 '13 at 14:12
    
(Note that your current approach ignores the fact that there could be two tables with the same name under two different schemas - in addition to the suggestions in my answer you may want to consider schema name as part of input and/or output.) –  Aaron Bertrand Jun 18 '13 at 15:36
add comment

migrated from stackoverflow.com Jun 18 '13 at 14:53

This question came from our site for professional and enthusiast programmers.

1 Answer

up vote 8 down vote accepted

One way would be to make a system procedure in master and then create a wrapper in your maintenance database. Note that this will only work for one database at a time.

First, in master:

USE [master];
GO
CREATE PROCEDURE dbo.sp_GetFragStats -- sp_prefix required
  @tableName    NVARCHAR(128) = NULL,
  @indexID      INT           = NULL,
  @partNumber   INT           = NULL,
  @Mode         NVARCHAR(20)  = N'DETAILED'
AS
BEGIN
  SET NOCOUNT ON;

  SELECT
    DatabaseName    = DB_NAME(),
    TableName       = t.name,
    IndexName       = i.name,
    IndexID         = s.index_id,
    PercentFragment = s.avg_fragmentation_in_percent,
    TotalFrags      = s.fragment_count,
    PagesPerFrag    = s.avg_fragment_size_in_pages,
    NumPages        = s.page_count,
    IndexType       = s.index_type_desc
    -- shouldn't s.partition_number be part of the output as well?
  FROM sys.tables AS t
  INNER JOIN sys.indexes AS i
    ON t.[object_id] = i.[object_id]
    AND i.index_id = COALESCE(@indexID, i.index_id)
    AND t.name = COALESCE(@tableName, t.name)
  CROSS APPLY
    sys.dm_db_index_physical_stats(DB_ID(), t.[object_id], 
      i.index_id, @partNumber, @Mode) AS s
  WHERE s.avg_fragmentation_in_percent > 10
  -- probably also want to filter on minimum page count too
  -- do you really care about a table that has 100 pages?
  ORDER BY 
    DatabaseName, TableName, IndexName, PercentFragment DESC;
END
GO
-- needs to be marked as a system object:
EXEC sp_MS_MarkSystemObject N'dbo.sp_GetFragStats';
GO

Now, in your maintenance database, create a wrapper that uses dynamic SQL to set the context correctly:

USE YourMaintenanceDatabase;
GO
CREATE PROCEDURE dbo.GetFragStats
  @DatabaseName SYSNAME,      -- can't really be NULL, right?
  @tableName    NVARCHAR(128) = NULL,
  @indexID      INT           = NULL,
  @partNumber   INT           = NULL,
  @Mode         NVARCHAR(20)  = N'DETAILED'
AS
BEGIN
  DECLARE @sql NVARCHAR(MAX);

  SET @sql = N'USE ' + QUOTENAME(@DatabaseName) + ';
    EXEC dbo.sp_GetFragStats @tableName, @indexID, @partNumber, @Mode;';

  EXEC sp_executesql 
    @sql,
    N'@tableName NVARCHAR(128),@indexID INT,@partNumber INT,@Mode NVARCHAR(20)',
    @tableName, @indexID, @partNumber, @Mode;
END
GO

(The reason the database name can't really be NULL is because you can't join to things like sys.objects and sys.indexes since they exist independently in each database. So perhaps have a different procedure if you want instance-wide information.)

Now you can call this for any other database, e.g.

EXEC YourMaintenanceDatabase.dbo.GetFragStats 
  @DatabaseName = N'AdventureWorks2012',
  @TableName    = N'SalesOrderHeader';

And you can always create a synonym in each database so you don't even have to reference the name of the maintenance database:

USE SomeOtherDatabase;`enter code here`
GO
CREATE SYNONYM dbo.GetFragStats FOR YourMaintenanceDatabase.dbo.GetFragStats;

Another way would be to use dynamic SQL, however this, too, will only work for one database at a time:

USE YourMaintenanceDatabase;
GO
CREATE PROCEDURE dbo.GetFragStats
  @DatabaseName SYSNAME,
  @tableName    NVARCHAR(128) = NULL,
  @indexID      INT           = NULL,
  @partNumber   INT           = NULL,
  @Mode         NVARCHAR(20)  = N'DETAILED'
AS
BEGIN
  SET NOCOUNT ON;

  DECLARE @sql NVARCHAR(MAX) = N'SELECT
    DatabaseName    = @DatabaseName,
    TableName       = t.name,
    IndexName       = i.name,
    IndexID         = s.index_id,
    PercentFragment = s.avg_fragmentation_in_percent,
    TotalFrags      = s.fragment_count,
    PagesPerFrag    = s.avg_fragment_size_in_pages,
    NumPages        = s.page_count,
    IndexType       = s.index_type_desc
  FROM ' + QUOTENAME(@DatabaseName) + '.sys.tables AS t
  INNER JOIN ' + QUOTENAME(@DatabaseName) + '.sys.indexes AS i
    ON t.[object_id] = i.[object_id]
    AND i.index_id = COALESCE(@indexID, i.index_id)
    AND t.name = COALESCE(@tableName, t.name)
  CROSS APPLY
    ' + QUOTENAME(@DatabaseName) + '.sys.dm_db_index_physical_stats(
        DB_ID(@DatabaseName), t.[object_id], i.index_id, @partNumber, @Mode) AS s
  WHERE s.avg_fragmentation_in_percent > 10
  ORDER BY 
    DatabaseName, TableName, IndexName, PercentFragment DESC;';

  EXEC sp_executesql @sql, 
    N'@DatabaseName SYSNAME, @tableName NVARCHAR(128), @indexID INT,
      @partNumber INT, @Mode NVARCHAR(20)',
    @DatabaseName, @tableName, @indexID, @partNumber, @Mode;
END
GO

Yet another way would be to create a view (or table-valued function) to union the table and index names of all your databases, however you'd have to hard-code the database names into the view, and maintain them as you add/remove databases that you want to allow to be included in this query. This would, unlike the others, allow you to retrieve stats for multiple databases at once.

First, the view:

CREATE VIEW dbo.CertainTablesAndIndexes
AS
  SELECT 
    db = N'AdventureWorks2012',
    t.[object_id],
    [table] = t.name,
    i.index_id,
    [index] = i.name
  FROM AdventureWorks2012.sys.tables AS t
  INNER JOIN AdventureWorks2012.sys.indexes AS i
  ON t.[object_id] = i.[object_id]

  UNION ALL

  SELECT 
    db = N'database2',
    t.[object_id],
    [table] = t.name,
    i.index_id,
    [index] = i.name
  FROM database2.sys.tables AS t
  INNER JOIN database2.sys.indexes AS i
  ON t.[object_id] = i.[object_id]

  -- ... UNION ALL ...
  ;
GO

Then the procedure:

CREATE PROCEDURE dbo.GetFragStats
  @DatabaseName NVARCHAR(128) = NULL,
  @tableName    NVARCHAR(128) = NULL,
  @indexID      INT           = NULL,
  @partNumber   INT           = NULL,
  @Mode         NVARCHAR(20)  = N'DETAILED'
AS
BEGIN
  SET NOCOUNT ON;

  SELECT
    DatabaseName    = DB_NAME(s.database_id),
    TableName       = v.[table],
    IndexName       = v.[index],
    IndexID         = s.index_id,
    PercentFragment = s.avg_fragmentation_in_percent,
    TotalFrags      = s.fragment_count,
    PagesPerFrag    = s.avg_fragment_size_in_pages,
    NumPages        = s.page_count,
    IndexType       = s.index_type_desc
  FROM dbo.CertainTablesAndIndexes AS v
  CROSS APPLY sys.dm_db_index_physical_stats
    (DB_ID(v.db), v.[object_id], v.index_id, @partNumber, @Mode) AS s
  WHERE s.avg_fragmentation_in_percent > 10
    AND v.index_id = COALESCE(@indexID, v.index_id)
    AND v.[table] = COALESCE(@tableName, v.[table])
    AND v.db = COALESCE(@DatabaseName, v.db)
  ORDER BY 
    DatabaseName, TableName, IndexName, PercentFragment DESC;
END
GO
share|improve this answer
add comment

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

Not the answer you're looking for? Browse other questions tagged or ask your own question.