728x90
----SQL 2005에서 행번호 매기기 -----------------------------------
Select Row_Number() OVER( ORDER BY release  ) AS RowNum, *
From cdlist
 
----TOP N절에 변수 사용하기
declare @pSRow smallint
set @pSRow = 30
select top(@pSRow) * from cdlist

'데이터베이스 > SQL Server' 카테고리의 다른 글

SQL Server 2005의 분할된 테이블 및 인덱스  (0) 2008.04.29
검색결과 xml 생성하기  (0) 2008.04.29
특정 행 구간 선택 하여 쿼리  (0) 2008.04.29
UPDATE 문 CASE  (0) 2008.04.29
Where 절 CASE문 사용  (0) 2008.04.29
728x90

차단 스크립트(Blocking Scripts)


부록은 본 문서에서 참조한 저장 프로시저의 소스 목록입니다. 필요에 따라 수정해서 사용하십시오.

sp_block

create proc dbo.sp_block (@spid bigint=NULL)
as
-- This stored procedure is provided "AS IS" with no warranties, and
-- confers no rights.
-- Use of included script samples are subject to the terms specified at
-- http://www.microsoft.com/info/cpyright.htm
--
-- T. Davidson
-- This proc reports blocks
-- 1. optional parameter @spid
--

select
t1.resource_type,
'database'=db_name(resource_database_id),
'blk object' = t1.resource_associated_entity_id,
t1.request_mode,
t1.request_session_id,
t2.blocking_session_id
from
sys.dm_tran_locks as t1,
sys.dm_os_waiting_tasks as t2
where
t1.lock_owner_address = t2.resource_address and
t1.request_session_id = isnull(@spid,t1.request_session_id)

인덱스 사용 정보 분석

아래 저장 프로시저들은 인덱스 사용량 분석에 사용할 수 있습니다.

get_indexstats

create proc dbo.get_indexstats
(@dbid smallint=-1
,@top varchar(100)=NULL
,@columns varchar(500)=NULL
,@order varchar(100)='lock waits'
,@threshold varchar(500)=NULL)
as
--
-- This stored procedure is provided "AS IS" with no warranties, and
-- confers no rights.
-- Use of included script samples are subject to the terms specified at
-- http://www.microsoft.com/info/cpyright.htm
--
-- T. Davidson
-- This proc analyzes index statistics including accesses, overhead,
-- locks, blocks, and waits
--
-- Instructions: Order of execution is as follows:
-- (1) truncate indexstats with init_indexstats
-- (2) take initial index snapshot using insert_indexstats
-- (3) Run workload
-- (4) take final index snapshot using insert_indexstats
-- (5) analyze with get_indexstats

-- @dbid limits analysis to a database
-- @top allows you to specify TOP n
-- @columns is used to specify what columns from
-- sys.dm_db_index_operational_stats will be included in
-- the report
-- For example, @columns='scans,lookups,waits' will include
-- columns
-- containing these keywords
-- @order used to order results
-- @threshold used to add a threshold,
-- example: @threshold='[block %] > 5' only include if
-- blocking is over 5%
--
------ definition of some computed columns returned
-- [blk %] = percentage of locks that cause blocks e.g. blk% = 100 * lock
-- waits / locks
-- [index usage] = range_scan_count + singleton_lookup_count +
-- leaf_insert_count
-- [nonleaf index overhead]=nonleaf_insert_count + nonleaf_delete_count +
-- nonleaf_update_count
-- [avg row lock wait ms]=row_lock_wait_in_ms/row_lock_wait_count
-- [avg page lock wait ms]=page_lock_wait_in_ms/page_lock_wait_count
-- [avg page latch wait ms]=page_latch_wait_in_ms/page_latch_wait_count
-- [avg pageio latch wait
-- ms]=page_io_latch_wait_in_ms/page_io_latch_wait_count
--------------------------------------------------------------------------
-------------------------
--- Case 1 - only one snapshot of sys.dm_db_operational_index_stats was
-- stored in
--- indexstats. This is an error - return errormsg to user
--- Case 2 - beginning snapshot taken, however some objects were not
-- referenced
--- at the time of the beginning snapshot. Thus, they will
-- not be in the initial
--- snapshot of sys.dm_db_operational_index_stats, use 0 for
-- starting values.
--- Print INFO msg for informational purposes.
--- Case 3 - beginning and ending snapshots, beginning values for all
-- objects and indexes
--- this should be the normal case, especially if SQL Server
-- is up a long time
--------------------------------------------------------------------------
-------------------------
set nocount on
declare @orderby varchar(100), @where_dbid_is varchar(100), @temp
varchar(500), @threshold_temptab varchar(500)
declare @cmd varchar(max),@col_stmt varchar(500),@addcol varchar(500)
declare @begintime datetime, @endtime datetime, @duration datetime,
@mincount int, @maxcount int

select @begintime = min(now), @endtime = max(now) from indexstats

if @begintime = @endtime
begin
print 'Error: indexstats contains only 1 snapshot of
sys.dm_db_index_operational_stats'
print 'Order of execution is as follows: '
print ' (1) truncate indexstats with init_indexstats'
print ' (2) take initial index snapshot using insert_indexstats'
print ' (3) Run workload'
print ' (4) take final index snapshot using insert_indexstats'
print ' (5) analyze with get_indexstats'
return -99
end

select @mincount = count(*) from indexstats where now = @begintime
select @maxcount = count(*) from indexstats where now = @endtime

if @mincount < @maxcount
begin
print 'InfoMsg1: sys.dm_db_index_operational_stats only contains
entries for objects referenced since last SQL re-cycle'
print 'InfoMsg2: Any newly referenced objects and indexes captured
in the ending snapshot will use 0 as a beginning value'
end

select @top = case
when @top is NULL then ''
else lower(@top)
end,
@where_dbid_is = case (@dbid)
when -1 then ''
else ' and i1.database_id = ' + cast(@dbid as varchar(10))
end,
--- thresholding requires a temp table
@threshold_temptab = case
when @threshold is NULL then ''
else ' select * from #t where ' + @threshold
end
--- thresholding requires temp table, add 'into #t' to select statement
select @temp = case (@threshold_temptab)
when '' then ''
else ' into #t '
end
select @orderby=case(@order)
when 'leaf inserts' then 'order by [' + @order + ']'
when 'leaf deletes' then 'order by [' + @order + ']'
when 'leaf updates' then 'order by [' + @order + ']'
when 'nonleaf inserts' then 'order by [' + @order + ']'
when 'nonleaf deletes' then 'order by [' + @order + ']'
when 'nonleaf updates' then 'order by [' + @order + ']'
when 'nonleaf index overhead' then 'order by [' + @order + ']'
when 'leaf allocations' then 'order by [' + @order + ']'
when 'nonleaf allocations' then 'order by [' + @order + ']'
when 'allocations' then 'order by [' + @order + ']'
when 'leaf page merges' then 'order by [' + @order + ']'
when 'nonleaf page merges' then 'order by [' + @order + ']'
when 'range scans' then 'order by [' + @order + ']'
when 'singleton lookups' then 'order by [' + @order + ']'
when 'index usage' then 'order by [' + @order + ']'
when 'row locks' then 'order by [' + @order + ']'
when 'row lock waits' then 'order by [' + @order + ']'
when 'block %' then 'order by [' + @order + ']'
when 'row lock wait ms' then 'order by [' + @order + ']'
when 'avg row lock wait ms' then 'order by [' + @order + ']'
when 'page locks' then 'order by [' + @order + ']'
when 'page lock waits' then 'order by [' + @order + ']'
when 'page lock wait ms' then 'order by [' + @order + ']'
when 'avg page lock wait ms' then 'order by [' + @order + ']'
when 'index lock promotion attempts' then 'order by [' + @order + ']'
when 'index lock promotions' then 'order by [' + @order + ']'
when 'page latch waits' then 'order by [' + @order + ']'
when 'page latch wait ms' then 'order by [' + @order + ']'
when 'pageio latch waits' then 'order by [' + @order + ']'
when 'pageio latch wait ms' then 'order by [' + @order + ']'
else ''
end

if @orderby <> '' select @orderby = @orderby + ' desc'
select
'start time'=@begintime,
'end time'=@endtime,
'duration (hh:mm:ss:ms)'=convert(varchar(50),
@endtime-@begintime,14),
'Report'=case (@dbid)
when -1 then 'all databases'
else db_name(@dbid)
end +
case
when @top = '' then ''
when @top is NULL then ''
when @top = 'none' then ''
else ', ' + @top
end +
case
when @columns = '' then ''
when @columns is NULL then ''
when @columns = 'none' then ''
else ', include only columns containing ' + @columns
end +
case(@orderby)
when '' then ''
when NULL then ''
when 'none' then ''
else ', ' + @orderby
end +
case
when @threshold = '' then ''
when @threshold is NULL then ''
when @threshold = 'none' then ''
else ', threshold on ' + @threshold
end

