Tuesday, February 08, 2011

SQL Server Change Data Capture ile Değişiklik Yapılan Kayıtların Loglanması

SQL Server Change Data Capture ile Değişiklik Yapılan Kayıtların Loglanması
İşletmelerde en büyük sorunlardan biri geçmişe yönelik silinen ya da değiştirilen kayıtların bilgilerinin tutulmasıdır.  SQL 2008 versiyonuna kadar bu işlemi yapabilmek için ya kullanılan paket programda bir değişiklik yaptırmak gerekiyordu ya da her tablonun altına trigger yazılması gerekiyordu. Bu yazılan triggerlar da değişen kayıtları algılayarak sizin istediğiniz formatta herhangi bir log tablosuna yazılmasını  amaçlamaktadır.
SQL Server 2008 ile birlikte ise CDC (Change Data Capture) isminde bir özellik geldi ki kendisi oldukça kullanışlı ve performanslı.  Performanslı diyorum çünkü bu arkadaş doğrudan log üzerinde çalışıyor.
Örnek olarak siz aşağıdaki gibi bir UPDATE cümlesi çalıştırdınız.
UPDATE CUSTOMERS SET ACTIVE=1
CUSTOMERS tablosunun 20 alandan oluştuğunu varsayalım oysa biz sadece bir alanı update ettik. Dolayısıyla SQL Server transaction log üzerinde sadece bir alanlık işlem hacmi söz konusu.
İşte Change Data Capture (buradan sonra CDC diyeceğim) sadece bu bilgiyi okuyarak arka planda veriyi logluyor.
Siz  CDC yi configure ederken belli bir süreliğine dataları loglayıp belli bir tarihten öncesini sildirebiliyorsunuz. Burada yazacağınız bir script ile önce bu datalara herhangi bir warehouse ortamına alıp daha sonra sistemden temizleyebilirsiniz.

Şimdi bu CDC nasıl çalışıyor bir bakalım.
1. Önce bir tablo oluşturalım.
CREATE TABLE dbo.Customers(ID int Primary Key NOT NULL,Name varchar(100) NOT NULL,Address
)
varchar(500) NOT NULL)
2.Database imizde CDC yi enable yapıyoruz.
EXEC sp_cdc_enable_db
3.Tablomuzda CDC yi enable yapalım.


EXEC
sp_cdc_enable_table @source_schema = N'dbo',@source_name = N'Customers',@role_name = NULL,@filegroup_name = N'',@supports_net_changes = 1
CDC yi enable ettikten sonra system tables altında aşağıdaki tablolar oluşur.

·         cdc.captured_columns - Adından da anlaşılacağı üzere değişikliklerin takip edileceği kritik alanları tutar. Bu tablo manuel olarak edit edilebilir durumda olup içeriği değiştirilebilir.
·         cdc.change_tables ­: Hangi tabloların değişiminin takip edileceği bilgisini tutar.
·         cdc.ddl_history ­ Şema bilgilerindeki değişiklikleri tutar.
·         cdc.lsn_time_mapping Asıl Tablo üzerinde yapılan her transaction işlemi bu tablo içerisinde tutulur ve içerisindeki lsn bilgisine göre hangi sırada yapıldığı bilgisi tutulur.
Ayrıca sistemde iki tane de otomatik job oluşur.
Bunlar,
cdc.TestDb_capture job:Logdaki bilgileri okuyarak değişen datayı loglar
cdc.TestDb_cleanup job:Belirlenen tarihten önceki tutulan datayı temizler.


4.Şimdi bir kayıt ekleyelim.
INSERT INTO CUSTOMERS (1,'MER','TRKYE')INSERT INTO CUSTOMERS (2,'AHMET','STANBUL')
5.UPDATE yapalım.
UPDATE CUSTOMERS SET ADDRESS='ANKARA' WHERE ID=1
6.DELETE Yapalım
DELETE FROM CUSTOMERS WHERE ID=1
Görüldüğü gibi tablo üzerinde 2 insert,1 update ve 1 delete işlemi yaptık. Burada sistemde 4 satır kaydın logunun tutulması gerekiyor. Bakalım görebilecek miyiz?
Sistemde loglanan kayıtları ya doğrudan ya da tarih parametresi alan table valued function lar ile görebiliyoruz. Bu tablodaki kayıtlar ise log sequence number (lsn) ile tutuluyor.

Log kayıtlarını ulaşmak  istediğimizde  eğer tablonun tamamına ulaşmak istiyor isek
select 
* from cdc.dbo_customers_CT şeklinde kullanıyoruz.
burada tablo formatı cdc.<schema>_<tablename>_CT şeklinde.

table valued functionlar  ise aşağıdaki gibi kullanılıyor.
DECLARE @from_lsn binary(10), @to_lsn binary(10);

--minimum lsn numarasını buluyoruz.
SET @from_lsn = sys.fn_cdc_get_min_lsn('dbo_customers');
--maximum lsn numarasını buluyoruz.
SET @to_lsn = sys.fn_cdc_get_max_lsn();
-- CDC ile ilgili işlemlerden Bir takım cdc function larını kullanıyoruz.

SELECT * FROM cdc.fn_cdc_get_all_changes_dbo_customers(@from_lsn, @to_lsn, 'all');

Buradan aşağıdaki sonucun döndüğünü görüyoruz.
Görüldüğü üzere sistem 4 adet fazladan alan ve sistemde yapılan değişiklik üzerine loglanan kayıtları getirdi.

