MS SQL巡检系列——检查外键字段是否缺少索引

前言感想:一时兴起,突然想写一个关于MS SQL的巡检系列方面的文章,因为我觉得这方面的知识分享是有价值,也是非常有意义的。一方面,很多经验不足的人,对于巡检有点茫然,不知道要从哪些方面巡检,另外一方面,网上关于MS SQL巡检方面的资料好像也不是特别多。写这个系列只是一个分享,自己的初衷是一个知识梳理、总结提炼过程,有些知识和脚本也不是原创,文章很多地方融入了自己的一些想法和见解的,不足和肤浅之处肯定也非常多,抛砖引玉,也希望大家提意见和建议、补充,指正其中的不足之处。Stay Hungry Stay Foolish!

 

MS SQL巡检系列——检查重复索引

MS SQL巡检系列——检查外键字段是否缺少索引

MS SQL巡检系列——检查数据库上一次DBCC CHECKDB的时间

 

对于绝大部分情况,外键字段都有必要建立对应的索引(注意,外键约束并不会自动建立索引),关于外键字段为什么要建立索引?下面从几个简单的例子来分析一下。我们先准备测试环境数据。

CREATE TABLE PRIMARY_TB
(
    PRODUCT_CD        VARCHAR(12)      ,
    PRODUCT_DATE    DATE             ,
    PRODUCT_DESC    VARCHAR(120)     ,
    CONSTRAINT PK_PRIMARY_TB  PRIMARY KEY CLUSTERED (PRODUCT_CD)
);
 
 
SET NOCOUNT ON;
GO
DECLARE @Index INT=1;
 
 
BEGIN TRAN
WHILE @Index <= 3000
BEGIN
 
    INSERT INTO dbo.PRIMARY_TB
    SELECT 'Prd' + CAST(@Index AS VARCHAR(4)), GETDATE() - CEILING(RAND()*200), 'production description' + CAST(@Index AS VARCHAR(4));
 
    SET @Index +=1;
END
 
COMMIT;
 
 
 
CREATE TABLE FK_TB
(
    FK_ID            INT IDENTITY(1,1),
    SALES_REGION    VARCHAR(32),
    SALES_CITY        VARCHAR(32),
    PRODUCT_CD        VARCHAR(12),
    SALIES_SUM        INT,
    CONSTRAINT PK_FK_TB PRIMARY KEY CLUSTERED (FK_ID)
)
GO
 
ALTER TABLE [dbo].[FK_TB]  WITH CHECK ADD  CONSTRAINT [FK_PRIMARY_TB_PRODUCT_CD] FOREIGN KEY([PRODUCT_CD])
REFERENCES [dbo].[PRIMARY_TB] ([PRODUCT_CD]) ON  DELETE CASCADE;
GO
 
 
SET NOCOUNT ON;
GO
DECLARE @Index INT=1;
 
BEGIN TRAN
WHILE @Index <=1000000
BEGIN
    INSERT INTO FK_TB
    SELECT 'REGION'+CAST(CEILING(RAND()*20) AS VARCHAR(2)), CAST(CEILING(RAND()*300) AS VARCHAR(3)),'Prd'+ CAST(CEILING(RAND()*3000) AS VARCHAR(8)),CEILING(RAND()*100000);
 
    SET @Index +=1;
 
END
 
COMMIT;
 
 
UPDATE STATISTICS dbo.PRIMARY_TB WITH FULLSCAN;
UPDATE STATISTICS dbo.FK_TB WITH FULLSCAN;
GO

 

1: 外键字段建立索引,在主表与子表JOIN操作时能提高性能,减少IO操作。

DBCC DROPCLEANBUFFERS;
GO
DBCC FREEPROCCACHE;
GO
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
 
SELECT  p.PRODUCT_CD ,
        p.PRODUCT_DATE ,
        f.SALES_REGION ,
        f.SALES_CITY ,
        f.SALIES_SUM