select @cmd = ' select i2.database_id, i2.object_id, i2.index_id,
i2.partition_number '
select @cmd = @cmd +' , begintime=case min(i1.now) when max(i2.now) then
NULL else min(i1.now) end '
select @cmd = @cmd +' , endtime=max(i2.now) '
select @cmd = @cmd +' into #i '
select @cmd = @cmd +' from indexstats i2 '
select @cmd = @cmd +' full outer join '
select @cmd = @cmd +' indexstats i1 '
select @cmd = @cmd +' on i1.database_id = i2.database_id '
select @cmd = @cmd +' and i1.object_id = i2.object_id '
select @cmd = @cmd +' and i1.index_id = i2.index_id '
select @cmd = @cmd +' and i1.partition_number = i2.partition_number '
select @cmd = @cmd +' where i1.now >= ''' +
convert(varchar(100),@begintime, 109) + ''''
select @cmd = @cmd +' and i2.now = ''' + convert(varchar(100),@endtime,
109) + ''''
select @cmd = @cmd + ' ' + @where_dbid_is + ' '
select @cmd = @cmd + ' group by i2.database_id, i2.object_id, i2.index_id,
i2.partition_number '
select @cmd = @cmd + ' select ' + @top + ' i.database_id,
db_name=db_name(i.database_id),
object=isnull(object_name(i.object_id),i.object_id), indid=i.index_id,
part_no=i.partition_number '

exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[leaf inserts]=i2.leaf_insert_count -
isnull(i1.leaf_insert_count,0)'

select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,@col_stmt=' ,
[leaf deletes]=i2.leaf_delete_count ?
isnull(i1.leaf_delete_count,0)'

select @cmd = @cmd + @addcol

exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[leaf updates]=i2.leaf_update_count ?
isnull(i1.leaf_update_count,0)'

select @cmd = @cmd + @addcol

exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[nonleaf inserts]=i2.nonleaf_insert_count ?
isnull(i1.nonleaf_insert_count,0)'

select @cmd = @cmd + @addcol

exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[nonleaf deletes]=i2.nonleaf_delete_count ?
isnull(i1.nonleaf_delete_count,0)'

select @cmd = @cmd + @addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[nonleaf updates]=i2.nonleaf_update_count ?
isnull(i1.nonleaf_update_count,0)'

select @cmd = @cmd + @addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[nonleaf index overhead]=(i2.nonleaf_insert_count ?
isnull(i1.nonleaf_insert_count,0)) + (i2.nonleaf_delete_count ?
isnull(i1.nonleaf_delete_count,0)) + (i2.nonleaf_update_count ?
isnull(i1.nonleaf_update_count,0))'

select @cmd = @cmd + @addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[leaf allocations]=i2.leaf_allocation_count ?
isnull(i1.leaf_allocation_count,0)'

select @cmd = @cmd + @addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[nonleaf allocations]=i2.nonleaf_allocation_count ?
isnull(i1.nonleaf_allocation_count,0)'

select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[allocations]=(i2.leaf_allocation_count ?
isnull(i1.leaf_allocation_count,0)) + (i2.nonleaf_allocation_count ?
isnull(i1.nonleaf_allocation_count,0))'

select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[leaf page merges]=i2.leaf_page_merge_count ?
isnull(i1.leaf_page_merge_count,0)'

select @cmd = @cmd + @addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[nonleaf page merges]=i2.nonleaf_page_merge_count ?
isnull(i1.nonleaf_page_merge_count,0)'

select @cmd = @cmd + @addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[range scans]=i2.range_scan_count ?
isnull(i1.range_scan_count,0)'

select @cmd = @cmd + @addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing= @columns,
@col_stmt=' ,[singleton lookups]=i2.singleton_lookup_count ?
isnull(i1.singleton_lookup_count,0)'

select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[index usage]=(i2.range_scan_count ?
isnull(i1.range_scan_count,0)) + (i2.singleton_lookup_count ?
isnull(i1.singleton_lookup_count,0)) + (i2.leaf_insert_count ?
isnull(i1.leaf_insert_count,0))'
select @cmd = @cmd + @addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[row locks]=i2.row_lock_count ?
isnull(i1.row_lock_count,0)'
select @cmd = @cmd + @addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[row lock waits]=i2.row_lock_wait_count ?
isnull(i1.row_lock_wait_count,0)'

select @cmd = @cmd + @addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[block %]=cast (100.0 * (i2.row_lock_wait_count ?
isnull(i1.row_lock_wait_count,0)) / (1 + i2.row_lock_count ?
isnull(i1.row_lock_count,0)) as numeric(5,2))'

select @cmd = @cmd + @addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[row lock wait ms]=i2.row_lock_wait_in_ms ?
isnull(i1.row_lock_wait_in_ms,0)'

select @cmd = @cmd + @addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[avg row lock wait ms]=cast ((1.0*(i2.row_lock_wait_in_ms
- isnull(i1.row_lock_wait_in_ms,0)))/(1 + i2.row_lock_wait_count -
isnull(i1.row_lock_wait_count,0)) as numeric(20,1))'
select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[page locks]=i2.page_lock_count ?
isnull(i1.page_lock_count,0)'
select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[page lock waits]=i2.page_lock_wait_count ?
isnull(i1.page_lock_wait_count,0)'
select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[page lock wait ms]=i2.page_lock_wait_in_ms ?
isnull(i1.page_lock_wait_in_ms,0)'
select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[avg page lock wait ms]=cast
((1.0*(i2.page_lock_wait_in_ms - isnull(i1.page_lock_wait_in_ms,0)))/(1 +
i2.page_lock_wait_count - isnull(i1.page_lock_wait_count,0)) as
numeric(20,1))'
select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[index lock promotion
attempts]=i2.index_lock_promotion_attempt_count ?
isnull(i1.index_lock_promotion_attempt_count,0)'
select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[index lock promotions]=i2.index_lock_promotion_count ?
isnull(i1.index_lock_promotion_count,0)'
select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[page latch waits]=i2.page_latch_wait_count ?
isnull(i1.page_latch_wait_count,0)'
select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[page latch wait ms]=i2.page_latch_wait_in_ms ?
isnull(i1.page_latch_wait_in_ms,0)'
select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[avg page latch wait ms]=cast
((1.0*(i2.page_latch_wait_in_ms - isnull(i1.page_latch_wait_in_ms,0)))/(1
+ i2.page_latch_wait_count - isnull(i1.page_latch_wait_count,0)) as
numeric(20,1))'
select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[pageio latch waits]=i2.page_io_latch_wait_count ?
isnull(i1.page_latch_wait_count,0)'
select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[pageio latch wait ms]=i2.page_io_latch_wait_in_ms ?
isnull(i1.page_latch_wait_in_ms,0)'
select @cmd = @cmd +@addcol
exec dbo.add_column
@add_stmt=@addcol out,
@cols_containing=@columns,
@col_stmt=' ,[avg pageio latch wait ms]=cast
((1.0*(i2.page_io_latch_wait_in_ms ?
isnull(i1.page_io_latch_wait_in_ms,0)))/(1 + i2.page_io_latch_wait_count ?
isnull(i1.page_io_latch_wait_count,0)) as numeric(20,1))'

select @cmd = @cmd +@addcol
select @cmd = @cmd + @temp
select @cmd = @cmd + ' from #i i '
select @cmd = @cmd + ' left join indexstats i1 on i.begintime = i1.now and
i.database_id = i1.database_id and i.object_id = i1.object_id and
i.index_id = i1.index_id and i.partition_number = i1.partition_number '

select @cmd = @cmd + ' left join indexstats i2 on i.endtime = i2.now and
i.database_id = i2.database_id and i.object_id = i2.object_id and
i.index_id = i2.index_id and i.partition_number = i2.partition_number '
select @cmd = @cmd + ' ' + @orderby + ' '
select @cmd = @cmd + @threshold_temptab
exec ( @cmd )
go

insert_indexstats

create proc insert_indexstats (@dbid smallint=NULL,
@objid int=NULL,
@indid int=NULL,
@partitionid int=NULL)
as
--
-- This stored procedure is provided "AS IS" with no warranties, and
confers no rights.
-- Use of included script samples are subject to the terms specified at
http://www.microsoft.com/info/cpyright.htm
-- This stored procedure stores a snapshot of
sys.dm_db_index_operational_stats into the table indexstas
-- for later analysis by the stored procedure get_indexstats. Please note
that the indexstats table has an additional
-- column to store the timestamp when the snapshot is taken
--
-- T. Davidson
-- snapshot sys.dm_db_index_operational_stats
--
declare @now datetime
select @now = getdate()
insert into indexstats
(database_id
,object_id
,index_id
,partition_number
,leaf_insert_count
,leaf_delete_count
,leaf_update_count
,leaf_ghost_count
,nonleaf_insert_count
,nonleaf_delete_count
,nonleaf_update_count
,leaf_allocation_count
,nonleaf_allocation_count
,leaf_page_merge_count
,nonleaf_page_merge_count
,range_scan_count
,singleton_lookup_count
,forwarded_fetch_count
,lob_fetch_in_pages
,lob_fetch_in_bytes
,lob_orphan_create_count
,lob_orphan_insert_count
,row_overflow_fetch_in_pages
,row_overflow_fetch_in_bytes
,column_value_push_off_row_count
,column_value_pull_in_row_count
,row_lock_count
,row_lock_wait_count
,row_lock_wait_in_ms
,page_lock_count
,page_lock_wait_count
,page_lock_wait_in_ms
,index_lock_promotion_attempt_count
,index_lock_promotion_count
,page_latch_wait_count
,page_latch_wait_in_ms
,page_io_latch_wait_count
,page_io_latch_wait_in_ms,
now)
select database_id
,object_id
,index_id
,partition_number
,leaf_insert_count
,leaf_delete_count
,leaf_update_count
,leaf_ghost_count
,nonleaf_insert_count
,nonleaf_delete_count
,nonleaf_update_count
,leaf_allocation_count
,nonleaf_allocation_count
,leaf_page_merge_count
,nonleaf_page_merge_count
,range_scan_count
,singleton_lookup_count
,forwarded_fetch_count
,lob_fetch_in_pages
,lob_fetch_in_bytes
,lob_orphan_create_count
,lob_orphan_insert_count
,row_overflow_fetch_in_pages
,row_overflow_fetch_in_bytes
,column_value_push_off_row_count
,column_value_pull_in_row_count
,row_lock_count
,row_lock_wait_count
,row_lock_wait_in_ms
,page_lock_count
,page_lock_wait_count
,page_lock_wait_in_ms
,index_lock_promotion_attempt_count
,index_lock_promotion_count
,page_latch_wait_count
,page_latch_wait_in_ms
,page_io_latch_wait_count
,page_io_latch_wait_in_ms
,@now
from sys.dm_db_index_operational_stats(@dbid,@objid,@indid,@partitionid)
go


init_index_operational_stats

CREATE proc dbo.init_index_operational_stats
as
--
-- This stored procedure is provided "AS IS" with no warranties, and
-- confers no rights.
-- Use of included script samples are subject to the terms specified at
-- http://www.microsoft.com/info/cpyright.htm
--
-- T. Davidson
--
-- create indexstats table if it doesn't exist, otherwise truncate
--
set nocount on
if not exists (select 1 from dbo.sysobjects where
id=object_id(N'[dbo].[indexstats]') and OBJECTPROPERTY(id, N'IsUserTable')
= 1)
create table dbo.indexstats (
database_id smallint NOT NULL
,object_id int NOT NULL
,index_id int NOT NULL
,partition_number int NOT NULL
,leaf_insert_count bigint NOT NULL
,leaf_delete_count bigint NOT NULL
,leaf_update_count bigint NOT NULL
,leaf_ghost_count bigint NOT NULL
,nonleaf_insert_count bigint NOT NULL
,nonleaf_delete_count bigint NOT NULL
,nonleaf_update_count bigint NOT NULL
,leaf_allocation_count bigint NOT NULL
,nonleaf_allocation_count bigint NOT NULL
,leaf_page_merge_count bigint NOT NULL
,nonleaf_page_merge_count bigint NOT NULL
,range_scan_count bigint NOT NULL
,singleton_lookup_count bigint NOT NULL
,forwarded_fetch_count bigint NOT NULL
,lob_fetch_in_pages bigint NOT NULL
,lob_fetch_in_bytes bigint NOT NULL
,lob_orphan_create_count bigint NOT NULL
,lob_orphan_insert_count bigint NOT NULL
,row_overflow_fetch_in_pages bigint NOT NULL
,row_overflow_fetch_in_bytes bigint NOT NULL
,column_value_push_off_row_count bigint NOT NULL
,column_value_pull_in_row_count bigint NOT NULL
,row_lock_count bigint NOT NULL
,row_lock_wait_count bigint NOT NULL
,row_lock_wait_in_ms bigint NOT NULL
,page_lock_count bigint NOT NULL
,page_lock_wait_count bigint NOT NULL
,page_lock_wait_in_ms bigint NOT NULL
,index_lock_promotion_attempt_count bigint NOT NULL
,index_lock_promotion_count bigint NOT NULL
,page_latch_wait_count bigint NOT NULL
,page_latch_wait_in_ms bigint NOT NULL
,page_io_latch_wait_count bigint NOT NULL
,page_io_latch_wait_in_ms bigint NOT NULL
,now datetime default getdate())
else truncate table dbo.indexstats
go


add_column

create proc dbo.add_column (
@add_stmt varchar(500) output,
@find varchar(100)=NULL,
@cols_containing varchar(500)=NULL,
@col_stmt varchar(max))
as
--
-- This stored procedure is provided "AS IS" with no warranties, and
-- confers no rights.
-- Use of included script samples are subject to the terms specified at
-- http://www.microsoft.com/info/cpyright.htm
--
-- T. Davidson
-- @add_stmt is the result passed back to the caller
-- @find is a keyword from @cols_containing
-- @cols_containing is the list of keywords to include in the report
-- @col_stmt is the statement that will be compared with @find.
-- If @col_stmt contains @find, include this statement.
-- set @add_stmt = @col_stmt
--
declare @length int, @strindex int, @EOS bit
if @cols_containing is NULL
begin
select @add_stmt=@col_stmt
return
end
select @add_stmt = '', @EOS = 0

while @add_stmt is not null and @EOS = 0
@dbid=-1,
select @strindex = charindex(',',@cols_containing)
if @strindex = 0
select @find = @cols_containing, @EOS = 1
else
begin
select @find = substring(@cols_containing,1,@strindex-1)
select @cols_containing =
substring(@cols_containing,
@strindex+1,
datalength(@cols_containing) - @strindex)
end
select @add_stmt=case
--when @cols_containing is NULL then NULL
when charindex(@find,@col_stmt) > 0 then NULL
else ''
end
end
--- NULL indicates this statement is to be passed back through out parm
@add_stmt
if @add_stmt is NULL select @add_stmt=@col_stmt
go


Wait states
이 저장 프로시저들은 차단 분석에 사용할 수 있습니다.

track_waitstats_2005

CREATE proc [dbo].[track_waitstats_2005] (
@num_samples int=10,
@delay_interval int=1,
@delay_type nvarchar(10)='minutes',
@truncate_history nvarchar(1)='N',
@clear_waitstats nvarchar(1)='Y')
as
--
-- This stored procedure is provided "AS IS" with no warranties, and
-- confers no rights.
-- Use of included script samples are subject to the terms specified at
-- http://www.microsoft.com/info/cpyright.htm
--
-- T. Davidson
-- @num_samples is the number of times to capture waitstats, default is 10
-- times
-- default delay interval is 1 minute
-- delaynum is the delay interval - can be minutes or seconds
-- delaytype specifies whether the delay interval is minutes or seconds
-- create waitstats table if it doesn't exist, otherwise truncate
-- Revision: 4/19/05
--- (1) added object owner qualifier
--- (2) optional parameters to truncate history and clear waitstats
set nocount on
if not exists (select 1
from sys.objects
where object_id = object_id ( N'[dbo].[waitstats]') and
OBJECTPROPERTY(object_id, N'IsUserTable') = 1)
create table [dbo].[waitstats]
([wait_type] nvarchar(60) not null,
[waiting_tasks_count] bigint not null,
[wait_time_ms] bigint not null,
[max_wait_time_ms] bigint not null,
[signal_wait_time_ms] bigint not null,
now datetime not null default getdate())

If lower(@truncate_history) not in (N'y',N'n')
begin
raiserror ('valid @truncate_history values are ''y'' or
''n''',16,1) with nowait
end
If lower(@clear_waitstats) not in (N'y',N'n')
begin
raiserror ('valid @clear_waitstats values are ''y'' or
''n''',16,1) with nowait
end
If lower(@truncate_history) = N'y'
truncate table dbo.waitstats

If lower (@clear_waitstats) = N'y'
-- clear out waitstats
dbcc sqlperf ([sys.dm_os_wait_stats],clear) with no_infomsgs

declare @i int,
@delay varchar(8),
@dt varchar(3),
@now datetime,
@totalwait numeric(20,1),
@endtime datetime,
@begintime datetime,
@hr int,
@min int,
@sec int

select @i = 1
select @dt = case lower(@delay_type)
when N'minutes' then 'm'
when N'minute' then 'm'
when N'min' then 'm'
when N'mi' then 'm'
when N'n' then 'm'
when N'm' then 'm'
when N'seconds' then 's'
when N'second' then 's'
when N'sec' then 's'
when N'ss' then 's'
when N's' then 's'
else @delay_type
end

if @dt not in ('s','m')
begin
raiserror ('delay type must be either ''seconds'' or
''minutes''',16,1) with nowait
return
end
if @dt = 's'
begin
select @sec = @delay_interval % 60, @min = cast((@delay_interval / 60)
as int), @hr = cast((@min / 60) as int)
end
if @dt = 'm'
begin
select @sec = 0, @min = @delay_interval % 60, @hr =
cast((@delay_interval / 60) as int)
end
select @delay= right('0'+ convert(varchar(2),@hr),2) + ':' +
+ right('0'+convert(varchar(2),@min),2) + ':' +
+ right('0'+convert(varchar(2),@sec),2)

