Thursday, August 12, 2010

20 Tests that your database design should pass

Recently i discovered a small application named SqlCop that integrates many database tests. I tried the tool and it was pretty much ok, although I’ve discovered some application bugs but also some of the queries(tests) were not working in my environment. So i decided to completely rewrite all those queries, without worrying about older Sql Server versions, and stick only with views from sys schema. Queries are tested on Sql Server 2005 and 2008.

--    Procedures or functions with prefix sp_
SELECT  Found = OBJECT_SCHEMA_NAME(o.object_id) + '.' + o.name
      , o.type_desc
FROM    sys.objects o
WHERE   o.is_ms_shipped = 0
        AND o.type IN ('P', 'FN', 'TF', 'IF', 'AF', 'FT', 'IS', 'PC', 'FS')
        AND LOWER(o.name) LIKE 'sp[_]%'
        AND o.name NOT IN ('sp_alterdiagram', 'sp_creatediagram',
                               'sp_dropdiagram', 'sp_helpdiagramdefinition',
                               'sp_helpdiagrams', 'sp_renamediagram',
                               'sp_upgraddiagrams')
ORDER BY Found

--    Tables with prefix tbl
SELECT  Found = OBJECT_SCHEMA_NAME(t.object_id) + '.' + t.name
      , t.type_desc
FROM    sys.tables t
WHERE   t.type = 'U'
        AND LOWER(t.name) LIKE 'tbl%'
ORDER BY Found

--    VarChar missing size
SELECT  Found = OBJECT_SCHEMA_NAME(o.object_id) + '.' + o.name
      , o.type_desc
FROM    sys.objects o
WHERE   o.is_ms_shipped = 0
        AND o.type IN ('P', 'FN', 'TF', 'IF', 'AF', 'FT', 'IS', 'PC', 'FS')
        AND REPLACE(REPLACE(REPLACE(LOWER(OBJECT_DEFINITION(o.object_id)), ' ', ''), '    ', ''), ']', '') LIKE '%varchar[^(]%'
        AND o.name NOT LIKE 'SqlQueryNotificationStoredProcedure-%'
ORDER BY Found

--    Decimal missing size
SELECT  Found = OBJECT_SCHEMA_NAME(o.object_id) + '.' + o.name
      , o.type_desc
FROM    sys.objects o
WHERE   o.is_ms_shipped = 0
        AND o.type IN ('P', 'FN', 'TF', 'IF', 'AF', 'FT', 'IS', 'PC', 'FS')
        AND (REPLACE(REPLACE(REPLACE(LOWER(OBJECT_DEFINITION(o.object_id)), ' ', ''), '    ', ''), ']', '') LIKE '%decimal[^(]%'
            OR
            REPLACE(REPLACE(REPLACE(LOWER(OBJECT_DEFINITION(o.object_id)), ' ', ''), '    ', ''), ']', '') LIKE '%numeric[^(]%'
            )
        AND o.name NOT LIKE 'SqlQueryNotificationStoredProcedure-%'
ORDER BY Found

