• 09. 约束与索引的联系


    之所以把约束和索引放到一起来看,主要是因为主键约束和唯一键约束,它们会自动创建一个对应的索引,先分别看下数据库中的几个约束。

    一 约束

    在关系型数据库里,通常有5种约束,示例如下:

    use tempdb
    go
    create table s
    (
    sid     varchar(20),
    sname   varchar(20),
    ssex    varchar(2)  check(ssex='' or ssex='') default '',
    sage    int         check(sage between 0 and 100),
    sclass  varchar(20) unique,
    constraint PK_s primary key (sid,sclass)
    )
    create table t
    (
    teacher  varchar(20) primary key,
    sid      varchar(20) not null,
    sclass   varchar(20) not null,
    num      int,
    foreign key(sid,sclass) references s(sid,sclass)
    )

    单独定义在某一列上的约束被称为列级约束,定义在多列上的约束则称为表级约束。

    1.主键约束

    在表中的一列或者多列上,定义主键来唯一标识表中的数据行,也就是数据库设计3范式里的第2范式;

    主键约束要求键值唯一且不能为空:primary key = unique constraint + not null constraint

    2.唯一键约束

    唯一约束和主键约束的区别就是:允许NULL,SQL Server 中唯一键列,仅可以有一行为NULL,ORACLE中可以有多行列值为NULL。

     一个表只能有一个主键,但可以有多个唯一键:unique index = unique constraint

    在一个允许为NULL的列上,想要保证非NULL值的唯一性,该怎么办?

    从SQL Server 2008开始,可以用筛选索引(filtered index)

    use tempdb
    GO
    create table tb5
    (
    id int null
    )
    create unique nonclustered index un_ix_01
    on tb5(id)
    where id is not null
    GO

     

    3.外键约束

    表中的一列或者多列,引用其他表的主键或者唯一键。外键定义如下:

    use tempdb
    GO
    --drop table tb1,tb2
    create table tb1
    (
    col1 int Primary key,
    col2 int
    )
    insert into tb1 values (2,2),(3,2),(4,2),(5,2)
    GO
    
    create table tb2
    (
    col3 int primary key,
    col4 int constraint FK_tb2 foreign key  references tb1(col1)
    )
    GO
    
    --从表里的同一个列既可以为自己的主键,也可以定义为外键
    --drop table tb1,tb2
    create table tb1
    (
    col1 int Primary key,
    col2 int
    )
    insert into tb1 values (2,2),(3,2),(4,2),(5,2)
    GO
    
    create table tb2
    (
    col3 int primary key, 
    col4 int 
    )
    
    alter table tb2 WITH NOCHECK
    add constraint FK_tb2 foreign key(col3) references tb1(col1)
    
    select object_name(constraint_object_id) constraint_name,
           object_name(parent_object_id) parent_object_name,
           col_name(parent_object_id,parent_column_id) parent_object_column_name,
           object_name(referenced_object_id) referenced_object_name,
           col_name(referenced_object_id,referenced_column_id) referenced_object_column_name
     from sys.foreign_key_columns 
    where referenced_object_id = object_id('tb1')

    外键开发维护过程中,常见的问题及解决方法:

    (1) 不能将主表中主键/唯一键的部分列作为外键,必须是全部列一起引用

    create table tb3
    (
    c1 int,
    c2 int,
    c3 int,  
    constraint PK_tb3 primary key (c1,c2)
    );
                                                                                                                                  
    create table tb4
    (
    c4 int constraint FK_tb4 foreign key references tb3(c1),
    c5 int,
    c6 int
    );
    /*
    Msg 1776, Level 16, State 0, Line 1
    There are no primary or candidate keys in the referenced table 'tb3' that match the referencing column list in the foreign key'FK_tb4'.
    Msg 1750, Level 16, State 0, Line 1
    Could not create constraint. See previous errors.
    */

     

    (2) 从表插入数据出错

    insert into tb2 values (1,1)
    /*
    Msg 547, Level 16, State 0, Line 1
    The INSERT statement conflicted with the FOREIGN KEY constraint "FK_tb2". The conflict occurred in database "tempdb", table "dbo.tb1", column 'col1'.
    */
    --从表在参照主表中的数据,可以先禁用外键(只是暂停约束检查)
    alter table tb2 NOCHECK constraint FK_tb2
    alter table tb2 NOCHECK constraint ALL
    --从表插入数据后,再启用外键
    insert into tb2 values (1,1),(3,3),(4,4)
    alter table tb2 CHECK constraint FK_tb2

     

    (3) 主表删除/更新数据出错

    --先删除从表tb2的数据或禁用外键,才能删除主表tb1中的值,否则报错如下
    --未被引用的行可被直接删除
    insert into tb2 values (2,2)
    delete from tb1
    GO
    /*
    Msg 547, Level 16, State 0, Line 3
    The DELETE statement conflicted with the REFERENCE constraint "FK_tb2". The conflict occurredin database "tempdb", table "dbo.tb2", column 'col4'.
    */

     

    (4) 清空/删除主表出错 

    --清空主表时,即便禁用外键,但外键关系依然存在,所以任然无法truncate
    truncate table tb1
    /*
    Msg 4712, Level 16, State 1, Line 2
    Cannot truncate table 'tb1' because it is being referenced by a FOREIGN KEY constraint.
    */
    
    --删除主表也不行
    drop table tb1
    /*
    Msg 3726, Level 16, State 1, Line 2
    Could not drop object 'tb1' because it is referenced by a FOREIGN KEY constraint.
    */
    
    --先truncate从表,再truncate主表也不行
    truncate table tb2
    truncate table tb1
    
    --得先drop从表,或者删除外键引用,主表才可以被truncate,或者改用delete语句
    alter table tb2 drop constraint FK_tb2
    truncate table tb1
    
    --最后再加上外键,注意with nocheck选项,因为主从表里数据不一致了,所以不检查约束,否则外键加不上
    alter table tb2 WITH NOCHECK
    add constraint FK_tb2 foreign key(col4) references tb1(col1)

    最后,虽然一个表上可以创建多个外键,但通常出于性能考虑,不推荐使用外键,数据参照完整性可以在程序里完成;

    4.CHECK约束

    可定义表达式以检查列值,通常出于性能考虑,不推荐使用。

    5.NULL 约束

    用于控制列是否允许为NULL。使用NULL时有几个注意点:

    (1) SQL SERVER中聚合函数是会忽略NULL值的;

    (2) 字符型的字段,如果not null,那这个字段不能为null值,但可以为'',这是空串,和null是不一样的;

    (3) NULL值无法直接参与比较/运算;

    declare @c varchar(100)
    set @c = null
    if @c<>'abc' or @c  = 'abc'
        print 'null'
    else
        print 'I donot know'
    GO
    declare @i int
    set @i = null
    print @i + 1

    在开发过程中,NULL会带来3值逻辑,不推荐使用,对于可能为NULL的值可用默认值等来代替。

    6.DEFAULT约束

    从系统视图来看,default也是被SQL Server当成约束来管理的。

    select * from sys.default_constraints

    (1) 常量/表达式/标量函数(系统,自定义、CLR函数)/NULL都可以被设置为默认值;

    (2) 利用默认值,向表中添加一个NOT NULL的列,如下:

    create table tb6(c1 int not null)
    insert into tb6 select 1
    alter table tb6 add c2 int default 35767 not null
    select * from tb6
    --在alter table完成前,表一直处于锁定状态;
    --如果向大型表添加列,对数据页的操作需要一些时间,最好事先做好评估。

    (3) 在insert/update列值时使用默认值

    --identity列不需要手动插入值,那么这时只要给c2插入一个值就可以了
    create table test_def1(c1 int identity(1,1), c2 int default 0)
    insert into test_def1(c2) values(1111)
    
    --如果c2想使用默认值?
    insert into test_def1(c2) values(DEFAULT)
    
    --如果全部使用默认值呢?
    insert into test_def1 DEFAULT VALUES
    
    --如果表里只有identity列呢?
    create table test_def2(c1 int identity(1,1))
    insert into test_def2 DEFAULT VALUES
    
    --update的时候也一样
    update test_def1 set c2 = DEFAULT where c2 = 1111

    二 索引

    定义约束时,并没有定义数据库实现约束的方法,目前的关系型数据库系统,主键和唯一键约束借助唯一索引来实现,所以在创建主键/唯一键时,都会自动生成一个同名的索引。

    那么由约束产生的唯一索引,和单独创建的唯一索引有什么联系和区别?

    1.创建主键或唯一键约束时,数据库自动创建唯一索引

    自动生成的该索引是无法删除的,因为这个索引要用于实现约束,在删除约束的时候,该索引也被删除。演示脚本如下:

    --create table
    CREATE TABLE TEST_CONS
    (
    ID             int,
    CODE           varchar(100)
    )
    --insert data
    INSERT INTO TEST_CONS
    SELECT 1,'test1'
    --add unique constraint
    ALTER TABLE TEST_CONS
      ADD CONSTRAINT UQ_TEST_CONS_ID UNIQUE NONCLUSTERED(ID)
    --retrieve constraint
    SELECT *
      FROM sys.objects
     WHERE parent_object_id = object_id('TEST_CONS') AND type = 'UQ'
    --查看约束,返回如下结果:
    /*
    name    object_id
    UQ_TEST_CONS_ID 1243151474
    */
    --retrieve index
    SELECT *
      FROM sys.indexes
     WHERE object_id = object_id('TEST_CONS') AND type = 2  --2为非聚集索引
    --查看约束产生的索引,返回如下结果:
    /*
    object_id   name
    1227151417  UQ_TEST_CONS_ID
    */
    --check constraint
    INSERT INTO TEST_CONS
    SELECT 1,'test1'
    --如果插入重复值提示:UNIQUE KEY 约束,返回如下错误:
    /*
    消息,级别,状态,第行
    违反了UNIQUE KEY 约束'UQ_TEST_CONS_ID'。不能在对象'dbo.TEST_CONS' 中插入重复键。
    */
     --drop index
     DROP INDEX UQ_TEST_CONS_ID ON TEST_CONS
    --如果删除由约束产生的索引,返回如下错误:
    /*
    消息,级别,状态,第行
    不允许对索引'TEST_CONS.UQ_TEST_CONS_ID' 显式地使用DROP INDEX。该索引正用于UNIQUE KEY 约束的强制执行。
    */
     --drop constraint
     ALTER TABLE TEST_CONS
      DROP CONSTRAINT UQ_TEST_CONS_ID
    --如果删除约束,索引也被删除,以下查询返回空结果集:
    --retrieve constraint
    SELECT *
      FROM sys.objects
     WHERE parent_object_id = object_id('TEST_CONS') AND type = 'UQ'
    --retrieve index
    SELECT *
      FROM sys.indexes
     WHERE object_id = object_id('TEST_CONS') AND type = 2  --2为非聚集索引
    --drop table
    DROP TABLE TEST_CONS

    另外,约束生成的索引,有些属性也是无法被修改的,比如:开关IGNORE_DUP_KEY,唯一的办法是:先删除约束,再重新定义约束/索引;单独定义的索引,则没有这个限制,如下例:

    use tempdb
    GO
    
    create table tb_cons(ID int constraint pk_tb_cons primary key)
    create unique clustered index pk_tb_cons on tb_cons(id) with(DROP_EXISTING = ON, FILLFACTOR = 90)
    
    alter index pk_tb_cons on tb_cons rebuild with(IGNORE_DUP_KEY = ON)
    /*
    Msg 1979, Level 16, State 1, Line 1
    Cannot use index option ignore_dup_key to alter index 'pk_tb_cons' as it enforces a primary or unique constraint.
    */
    exec sp_helpindex tb_cons
    
    --单独创建的唯一索引,属性可以随意修改
    create unique index ix_tb_cons on tb_cons(id)
    alter index ix_tb_cons on tb_cons rebuild with(IGNORE_DUP_KEY = ON, ONLINE = ON)
    
    drop table tb_cons

    查看主键/唯一键约束生成的索引和列

    select a.name as index_name,
           c.name as column_name,
           a.is_primary_key,
           a.is_unique_constraint
    from sys.indexes a 
    inner join sys.index_columns b
    on a.object_id=b.object_id
    inner join sys.columns c
    on a.object_id=c.object_id and b.column_id=c.column_id
    where (a.is_primary_key=1 or a.is_unique_constraint=1)
      --and a.object_id=object_id('your_table_name') 
    
    select * from sys.objects where type='PK'
    select * From sys.key_constraints where type='PK'
    select * from sysconstraints --sql server 2000

    在保证数据唯一性上,唯一索引、唯一约束并没有区别,那么应该使用约束还是索引?

    约束定义通常出现在数据库逻辑结构设计阶段,即定义表结构时,索引定义通常出现在数据库物理结构设计/查询优化阶段。

    从功能上来说唯一约束和唯一索引没有区别,但在数据库维护上则不太一样,对于唯一约束可以用唯一索引代替,以方便维护,但是主键约束则没法代替。

    2. 先创建唯一索引,再创建该索引字段的唯一约束

    这时数据库并不会使用已存在的唯一索引,此时会提示已存在同名索引,约束创建失败,如果指定不同名的约束,则会生成另个唯一索引。演示脚本如下: 

    --create table
    CREATE TABLE TEST_CONS
    (
    ID             int,
    CODE           varchar(100)
    )
    --insert data
    INSERT INTO TEST_CONS
    SELECT 1,'test1'
    --create index
    CREATE UNIQUE INDEX UQ_TEST_CONS_ID
    ON TEST_CONS(ID)
    --retrieve constraint
    SELECT *
      FROM sys.objects
     WHERE parent_object_id = object_id('TEST_CONS') AND type = 'UQ'
                                                                                         
    --retrieve index
    SELECT *
      FROM sys.indexes
     WHERE object_id = object_id('TEST_CONS') AND type = 2  --2为非聚集索引
    --check index
    INSERT INTO TEST_CONS
    SELECT 1,'test1'
    --此时提示为:唯一索引
    /*
    消息2601,级别14,状态1,第1 行
    不能在具有唯一索引'UQ_TEST_CONS_ID' 的对象'dbo.TEST_CONS' 中插入重复键的行。
    */
    --add constraint
    ALTER TABLE TEST_CONS
      ADD CONSTRAINT UQ_TEST_CONS_ID UNIQUE NONCLUSTERED(ID)
    --此时无法创建与索引同名的唯一约束,因为约束会去生成同名的索引
    /*
    消息1913,级别16,状态1,第2 行
    操作失败,因为在表'TEST_CONS' 上已存在名称为'UQ_TEST_CONS_ID' 的索引或统计信息。
    消息1750,级别16,状态0,第2 行
    无法创建约束。请参阅前面的错误消息。
    */
    --add constraint
    ALTER TABLE TEST_CONS
      ADD CONSTRAINT UQ_TEST_CONS_ID_1 UNIQUE NONCLUSTERED(ID)
    --换个名字当然是可以成功的,但此时又生成了唯一索引UQ_TEST_CONS_ID_1
    --drop table
    DROP TABLE TEST_CONS

    3.主键是否是聚集索引?

    SQL Server默认在定义主键时,将生成的唯一索引定义为聚集,刚刚接触的时候容易被搞混淆了。主键对应的索引也可以非聚集,如下:

    use tempdb
    GO
    create table test_pk(id int not null)
    alter table test_pk add constraint PK_test_pk primary key nonclustered(id);

    SQL Server中定义主键时,默认生成聚集索引,唯一的好处是主键列范围扫描/查找的效率比较高,但数据插入效率欠佳(聚集索引,非聚集索引,都得被维护一次),并且主键列如果选择的不好,会影响其他非聚集索引的性能。

    ORACLE中定义主键时,默认生成非聚集索引,不利于主键列的范围扫描/查找,但是对于数据插入效率更佳,这是不同数据库产品各自的权衡。

  • 相关阅读:
    Scilab 的画图函数(2)
    Webapp的display-name问题
    记录:在老XPS1330上安装CentOS7
    包含Blob字段的表无法Export/Import
    记一段脚本的诞生
    一个短小的JS函数,用来得到仅仅包含不重复元素的数组
    然并卵
    Linux下的定时任务Crontab
    两段用来启动/重启Linux下Tomcat的Perl脚本
    JavaScript中给二维数组动态添加元素的质朴方法
  • 原文地址:https://www.cnblogs.com/seusoftware/p/3448978.html
Copyright © 2020-2023  润新知