if @hr > 23 or @min > 59 or @sec > 59
begin
select 'delay interval and type: ' + convert
(varchar(10),@delay_interval) + ',' + @delay_type + ' converts to ' +
@delay
raiserror ('hh:mm:ss delay time cannot > 23:59:59',16,1) with nowait
return
end
while (@i <= @num_samples)
begin
select @now = getdate()
insert into [dbo].[waitstats] (
[wait_type],
[waiting_tasks_count],
[wait_time_ms],
[max_wait_time_ms],
[signal_wait_time_ms],
now)
select
[wait_type],
[waiting_tasks_count],
[wait_time_ms],
[max_wait_time_ms],
[signal_wait_time_ms],
@now
from sys.dm_os_wait_stats

insert into [dbo].[waitstats] (
[wait_type],
[waiting_tasks_count],
[wait_time_ms],
[max_wait_time_ms],
[signal_wait_time_ms],
now)
select
'Total',
sum([waiting_tasks_count]),
sum([wait_time_ms]),
0,
sum([signal_wait_time_ms]),
@now
from [dbo].[waitstats]
where now = @now

select @i = @i + 1
waitfor delay @delay
end
--- create waitstats report
execute dbo.get_waitstats_2005
go
exec dbo.track_waitstats @num_samples=6
,@delay_interval=30
,@delay_type='s'
,@truncate_history='y'
,@clear_waitstats='y'