Burada
__$start_lsn log: sequence number bilgisini içeriyor. Buradan kayıt tarihine erişebiliyoruz.
__$seqval: Sequnce değeri yani işlemin hangi sırada gerçekleştiği bilgisine erişmek için bu alan kullanılıyorç
__$operation:2 Insert, 4 Update ve 1 Delete için kullanılıyor.
__$operation:1 Insert,Delete 0 Update

anlamına gelmektedir.



Burada kayıt zamanını elde etmek istediğimizde
sys.fn_cdc_map_lsn_to_time function ının kullanıyoruz.

select sys.fn_cdc_map_lsn_to_time(__$start_lsn) as KayitZamani,
* from cdc.dbo_customers_CT


Burada oluşan log kayıtlarını temizlemek için ise
sp_cdc_cleanup_change_table
komutunu kullanıyoruz.

Kullanımı aşağıdaki gibi.


-- aşağıdaki kod  3 gün öncesine ait logları temizliyor.
declare @lsn binary(10);
set @lsn = sys.fn_cdc_map_time_to_lsn('largest less than or equal',getdate()-3);
exec sys.sp_cdc_cleanup_change_table @capture_instance = 'dbo_Customers', @low_water_mark=@lsn

--CDC yi disable etmek için ise
sp_cdc_disable_db,
sp_cdc_disable_table
komutları kullanlr
EXECUTE sp_cdc_disable_table@source_schema = N'dbo',@source_name = N'Customers',@capture_instance = N'dbo_Customers'

Sonuç:
1CDC gerçekten çok ihtiyaç duyulan ve çok kullanışlı bir araç.

2.Sistemdeki insert, update ve delete leri loglayabiliyor.

3.Eğer update cümlesinde kayıt değişmiyor ise gereksiz yer teşkil etmiyor.
Örneğin: UPDATE CUSTOMERS SET NAME=NAME cümlesini çalıştırdığımızda herhangi bir loglama yapmıyor çünkü değişen bir şey yok.

4.Eğer kayıt loglargen bu kaydın kim tarafından hangi client bilgisayardan hangi user ile hangi ip den hangi bağlantı şekliyle, hangi client access uygulaması ile değiştirildiği gibi detay bilgileri de loglamak istiyor isek çok büyük transactionların olduğu tablolarda performans problemlerine yol açabilir. Çünkü her insert edilen kayıt eş zamanlı olarak başka bir tabloya yazılmakta ve transaction bilgileri de cdc.lsn_time_mapping tablosuna yazılmaktadır. Bu tarz durumlarda trigger gibi alternatif çözümler düşünülebilir.

Thursday, January 27, 2011

SQL SERVER 2008'DE ALAN TİPİNİ DEĞİŞTİRME

Sql server 2008 de en şaşırdığım şey oluşturduğum bir table'da alanların tipini ya da uzunluğunu değiştirmeme izin vermemesiydi. Bunu yapmaya çalıştığımda aşağıdaki hatayı alıordum.
 Bir kaç kere temp tablo oluşturup dataları taşıyıp sonra temp tablo ile asıl tablon un adını değiştirerek çözüm bulabilmiştim. Ama tablonuzda Identity alan varsa bu durum veri bütünlüğü açısından çok tehlikeli. Meğer çok kolay bir yolu varmış. Bu özellik management studio ile alakalı bir opsiyonmuş. Options menüsünden Saving Changes Not Permitted alanındaki tik in kaldırılması yeterliymiş. Aşağıda resmi mevcut.

Tuesday, January 25, 2011

SQL SERVER VE JUMBO PAKET SORUNU

Geçenlerde bir sistem upgrade'i yaptık. Server ımız değişti, Sistemde database olarak SQL 2005'ten SQL 2008'e, İşletim sistemi olarak Windows Server 2003'ten Windows Server 2008'e geçtik ve cluster yapısı kurduk. ERP Sistemimizde de versiyon geçişi yaptık ve yeni versiyonun çalışması için client bilgisayarlarda mdac versiyon upgrade'ine ihtiyaç duyduk.
Sonuç olarak çok ilginç bir sorunla karşılaştık. Kullanıcı tarafındaki çok basit bir işlem kimi bilgisayarda 1 sn sürerken kimi bilgisayarda 20 sn sürüyordu. Sonuç olarak sorun server kaynaklı, İşletim sistemi kaynaklı, SQL 2008 kaynaklı, Erp programı kaynaklı, ya da mdac kaynaklı olabilirdi. Çünkü bunların hepsi de değişmişti. Epey bir inceleme yaptık sorun üzerinde.
Öncelikle şunu söyleyim bu ayarla alakalı sql server üzerinde bir article bulamadım. Ancak başka uygulamalarda benzer sıkıntılar yaşanmış onun üzerine bu konu üzerine gittik. Burada yeni ethernetler Jumbo frame denilen yapıyı destekliyor ve normalde 1500 byte lık olan network paketleri 9000 byte olarak tek seferde gönderiyor. Paketleri parçalama işini client ın etherneti yapıyor. Özellikle benzer işlem tekrarlarında bu durumu sistem otomatik olarak yapıyor yani kendince optimize etmeye çalışıyor. Client'ta özellikle döngüye takıp aynı sonucu döndüren tek satırlık ya da çoğunlukla sıfır satırlık select cümlelerinin kullanıldığı yerlerde bu özellik devreye giriyor. Eğer karşıdaki ethernet jumbo paketi desteklemiyor ise paket tekrardan servera gönderiliyor bu kez server bu paketi tekrardan parçalayıp client a gönderiyor. Bu da her paket için yapıldığında yaklaşık 10 kat bazen daha fazla gecikmeye sebep oluyor.
Çözüm iki türlü ya serverdan Large Recive Offload Data özelliğini disable etmek ya da client da jumbo paket size değerini arttırmak. Ancak clientta işlem yapmanın iki dezavantajı var bunlardan biri ethernet ya da switchler desteklemiyor olabilir ikincisi de bu özellik enable yapıldığında networkte büyük paketler dolaşmaya başladığından networku tıkayabilir. Bu konuda bir kaç kişiye sorduk pek önermediler büyük paketleri. En doğrusu server üzerinde bu ayarı disable etmek gibi görünüyor.
Biz bu ayarı serverda disable ederek sorunu çözdük. Zaten eski serverda ethernet desteklemiyormuş ondan sorun olmamış.
Bu bahsettiğim sorundan kaynaklı sıkıntı olduğunu düşündüğün makinada sorun olup olmadığını anlamak için performance monitorden send packet/sec değerlerine bakılabilir. Hızlı makina ile yavaş makina arasındaki fark en az 10 kat oluyor. Aşağıda bu ayarın nasıl yapıldığının resmi mevcut.