--    Using undocumented procedures
DECLARE @Temp TABLE(ProcedureName VARCHAR(50))
INSERT INTO @Temp VALUES('sp_MStablespace')
INSERT INTO @Temp VALUES('sp_who2')
INSERT INTO @Temp VALUES('sp_tempdbspace')
INSERT INTO @Temp VALUES('sp_MSkilldb')
INSERT INTO @Temp VALUES('sp_MSindexspace')
INSERT INTO @Temp VALUES('sp_MShelptype')
INSERT INTO @Temp VALUES('sp_MShelpindex')
INSERT INTO @Temp VALUES('sp_MShelpcolumns')
INSERT INTO @Temp VALUES('sp_MSforeachtable')
INSERT INTO @Temp VALUES('sp_MSforeachdb')
INSERT INTO @Temp VALUES('sp_fixindex')
INSERT INTO @Temp VALUES('sp_columns_rowset')
INSERT INTO @Temp VALUES('sp_MScheck_uid_owns_anything')
INSERT INTO @Temp VALUES('sp_MSgettools_path')
INSERT INTO @Temp VALUES('sp_gettypestring')
INSERT INTO @Temp VALUES('sp_MSdrop_object')
INSERT INTO @Temp VALUES('sp_MSget_qualified_name')
INSERT INTO @Temp VALUES('sp_MSgetversion')
INSERT INTO @Temp VALUES('xp_dirtree')
INSERT INTO @Temp VALUES('xp_subdirs')
INSERT INTO @Temp VALUES('xp_enum_oledb_providers')
INSERT INTO @Temp VALUES('xp_enumcodepages')
INSERT INTO @Temp VALUES('xp_enumdsn')
INSERT INTO @Temp VALUES('xp_enumerrorlogs')
INSERT INTO @Temp VALUES('xp_enumgroups')
INSERT INTO @Temp VALUES('xp_fileexist')
INSERT INTO @Temp VALUES('xp_fixeddrives')
INSERT INTO @Temp VALUES('xp_getnetname')
INSERT INTO @Temp VALUES('xp_readerrorlog')
INSERT INTO @Temp VALUES('sp_msdependencies')
INSERT INTO @Temp VALUES('xp_qv')
INSERT INTO @Temp VALUES('xp_delete_file')
INSERT INTO @Temp VALUES('sp_checknames')
INSERT INTO @Temp VALUES('sp_enumoledbdatasources')
INSERT INTO @Temp VALUES('sp_MS_marksystemobject')
INSERT INTO @Temp VALUES('sp_MSaddguidcolumn')
INSERT INTO @Temp VALUES('sp_MSaddguidindex')
INSERT INTO @Temp VALUES('sp_MSaddlogin_implicit_ntlogin')
INSERT INTO @Temp VALUES('sp_MSadduser_implicit_ntlogin')
INSERT INTO @Temp VALUES('sp_MSdbuseraccess')
INSERT INTO @Temp VALUES('sp_MSdbuserpriv')
INSERT INTO @Temp VALUES('sp_MSloginmappings')
INSERT INTO @Temp VALUES('sp_MStablekeys')
INSERT INTO @Temp VALUES('sp_MStablerefs')
INSERT INTO @Temp VALUES('sp_MSuniquetempname')
INSERT INTO @Temp VALUES('sp_MSuniqueobjectname')
INSERT INTO @Temp VALUES('sp_MSuniquecolname')
INSERT INTO @Temp VALUES('sp_MSuniquename')
INSERT INTO @Temp VALUES('sp_MSunc_to_drive')
INSERT INTO @Temp VALUES('sp_MSis_pk_col')
INSERT INTO @Temp VALUES('xp_get_MAPI_default_profile')
INSERT INTO @Temp VALUES('xp_get_MAPI_profiles')
INSERT INTO @Temp VALUES('xp_regdeletekey')
INSERT INTO @Temp VALUES('xp_regdeletevalue')
INSERT INTO @Temp VALUES('xp_regread')
INSERT INTO @Temp VALUES('xp_regenumvalues')
INSERT INTO @Temp VALUES('xp_regaddmultistring')
INSERT INTO @Temp VALUES('xp_regremovemultistring')
INSERT INTO @Temp VALUES('xp_regwrite')
INSERT INTO @Temp VALUES('xp_varbintohexstr')
INSERT INTO @Temp VALUES('sp_MSguidtostr')
SELECT Found = OBJECT_SCHEMA_NAME(o.object_id) + '.' + o.name
      , o.type_desc
FROM   sys.objects o
        INNER Join @Temp T ON LOWER(OBJECT_DEFINITION(o.object_id)) Like '%' + LOWER(T.ProcedureName) + '%'     
WHERE  o.is_ms_shipped = 0
        AND o.type IN ('P', 'FN', 'TF', 'IF', 'AF', 'FT', 'IS', 'PC', 'FS')
        AND o.name NOT LIKE 'SqlQueryNotificationStoredProcedure-%'
ORDER BY Found

--    Procedures without SET NOCOUNT ON
SELECT Found = OBJECT_SCHEMA_NAME(o.object_id) + '.' + o.name
      , o.type_desc