get_waitstats_2005

CREATE proc [dbo].[track_waitstats_2005] (
@num_samples int=10,
@delay_interval int=1,
@delay_type nvarchar(10)='minutes',
@truncate_history nvarchar(1)='N',
@clear_waitstats nvarchar(1)='Y')
as
--
-- This stored procedure is provided "AS IS" with no warranties, and
-- confers no rights.
-- Use of included script samples are subject to the terms specified at
-- http://www.microsoft.com/info/cpyright.htm
--
-- T. Davidson
-- @num_samples is the number of times to capture waitstats, default is 10
-- times
-- default delay interval is 1 minute
-- delaynum is the delay interval - can be minutes or seconds
-- delaytype specifies whether the delay interval is minutes or seconds
-- create waitstats table if it doesn't exist, otherwise truncate
-- Revision: 4/19/05
--- (1) added object owner qualifier
--- (2) optional parameters to truncate history and clear waitstats
set nocount on
if not exists (select 1
from sys.objects
where object_id = object_id ( N'[dbo].[waitstats]') and
OBJECTPROPERTY(object_id, N'IsUserTable') = 1)
create table [dbo].[waitstats]
([wait_type] nvarchar(60) not null,
[waiting_tasks_count] bigint not null,
[wait_time_ms] bigint not null,
[max_wait_time_ms] bigint not null,
[signal_wait_time_ms] bigint not null,
now datetime not null default getdate())

If lower(@truncate_history) not in (N'y',N'n')
begin
raiserror ('valid @truncate_history values are ''y'' or
''n''',16,1) with nowait
end
If lower(@clear_waitstats) not in (N'y',N'n')
begin
raiserror ('valid @clear_waitstats values are ''y'' or
''n''',16,1) with nowait
end
If lower(@truncate_history) = N'y'
truncate table dbo.waitstats

If lower (@clear_waitstats) = N'y'
-- clear out waitstats
dbcc sqlperf ([sys.dm_os_wait_stats],clear) with no_infomsgs

declare @i int,
@delay varchar(8),
@dt varchar(3),
@now datetime,
@totalwait numeric(20,1),
@endtime datetime,
@begintime datetime,
@hr int,
@min int,
@sec int

select @i = 1
select @dt = case lower(@delay_type)
when N'minutes' then 'm'
when N'minute' then 'm'
when N'min' then 'm'
when N'mi' then 'm'
when N'n' then 'm'
when N'm' then 'm'
when N'seconds' then 's'
when N'second' then 's'
when N'sec' then 's'
when N'ss' then 's'
when N's' then 's'
else @delay_type
end

if @dt not in ('s','m')
begin
raiserror ('delay type must be either ''seconds'' or
''minutes''',16,1) with nowait
return
end
if @dt = 's'
begin
select @sec = @delay_interval % 60, @min = cast((@delay_interval / 60)
as int), @hr = cast((@min / 60) as int)
end
if @dt = 'm'
begin
select @sec = 0, @min = @delay_interval % 60, @hr =
cast((@delay_interval / 60) as int)
end
select @delay= right('0'+ convert(varchar(2),@hr),2) + ':' +
+ right('0'+convert(varchar(2),@min),2) + ':' +
+ right('0'+convert(varchar(2),@sec),2)

if @hr > 23 or @min > 59 or @sec > 59
begin
select 'delay interval and type: ' + convert
(varchar(10),@delay_interval) + ',' + @delay_type + ' converts to ' +
@delay
raiserror ('hh:mm:ss delay time cannot > 23:59:59',16,1) with nowait
return
end
while (@i <= @num_samples)
begin
select @now = getdate()
insert into [dbo].[waitstats] (
[wait_type],
[waiting_tasks_count],
[wait_time_ms],
[max_wait_time_ms],
[signal_wait_time_ms],
now)
select
[wait_type],
[waiting_tasks_count],
[wait_time_ms],
[max_wait_time_ms],
[signal_wait_time_ms],
@now
from sys.dm_os_wait_stats

insert into [dbo].[waitstats] (
[wait_type],
[waiting_tasks_count],
[wait_time_ms],
[max_wait_time_ms],
[signal_wait_time_ms],
now)
select
'Total',
sum([waiting_tasks_count]),
sum([wait_time_ms]),
0,
sum([signal_wait_time_ms]),
@now
from [dbo].[waitstats]
where now = @now

select @i = @i + 1
waitfor delay @delay
end
--- create waitstats report
execute dbo.get_waitstats_2005
go
exec dbo.track_waitstats @num_samples=6
,@delay_interval=30
,@delay_type='s'
,@truncate_history='y'
,@clear_waitstats='y'

get_waitstats_2005

CREATE proc [dbo].[get_waitstats_2005] (
@report_format varchar(20)='all',
@report_order varchar(20)='resource')
as
-- This stored procedure is provided "AS IS" with no warranties, and
-- confers no rights.
-- Use of included script samples are subject to the terms specified at
-- http://www.microsoft.com/info/cpyright.htm
--
-- this proc will create waitstats report listing wait types by
-- percentage.
-- (1) total wait time is the sum of resource & signal waits,
-- @report_format='all' reports resource & signal
-- (2) Basics of execution model (simplified)
-- a. spid is running then needs unavailable resource, moves to
-- resource wait list at time T0
-- b. a signal indicates resource available, spid moves to
-- runnable queue at time T1
-- c. spid awaits running status until T2 as cpu works its way
-- through runnable queue in order of arrival
-- (3) resource wait time is the actual time waiting for the
-- resource to be available, T1-T0
-- (4) signal wait time is the time it takes from the point the
-- resource is available (T1)
-- to the point in which the process is running again at T2.
-- Thus, signal waits are T2-T1
-- (5) Key questions: Are Resource and Signal time significant?
-- a. Highest waits indicate the bottleneck you need to solve
-- for scalability
-- b. Generally if you have LOW% SIGNAL WAITS, the CPU is
-- handling the workload e.g. spids spend move through
-- runnable queue quickly
-- c. HIGH % SIGNAL WAITS indicates CPU can't keep up,
-- significant time for spids to move up the runnable queue
-- to reach running status
-- (6) This proc can be run when track_waitstats is executing
--
-- Revision 4/19/2005
-- (1) add computation for CPU Resource Waits = Sum(signal waits /
-- total waits)
-- (2) add @report_order parm to allow sorting by resource, signal
-- or total waits
--
set nocount on

declare @now datetime,
@totalwait numeric(20,1),
@totalsignalwait numeric(20,1),
@totalresourcewait numeric(20,1),
@endtime datetime,@begintime datetime,
@hr int,
@min int,
@sec int

if not exists (select 1
from sysobjects
where id = object_id ( N'[dbo].[waitstats]') and
OBJECTPROPERTY(id, N'IsUserTable') = 1)
begin
raiserror('Error [dbo].[waitstats] table does not exist',
16, 1) with nowait
return
end

if lower(@report_format) not in ('all','detail','simple')
begin
raiserror ('@report_format must be either ''all'',
''detail'', or ''simple''',16,1) with nowait
return
end
if lower(@report_order) not in ('resource','signal','total')
begin
raiserror ('@report_order must be either ''resource'',
''signal'', or ''total''',16,1) with nowait
return
end
if lower(@report_format) = 'simple' and lower(@report_order) <> 'total'
begin
raiserror ('@report_format is simple so order defaults to
''total''',
16,1) with nowait
select @report_order = 'total'
end


select
@now=max(now),
@begintime=min(now),
@endtime=max(now)
from [dbo].[waitstats]
where [wait_type] = 'Total'

--- subtract waitfor, sleep, and resource_queue from Total
select @totalwait = sum([wait_time_ms]) + 1, @totalsignalwait =
sum([signal_wait_time_ms]) + 1
from waitstats
where [wait_type] not in (
'CLR_SEMAPHORE',
'LAZYWRITER_SLEEP',
'RESOURCE_QUEUE',
'SLEEP_TASK',
'SLEEP_SYSTEMTASK',
'Total' ,'WAITFOR',
'***total***') and
now = @now

select @totalresourcewait = 1 + @totalwait - @totalsignalwait

-- insert adjusted totals, rank by percentage descending
delete waitstats
where [wait_type] = '***total***' and
now = @now

insert into waitstats
select
'***total***',
0,@totalwait,
0,
@totalsignalwait,
@now

select 'start time'=@begintime,'end time'=@endtime,
'duration (hh:mm:ss:ms)'=convert(varchar(50),@endtime-
@begintime,14),
'report format'=@report_format, 'report order'=@report_order

if lower(@report_format) in ('all','detail')
begin
----- format=detail, column order is resource, signal, total. order by
resource desc
if lower(@report_order) = 'resource'
select [wait_type],[waiting_tasks_count],
'Resource wt (T1-T0)'=[wait_time_ms]-[signal_wait_time_ms],
'res_wt_%'=cast (100*([wait_time_ms] -
[signal_wait_time_ms]) /@totalresourcewait as
numeric(20,1)),
'Signal wt (T2-T1)'=[signal_wait_time_ms],
'sig_wt_%'=cast (100*[signal_wait_time_ms]/@totalsignalwait as
numeric(20,1)),
'Total wt (T2-T0)'=[wait_time_ms],
'wt_%'=cast (100*[wait_time_ms]/@totalwait as numeric(20,1))
from waitstats
where [wait_type] not in (
'CLR_SEMAPHORE',
'LAZYWRITER_SLEEP',
'RESOURCE_QUEUE',
'SLEEP_TASK',
'SLEEP_SYSTEMTASK',
'Total',
'WAITFOR') and
now = @now
order by 'res_wt_%' desc

