【问题标题】:Preserve SQL Indexes While Altering Column Datatype在更改列数据类型时保留 SQL 索引
【发布时间】:2010-11-18 12:09:06
【问题描述】:

我有一个 smalldatetime 列,需要将其更改为 datetime 列。这将成为安装过程的一部分,因此它不能是手动过程。不幸的是,该列有几个索引和一个非空约束。索引与性能相关,只需要使用新数据类型保留。是否可以编写一个语句,让我在保留相关信息的同时仍然更改列数据类型?如果是这样,如何做到这一点?

【问题讨论】:

    标签: sql sql-server tsql


    【解决方案1】:

    最好的办法是创建一个返回给定表/列的索引脚本的过程。因此,您可以仅从正在更改的列中删除索引,而不是从表中删除所有索引,而创建索引可能会有些昂贵。

    1. 将过程的结果存储在数据表中
    2. 删除列的索引
    3. 修改您的专栏
    4. 重建存储在数据表中的索引

      -- objective   : Generates indices scripting using specified column
      -- Parameters : 
      --     @Tabela  -> Name of the table that the column belongs to 
      --     @Coluna -> Name of the column that will be searched for the indices to generate the script
      --Use: proc_ScriptIndexColumn 'TableName', 'ColumnName'
      
      SET ANSI_NULLS ON
      GO
      SET QUOTED_IDENTIFIER ON
      GO
      Create Proc proc_ScriptIndexColumn (@Tabela VARCHAR(4000), @Coluna VARCHAR(4000)) 
      AS 
      BEGIN     
           DECLARE @isql_key VARCHAR(4000), 
               @isql_incl VARCHAR(4000), 
               @tableid INT, 
               @indexid INT         
       DECLARE @tablename VARCHAR(4000), 
               @indexname VARCHAR(4000)         
       DECLARE @isunique INT, 
               @isclustered INT, 
               @indexfillfactor INT         
       DECLARE @srsql VARCHAR(MAX)        
       DECLARE @ScriptsRetorno TABLE 
               (Script VARCHAR(MAX))        
       DECLARE index_cursor CURSOR  
         FOR 
           SELECT tablename = OBJECT_NAME(i.[object_id]), 
                  tableid       = i.[object_id], 
                  indexid       = i.index_id, 
                  indexname     = i.name, 
                  isunique      = i.is_unique, 
                  CASE I.type_desc 
                       WHEN 'CLUSTERED' THEN 1 
                       ELSE 0 
                  END                     AS isclustered, 
                  indexfillfactor = i.fill_factor                  
           FROM   sys.indexes             AS i 
                  INNER JOIN SYSOBJECTS   AS O 
                       ON  I.[object_id] = O.ID 
                  INNER JOIN sys.index_columns AS ic 
                       ON  (ic.column_id > 0 
                               AND (ic.key_ordinal > 0 
                                       OR ic.partition_ordinal = 0 
                                       OR ic.is_included_column != 0 
                                   )) 
                       AND (   ic.index_id = CAST(i.index_id AS INT) 
                               AND ic.object_id = i.[object_id] 
                           ) 
                  INNER JOIN sys.columns  AS sc 
                          ON  sc.object_id = ic.object_id 
                       AND sc.column_id = ic.column_id 
           WHERE  O.XTYPE = 'U' 
                  AND i.typE = 2 /*Non clustered*/ 
                  AND i.is_unique = 0 
                  AND i.is_hypothetical = 0 
                  AND UPPER(OBJECT_NAME(i.[object_id])) = UPPER(@Tabela) 
                  AND UPPER(sc.name) = UPPER(@Coluna)       
      
       OPEN index_cursor  
       FETCH NEXT FROM index_cursor INTO @tablename,@tableid, @indexid,@indexname ,  
       @isunique ,@isclustered , @indexfillfactor       
       WHILE @@fetch_status <> -1 
       BEGIN 
           SELECT @isql_key = '', 
                  @isql_incl = ''           
           SELECT @isql_key = CASE ic.is_included_column 
                                   WHEN 0 THEN CASE ic.is_descending_key 
                                                    WHEN 1 THEN @isql_key +COALESCE(sc.name, '') + 
                                                         ' DESC, ' 
                                                    ELSE @isql_key + COALESCE(sc.name, '')  
                                                         + ' ASC, ' 
                                               END 
                                   ELSE @isql_key 
                               END, 
                  --include column  
                  @isql_incl = CASE ic.is_included_column 
                                    WHEN 1 THEN CASE ic.is_descending_key 
                                                     WHEN 1 THEN @isql_incl + 
                                                          COALESCE(sc.name, '') + 
                                                          ', ' 
                                                     ELSE @isql_incl + COALESCE(sc.name, '')  
                                                          + ', ' 
                                                END 
                                    ELSE @isql_incl 
                               END 
           FROM   sysindexes i 
                  INNER JOIN sys.index_columns AS ic 
                       ON  ( 
                               ic.column_id > 0 
                               AND ( 
                                       ic.key_ordinal > 0 
                                       OR ic.partition_ordinal = 0 
                                       OR ic.is_included_column != 0 
                                   ) 
                           ) 
                       AND (ic.index_id = CAST(i.indid AS INT) AND ic.object_id = i.id) 
                  INNER JOIN sys.columns AS sc 
                         ON  sc.object_id = ic.object_id 
                       AND sc.column_id = ic.column_id 
           WHERE  i.indid > 0 
                  AND i.indid < 255 
                  AND (i.status & 64) = 0 
                  AND i.id = @tableid 
                  AND i.indid = @indexid 
           ORDER BY 
                  i.name, 
                  CASE ic.is_included_column 
                       WHEN 1 THEN ic.index_column_id 
                       ELSE ic.key_ordinal 
                  END           
           IF LEN(@isql_key) > 1 
               SET @isql_key = LEFT(@isql_key, LEN(@isql_key) -1)  
      
           IF LEN(@isql_incl) > 1 
               SET @isql_incl = LEFT(@isql_incl, LEN(@isql_incl) -1)            
           SET @srsql = 'CREATE ' + 'INDEX [' + @indexname + ']' + ' ON [' + @tablename 
               + '] '           
           SET @srsql = @srsql + '(' + @isql_key + ')'              
           IF (@isql_incl <> '') 
               SET @srsql = @srsql + ' INCLUDE(' + @isql_incl + ')'             
           IF (@indexfillfactor <> 0) 
                SET @srsql = @srsql + ' WITH ( FILLFACTOR = ' + CONVERT(VARCHAR(10), @indexfillfactor) 
                   + ')'            
           FETCH NEXT FROM index_cursor INTO @tablename,@tableid,@indexid,@indexname,  
           @isunique ,@isclustered , @indexfillfactor           
           INSERT INTO @ScriptsRetorno 
           VALUES 
             (@srsql) 
       END  
       CLOSE index_cursor  
       DEALLOCATE index_cursor   
       SELECT * 
       FROM   @ScriptsRetorno 
      RETURN @@ERROR 
      END 
      

    【讨论】:

      【解决方案2】:

      在索引、唯一约束、外键约束或检查约束到位的情况下,您不能将数据类型从 smalldatetime 更改为 datetime。在更改类型之前,您必须将它们全部删除。那么:

      alter table T alter column TestDate datetime not null
      

      然后重新创建仍然适用的约束和索引。


      生成 drop 和创建的一些不同方法:

      1) 如果您为所有索引和约束指定了明确的名称,那么您的安装程序可以在每个环境(开发、测试、用户验收测试、性能测试等、生产环境)中运行静态脚本。

      要生成这个显式脚本,您可以: a) 使用 SSMS(或使用 SQL Server 2000,企业管理器)编写创建和删除语句的脚本。 b) 从您的源代码存储库中工作以发现依赖对象的名称和定义,并将适当的静态脚本放在一起。 c) 尝试运行 alter 语句。看看它失败了。查找定义并手写 drop 并创建。 (就个人而言,这对于编写 drop 来说非常有用,但不太擅长创建。)

      2) 如果您没有为所有索引和约束指定明确的名称,那么您的安装程序将必须在数据字典中查询适当的名称并使用动态 SQL 以正确的顺序在更改之前运行 drop column 语句,然后以正确的顺序在 alter column 之后创建。

      如果您知道没有约束,只有索引,这会更简单。

      可能有一些工具或库已经知道如何做到这一点。

      另外,如果这是一个打包的应用程序,您可能无法确定本地 DBA 没有添加索引。

      注意:如果存在唯一约束,它将建立一个索引,您将无法使用 DROP INDEX 删除该索引。

      【讨论】:

      • 我能够找到创建索引背后的源 sql,并且能够在更改类型之前使用它们删除那些。至于约束,我通过google找到了一个查询,可以用来确定随机生成的约束名称。
      • Declare @constraintName as nvarchar(100) Declare @sql nvarchar(1000) select @constraintName = O.name from sysobjects AS O left join sysobjects AS T on O.parent_obj = T.id where isnull( objectproperty(O.id,'IsMSShipped'),1) = 0 and O.name not like '%dtproper%' and O.name not like 'dt[_]%' and T.name = 'MyTable' and O. name like 'DF__MyTabl__MyCol%' if not @constraintName is null begin select @sql = 'ALTER TABLE [MyTable] DROP CONSTRAINT [' + @constraintName + ']' 执行 sp_executesql @sql end
      【解决方案3】:

      如果您只是更改大小,索引仍将保留在桌面上。

      如果您要更改数据类型,则会收到一条错误消息,指出对象依赖于您尝试更改的列,因此您将无法更改它。

      您可以手动或通过脚本编写有问题的索引。在 SSMS 中,右键单击表并编写有问题的对象。

      如果您想要编程索引脚本,这里有一个我一直在使用的存储过程,我从我的前同事那里得到。

      Drop Proc ScriptIndex
      GO
      Create Proc ScriptIndex
          @TableName      VarChar (Max),
          @IndexScript    VarChar (Max) OUTPUT
      AS
      
      -- Get all existing indexes, EXCEPT the primary keys
      DECLARE cIX CURSOR FOR
      SELECT OBJECT_NAME(SI.Object_ID), SI.Object_ID, SI.Name, SI.Index_ID
      FROM Sys.Indexes SI 
          LEFT JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS TC 
              ON SI.Name = TC.CONSTRAINT_NAME 
              AND OBJECT_NAME(SI.Object_ID) = TC.TABLE_NAME
      WHERE 1=1
          AND OBJECT_NAME(SI.Object_ID) = @TableName
          AND TC.CONSTRAINT_NAME IS NULL
          AND OBJECTPROPERTY(SI.Object_ID, 'IsUserTable') = 1
      ORDER BY OBJECT_NAME(SI.Object_ID), SI.Index_ID
      
      DECLARE @IxTable SYSNAME
      DECLARE @IxTableID INT
      DECLARE @IxName SYSNAME
      DECLARE @IxID INT
      
      -- Loop through all indexes
      OPEN cIX
      FETCH NEXT FROM cIX INTO @IxTable, @IxTableID, @IxName, @IxID
      WHILE (@@FETCH_STATUS = 0)
      BEGIN
         DECLARE @IXSQL NVARCHAR(4000) 
         DECLARE @PKSQL NVARCHAR(4000) 
         SET @PKSQL = ''
         SET @IXSQL = 'CREATE '
      
         -- Check if the index is unique
         IF (INDEXPROPERTY(@IxTableID, @IxName, 'IsUnique') = 1)
            SET @IXSQL = @IXSQL + 'UNIQUE '
         -- Check if the index is clustered
         IF (INDEXPROPERTY(@IxTableID, @IxName, 'IsClustered') = 1)
            SET @IXSQL = @IXSQL + 'CLUSTERED '
      
         SET @IXSQL = @IXSQL + 'INDEX ' + @IxName + ' ON ' + @IxTable + '('
      
         -- Get all columns of the index
         DECLARE cIxColumn CURSOR FOR 
            SELECT SC.Name
            FROM Sys.Index_Columns IC
               JOIN Sys.Columns SC ON IC.Object_ID = SC.Object_ID AND IC.Column_ID = SC.Column_ID
            WHERE IC.Object_ID = @IxTableID AND Index_ID = @IxID
            ORDER BY IC.Index_Column_ID
      
         DECLARE @IxColumn SYSNAME
         DECLARE @IxFirstColumn BIT SET @IxFirstColumn = 1
      
         -- Loop throug all columns of the index and append them to the CREATE statement
         OPEN cIxColumn
         FETCH NEXT FROM cIxColumn INTO @IxColumn
         WHILE (@@FETCH_STATUS = 0)
         BEGIN
            IF (@IxFirstColumn = 1)
               SET @IxFirstColumn = 0
            ELSE
               SET @IXSQL = @IXSQL + ', '
      
            SET @IXSQL = @IXSQL + @IxColumn
      
            FETCH NEXT FROM cIxColumn INTO @IxColumn
         END
         CLOSE cIxColumn
         DEALLOCATE cIxColumn
      
         SET @IXSQL = @IXSQL + ')'
         -- Print out the CREATE statement for the index
         PRINT @IXSQL
      
         FETCH NEXT FROM cIX INTO @IxTable, @IxTableID, @IxName, @IxID
      END
      
      CLOSE cIX
      DEALLOCATE cIX
      
      GO
      Declare @TableName VarChar (Max), @IndexScript VarChar (Max)
      
      Exec ScriptIndex 'Client', @IndexScript OUTPUT
      Print @IndexScript
      

      【讨论】:

      • 这是一个非常有用的 SP,但它不区分索引中的列和包含的列,从而威胁所有列。
      • 我对答案进行了编辑,但有 2 个人拒绝了它,第三个人批准了它。虽然我很欣赏他们的代码审查,但我认为拒绝是错误的。字符串列和表格在上面的代码中应该有括号:@IxName 周围的第 43 行,左括号后的第 43 行,列变量周围的第 64 行,右括号的左边的第 73 行。
      【解决方案4】:

      编辑:这取决于原始和更改的数据类型。 如果您尝试将列从 varchar 更改为 nvarchar,它将失败。 然而,如果将列从 varchar(16) 更改为 varchar(32),它将成功。

      --Disable Index
      ALTER INDEX MyIndex ON MyTable DISABLE
      GO
      
      -- Change column datatype
      
      --Enable Index
      ALTER INDEX MyIndex ON MyTable REBUILD
      GO
      

      如果更改列的类型,则必须重建使用该列的所有索引。

      但除非您拥有大量数据(或 24/7 全天候运行),否则重建索引没什么大不了的。只需安排一个维护时段。

      【讨论】:

      • 我想这对 Phillip 来说没问题,只要他们坚持使用新数据类型自动重建即可。
      • 当我尝试在 [MyTable] DISABLE 上运行 ALTER INDEX [myIndex] 时,我收到错误“关键字 'INDEX' 附近的语法不正确。我错过了什么吗?
      • 自从发表此评论后,我发现 SQL Server 2000 不允许您禁用索引。它必须被删除和创建。我们有一些服务器仍在 SQL Server 2000 上,因此禁用命令对我不起作用。
      • 至少在 SSMS 2016 上至少针对 SS 2008 R2,当我更改索引使用的至少 VarChar(以及 prolly NVarChar)的大小时:a)增加不需要删除/禁用索引。 b) 减少需要 Drop(不仅仅是 Disable)然后重新创建索引。
      • 如果我将列类型从 Int 更改为 BigInt,它会起作用吗?此列已编入索引。
      猜你喜欢
      • 1970-01-01
      • 2023-04-04
      • 2021-01-15
      • 2015-01-07
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-06-18
      • 1970-01-01
      相关资源
      最近更新 更多