FROM   sys.objects o
WHERE  o.is_ms_shipped = 0
        AND o.type = 'P'
        AND UPPER(OBJECT_DEFINITION(o.object_id)) NOT LIKE '%SET NOCOUNT ON%'
        AND o.name NOT LIKE 'SqlQueryNotificationStoredProcedure-%'
ORDER BY Found

--    Procedures with SET ROWCOUNT
SELECT Found = OBJECT_SCHEMA_NAME(o.object_id) + '.' + o.name
      , o.type_desc
FROM   sys.objects o
WHERE  o.is_ms_shipped = 0
        AND o.type = 'P'
        AND UPPER(OBJECT_DEFINITION(o.object_id)) LIKE '%SET ROWCOUNT%'
        AND o.name NOT LIKE 'SqlQueryNotificationStoredProcedure-%'
ORDER BY Found

--    Procedures with @@identity
SELECT Found = OBJECT_SCHEMA_NAME(o.object_id) + '.' + o.name
      , o.type_desc
FROM   sys.objects o
WHERE  o.is_ms_shipped = 0
        AND o.type = 'P'
        AND o.name <> 'sp_creatediagram'
        AND LOWER(OBJECT_DEFINITION(o.object_id)) LIKE '%@@identity%'
        AND o.name NOT LIKE 'SqlQueryNotificationStoredProcedure-%'
ORDER BY Found

--    Invalid characters in column or object names
SELECT Found = OBJECT_SCHEMA_NAME(o.object_id) + '.' + o.name
      , o.type_desc
FROM   sys.objects o
WHERE  o.is_ms_shipped = 0
        AND LOWER(o.name) LIKE '%[^a-z0-9_.-]%'
        AND o.name NOT LIKE 'SqlQueryNotificationStoredProcedure-%'
UNION ALL
SELECT Found = OBJECT_SCHEMA_NAME(o.object_id) + '.' + o.name + '.' + c.name
      , type_desc = 'COLUMN'
FROM   sys.columns c
        INNER JOIN sys.objects o ON c.object_id = o.object_id
WHERE  o.is_ms_shipped = 0
        AND LOWER(c.name) LIKE '%[^a-z0-9_.-]%'
ORDER BY Found

--    Using of float or real data types
SELECT Found = OBJECT_SCHEMA_NAME(o.object_id) + '.' + o.name
      , o.type_desc
FROM   sys.objects o
WHERE  o.is_ms_shipped = 0
        AND o.name <> 'sp_creatediagram'
        AND o.name NOT LIKE 'SqlQueryNotificationStoredProcedure-%'
        AND (LOWER(OBJECT_DEFINITION(o.object_id)) LIKE '%float%'
            OR
            LOWER(OBJECT_DEFINITION(o.object_id)) LIKE '%real%')
UNION ALL
SELECT Found = OBJECT_SCHEMA_NAME(o.object_id) + '.' + o.name + '.' + c.name
      , type_desc = 'COLUMN'
FROM   sys.columns c
        INNER JOIN sys.objects o ON c.object_id = o.object_id
        INNER JOIN sys.types t ON c.system_type_id = t.system_type_id
WHERE  o.is_ms_shipped = 0
        AND t.name IN ('float', 'real')
ORDER BY FOUND

--    Using of text, ntext or image data types
SELECT Found = OBJECT_SCHEMA_NAME(o.object_id) + '.' + o.name
      , o.type_desc
FROM   sys.objects o
WHERE  o.is_ms_shipped = 0
        AND o.name <> 'sp_creatediagram'
        AND o.name NOT LIKE 'SqlQueryNotificationStoredProcedure-%'
        AND (LOWER(OBJECT_DEFINITION(o.object_id)) LIKE '%text%'
            OR
            LOWER(OBJECT_DEFINITION(o.object_id)) LIKE '%ntext%'
            OR
            LOWER(OBJECT_DEFINITION(o.object_id)) LIKE '%image%')
UNION ALL
SELECT Found = OBJECT_SCHEMA_NAME(o.object_id) + '.' + o.name + '.' + c.name
      , type_desc = 'COLUMN'
FROM   sys.columns c
        INNER JOIN sys.objects o ON c.object_id = o.object_id
        INNER JOIN sys.types t ON c.system_type_id = t.system_type_id