----- format=detail, column order signal, resource, total. order by signal
desc
if lower(@report_order) = 'signal'
select [wait_type],
[waiting_tasks_count],
'Signal wt (T2-T1)'=[signal_wait_time_ms],
'sig_wt_%'=cast (100*[signal_wait_time_ms]/@totalsignalwait
as numeric(20,1)),
'Resource wt (T1-T0)'=[wait_time_ms]-[signal_wait_time_ms],
'res_wt_%'=cast (100*([wait_time_ms] -
[signal_wait_time_ms]) /@totalresourcewait as
numeric(20,1)),
'Total wt (T2-T0)'=[wait_time_ms],
'wt_%'=cast (100*[wait_time_ms]/@totalwait as
numeric(20,1))
from waitstats
where [wait_type] not in (
'CLR_SEMAPHORE',
'LAZYWRITER_SLEEP',
'RESOURCE_QUEUE',
'SLEEP_TASK',
'SLEEP_SYSTEMTASK',
'Total',
'WAITFOR') and
now = @now
order by 'sig_wt_%' desc

----- format=detail, column order total, resource, signal. order by total
desc
if lower(@report_order) = 'total'
select
[wait_type],
[waiting_tasks_count],
'Total wt (T2-T0)'=[wait_time_ms],
'wt_%'=cast (100*[wait_time_ms]/@totalwait as numeric(20,1)),
'Resource wt (T1-T0)'=[wait_time_ms]-[signal_wait_time_ms],
'res_wt_%'=cast (100*([wait_time_ms] -
[signal_wait_time_ms]) /@totalresourcewait as numeric(20,1)),
'Signal wt (T2-T1)'=[signal_wait_time_ms],
'sig_wt_%'=cast (100*[signal_wait_time_ms]/@totalsignalwait as
numeric(20,1))
from waitstats
where [wait_type] not in (
'CLR_SEMAPHORE',
'LAZYWRITER_SLEEP',
'RESOURCE_QUEUE',
'SLEEP_TASK',
'SLEEP_SYSTEMTASK',
'Total',
'WAITFOR') and
now = @now
order by 'wt_%' desc
end
else
---- simple format, total waits only
select
[wait_type],
[wait_time_ms],
percentage=cast (100*[wait_time_ms]/@totalwait as numeric(20,1))
from waitstats
where [wait_type] not in (
'CLR_SEMAPHORE',
'LAZYWRITER_SLEEP',
'RESOURCE_QUEUE',
'SLEEP_TASK',
'SLEEP_SYSTEMTASK',
'Total',
'WAITFOR') and
now = @now
order by percentage desc

---- compute cpu resource waits
select
'total waits'=[wait_time_ms],
'total signal=CPU waits'=[signal_wait_time_ms],
'CPU resource waits % = signal waits / total waits'=
cast (100*[signal_wait_time_ms]/[wait_time_ms] as
numeric(20,1)),
now
from [dbo].[waitstats]
where [wait_type] = '***total***'
order by now
go


declare @now datetime
select @now = getdate()
select getdate()

[역주1] 원문에서는 RECOMPILE 쿼리 힌트를 추가로 언급하고 있으나 온라인 설명서, 프로파일러 등을 참조해 보면 OPTION(RECOMPILE)과 같은 것으로 판단됩니다.

[역주2] 원문에 별다른 언급이 없어서, 역자 임의로 추가합니다

[역주3] sys.dm_exec_query_optimizer_info DMV에는 원문에 언급된 두 가지 카운터 이외에 상당히 많은 카운터 항목들이 제공됩니다. 오히려 더 유용한 정보들이 많이 있습니다. 반드시 살펴보시길 권장합니다.

[역주4] 카디널리티(Cardinality)는 수학에서 집합의 원소의 개수를 의미합니다. 여기서는 테이블의 행 수를 의미합니다.

[역주5] 역자가 실제로 튜닝 컨설팅을 하면서 경험한 바로는 DOP의 개수에 따라서 2배 이상의 높은 CPU가 사용되는 경우를 자주 경험했습니다.

[역주6] 원문 코드의 주석 처리가 제대로 되어 있지 못해, 역자의 판단으로 주석 처리를 합니다.

[역주7] 한글 버전을 기준으로 언급합니다.

[역주8] SQL Server가 클라이언트로 결과 집합을 전송하는 기본 방식으로, 이전의 Firehose에 해당합니다. 전진-전용, 읽기-전용 방식입니다.

[역주9] 원본의 주소가 달라 변경합니다.

[역주10] 원본의 주소가 달라 변경합니다.

[역주11] 온라인 설명서의 번역을 따릅니다.

[역주12] 제품에 포함된 한글 오류 메시지와는 다르게 번역하였습니다.

[역주13] 원본에는 생략되었던 열입니다. 결과와 맞추기 위해 추가합니다.

[역주14] 원문의 offset 계산 공식은 잘못되었습니다. 온라인 설명서에서 sys.dm_exec_sql_text DMV의 예제를 참조하십시오.

[역주15] 한글 오류 메시지에는 victim에 대한 직접적인 해석 대신 “교착 상태가 발생...”으로 표현되어 있습니다. 해당 메시지를 직접 참조하십시오.

[역주16] 추적 플래그(Trace Flag)를 말합니다.

[역주17] 온라인 설명서에 찾을 수 없으며, 쿼리로도 실행되지 않습니다. 역자의 판단으로 내용 상 sys.dm_os_wait_stats 해당될 것으로 보입니다.

[역주18] 원문에서 실수로 반복된 항목이 아닐까 생각됩니다.

[역주19] 원본 예제는 각 예제별 설명과 실제 코드 간의 차이가 있어서, 역자 임의로 조정했습니다.

[역주20] 이 부분을 실제 Database ID로 변경하거나, DB_ID() 함수를 사용합니다.

728x90
[SQL 서버 2005 실전 활용] ① 더 강력해진 T-SQL
SQL 서버의 핵심 T-SQL의 새로운 기능

한용희 (롯데정보통신)2005/04/28
연재순서
1회. 더 강력해진 T-SQL의 새로운 기능
2회. 닷넷과의 통합
3회. 관리자를 위한 SQL 서버 2005
4회. DB 보호와 복구
그동안 유콘(Yucon)이라는 코드명으로 불렸던 차기 SQL 서버가 드디어 명칭을 SQL 서버 2005로 확정을 지으면서 올 하반기 출시를 앞두고 있다. SQL 서버 2005에 대한 전체적인 소개 글이 게재된 바 있지만 이번에는 실제 예를 통해 과연 SQL 서버 2005에서는 어떤 변화가 있는지 살펴본다.

연재 가이드

운영체제 : 윈도우 2000, 윈도우 2003, 윈도우 XP
개발도구 : MS SQL 서버 2005 베타 2, 비주얼 스튜디오 2005 베타 1
기초지식 : MS SQL 서버 2000, C#
응용분야 : MS SQL 서버 2005 관리와 개발
SQL 서버 2005가 서서히 모습을 드러내고 있다. 현재 베타 2까지 발표된 상태이며, 올 하반기에 정식 버전이 나올 예정이다. SQL 서버 2005 출시 소식이 전해지면서 심지어 “지금까지의 SQL 서버는 다 잊어라”, “T-SQL을 쓰지 않고 닷넷으로 전부 통합되기 때문에 처음부터 다시 배워야 한다”는 이야기까지 있었다. 하지만 SQL 서버 2005가 실체를 드러내면서 그러한 소문은 사실이 아니었음이 드러났다.

우선 닷넷에서는 SQL 서버의 저장 프로시저, 사용자 정의 데이터 타입, 그룹 함수, 사용자 정의 함수, 트리거 등을 만들 수 있다. 하지만 닷넷의 C#, VB.NET은 객체지향적 언어이지만, 집합적 언어는 아니다. 그래서 닷넷 언어를 이용하면 테이블의 행마다 어떤 일을 처리해 주어야 한다. 그러나 SQL문은 언어 자체가 집합적이므로, SQL문을 이용하면 테이블 전체를 핸들링할 수 있고, 전체를 핸들링하는 것이 속도면에서도 더 빠르다.

결국 닷넷은 T-SQL의 기능을 확장하기 위한 도구이지 T-SQL을 대체할 수는 없다. 대부분의 DML 구문(SELECT, INSERT 등)에서는 T-SQL이 닷넷보다 빠르다. 단 닷넷으로 작성하면 좋은 경우가 있는데, 그것은 CPU 작업을 많이 하는 작업(수학적 계산, 암호화 계산 등)이나, T-SQL로는 할 수 없는 시스템 외부와의 연동 작업 등이다. 이제 T-SQL의 변화된 모습을 직접 확인해 보자.

SQL 서버 2005 따라하기  
SQL 서버 2005는 툴에서도 많은 변화가 있다. 툴에 대한 자세한 설명은 세 번째 연재에서 하기로 하고 여기서는 일단 이번 연재를 따라하기 위한 간단한 사용법을 보자. 먼저 SQL 서버 2005 베타 2를 설치하고 나면, 기존 엔터프라이즈 관리자라든지, 쿼리 분석기가 보이지 않을 것이다. 2005에서는 이 두 가지 툴을 하나로 통합했는데, 그것이 SQL 서버 매니지먼트 스튜디오이다.