SQL SERVER'da SUSPECT MODA DÜŞEN BİR DATABASE' İ KURTARMA



Bir database'in suspect moda düşmesi demek database dosyalarından (mdf,ldf,ndf) en az birini okurken bir sorunla karşılaşmış olması anlamına gelir.


Genel olarak bu sorunlar,
  1. Database dosyaları bozulmuş olabilir.
  2. Sistemde yeterli disk alanı kalmamış olabilir.
  3. Yeterli memory kalmamış olabilir.
  4. Database dosyaları silinmiş ya da işletim sistemi dosyaların kullanılmasına izin vermiyor olabilir.
  5. Server düzgün kapatılmadığı için ya da bir takım donanımsal sorunlar yüzünden dosyalar okunamıyor olabilir.




Bu moda düşen bir database'i normale çevirmek için aşağıdaki komutları kullanırız.

--Database'in statüsünü resetleme komutu. Böylece manuel müdaheleye izin verir.
EXEC sp_resetstatus 'dbName';
--Database'i emergency moda çekiyoruz.
ALTER DATABASE dbName SET EMERGENCY ;
--Database tablolarında ya da dosyalarında bir bozukluk var mı onu kontrol eden komut.
DBCC checkdb('dbNAme')
--Database'i single moda çekiyoruz.
ALTER DATABASE SET dbName SINGLE_USER WITH ROLLBACK IMMEDIATE
--Database'de bir bozukluk var ise veri kaybına izin verecek şekilde düzenleme yapıyoruz.


--Log dosyası silinir ve yeni bir yeni boş bir log dosyası oluşturulur. Bu sırada log dosyasındaki --kaydedilmeyen veriler silinir.
DBCC CheckDB ('dbName', REPAIR_ALLOW_DATA_LOSS)
--Database'i kullanıma açıyoruz.
ALTER DATABASE dbName SET MULTI_USER
Database'imizi artık gönül rahatlığıyla selectleyebiliriz:)

TABLOLARI BİR FILE GROUP'TAN DİĞERİNE TAŞIMA SCRİPTİ

Geçenlerde toplu halde history tablolarını başka bir diske dolayısıyla başka bir file gruop'a taşıma ihtiyacımız doğdu. Söz konusu tablo sayısı 2000 civarında olunca küçük bir araştırma sonucu aşağıdaki scripti buldum. Paylaşmak istedim. Zira kendisi çok kullanışlı.
Parametreler şu şekilde:

--mevcut filegroup un id si. select * from SYSFiles diyerek id lerini grebilirsiniz.@SourceFileGroupID INT=1--tama yapacamz filegroup un id si. select * from SYSFiles diyerek id lerini grebilirsiniz.@TargetFileGroupID INT=2,--Tamak istediimiz tablonun ad@TableToMove NVARCHAR(128),--Primary key ve Unique constraintsleri de tamak istiyorsanz kullanacanz parametre@MovePKAndAllUniqueConstraints BIT=1--Indezkeri de tamak istiyorsanz kullanacanz parametre@MoveAllNonClusteredIndexes