WHERE  o.is_ms_shipped = 0
        AND t.name IN ('text', 'ntext', 'image')
ORDER BY FOUND

--    Collation problem. Column's collation is different from database collation
SELECT Found = OBJECT_SCHEMA_NAME(o.object_id) + '.' + o.name + '.' + c.name
      , type_desc = 'COLUMN'
FROM   sys.columns c
        INNER JOIN sys.objects o ON c.object_id = o.object_id
WHERE  o.is_ms_shipped = 0
        AND CAST(c.collation_name AS VARCHAR(50)) <> CAST(DATABASEPROPERTYEX(DB_NAME(), 'Collation') AS VARCHAR(50))

--    Empty tables
SET NOCOUNT ON ;

CREATE TABLE #EmptyTables (Table_Name VARCHAR(100)) 

EXEC sp_msforeachtable 'If Not Exists(Select * From ?)
    Insert Into #EmptyTables Values(''?'')'

SELECT  Table_Name
FROM    #EmptyTables
ORDER BY Table_Name

DROP TABLE #EmptyTables

--    Tables without primary key
SELECT  Found = OBJECT_SCHEMA_NAME(t.object_id) + '.' + t.name
      , type_desc = 'PRIMARY_KEY'
FROM    sys.tables t
        LEFT OUTER JOIN (SELECT object_id FROM sys.indexes WHERE is_primary_key = 1) i ON t.object_id = i.object_id
WHERE i.object_id IS NULL

--    Tables without clustered index
SELECT  Found = OBJECT_SCHEMA_NAME(t.object_id) + '.' + t.name
      , type_desc = 'TABLE'
FROM sys.tables t
    LEFT OUTER JOIN (SELECT object_id FROM sys.indexes WHERE type = 1) i ON t.object_id = i.object_id
WHERE i.object_id IS NULL
ORDER BY Found

--    Missing foreign Keys
--      If a column ends with id but is not a foreign key, it should be renamed
SELECT  Found = OBJECT_SCHEMA_NAME(t.object_id) + '.' + t.name + '.' + c.name
      , type_desc = 'FOREIGN_KEY_CONSTRAINT'
FROM    sys.tables t
        INNER JOIN sys.columns c ON t.object_id = c.object_id
        LEFT OUTER JOIN (SELECT fk.parent_object_id, fkc.parent_column_id
                        FROM    sys.foreign_keys fk
                                INNER JOIN sys.foreign_key_columns fkc ON fk.object_id = fkc.constraint_object_id
                        ) fkc ON c.object_id = fkc.parent_object_id AND c.column_id = fkc.parent_column_id
        LEFT OUTER JOIN (SELECT ic.object_id, ic.column_id
                        FROM sys.index_columns ic
                            INNER JOIN sys.indexes i ON ic.object_id = i.object_id AND ic.index_id = i.index_id
                            INNER JOIN (SELECT object_id
                                        FROM sys.index_columns
                                        GROUP BY object_id, index_id
                                        HAVING COUNT(*) = 1) icu ON ic.object_id = icu.object_id
                        WHERE i.is_primary_key = 1
                        ) ic ON c.object_id = ic.object_id AND c.column_id = ic.column_id
WHERE   fkc.parent_column_id IS Null
        AND ic.object_id IS NULL
        AND t.name <> 'sysdiagrams'
        AND t.name NOT LIKE '%Log'
        AND t.name NOT LIKE 'Log%'
        AND c.name Like '%Id'
ORDER BY FOUND

--    Uniqueidentifier column used in clustered index as first column and have default value newid()
--        check for default value is commented to show all clustered indexes with first column with uniqueidentifier type
--        it is posible that having such first column and that to be fine if that is a foreign key column.
--        But if you set a default values on all such columns to newid(), even so you set their values in insert procedure,
--        then you can uncomment the last check, and with that remove the ones that are fine as described.
SELECT  Found = OBJECT_SCHEMA_NAME(t.object_id) + '.' + t.name + ': ' + i.name + ' (' + c.name + ')'
      , type_desc = 'CLUSTERED_INDEX'