<화면 1> SQL 서버 매니지먼트 스튜디오 화면

SQL 서버 매니지먼트 스튜디오를 시작한 뒤 화면 왼쪽 상단에 있는 ‘New Query’ 버튼을 눌러서 새로운 창을 열면 이번 실습을 따라 할 수 있다. 기존에 SQL 서버를 설치하면 항상 샘플 데이터베이스로 Northwind와 pubs가 따라다녔다. 이제는 이 데이터베이스가 기본적으로 설치되지 않는 대신 AdventureWorks 데이터베이스가 새로 등장했다. AdventureWorks에는 SQL 서버 2005에서 새롭게 소개한 개념들이 많이 포함되어 있으므로 실습을 쉽게 할 수 있다.

SQL 서버 2005 T-SQL에서는 문장 끝에 세미콜론(;)을 허용한다. 이전 버전과의 호환성을 위해 문장 끝에 세미콜론을 쓰지 않아도 되지만 CTE(Common Table Expression)를 구현할 때에는 CTE 문장 앞에는 세미콜론이 있어야 한다. 그러나 이제는 문장 끝에 세미콜론이 있는 것이 가독성에도 좋으므로 세미콜론을 붙이는 것이 좋은 프로그래밍 습관이 될 것이다. 이번 예제는 모두 문장 끝에 세미콜론을 붙였다.

TOP 구문의 개선
SQL 서버 2000에서는 TOP 문장에 변수를 쓸 수 없었다. 굳이 쓰려고 한다면, 동적 SQL문을 이용하여 EXEC 구문으로 수행하는 방법이 있었다. 하지만 이 방법은 많이 불편해 왔던 것이 사실이다. SQL문 자체를 매번 동적 SQL 구문으로 만드는 것은 디버깅을 어렵게 하고, 가독성을 낮추는 결과를 초래하기 때문이다. SQL 서버 2005에서는 이런 문제를 해결하여 변수 사용이 가능하다. 다음 예제를 보자.

DECLARE @n INT;
SET @n = 3;

SELECT TOP (@n) EmployeeID, Title
FROM HumanResources.Employee;


EmployeeID Title
---------- ---------------------
1 Production Technician - WC60
2 Marketing Assistant
3 Engineering Manager

(3 row(s) affected)

여기서 한 가지 주의할 것은 ()이다. 상수일 때에는 없어도 무방하지만(SQL 서버 2000과의 호환성을 위해), 상수가 아니거나 데이터를 변경하는 구문에서는 반드시 괄호를 써주어야 한다. 괄호 안에 변수 뿐만 아니라 표현식이 들어올 수 있다. SQL문도 좋고, 함수도 좋다. 다음 예제는 부서의 개수만큼 사원의 정보를 읽어오는 예제이다. 부서가 총 16개 있으므로 16명의 사원 정보를 읽어 왔다.

SELECT TOP (
    SELECT COUNT(DepartmentID)
    FROM HumanResources.Department
) EmployeeID, Title
FROM HumanResources.Employee;


EmployeeID Title
----------- ----------------------
1 Production Technician - WC60
2 Marketing Assistant
3 Engineering Manager
...

(16 row(s) affected)

TOP문의 또 다른 변화는 INSERT, DELETE, UPDATE와 같은 DML 구문에서도 쓸 수 있다는 것이다. 다음 예제는 첫 번째에 있는 사원의 성별을 바꾸는 구문이다.

UPDATE TOP (1) HumanResources.Employee
SET Gender = 'F';

새로운 순위 함수
일반적으로 페이징 처리를 할 때에는 두 가지 방법이 있다. 먼저 클라이언트 쪽에서 모든 데이터를 읽어서 페이징 처리를 하는 방법이 있고, DB 쪽에서 페이징 처리를 해서 해당 페이지만을 읽어오는 방법이 있다. 첫 번째 방법은 모든 데이터를 읽어야 하므로, 데이터가 많을 때에는 사용하지 못한다. 두 번째 방법 또한 DB 쪽에서 처리를 하려면 동적 SQL문을 이용하여 복잡한 쿼리문을 작성해야 하므로 쉽게 구현하기가 힘들었다. 이러한 불편을 개선하기 위해 SQL 서버 2005에서는 두 가지 해결책을 내 놓았다.

첫 번째는 ADO.NET에서 페이징 처리를 하는 것이고, 두 번째는 T-SQL문을 이용하는 것이다. ADO.NET에서 하는 방법은 다음에 다룰 것이고, 이번 연재에서는 T-SQL문에서 하는 방법에 대해 알아보자. 다음 예제는 사원을 생년월일 순으로 정렬한 예제이다.

SELECT Row_Number() OVER( ORDER BY BirthDate ) AS RowNum,
    EmployeeID, BirthDate
FROM HumanResources.Employee;

RowNum EmployeeID BirthDate
------ ------------ ------------------
1 282 1930-01-11 00:00:00.000
2 233 1932-12-30 00:00:00.000
3 253 1933-01-05 00:00:00.000
4 240 1933-01-08 00:00:00.000
5 235 1933-01-14 00:00:00.000
6 224 1933-01-17 00:00:00.000
...


이제는 Row_Number()라는 함수를 이용하면 행 번호를 출력할 수 있다. 과거 SQL 서버 2000에서는 행 번호를 출력하는 함수가 없어 무척 불편했는데, 이제는 편하게 행 번호를 출력할 수 있다. 그런데 여기서 한 가지 주의할 것이 있다. 앞에서 OVER 다음에 오는 ORDER BY 문장은 행 번호를 어떤 순서로 매길 것인지를 정하는 구문이다. 만약 이 문장 FROM 절 다음에 또 다른 ORDER BY절이 온다면, 이는 행 번호를 다 매긴 후에 ORDER BY 구문에 의해서 행을 정렬하라는 의미가 된다.

따라서 FROM 이후에 ORDER BY EmpolyeeID라는 문장을 넣어 준다면, RowNum는 EmployeeID에 의해 정렬이 되므로 흐트러지게 된다. 직접 해보면 이해가 빠를 것이다. 지면 관계상 예제는 ‘이달의 디스켓’으로 대신한다. 이제 행 번호를 이용하여 페이징 처리를 해보자.

SELECT *
FROM(
    SELECT Row_Number() OVER( ORDER BY BirthDate ) AS RowNum,
        EmployeeID, BirthDate
    FROM HumanResources.Employee
) A
WHERE A.RowNum BETWEEN 4 AND 8;


RowNum  EmployeeID         BirthDate
---          ------------      -----------------------
4                240                 1933-01-08 00:00:00.000
5                235                 1933-01-14 00:00:00.000
6                224                 1933-01-17 00:00:00.000
7                281                 1934-04-10 00:00:00.000
8                268                 1941-11-17 00:00:00.000

(5 row(s) affected)

간단하게 페이징 처리하는 것을 볼 수 있을 것이다. 여기서 한 가지 단점이 있는데, Row_Number()라는 함수는 WHERE절 이후에 판단한다는 것이다. 즉 WHERE에 의해 SELECT를 범위를 정한 후에 행 번호를 구할 수 있다. 따라서 WHERE절에는 Row_Number()라는 함수를 쓸 수가 없다. 그러므로 이와 같이 전체를 읽은 후에 페이징 처리를 해야 하는 단점이 있다.

그밖에 새로운 순위 함수로는 Rank(), Dense_Rank(), NTile() 등이 있다. Rank는 같은 순위가 있을 경우 다음에는 그 만큼 순위를 건너뛰는 것이고, DenseRank는 순위를 건너뛰지 않는다. NTile은 전체를 NTile의 개수로 나눈 후 공평하게 순위를 배정하는 방법이다. 자세한 예제는 ‘이달의 디스켓’을 참고하기 바란다.

데이터를 조작할 때 유용한 OUTPUT 구문
SQL로 프로그래밍을 하다보면 삽입, 삭제, 업데이트시 이들 연산에 의해 일어난 결과 값을 알고 싶을 때가 있다. 예를 들면 데이터를 삭제할 때 무슨 데이터를 삭제했는지 알고 싶을 때가 있을 것이다. 이때에는 보통 삭제하기 전에 삭제할 값을 다른 테이블에 따로 저장해 두고, 삭제한 다음에 다른 테이블에 잠시 넣어둔 데이터를 조회함으로써 그 결과 값을 알 수 있었다. 이제는 이렇게 하지 않고도, 자신이 삭제한 값을 바로 알 수가 있다.

삽입의 경우도 마찬가지이다. IDENTITY 컬럼이 있는 테이블의 경우, 데이터를 삽입을 한 후 다시 그 테이블을 조회해야만 자신이 삽입한 행의 IDENTITY 값을 알 수가 있었다. 이제는 삽입을 할 때 바로 알 수가 있다. 다음 예제를 보자.

DECLARE @Tmp1 TABLE
(
    Num INT IDENTITY(1,1),
    Data varchar(100)
);

DECLARE @Tmp2 TABLE
(
    Num INT ,
    Data varchar(100)
);

INSERT INTO @Tmp1 VALUES('1 Data');
INSERT INTO @Tmp1 VALUES('2 Data');

INSERT INTO @Tmp1
OUTPUT inserted.* INTO @Tmp2
VALUES('3 Data');