FROM    dbo.PRIMARY_TB p
        INNER JOIN dbo.FK_TB f ON p.PRODUCT_CD = f.PRODUCT_CD
WHERE p.PRODUCT_CD ='Prd131';
SET STATISTICS IO OFF;
SET STATISTICS TIME OFF;

 

如下截图所示,如果外键字段缺少索引,这两个表关联查询时,子表就会走扫描(此处测试是聚集索引扫描),如果子表非常大(例如此处案例所示),IO开销就比较大。

 

我们对外键约束字段PRODUCT_CD建立下面非聚集索引IDX_FK_TB,然后对比两者的执行计划和IO开销

CREATE INDEX IDX_FK_TB ON dbo.FK_TB(PRODUCT_CD);
 
DBCC DROPCLEANBUFFERS;
GO
DBCC FREEPROCCACHE;
GO
 
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
SELECT  p.PRODUCT_CD ,
        p.PRODUCT_DATE ,
        f.SALES_REGION ,
        f.SALES_CITY ,
        f.SALIES_SUM
FROM    dbo.PRIMARY_TB p
        INNER JOIN dbo.FK_TB f ON p.PRODUCT_CD = f.PRODUCT_CD
WHERE p.PRODUCT_CD ='Prd131'
 
SET STATISTICS IO OFF;
SET STATISTICS TIME OFF;

 

你会发现执行计划从原来的聚集索引扫描(Clustered Index Scan)变成了索引查找(Index Seek),IO的减少也是非常明显的。因为这里仅仅是测试数据,复杂的生产环境,性能的提升有可能比这更加明显。

 

2:如果外键约束为CASCADE(on update/delete)时,则当修改被引用行(referenced row)时,所有引用行(referencing rows )也必须修改(更新或级联删除)。外键列上的索引能减小锁的粒度和范围,从而提高效率和性能。如下所示:

 

我们先看看缺少索引的情况。

DROP INDEX IDX_FK_TB ON dbo.FK_TB;
 
 
 
DBCC DROPCLEANBUFFERS;
GO
DBCC FREEPROCCACHE;
GO
 
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
 
DELETE FROM dbo.PRIMARY_TB WHERE PRODUCT_CD IN ('Prd132','Prd133')
GO
SET STATISTICS IO OFF;
SET STATISTICS TIME OFF;
CREATE INDEX IDX_FK_TB ON dbo.FK_TB(PRODUCT_CD);
 
GO
 
DBCC DROPCLEANBUFFERS;
 
GO
 
DBCC FREEPROCCACHE;
 
GO
 
SET STATISTICS IO ON;
 
SET STATISTICS TIME ON;
 
DELETE FROM dbo.PRIMARY_TB WHERE PRODUCT_CD IN ('Prd134','Prd135')
 
GO
 
SET STATISTICS IO OFF;
 
SET STATISTICS TIME OFF;

 

3:如果外键关系为NO ACTION(on update/delete)时,那么被引用的行不能被删除,因为这个操作会导致引用行变成“孤立”。删除之前,数据库会为了有效地查找、定位行,外键列上创建索引也非常有帮助。

 

上面肤浅的构造了简单案例,并对比外键约束字段拥有索引和缺少索引时,SQL的执行计划和IO的差异,那么接下来,我们进入正题,巡检的时候,我们必须通过脚本找到数据库里面外键字段没有索引的相关表和信息,并生成对应的创建索引的脚本。如下所示。

/*
One or more tables found, with foreign key constraint defined but no supporting indexes created on the foreign key columns.

SQL Server doesnt put an index on foreign key columns by default and indexing foreign key fields in referencing tables is not required. 

Foreign key columns usage must evaluated to determine whether or not indexing this column will help up increase the current
workloads performance by enhancing join performance, reducing table locking (for full table scans) while cascading updates and 
deletes, etc.

*/
 