FROM sys.index_columns ic
    INNER JOIN sys.indexes i ON ic.object_id = i.object_id AND ic.index_id = i.index_id
    INNER JOIN sys.tables t ON i.object_id = t.object_id
    INNER JOIN sys.columns c ON t.object_id = c.object_id AND ic.column_id = c.column_id
    INNER JOIN sys.types tp ON c.system_type_id = tp.system_type_id
WHERE i.type = 1
        AND ic.column_id = 1
        AND tp.name = 'uniqueidentifier'
        --AND c.default_object_id <> 0
ORDER BY Found

--    Missing index for foreign key columns
--  It is posible that creating indexes for all foreign keys is not required. So you should decide for which ones to create indexes.
SET NOCOUNT ON ;
CREATE TABLE #SmallTables (object_id int) 
EXEC sp_msforeachtable 'If Not Exists(SELECT * FROM (Select Rows = Count(*) From ?) R WHERE Rows < 100)
    Insert Into #SmallTables Values(OBJECT_ID(''?''))'

SELECT  Found = OBJECT_SCHEMA_NAME(t.object_id) + '.' + t.name + '.' + fkc.name + ' (' + c.name + ')'
      , type_desc = 'INDEX'
      , Resolution = 'CREATE NONCLUSTERED INDEX [IX_' + t.name + '_' + c.name + '] ON [' + OBJECT_SCHEMA_NAME(t.object_id) + '].[' + t.name + ']([' + c.name + '])'
FROM    sys.tables t
        INNER JOIN sys.columns c ON t.object_id = c.object_id
        INNER JOIN (SELECT fk.parent_object_id, fkc.parent_column_id, fk.name
                        FROM    sys.foreign_keys fk
                                INNER JOIN sys.foreign_key_columns fkc ON fk.object_id = fkc.constraint_object_id
                        ) fkc ON c.object_id = fkc.parent_object_id AND c.column_id = fkc.parent_column_id
        LEFT OUTER JOIN (SELECT ic.object_id, ic.column_id
                        FROM sys.index_columns ic
                            INNER JOIN sys.indexes i ON ic.object_id = i.object_id AND ic.index_id = i.index_id
                            INNER JOIN sys.index_columns icu ON ic.object_id = icu.object_id
                        WHERE ic.index_column_id = 1
                        ) ic ON fkc.parent_object_id = ic.object_id AND fkc.parent_column_id = ic.column_id
--        INNER JOIN sys.types tp ON c.system_type_id = tp.system_type_id
        INNER JOIN #SmallTables st ON t.object_id = st.object_id
WHERE   ic.object_id IS NULL
        AND t.name <> 'sysdiagrams'
        AND t.name NOT LIKE '%Log'
        AND t.name NOT LIKE 'Log%'
        --AND tp.name <> 'uniqueidentifier'
ORDER BY FOUND

DROP TABLE #SmallTables

--    Wide tables
SELECT  Found = OBJECT_SCHEMA_NAME(t.object_id) + '.' + OBJECT_NAME(t.object_id)
      , type_desc = 'TABLE'
FROM    sys.tables t
        INNER JOIN sys.columns c ON t.object_id = c.object_id
        INNER JOIN sys.types tp ON c.system_type_id = tp.system_type_id
GROUP BY t.object_id
HAVING  SUM(ISNULL(NULLIF(CONVERT(BIGINT, tp.max_length), 8000), 0) + CASE WHEN c.collation_name IS NULL THEN 0 ELSE c.max_length END) > 8060