DELETE TOP(1) FROM @Tmp1
OUTPUT deleted.* INTO @Tmp2;

SELECT * FROM @Tmp2;



Num     Data
--      ----------
3          3 Data
1          1 Data

첫 번째는 삽입을 할 때 OUTPUT 구문을 이용하여 그 삽입한 값을 받아왔고, 두 번째는 삭제를 할 때 삭제한 값을 OUTPUT 구문을 이용하여 받아왔다. 이를 이용하면 데이터를 조작하는 구문에서 쉽게 결과 값을 받아 올 수 있다.

CTE의 재귀 기능
테이블을 디자인하다 보면, 간혹 자기 자신을 참조하는 테이블을 디자인하는 경우가 있다. 예를 들면 사원 테이블의 경우 관리자 또한 사원이기 때문에 그 안에 포함하여 디자인하는 경우가 있다. 한 예로 AdventureWorks의 Employee 테이블을 보면 다음과 같이 사원과 관리자의 컬럼이 있다.

SELECT E mployeeID
    ,ManagerID
FROM    HumanResources.Employee;

EmployeeID    ManagerID
-----------    -----------
109                 NULL
4                     3
9                     3
11                    3
158                  3
263                  3
267                  3
270                  3
2                     6
46                   6
...

109의 관리자는 없으므로 사장이 될 것이고, 4번의 관리자는 3번이다. 이런 식으로 이루어진 테이블의 값을 조회하는 데 있어 3번의 부하 직원을 모두 조회하는 경우를 보자.

SELECT E1.EmployeeID [관리자1]
    ,E2.EmployeeID [관리자2]
    ,E3.EmployeeID [관리자3]
FROM HumanResources.Employee E1
        LEFT OUTER JOIN HumanResources.Employee E2 on E2.ManagerID = E1.EmployeeID
        LEFT OUTER JOIN HumanResources.Employee E3 on E3.ManagerID = E2.EmployeeID
WHERE E1.EmployeeID = 3;



관리자 1    관리자 2    관리자 3
--------- --------- ---------
3               4             NULL
3               9             NULL
3               11           NULL
3               158          79
3               158          114
3               158          217
3               263          5
3               263          265
3               267          NULL
3               270          NULL

(10 row(s) affected)

3번은 158번을 관리하고 있고 79번은 158의 관리를 받으므로, 결국 3번의 관리를 받는 직원으로 간주할 수 있다. 그런데 이러한 쿼리는 몇 가지 문제점을 가지고 있다. 조직의 깊이가 어느 레벨까지 내려갈지도 모르는 것이고, 조직의 변동에 따라 JOIN문을 추가해야 하므로 소스코드의 수정이 있어야 한다. 또한 불필요한 NULL 정보를 리턴하고 있어 정보의 전달 과정 또한 매끄럽지 못하다. 이를 좀 더 유연성있고 쉽게 표현해 보자. 다음은 SQL 서버 2005에서 가능한 구문이다.

WITH EmpCTE(MgrID, EmpID)
AS
(
    SELECT E.ManagerID, E.EmployeeID
    FROM    HumanResources.Employee E
    WHERE    ManagerID = 3
    UNION ALL
    SELECT     E.ManagerID, E.EmployeeID
    FROM    HumanResources.Employee E
        JOIN EmpCTE ON EmpCTE.EmpID = E.ManagerID
)
SELECT * FROM EmpCTE;

MgrID    EmpID
------  -----------
3           4
3           9
3           11
3           158
3           263
3           267
3           270
263         5
263         265
158         79
158         114
158         217

(12 row(s) affected)

좀 더 유연성 있는 결과가 나왔다. 조직이 어떻게 변하든, 레벨이 얼마다 더 깊어지든 상관없이 소스코드를 고치지 않고서도 좋은 결과를 낼 수 있다. 이 구문은 CTE(Common Table Expression)라는 SQL 서버 2005에서 새로 소개된 기능을 사용한 것이다. 그런데 그중에서도 재귀 CTE 구문을 이용한 것이다. 사실 CTE라는 것은 Derived Table, 뷰, 임시 테이블 등 어떤 것으로도 대체할 수 있는 구문이다. 그러므로 이와 같이 재귀 구문으로 쓰지 않는 한 새로운 점이 없는 구문이다. 사실 마이크로소프트에서도 이 재귀 기능 때문에 CTE라는 구문을 도입한 것이다.

CTE는 일종의 임시적인 가상 뷰로 보면 된다. 왜 임시적이냐 하면 CTE는 DML 구문(예, SELECT)과 같은 구문에 붙여서 사용하기 때문이다. 단독으로는 쓸 수 없다. CTE는 정의할 때 생기는 것이 아니라 실제로 구현할 때 그 구문이 실행되는 구문이다. 앞에서 보면 EmpCTE라는 것을 정의하고 그 밑에 있는 SELECT문에서 사용하고 있다.

앞의 CTE 구문의 보면 UNION ALL 구문을 기준으로 상단의 Anchor 멤버와 하단의 recursive 멤버로 나눌 수 있다. 상단 구문은 재귀 호출의 기준이 되는 구문으로 재귀 호출되는 구문이 없는 표현이 있어야 한다. 그래서 이 구문에서는 사장은 관리자가 없으므로 사장을 조회하도록 하였다. 하단의 재귀 구문에서는 자기 자신을 참조하여 재귀 구문을 수행하는 부분이므로 사원-관리자 관계를 조인하고 있다. 이제 이를 이용하면 재귀 구문도 쉽게 구현할 수 있을 것이다.

CASE문을 대체하는 PIVOT과 UNPIVOT
SQL 서버 2000에서 관계형 데이터에 행별로 저장된 값을, 가로 테이블로 된 형식으로 보기 위해서는 CASE문을 써야만 그렇게 볼 수 있었다. 하지만 SQL 서버 2005에서는 PIVOT 연산자를 이용하여 간단히 구현할 수 있다. 자세한 내용은 본지 12월호에 소개되었기 때문에 예제만 보도록 하자. 한 예로 년도별 판매사원의 매출을 구하는 예제를 보도록 하자. 다음은 SQL 서버 2000 방식으로 구현한 예제이다.

SELECT SalesPersonID
    ,SUM( case Year(OrderDate) when 2002 then TotalDue else 0 end ) as [2002]
    ,SUM( case Year(OrderDate) when 2003 then TotalDue else 0 end ) as [2003]
    ,SUM( case Year(OrderDate) when 2004 then TotalDue else 0 end ) as [2004]
FROM    Sales.SalesOrderHeader
GROUP BY SalesPersonID;


SalesPersonID  2002  2003  2004
------------- ------------- ------------- -------------
278 1604754.5514 1851628.4003 755593.2997
281 2973850.1213 3177895.6297 1429353.8926
275 4137233.9019 5244417.2148 2053782.7569

...

이번에는 2005 방식으로 구현한 예이다.

WITH C ( SalesPersonID, TheYear, TotalDue)
AS
(
    SELECT    SalesPersonID
            , Year( OrderDate) AS TheYear
            , TotalDue
    FROM    Sales.SalesOrderHeader
   
)
SELECT    SalesPersonID , [2002],[2003],[2004]
FROM    C
PIVOT    ( SUM(TotalDue) FOR TheYear IN ( [2002],[2003],[2004]) ) AS PVT;


SalesPersonID  2002  2003  2004
------------- ------------- ------------- -------------
NULL 7216029.7246 10819121.9238 10796844.5288
268 530374.4999 610881.0169 333855.4924
275 4137233.9019 5244417.2148 2053782.7569


대용량 데이터 타입
SQL 서버 2005에서는 기존의 대용량 데이터를 다루는 데 사용했던 text, ntext, image 데이터 타입 대신에 새로운 데이터 타입을 소개하고 있다(사실 앞의 3가지 데이터 타입은 이제 사라질 예정이라고 한다). 그것은 text->varchar(max), ntext->nvarchar(max), image->varbinary(max)이다. 이들은 최대 2GB까지 데이터를 저장할 수 있다. 이들 데이터 타입은 마치 문자열 데이터를 다루듯이 다룰 수 있기 때문에 대부분의 문자열 함수를 지원한다. 또한 이전에는 이러한 대용량 데이터를 처리하기 위해서는 별도의 구문이 필요했지만 이제는 그냥 보통 데이터 타입을 쓰듯이 그대로 쓸 수 있다. 다음 예는 varchar(max) 데이터 타입을 선언한 예이다.

CREATE TABLE Test
(
    Num     int IDENTITY(1,1),
    Vc    varchar(max)
);

XML 데이터 타입
SQL 서버 2000에서는 XML 데이터는 SQL 서버 엔진 안에 속하지 못하고 변방에서 맴돌았다. SQL 서버 2000에서는 XML 데이터를 마치 문자열 데이터처럼 다뤘기 때문에 별도의 함수나 구문을 사용해야만 했다. 하지만 SQL 서버 2005에서는 XML도 당당히 기본 데이터 타입으로 자리잡고 있다.

XML 데이터 타입에는 크게 두 가지 형태가 있다. typed와 untyped이다. type는 well formed XML 형식을 지원한다. XSD로 만든 스키마를 연결해 주면 스키마에 맞는 XML 데이터만 저장할 수 있다. 반면 untyped는 이러한 스키마 없이 그냥 생성한 XML 데이터 타입을 말한다. 아무래도 typed XML 데이터 타입을 쓰는 것이 속도나 기능면에서 여러 가지로 유리한 점이 있다. 다음 구문은 스키마를 정의하는 구문이다.