;
WITH    FKTable
          AS ( SELECT   SCHEMA_NAME(po.schema_id) AS 'parent_schema_name' ,
                        OBJECT_NAME(fkc.parent_object_id) AS 'parent_table_name' ,
                        OBJECT_NAME(constraint_object_id) AS 'constraint_name' ,
                        SCHEMA_NAME(ro.schema_id) AS 'referenced_schema' ,
                        OBJECT_NAME(referenced_object_id) AS 'referenced_table_name' ,
                        ( SELECT    '[' + COL_NAME(k.parent_object_id,
                                                   parent_column_id) + ']' AS [data()]
                          FROM      sys.foreign_key_columns (NOLOCK) AS k
                                    INNER JOIN sys.foreign_keys  (NOLOCK) ON k.constraint_object_id = object_id
                                                              AND k.constraint_object_id = fkc.constraint_object_id
                          ORDER BY  constraint_column_id
                        FOR
                          XML PATH('')
                        ) AS 'parent_colums' ,
                        ( SELECT    '[' + COL_NAME(k.referenced_object_id,
                                                   referenced_column_id) + ']' AS [data()]
                          FROM      sys.foreign_key_columns (NOLOCK) AS k
                                    INNER JOIN sys.foreign_keys  (NOLOCK) ON k.constraint_object_id = object_id
                                                              AND k.constraint_object_id = fkc.constraint_object_id
                          ORDER BY  constraint_column_id
                        FOR
                          XML PATH('')
                        ) AS 'referenced_columns'
               FROM     sys.foreign_key_columns fkc ( NOLOCK )
                        INNER JOIN sys.objects po ( NOLOCK ) ON fkc.parent_object_id = po.object_id
                        INNER JOIN sys.objects ro ( NOLOCK ) ON fkc.referenced_object_id = ro.object_id
               WHERE    po.type = 'U'
                        AND ro.type = 'U'
               GROUP BY po.schema_id ,
                        ro.schema_id ,
                        fkc.parent_object_id ,
                        constraint_object_id ,
                        referenced_object_id
             ),
 
        /* Index Columns */
        IndexColumnsTable
          AS ( SELECT   SCHEMA_NAME(o.schema_id) AS 'schema_name' ,
                        OBJECT_NAME(o.object_id) AS TableName ,
                        ( SELECT    CASE key_ordinal
                                      WHEN 0 THEN NULL
                                      ELSE '[' + COL_NAME(k.object_id,
                                                          column_id) + ']'
                                    END AS [data()]
                          FROM      sys.index_columns (NOLOCK) AS k
                          WHERE     k.object_id = i.object_id
                                    AND k.index_id = i.index_id
                          ORDER BY  key_ordinal ,
                                    column_id
                        FOR
                          XML PATH('')
                        ) AS cols
               FROM     sys.indexes (NOLOCK) AS i
                        INNER JOIN sys.objects o ( NOLOCK ) ON i.object_id = o.object_id
                        INNER JOIN sys.index_columns ic ( NOLOCK ) ON ic.object_id = i.object_id
                                                              AND ic.index_id = i.index_id
                        INNER JOIN sys.columns c ( NOLOCK ) ON c.object_id = ic.object_id
                                                              AND c.column_id = ic.column_id
               WHERE    o.type = 'U'
                        AND i.index_id > 0
               GROUP BY o.schema_id ,
                        o.object_id ,
                        i.object_id ,
                        i.name ,
                        i.index_id ,
                        i.type
             ),
        FKWithoutIndexTable
          AS ( SELECT   fk.parent_schema_name AS SchemaName ,
                        fk.parent_table_name AS TableName ,
                        fk.referenced_schema AS ReferencedSchemaName ,
                        fk.referenced_table_name AS ReferencedTableName ,
                        fk.constraint_name AS ConstraintName ,
                        fk.referenced_columns AS Referenced_Columns ,
                        fk.parent_colums AS Parent_Columns
               FROM     FKTable fk
               WHERE    NOT EXISTS ( SELECT 1
                                     FROM   IndexColumnsTable ict
                                     WHERE  fk.parent_schema_name = ict.schema_name
                                            AND fk.parent_table_name = ict.TableName
                                            AND fk.parent_colums = LEFT(ict.cols,
                                                              LEN(fk.parent_colums)) )
             )
    SELECT  @@SERVERNAME AS InstanceName ,
            DB_NAME() AS DatabaseName ,
            SchemaName ,
            TableName ,
            Parent_Columns ,
            ReferencedSchemaName ,
            ReferencedTableName ,
            Referenced_Columns ,
            ConstraintName
    INTO    #ForeignKeyWithOutIndex
    FROM    FKWithoutIndexTable
    ORDER BY DatabaseName ,
            SchemaName ,
            TableName;
 
 
 
 