--    Missing index
;WITH    missing_index_impact AS (
SELECT   dm_db_missing_index_groups.index_handle,
        SUM(( dm_db_missing_index_group_stats.avg_total_user_cost
              * dm_db_missing_index_group_stats.avg_user_impact
              * ( dm_db_missing_index_group_stats.user_seeks
                  + dm_db_missing_index_group_stats.user_scans ) )) AS "total_impact",
        ( 100.00
          * SUM(dm_db_missing_index_group_stats.avg_total_user_cost
                * dm_db_missing_index_group_stats.avg_user_impact
                * ( dm_db_missing_index_group_stats.user_seeks
                    + dm_db_missing_index_group_stats.user_scans ))
          / SUM(SUM(dm_db_missing_index_group_stats.avg_total_user_cost
                    * dm_db_missing_index_group_stats.avg_user_impact
                    * ( dm_db_missing_index_group_stats.user_seeks
                        + dm_db_missing_index_group_stats.user_scans ))) OVER ( ) ) AS "percent_impact",
        ROW_NUMBER() OVER ( ORDER BY SUM(avg_total_user_cost
                                         * avg_user_impact
                                         * ( user_seeks
                                             + user_scans )) DESC ) AS rn
    FROM     sys.dm_db_missing_index_groups AS dm_db_missing_index_groups
            JOIN sys.dm_db_missing_index_group_stats AS dm_db_missing_index_group_stats ON dm_db_missing_index_groups.index_group_handle = dm_db_missing_index_group_stats.group_handle
    GROUP BY    dm_db_missing_index_groups.index_handle
),
agg_missing_index_impact AS (
    SELECT   missing_index_impact_1.index_handle,
            missing_index_impact_1.total_impact,
            SUM(missing_index_impact_2.total_impact) AS running_total_impact,
            missing_index_impact_1.percent_impact,
            SUM(missing_index_impact_2.percent_impact) AS running_total_percent,
            missing_index_impact_1.rn
    FROM     missing_index_impact AS missing_index_impact_1
            JOIN missing_index_impact AS missing_index_impact_2 ON missing_index_impact_1.rn <= missing_index_impact_2.rn
    GROUP BY    missing_index_impact_1.index_handle,
            missing_index_impact_1.total_impact,
            missing_index_impact_1.percent_impact,
            missing_index_impact_1.rn
),
missing_index_details AS (
    SELECT   dm_db_missing_index_details.database_id,
            dm_db_missing_index_details."object_id",
            dm_db_missing_index_details.index_handle,
            dm_db_missing_index_details."statement",
            dm_db_missing_index_details.equality_columns,
            dm_db_missing_index_details.inequality_columns,
            dm_db_missing_index_details.included_columns
    FROM    sys.dm_db_missing_index_details AS dm_db_missing_index_details
)
SELECT  agg_missing_index_impact.rn,
    missing_index_details."statement",
    missing_index_details.equality_columns,
    missing_index_details.inequality_columns,
    missing_index_details.included_columns,
    agg_missing_index_impact.running_total_impact,
    agg_missing_index_impact.total_impact,
    agg_missing_index_impact.running_total_percent,
    agg_missing_index_impact.percent_impact,
    "key".index_key_column_count,
    "key".index_key_column_bytes,
    "all".index_all_column_count,
    "all".index_all_column_bytes
FROM    agg_missing_index_impact AS agg_missing_index_impact
    JOIN missing_index_details AS missing_index_details ON agg_missing_index_impact.index_handle = missing_index_details.index_handle
    JOIN ( SELECT   missing_index_details1.index_handle,
                    COUNT(*) AS index_key_column_count,
                    SUM(COL_LENGTH(missing_index_details1."statement",
                                   dm_db_missing_index_columns1.column_name)) AS index_key_column_bytes
           FROM     missing_index_details AS missing_index_details1
                    CROSS APPLY sys.dm_db_missing_index_columns(missing_index_details1.index_handle) AS dm_db_missing_index_columns1
           WHERE    dm_db_missing_index_columns1.column_usage = 'EQUALITY'
                    OR dm_db_missing_index_columns1.column_usage = 'INEQUALITY'
           GROUP BY    missing_index_details1.index_handle
         ) AS "key" ON missing_index_details.index_handle = "key".index_handle
    JOIN ( SELECT   missing_index_details2.index_handle,
                    COUNT(*) AS index_all_column_count,
                    SUM(COL_LENGTH(missing_index_details2."statement",
                                   dm_db_missing_index_columns2.column_name)) AS index_all_column_bytes
           FROM     missing_index_details AS missing_index_details2
                CROSS APPLY sys.dm_db_missing_index_columns(missing_index_details2.index_handle) AS dm_db_missing_index_columns2
           GROUP BY    missing_index_details2.index_handle
         ) AS "all" ON missing_index_details.index_handle = "all".index_handle
--WHERE missing_index_details.database_id = ?
ORDER BY    agg_missing_index_impact.rn ASC ;