CREATE XML SCHEMA COLLECTION MyXMLSchema AS
N'<?xml version="1.0" encoding="utf-16"?>
<xs:schema id="XMLSchema1" targetNamespace="http://tempuri.org/XMLSchema1.xsd" elementFormDefault="qualified" xmlns="http://tempuri.org/XMLSchema1.xsd" xmlns:mstns="http://tempuri.org/XMLSchema1.xsd" xmlns:xs="http://www.w3.org/2001/XMLSchema">
    <xs:element name="MyXML">
        <xs:complexType>
            <xs:sequence>
                <xs:element name="ID" type="xs:int" />
                <xs:element name="Name" type="xs:string" />
            </xs:sequence>
        </xs:complexType>
    </xs:element>
</xs:schema>';

스키마는 비주얼 스튜디오에서 만든 후 쿼리문에 붙여 넣었다. 앞를 보면 ID, Name이라는 두 가지 요소가 있고, ID는 INT형, Name은 문자형으로 정의하고 있다. 이제 이 스키마를 이용하는 테이블을 만들어 보자.

CREATE TABLE MyXMLTest
(
    Num    int IDENTITY(1,1),
    XMLData    XML( MyXMLSchema)
);

간단히 데이터 타입을 선언하고 스키마도 같이 기술해 주고 있다. 여기에 한번 데이터를 삽입해 보자.

INSERT INTO MyXMLTest VALUES
( '<MyXML xmlns="http://tempuri.org/XMLSchema1.xsd">
        <ID> 10 </ID>
        <Name> "Hong Gil Dong" </Name>
</MyXML>'
);

INSERT INTO MyXMLTest VALUES
( '<MyXML xmlns="http://tempuri.org/XMLSchema1.xsd">
        <ID> 20 </ID>
        <Name> "Kim Su Jung" </Name>
</MyXML>'
);

INSERT INTO MyXMLTest VALUES
( '<MyXML xmlns="http://tempuri.org/XMLSchema1.xsd">
        <ID> "CXX" </ID>
        <Name> "Choi Man Ho" </Name>
</MyXML>'
);


(1 row(s) affected)

(1 row(s) affected)
Msg 6926, Level 16, State 1, Line 1
XML Validation: Invalid simple type value: '"CXX"'

결과를 보면 처음 두 데이터는 잘 들어갔는데, 마지막 데이터는 오류를 내고 들어가지 못했다. 이유는 스키마 규칙을 어겼기 때문이다. ID에 문자열이 있기 때문에 에러가 나면서 데이터 삽입을 거부하고 있다. 이제 결과를 조회해 보자.

SELECT * FROM MyXMLTest;

Num         XMLData
-----------------------------------------------------------------
1       <MyXMLxmlns="http://tempuri.org/XMLSchema1.xsd"> <ID>10</ID><Name> "Hong Gil Dong" </Name></MyXML>
2           <MyXML xmlns= "http://tempuri.org/XMLSchema1.xsd"> <ID>20</ID><Name> "Kim Su Jung" </Name></MyXML>

(2 row(s) affected)

예상대로 2행만 삽입이 되었다. 이제 이 Typed XML 데이터형을 가지고 XML 데이터 검색에 강한 X쿼리를 쓸 수 있다. 다음 예제는 ID가 10인 데이터를 검색하는 구문이다.

SELECT * FROM MyXMLTest
WHERE XMLData.exist( 'declare namespace xd="http://tempuri.org/XMLSchema1.xsd"     /xd:MyXML[xd:ID = 10]') = 1;

Num        XMLData
------------------------------------------------------------------
1        <MyXML xmlns="http://tempuri.org/XMLSchema1.xsd"> <ID>10</ID><Name> "Hong Gil Dong" </Name></MyXML>

(1 row(s) affected)

EXIST 함수의 경우 존재하면 1을 반환하고 없으면 0을 반환한다. 따라서 ID가 10인 데이터가 있으므로 하나의 행을 반환했다. 이제 이 X쿼리문을 이용하면 좀 더 쉽게 XML 데이터를 검색할 수 있을 것이다.

예외처리
SQL 서버 2000에서의 오류 처리 기능은 @@ERROR 변수 값을 확인하면 됐다. 하지만 이 변수 값은 단 하나의 SQL문에서만 생명력이 있기 때문에 에러가 날 만한 구문이 많이 있다면 모두 그 부분에서 @@ERROR 변수를 확인해야만 했다. 하지만 이제는 간단하게 TRY-CATCH 구문으로 묶어 줌으로써 이를 간단하게 해결할 수 있다. 이 구문은 이미 다른 언어에서는 많이 사용하는 구문이기 때문에 이해하기 어렵지는 않을 것이다.

SET XACT_ABORT ON;

BEGIN TRY
    BEGIN TRAN
        DELETE FROM Sales.SalesOrderHeader
        WHERE SalesOrderID = 43659;
    COMMIT
END TRY

BEGIN CATCH
    ROLLBACK
    SELECT ERROR_NUMBER() AS ErrorNumber,
        ERROR_MESSAGE() as ErrorMessage;
END CATCH


ErrorNumber    ErrorMessage
-----------------------------------------------------------------
547        DELETE statement conflicted with REFERENCE constraint 'FK_SalesOrderDetail_SalesOrderHeader_SalesOrderID'. The conflict occurred in database 'AdventureWorks', table 'SalesOrderDetail', column 'SalesOrderID'.

(1 row(s) affected)

이 예제는 외래키 위반 사례를 TRY-CATCH 구문으로 묶어본 것이다. 하지만 이러한 예외처리는 심각하지 않는 구문에서만 유효하다. 다음과 같은 경우를 보자.

CREATE PROC TestTran
AS
SET XACT_ABORT ON;


BEGIN TRY
    BEGIN TRAN
        DELETE PPP
    COMMIT
END TRY

BEGIN CATCH
    ROLLBACK
    SELECT ERROR_NUMBER() AS ErrorNumber,
        ERROR_MESSAGE() as ErrorMessage;
END CATCH

GO
EXEC TestTran;
GO
SELECT 'Orphaned transaction' , @@TRANCOUNT;


Msg 208, Level 16, State 1, Procedure TestTran, Line 10
Invalid object name 'PPP'.

------------------------  -----------
Orphaned transaction 1

(1 row(s) affected)

PPP라는 테이블은 존재하지 않는다. 이러한 심각한 오류의 경우 CATCH 문에서 걸리지 않는다. 그래서 롤백되지 않는 분리된 트랜잭션이 남아 있게 된다. SQL 서버 2000에서도 이 경우는 마찬가지이므로 주의해야 한다.

DDL 트리거
SQL 서버 2000에서의 트리거는 데이터를 조작하는 구문(INSERT, UPDATE 등)에서만 사용이 가능했다. 하지만 이제는 DDL 이벤트(테이블, 뷰, 프로시저 등을 생성하거나 삭제)에서도 사용이 가능하다. 다음 예제를 보자.

CREATE TRIGGER NoTableUpdate
ON DATABASE FOR DROP_TABLE, ALTER_TABLE
AS
PRINT 'DROP TABLE and ALTER TABLE statement are not allowed';
ROLLBACK;
DROP TABLE dbo.MyXMLTest;

DROP TABLE and ALTER TABLE statement are not allowed
Msg 3609, Level 16, State 2, Line 1
Transaction ended in trigger. Batch has been aborted.

현재 테이블 삭제와 변경시 발생하는 트리거를 정의하고 테이블 삭제와 변경을 못하도록 막아 놓았다. 그래서 실제로 테이블 삭제 테스트를 해보면 에러가 발생하면서 테이블 삭제가 되지 않는다. 이를 응용하면 관리자가 테이블이나 SP를 관리하는 데 도움을 줄 수 있다. DB를 관리하다 보면 잘 되던 프로그램이 갑자기 이상한 결과 값을 반환하거나 심한 경우 전체 DB가 다운되는 경우가 있다. 여러 가지 원인이 있을 수 있지만, 그중에서도 SP를 변경해서 생기는 경우가 종종 있다. 이럴 때 SP 변경 트리거를 걸어서 로그 관리를 한다면 원인이 되는 SP를 쉽게 찾을 수 있을 것이다. 또한 테이블 변경의 경우 매우 중요한 이슈이므로 테이블 변경 트리거를 걸어서 바로 알림 메시지(이메일, SMS 등)를 받을 수도 있다.

이것이 전부는 아니다
이것으로 T-SQL의 변화에 관해 짧게 소개를 해보았다. 하지만 지면상 많은 부분을 소개하지 못해 아쉬울 따름이다. T-SQL은 현재 소개한 것 말고도 많은 변화가 있다. 대략 나열해 보면 다음과 같다.

◆ 스냅샷 격리 - 쓰기 잠금을 수행하지 않는 추가적인 격리 레벨
◆ 문장 단위 재컴파일 - SP 전체 재컴파일이 아닌 문장 단위의 재컴파일 기능 지원
◆ TABLESAMPLE 구문 - 테이블의 샘플 데이터 조회
◆ APPLY - 사용자 정의 함수를 위한 새로운 JOIN 연산자

특히 문장 단위의 재컴파일 기능은 성능 면에서 좋은 효과를 줄 것으로 기대하고 있다. 다음 글에는 닷넷과 연동하는 부분에 대해 다룰 예정이다.@

* 이 기사는 ZDNet Korea의 제휴매체인 마이크로소프트웨어에 게재된 내용입니다.

+ Recent posts