BIT=1create
@SourceFileGroupID
PROC [dbo].[spMOVEDATAFILE]INT=1,@TargetFileGroupID INT=2,@TableToMove NVARCHAR(128),@MovePKAndAllUniqueConstraints BIT=1,@MoveAllNonClusteredIndexes BIT=1AS
DECLARE
@ScriptMsg NVARCHAR(512)DECLARE
DECLARE
DECLARE
DECLARE
DECLARE
DECLARE
DECLARE
@DatabaseName SYSNAME @ServerName SYSNAME @TableHasCI BIT @TableHasIdent BIT @TableHasPK BIT @TableHasUQ BIT @File1Name NVARCHAR(128)DECLARE @File2Name NVARCHAR(128)DECLARE @IdentColName NVARCHAR(128)DECLARE @ColList NVARCHAR(1024)DECLARE @indid NVARCHAR(128)DECLARE @Type CHAR(2)DECLARE @KeyName NVARCHAR(128)DECLARE @AssocFKeyName NVARCHAR(128)DECLARE @FKTableName NVARCHAR(128)DECLARE @CIName NVARCHAR(128)DECLARE
DECLARE
DECLARE
DECLARE
@IsPadIndex BIT @i INT @j INT @SQLStr NVARCHAR(4000)-- This temp table holds the column names of keys/constraints, and such.IF OBJECT_ID('tempdb..#tblColTable', 'U') IS NOT NULLDROP TABLE #tblColTableCREATE TABLE #tblColTable (Idx INT IDENTITY(1, 1),ColName NVARCHAR(128),IdxOrder CHAR(4))-- This temp table is used to store the key/constraint properties
-- of the moved table.
IF OBJECT_ID('tempdb..#tblKeysTable', 'U') IS NOT NULLDROP TABLE #tblKeysTableCREATE TABLE #tblKeysTable (Idx INT IDENTITY(1, 1),KeyName NVARCHAR(128),indid INT,Type CHAR(2))-- This temp table holds the foreign keys of the table.
-- The SQLStmt column is used to build dynamic SQL statements
-- that are related to these foreign keys.
IF OBJECT_ID('tempdb..#tblFKTable', 'U') IS NOT NULLDROP TABLE #tblFKTableCREATE TABLE #tblFKTable (Idx INT IDENTITY(1, 1),ForeignTableName NVARCHAR(128),KeyName NVARCHAR(128),SQLStmt NVARCHAR(1024))-- This temp table holds the colunms of the foriegn key of the table.IF OBJECT_ID('tempdb..#tblFKColTable', 'U') IS NOT NULLDROP TABLE #tblFKColTableCREATE TABLE #tblFKColTable (Idx INT IDENTITY(1, 1),ColName NVARCHAR(128),FOrP
CHAR(1))-- Get server and database namesSET @ServerName = CAST(ISNULL(SERVERPROPERTY('ServerName'), 'Unknown') AS SYSNAME)SET @DatabaseName = db_name()-- Some basic verifications:
-- 1. Check that file groups exist, and that the table exists.
SET @File1Name = FILEGROUP_NAME(@SourceFileGroupID)IF @File1Name IS NULLBEGIN
IF
@SourceFileGroupID IS NULLSET @SourceFileGroupID = 'NULL'SET @ScriptMsg = N'The source file group ' + CAST(@SourceFileGroupID AS VARCHAR(64)) + N' does not exist on the database ' + @DatabaseName + N', on server ' + @ServerName + N'. Please provide a valid filegroup id.'RAISERROR(@ScriptMsg, 16, 1)RETURN
END
SET
@File2Name = FILEGROUP_NAME(@TargetFileGroupID)IF @File2Name IS NULLBEGIN
IF
@TargetFileGroupID IS NULLSET @TargetFileGroupID = 'NULL'SET @ScriptMsg = N'The target file group ' + CAST(@TargetFileGroupID AS VARCHAR(64)) + N' does not exist on the database ' + @DatabaseName + N', on server ' + @ServerName + N'. Please provide a valid filegroup id.'RAISERROR(@ScriptMsg, 16, 1)RETURN
END
IF
@SourceFileGroupID = @TargetFileGroupIDBEGIN
SET
@ScriptMsg = N'The file groups provided are the same. This is not allow in this script.'RAISERROR(@ScriptMsg, 16, 1)RETURN
END
SET
@TableToMove = LTRIM(RTRIM(@TableToMove))IF RIGHT(@TableToMove, 1) = ']'SET @TableToMove = LEFT(@TableToMove, LEN(@TableToMove) - 1)IF LEFT(@TableToMove, 1) = '['SET
@TableToMove = RIGHT(@TableToMove, LEN(@TableToMove) - 1)-- Validate the table name and check that it exists in the system catalog.IF @TableToMove IS NULL OR @TableToMove = ''BEGIN
SET
@ScriptMsg = N'The table name provided in the script is either null or empty, on server '+ @ServerName + N' and database ' + @DatabaseName+ N'. Please provide a valid table name.'RAISERROR(@ScriptMsg, 16, 1)RETURN
END
IF
NOT EXISTS(SELECT * FROM INFORMATION_SCHEMA.TABLESWHERE TABLE_NAME = @TableToMove AND TABLE_TYPE = 'BASE TABLE')BEGIN
SET
@ScriptMsg = N'The table name provided in the script is not found in database '+ @DatabaseName + N', on server ' + @ServerName + N'. Please provide a valid table name.'RAISERROR(@ScriptMsg, 16, 1)RETURN
END
-- Check that the table is indeed defined on the source file group.IF (SELECT TOP 1 groupid FROM sysindexes WHERE id = OBJECT_ID(@TableToMove) and indid IN (0, 1)) <> @SourceFileGroupIDBEGIN
SET
+
@ScriptMsg = N'The table ' + @TableToMove + ' is not found on filegroup ' + CAST(@SourceFileGroupID AS VARCHAR(32)) N'. Please provide a valid table name and source file group.'RAISERROR(@ScriptMsg, 16, 1)RETURN
END
-- 2. Check that the target file group is not read-only.IF FILEGROUPPROPERTY(FILEGROUP_NAME(@TargetFileGroupID), 'IsReadOnly') = 1BEGIN
SET
@ScriptMsg = N'The taget file group (i.e., with file group id = ' + CAST(@TargetFileGroupID AS VARCHAR(32)) + N') is read-only. Aborting table move.'RAISERROR(@ScriptMsg, 16, 1)RETURN
END
-- 3. If we have gotten this far, then it is ok to move the table to the
-- requested filegroup.
-- First thing first: Check whether the table has a clustered index.
SET @TableHasCI = OBJECTPROPERTY(OBJECT_ID(@TableToMove), 'TableHasClustIndex')-- If not - check whether the table has an identity column.
-- If it does - apply the CI with the new filegroup on the identity column.
-- Once done - remove the CI. If it does not - check whether the table has a primary
-- key and apply the CI there on the new file group, and then drop the CI.
-- If the table does not have an identity column, or a primary key,
-- then a new identity column is created for the table and the CI
-- is applied on it, and then the CI and the identity column are removed.
-- This whole shabang is done in order to make the CI creation as fast as possible.
-- The case where the table does not have a clustered index to begin with implies
-- bad table design, and should not be common anyhow.
IF @TableHasCI = 0BEGIN
SET
@TableHasIdent = OBJECTPROPERTY(OBJECT_ID(@TableToMove), 'TableHasIdentity')IF @TableHasIdent = 0BEGIN
SET
@TableHasPK = OBJECTPROPERTY(OBJECT_ID(@TableToMove), 'TableHasPrimaryKey')SET @TableHasUQ = OBJECTPROPERTY(OBJECT_ID(@TableToMove), 'TableHasUniqueCnst')-- Only if the table has no PK/UQ or clustered index, then create an identity
-- column on it. This new column will hold the CI.
IF @TableHasPK = 0 AND @TableHasUQ = 0BEGIN
EXEC
[This_Is_My_Ident_Col_Name] BIGINT IDENTITY (1, 1) '
(N' ALTER TABLE [' + @TableToMove + N'] ADD)SET @IdentColName = 'This_Is_My_Ident_Col_Name'-- Apply the CI on the identity column. We don't create the CI
-- as unique, since the identity column may be non-unique,
-- due to reseeding.
EXEC
ON ['
ON ['
(N'CREATE CLUSTERED INDEX [This_Is_My_Clsuetered_Index_Name] + @TableToMove + N']([' + @IdentColName + ']) + @File2Name + N']')-- The table is now moved -> Remove the CI.EXEC(N'DROP INDEX [' + @TableToMove + N'].[This_Is_My_Clsuetered_Index_Name]')-- Finally, remove the added identity columnEXEC
DROP COLUMN [This_Is_My_Ident_Col_Name] '
(N' ALTER TABLE [' + @TableToMove + N'])END
ELSE
BEGIN
-- In this case, the table has a PK/UQ, so we might as well
-- apply the CI on the column(s) of the PK/UQ.
-- First, get the column(s) of the PK/UQ.
SELECT @KeyName = CONSTRAINT_NAMEFROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WITH (NOLOCK)WHERE TABLE_NAME = @TableToMoveAND CONSTRAINT_TYPE = 'PRIMARY KEY'IF @@ROWCOUNT = 0SELECT TOP 1 @KeyName = CONSTRAINT_NAMEFROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS WITH (NOLOCK)WHERE TABLE_NAME = @TableToMoveAND CONSTRAINT_TYPE = 'UNIQUE'-- The varialbe @KeyName now holds the name of the PK/UQINSERT INTO #tblColTable (ColName, IdxOrder)SELECT COL_NAME(OBJECT_ID(@TableToMove), colid),-- append the DESC/ASC string, based on the ASC/DESC order of the PK columnsCASE WHEN INDEXKEY_PROPERTY(OBJECT_ID(@TableToMove),INDEXPROPERTY(OBJECT_ID(@TableToMove),@KeyName,'IndexID'),keyno,'IsDescending') = 1THEN 'DESC'ELSE 'ASC'END
FROM
sysindexkeysWHERE
AND
id = OBJECT_ID(@TableToMove) indid = INDEXPROPERTY(OBJECT_ID(@TableToMove), @KeyName, 'IndexID')IF @@ROWCOUNT > 0SET @i = 1SET @ColList = N''WHILE EXISTS(SELECT * FROM #tblColTable WHERE Idx = @i)BEGIN
SELECT
@ColList = @ColList + N'[' + ColName + N'] ' + IdxOrder + N' ,'FROM #tblColTableWHERE Idx = @iSET @i = @i + 1END
SET
@ColList = LEFT(@ColList, LEN(@ColList) - 1)-- Now, apply the CI on the primary key columns. The CI is not
-- created as a unique CI, since if the PK/UQ was added with the NOCHECK
-- option, there could be duplicate entries in the PK/UQ.
EXEC
ON ['
ON ['
(N'CREATE CLUSTERED INDEX [This_Is_My_Clsuetered_Index_Name] + @TableToMove + N'](' + @ColList + ') + @File2Name + N']')-- The last command moved the CI (and thus the table), so we
-- can now drop the CI.
EXEC(N'DROP INDEX [' + @TableToMove + N'].[This_Is_My_Clsuetered_Index_Name]')END
END
ELSE
BEGIN
-- Here, the table originally had an identity. We apply the CI
-- on the identity column, and then remove it.
SELECT @IdentColName = COLUMN_NAMEFROM INFORMATION_SCHEMA.COLUMNS WITH (NOLOCK)WHERE TABLE_NAME = @TableToMoveAND COLUMNPROPERTY(OBJECT_ID(@TableToMove), COLUMN_NAME, 'IsIdentity') = 1EXEC
ON ['
ON ['
(N'CREATE CLUSTERED INDEX [This_Is_My_Clsuetered_Index_Name] + @TableToMove + N']([' + @IdentColName + ']) + @File2Name + N']')-- The table is now moved -> Remove the CI.EXEC(N'DROP INDEX [' + @TableToMove + N'].[This_Is_My_Clsuetered_Index_Name]')END
END
ELSE
BEGIN
-- Now, for the big ELSE. The ELSE applies to the case where the
-- table already has a clustered index. Here, we select the name of the
-- existing clustered index, then drop it from the table, and recreate
-- it on the other filegroup (on the same columns and order as was
-- originally defined for the table).
-- If the CI is also a PK/UQ/unique index, then we first check all foreign
-- keys for the PK/UQ/UI, drop them if they exist, drop the PK/UQ/UI
-- then recreate the PK/UQ/UI as CLUSTERED, and then reapply all the
-- foreign keys constraints. If the CI is non-unique (thus is not
-- associated with a PK/UQ/UI), we just drop and recreate it on the
-- target file group.
SELECT @CIName = [name]FROM sysindexes WITH (NOLOCK)WHERE
AND
id = OBJECT_ID(@TableToMove) indid = 1DELETE FROM #tblColTableINSERT INTO #tblColTable (ColName, IdxOrder)SELECT COL_NAME(OBJECT_ID(@TableToMove), colid),-- append the DESC/ASC string, based on the ASC/DESC order of the PK columnsCASE WHEN INDEXKEY_PROPERTY(OBJECT_ID(@TableToMove),INDEXPROPERTY(OBJECT_ID(@TableToMove),@CIName,'IndexID'),keyno,'IsDescending') = 1THEN 'DESC'ELSE 'ASC'END
FROM
sysindexkeys WITH (NOLOCK)WHERE
AND
id = OBJECT_ID(@TableToMove) indid = 1ORDER
SELECT
BY keyno ASC @i = MIN(Idx)FROM #tblColTableSET @ColList = N''WHILE EXISTS(SELECT * FROM #tblColTable WHERE Idx = @i)BEGIN
SELECT
@ColList = @ColList + N'[' + ColName + N'] ' + IdxOrder + N' ,'FROM #tblColTableWHERE Idx = @iSET @i = @i + 1END
SET
@ColList = LEFT(@ColList, LEN(@ColList) - 1)-- Check whether the clustered index is also the PK, or a unique constraint (UQ),
-- or a unique index (UI) that is neither a PK or a UQ.
-- If the CI is either one of the above, we first check whether any foreign keys
-- reference this PK/UQ/UI. If so - we drop the FKs, then drop the PK/UQ/UI,
-- then recreate the PK/UQ/UI on the target filegroup, and then recreate all
-- the foreign keys dropped earlier.
-- If the CI is other than the above (i.e., it is a non-unique clustered index)
-- then we simply drop it and recreate it on the target filegroup.
IF OBJECTPROPERTY(OBJECT_ID(@CIName), 'IsPrimaryKey') = 1OR OBJECTPROPERTY(OBJECT_ID(@CIName), 'IsUniqueCnst') = 1OR INDEXPROPERTY(OBJECT_ID(@TableToMove), @CIName, 'IsUnique') = 1BEGIN-- This case stands for a CI which is a PK/UQ/UI.
-- First, we drop all foreign keys associated with the PK/UQ/UI.
-- These FK constraints will be reapplied on the PK later,
-- (i.e., after the PK/UQ/UI is recreated on the target filegroup).
DELETE FROM #tblFKTable-- Get all the FK constraints associated with the PK/UQ/UI.
-- Here, we query sysreferences so we could get our hands on all the
-- foreign keys that reference the PK/UQ/UI of the table
-- that needs to be moved.
INSERT INTO #tblFKTable (ForeignTableName, KeyName)SELECT OBJECT_NAME(fkeyid), OBJECT_NAME(constid)FROM sysreferences WITH (NOLOCK)WHERE
AND
rkeyid = OBJECT_ID(@TableToMove) rkeyindid = INDEXPROPERTY(OBJECT_ID(@TableToMove), @CIName, 'IndexID')SELECT @AssocFKeyName = MIN(KeyName)FROM #tblFKTableWHILE @AssocFKeyName IS NOT NULLBEGIN-- Get the list of primary and then foreign columns
-- for the collected FK constraints. The CASCADE UPDATE,
-- CASCADE DELETE, and NOT FOR REPLICATION properties
-- of the FK are considered later.
SELECT @FKTableName = ForeignTableNameFROM #tblFKTableWHERE KeyName = @AssocFKeyNameDELETE FROM #tblFKColTable-- First, the tables of the foreign table. The select is ordered by keyno
-- so the order of columns in the FK will remain unchanged by the
-- drop/recreate operation.
INSERT INTO #tblFKColTable (ColName, FOrP)SELECT COL_NAME(fkeyid, fkey), 'F'FROM sysforeignkeysWHERE constid = OBJECT_ID(@AssocFKeyName)ORDER BY keyno-- Similarly, for the primary table.INSERT INTO #tblFKColTable (ColName, FOrP)SELECT COL_NAME(rkeyid, rkey), 'P'FROM sysforeignkeysWHERE constid = OBJECT_ID(@AssocFKeyName)ORDER BY keyno-- We now build the FK creation statementSELECT @j = MIN(Idx)FROM #tblFKColTableSET @SQLStr = N'ALTER TABLE [' + @FKTableName + N'] '+ N' WITH NOCHECK ADD CONSTRAINT [' + @AssocFKeyName + N'] '+ N' FOREIGN KEY ('-- Foreign table columnsWHILE EXISTS(SELECT * FROM #tblFKColTable WHERE Idx = @J AND FOrP = 'F')BEGIN
SELECT
@SQLStr = @SQLStr + N'[' + ColName + N'],'FROM #tblFKColTableWHERE Idx = @jSET @j = @j + 1END
SET
@SQLStr = LEFT(@SQLStr, LEN(@SQLStr) - 1) + N') REFERENCES [' + @TableToMove + N']('-- Primary table columnsWHILE EXISTS(SELECT * FROM #tblFKColTable WHERE Idx = @J AND FOrP = 'P')BEGIN
SELECT
@SQLStr = @SQLStr + N'[' + ColName + N'],'FROM #tblFKColTableWHERE Idx = @jSET @j = @j + 1END
SET
@SQLStr = LEFT(@SQLStr, LEN(@SQLStr) - 1) + ') 'IF OBJECTPROPERTY(OBJECT_ID(@AssocFKeyName), 'CnstIsDeleteCascade') = 1SET @SQLStr = @SQLStr + N' ON DELETE CASCADE 'IF OBJECTPROPERTY(OBJECT_ID(@AssocFKeyName), 'CnstIsUpdateCascade') = 1SET @SQLStr = @SQLStr + N' ON UPDATE CASCADE 'IF OBJECTPROPERTY(OBJECT_ID(@AssocFKeyName), 'CnstIsNotRepl') = 1SET
@SQLStr = @SQLStr + N' NOT FOR REPLICATION '-- Now, store this SQL statement, and drop the FK constraintUPDATE #tblFKTableSET SQLStmt = @SQLStrWHERE KeyName = @AssocFKeyName-- Drop the constraintEXEC(N'ALTER TABLE [' + @FKTableName + '] DROP CONSTRAINT [' + @AssocFKeyName + N']')SELECT @AssocFKeyName = MIN(KeyName)FROM #tblFKTableWHERE @AssocFKeyName < ispadindex =" INDEXPROPERTY(OBJECT_ID(@TableToMove)," ispadindex =" 1" assocfkeyname =" MIN(KeyName)" sqlstr =" SQLStmt" keyname =" @AssocFKeyName" assocfkeyname =" MIN(KeyName)" ispadindex =" INDEXPROPERTY(OBJECT_ID(@TableToMove)," ispadindex =" 1" moveallnonclusteredindexes =" 1" movepkandalluniqueconstraints =" 1" parent_obj =" OBJECT_ID(@TableToMove)" groupid =" @SourceFileGroupID"> 0 AND a.indid < moveallnonclusteredindexes =" 1" name =" b.KeyName" id =" OBJECT_ID(@TableToMove)" groupid =" @SourceFileGroupID"> 0 AND a.indid < 255END-- Now, loop through all keys/indexes collected in #tblKeysTable
-- and move them one by one to the new filegroup, while
-- maintaining the same column order they were previously defined on.
WHILE EXISTS(SELECT * FROM #tblKeysTable)BEGIN
SET
@indid = NULLSET @Type = NULLSET @KeyName = NULLSELECT TOP 1 @indid = indid,@Type = Type,@KeyName = KeyNameFROM #tblKeysTableDELETE FROM #tblColTableINSERT INTO #tblColTable (ColName, IdxOrder)SELECT COL_NAME(OBJECT_ID(@TableToMove), colid),-- append the DESC/ASC string, based on the ASC/DESC order of the PK columnsCASE WHEN INDEXKEY_PROPERTY(OBJECT_ID(@TableToMove),INDEXPROPERTY(OBJECT_ID(@TableToMove),@KeyName,'IndexID'),keyno,'IsDescending') = 1THEN 'DESC'ELSE 'ASC'END
FROM
sysindexkeys WITH (NOLOCK)WHERE
AND
id = OBJECT_ID(@TableToMove) indid = @indidORDER
SELECT
BY keyno ASC @i = MIN(Idx)FROM #tblColTableSET @ColList = N''WHILE EXISTS(SELECT * FROM #tblColTable WHERE Idx = @i)BEGIN
SELECT
@ColList = @ColList + N'[' + ColName + N'] ' + IdxOrder + N' ,'FROM #tblColTableWHERE Idx = @iSET @i = @i + 1END
SET
@ColList = LEFT(@ColList, LEN(@ColList) - 1)-- Drop the object, and then recreate it on the new filegroup.
-- Note: If a PK/UQ or a unique index (UI) is on the source file group,
-- then it must be a NONCLUSTERED PK/UQ, since the CLUSTERED PK/UQ/UI was
-- already handled above.
-- Also - we first check whether the PK/UQ/UI are used in any foreign keys, before
-- we drop/recreate them. If they are - then the FKs are first dropped,
-- then the PK/UQ/UI is dropped and recreated, and the FK constraints
-- are then recreated as well.
IF @Type IN ('PK', 'UQ', 'UI')BEGIN
DELETE
FROM #tblFKTable-- Get all the FK constraints associated with the PK/UQ/UI (UI = unique index).
-- Here, we must query sysreferences since it can identify which
-- foreign keys reference the table that we are moving, as well as
-- each individual unique constraint/index.
INSERT INTO #tblFKTable (ForeignTableName, KeyName)SELECT OBJECT_NAME(fkeyid), OBJECT_NAME(constid)FROM sysreferences WITH (NOLOCK)WHERE
AND
rkeyid = OBJECT_ID(@TableToMove) rkeyindid = INDEXPROPERTY(OBJECT_ID(@TableToMove), @KeyName, 'IndexID')SELECT @AssocFKeyName = MIN(KeyName)FROM #tblFKTableWHILE @AssocFKeyName IS NOT NULLBEGIN-- Get the list of primary and then foreign columns
-- for the collected FK constraints. The CASCADE UPDATE,
-- CASCADE DELETE, and NOT FOR REPLICATION properties
-- of the FK are considered later.
SELECT @FKTableName = ForeignTableNameFROM #tblFKTableWHERE KeyName = @AssocFKeyNameDELETE FROM #tblFKColTable-- First, the tables of the foreign table. The select is ordered by keyno
-- so the order of columns in the FK will remain unchanged by the
-- drop/recreate operation.
INSERT INTO #tblFKColTable (ColName, FOrP)SELECT COL_NAME(fkeyid, fkey), 'F'FROM sysforeignkeysWHERE constid = OBJECT_ID(@AssocFKeyName)ORDER BY keyno-- Similarly, for the primary table.INSERT INTO #tblFKColTable (ColName, FOrP)SELECT COL_NAME(rkeyid, rkey), 'P'FROM sysforeignkeysWHERE constid = OBJECT_ID(@AssocFKeyName)ORDER BY keyno-- We now build the FK creation statementSELECT @j = MIN(Idx)FROM #tblFKColTableSET @SQLStr = N'ALTER TABLE [' + @FKTableName + N'] '+ N' WITH NOCHECK ADD CONSTRAINT [' + @AssocFKeyName + N'] '+ N' FOREIGN KEY ('-- Foreign table columnsWHILE EXISTS(SELECT * FROM #tblFKColTable WHERE Idx = @J AND FOrP = 'F')BEGIN
SELECT
@SQLStr = @SQLStr + N'[' + ColName + N'],'FROM #tblFKColTableWHERE Idx = @jSET @j = @j + 1END
SET
@SQLStr = LEFT(@SQLStr, LEN(@SQLStr) - 1) + N') REFERENCES [' + @TableToMove + N']('-- Primary table columnsWHILE EXISTS(SELECT * FROM #tblFKColTable WHERE Idx = @J AND FOrP = 'P')BEGIN
SELECT
@SQLStr = @SQLStr + N'[' + ColName + N'],'FROM #tblFKColTableWHERE Idx = @jSET @j = @j + 1END
SET
@SQLStr = LEFT(@SQLStr, LEN(@SQLStr) - 1) + ') 'IF OBJECTPROPERTY(OBJECT_ID(@AssocFKeyName), 'CnstIsDeleteCascade') = 1SET @SQLStr = @SQLStr + N' ON DELETE CASCADE 'IF OBJECTPROPERTY(OBJECT_ID(@AssocFKeyName), 'CnstIsUpdateCascade') = 1SET @SQLStr = @SQLStr + N' ON UPDATE CASCADE 'IF OBJECTPROPERTY(OBJECT_ID(@AssocFKeyName), 'CnstIsNotRepl') = 1SET
@SQLStr = @SQLStr + N' NOT FOR REPLICATION '-- Now, store this SQL statement, and drop the FK constraintUPDATE #tblFKTableSET SQLStmt = @SQLStrWHERE KeyName = @AssocFKeyName-- Drop the constraintEXEC(N'ALTER TABLE [' + @FKTableName + '] DROP CONSTRAINT [' + @AssocFKeyName + N']')SELECT @AssocFKeyName = MIN(KeyName)FROM #tblFKTableWHERE @AssocFKeyName < KeyNameEND-- Now, drop the PK/UQ/UI and recreate itIF @Type = 'PK'BEGIN
EXEC
DROP CONSTRAINT ['
(N' ALTER TABLE [' + @TableToMove + N'] + @KeyName + N'] ')EXEC
WITH NOCHECK ADD CONSTRAINT ['
PRIMARY KEY NONCLUSTERED ('
ON ['
(N' ALTER TABLE [' + @TableToMove + N'] + @KeyName + N'] + @ColList + N') + @File2Name + N']')END
ELSE
BEGIN
IF
@Type = 'UQ'BEGIN
EXEC
DROP CONSTRAINT ['
(N' ALTER TABLE [' + @TableToMove + N'] + @KeyName + N'] ')EXEC
WITH NOCHECK ADD CONSTRAINT ['
UNIQUE NONCLUSTERED ('
ON ['
(N' ALTER TABLE [' + @TableToMove + N'] + @KeyName + N'] + @ColList + N') + @File2Name + N']')END
ELSE
-- must be UIBEGIN
SET
@IsPadIndex = INDEXPROPERTY(OBJECT_ID(@TableToMove), @KeyName, 'IsPadIndex')EXEC(N' DROP INDEX [' + @TableToMove + N'].[' + @KeyName + N']')IF @IsPadIndex = 1EXEC
ON ['
WITH PAD_INDEX
ON ['
(N' CREATE UNIQUE NONCLUSTERED INDEX [' + @KeyName + N'] + @TableToMove + N'](' + @ColList + N') + @File2Name + N']')ELSE
EXEC
ON ['
ON ['
(N' CREATE UNIQUE NONCLUSTERED INDEX [' + @KeyName + N'] + @TableToMove + N'](' + @ColList + N') + @File2Name + N']')END
END
-- And recreate the FK constraints attached to the PK/UQ
-- that was just recreated.
-- Recreate the FK constraint of the table.
SELECT @AssocFKeyName = MIN(KeyName)FROM #tblFKTableWHILE @AssocFKeyName IS NOT NULLBEGIN
SELECT
@SQLStr = SQLStmtFROM #tblFKTableWHERE KeyName = @AssocFKeyNameEXEC(@SQLStr)SELECT @AssocFKeyName = MIN(KeyName)FROM #tblFKTableWHERE @AssocFKeyName < KeyNameEND
END
IF
@Type = 'I'BEGIN
SET
@IsPadIndex = INDEXPROPERTY(OBJECT_ID(@TableToMove), @KeyName, 'IsPadIndex')EXEC(N' DROP INDEX [' + @TableToMove + N'].[' + @KeyName + N']')IF @IsPadIndex = 1EXEC
ON ['
WITH PAD_INDEX
ON ['
(N' CREATE NONCLUSTERED INDEX [' + @KeyName + N'] + @TableToMove + N'](' + @ColList + N') + @File2Name + N']')ELSE
EXEC
ON ['
ON ['
(N' CREATE NONCLUSTERED INDEX [' + @KeyName + N'] + @TableToMove + N'](' + @ColList + N') + @File2Name + N']')END
DELETE
FROM #tblKeysTableWHERE KeyName = @KeyNameAND
indid = @indidEND
PRINT
@TABLENAME