--输出临时表数据
SELECT  *
FROM    #ForeignKeyWithOutIndex;
 
 
--生成外键字段缺少的索引,请抽查、检验,确认后批量执行
SELECT  'CREATE INDEX IX_' + LTRIM(RTRIM(TableName)) + '_'
        + SUBSTRING(Parent_Columns, 2, LEN(Parent_Columns) - 2) + '  ON '
        + LTRIM(RTRIM(SchemaName)) + '.' + LTRIM(RTRIM(TableName)) + '('
        + Parent_Columns + ');'
FROM    #ForeignKeyWithOutIndex;
 
 
--删除临时表
DROP TABLE #ForeignKeyWithOutIndex;
 

在创建这些索引前最好检查、确认一下,外键字段创建索引能提高性能,但是肯定也要特殊的场景和上下文不适合,所以最好根据实际情况决定。索引创建之后,通过监控工具监控一下数据库性能、等待事件的变化。

 

参考资料:

http://stackoverflow.com/questions/3650690/should-every-sql-server-foreign-key-have-a-matching-index

http://sqlblog.com/blogs/greg_low/archive/2008/07/29/indexing-foreign-keys-should-sql-server-do-that-automatically.aspx

http://www.sqlskills.com/blogs/kimberly/when-did-sql-server-stop-putting-indexes-on-foreign-key-columns/

 

时间: 2024-09-20 08:48:52

MS SQL巡检系列&mdash;&mdash;检查外键字段是否缺少索引的相关文章

MS SQL巡检系列&amp;mdash;&amp;mdash;检查重复索引

前言感想:一时兴起,突然想写一个关于MS SQL的巡检系列方面的文章,因为我觉得这方面的知识分享是有价值,也是非常有意义的.一方面,很多经验不足的人,对于巡检有点茫然,不知道要从哪些方面巡检,另外一方面,网上关于MS SQL巡检方面的资料好像也不是特别多.写这个系列只是一个分享,自己的初衷是一个知识梳理.总结提炼过程,有些知识和脚本也不是原创,文章很多地方也是融入了自己的一些想法和见解的,不足和肤浅之处肯定也非常多,抛砖引玉,也希望大家提意见和建议.补充,指正其中的不足之处.Stay Hungr

MS SQL巡检系列&amp;mdash;&amp;mdash;检查数据库上一次DBCC CHECKDB的时间

DBCC CHECKDB检查指定数据库中的所有对象的逻辑和物理完整性,具体请参考MSDN文档.我们必须定期对数据库做完整性检查(DBCC CHECKDB),以便能及时发现一些数据库损坏(Corruption)的情况.如果你的数据库长时间没有做DBCC CHECKDB,这样是做是不合理,并且很危险的.那么我们怎么检查数据库上一次做DBCC CHECKDB的时间呢? 可以通过DBCC DBINFO来获取上一次做DBCC CHECKDB时间,DBCC DBINFO (db_name) 显示数据库的结构

Easyui datagrid 实现外键字段转义

问题描述 Easyui datagrid 实现外键字段转义 推荐人,关联的是同一张表的ID,怎么让1显示张, page = page == null ? 1 : page; rows = rows == null ? 1 : rows; List<TBL_M_SECURITY_STAFF> list = db.TBL_M_SECURITY_STAFF.OrderBy(a => a.Security_Staff_ID).Skip((Convert.ToInt32(page) - 1) *

基于MVC4+EasyUI的Web开发框架经验总结(9)--在Datagrid里面实现外键字段的转义操作

我们在使用EasyUI的时候,很多情况下需要使用到表格控件datagrid,这个控件控件非常强大,使用起来很简洁,但是我在使用中,发现对于一个表里面的外键字段进行转义,并显示引用表的一些名称的操作,却显得比较难以实现,找了很多资料,基本上没有找到对应的解决方案.本文主要介绍我对这种外键字段转义的操作的实现方式,以便供大家参考了解. 1.DataGrid的初始化操作 在了解对内容的解析前,我们先来了解EasyUI里面Datagrid的初始化操作过程,然后逐步进行分析,寻求解决方式. 一般情况下,D

SQL Server中的完整性和外键

server 所谓完整性就是数据库中数据的一致性及有效性通常的说法有实体(行)完整性,域(列)完整性,及参考完整性(参考SQL Server). 实体完整性定义表中的所有行能唯一的标识,一般用主键,唯一索引 UNIQUE关键字,及IDENTITY属性比如说我们的身份证号码,可以唯一标识一个人. 域完整性通常指数据的有效性,限制数据类型,缺省值,规则,约束,是否可以为空,域完整性可以确保不会输入无效的值. 参考完整性维护表间数据的有效性,完整性,通常通过建立外部键联系另一表的主键实现,比如图书管理

Oracle中检查外键是否有索引的SQL脚本分享_oracle

复制代码 代码如下: COLUMN COLUMNS format a30 word_wrapped COLUMN tablename format a15 word_wrapped COLUMN constraint_name format a15 word_wrapped SELECT TABLE_NAME,        CONSTRAINT_NAME,        CNAME1 || NVL2(CNAME2, ',' || CNAME2, NULL) ||        NVL2(CNA

ORACLE中关于外键缺少索引的探讨和总结

    在ORACLE数据库中,定义外键约束时,ORACLE是不会自动创建对应索引的,必须手动在外键约束相关的列上创建索引.那么外键字段上是否有必要创建索引呢?如果有必要的话,巡检时,如何找出外键字段上没有创建索引的相关表,并生成对应的索引的脚本呢?   外键缺失索引影响   外键列上缺少索引会带来三个问题,限制并发性.影响性能.还有可能造成死锁.所以对于绝大部分场景,我们应该尽量考虑在外键上面创建索引   1. 影响性能. 如果子表外键没有创建索引,那么当父表查询关联子表时,子表将进行全表扫描

外键列上是否需要索引

其实这个问题应该算是老生常谈了.这两天看concept看到这里,于是就在说说这个问题.     外键列上缺少索引会带来两个问题,限制并发性.影响性能.而这两个问题中的任意一个都可能会造成严重性能问题. 无论是Oracle的官方文档,还是在Tom的书中都说明了两种情况下可以忽略外键上的索引.其实我认为不需要那么麻烦,与增加一个索引所带来的性能开销和磁盘空间开销相比,确实索引可能引发的问题要严重得多.因此,我会选择在所有的外键列上添加索引,虽然可能导致创建了部分多余的索引,但是这样相除了外键约束由于

外键要建立索引的原理和实验

项目中,我们要求凡是有主子关系的表都要使用外键约束,来保证主子表之间关系的正确,不推荐由应用自己控制这种关系. 但发现有时开发人员提交SQL语句时未必会注意外键列需要定义索引,或者不清楚为什么外键列需要建立索引,网上一些所谓的"宝典"也会将外键列建索引作为其中的一条,包括TOM大师,曾说过: 导致死锁的头号原因是外键未加索引(第二号原因是表上的位图索引遭到并发更新).在以下两种情况下,Oracle在修改父表后会对子表加一个全表锁: 1)如果更新了父表的主键(倘若遵循关系数据库的原则,即