让我提前为这个问题的长度道歉。如果不给出所有定义,我看不出如何问它。
我继承了一个 SQL Server 2005 数据库,其中包括一个自主开发的更改跟踪实现。通过触发器,对数据库中几乎每个字段的更改都存储在一组三个表中。在这个数据库的应用中,用户可以请求各种item的历史,返回的不仅仅是item本身的变化,还有相关表的变化。问题在于,在某些情况下,它的速度非常缓慢,并且在某些情况下,请求最终会使应用程序崩溃。当有人请求历史记录时,客户端还报告了其他用户遇到问题。
存储变更数据的表如下:
CREATE TABLE [dbo].[tblSYSChangeHistory](
[id] [bigint] IDENTITY(1,1) NOT NULL,
[date] [datetime] NULL,
[obj_id] [int] NULL,
[uid] [varchar](50) NULL
此表跟踪已更改的表。Obj_id 是 Object_ID() 返回的值。
CREATE TABLE [dbo].[tblSYSChangeHistory_Items](
[id] [bigint] IDENTITY(1,1) NOT NULL,
[h_id] [bigint] NOT NULL,
[item_id] [int] NULL,
[action] [tinyint] NULL
此表跟踪已更改的项目。h_id 是 tblSYSChangeHistory 的外键。item_id 是指定表中更改项的 PK。action 表示插入、删除或更改。
CREATE TABLE [dbo].[tblSYSChangeHistory_Details](
[id] [bigint] IDENTITY(1,1) NOT NULL,
[i_id] [bigint] NOT NULL,
[col_id] [int] NOT NULL,
[prev_val] [varchar](max) NULL,
[new_val] [varchar](max) NULL
此表跟踪各个更改。i_id 是 tblSYSChangeHistory_Items 的外键。col_id 表示更改了哪一列,prev_val 和 new_val 表示该字段的原始值和新值。
实际上有第四个表支持这种架构。tblSYSChangeHistory_Objects 将简单的英语操作描述映射到数据库中的特定表。
查找项目历史记录的代码非常复杂。这是一个很长的 SP 的一个分支。相关参数如下:
@action varchar(50),
@obj_id bigint = 0,
@uid varchar(50) = '',
@prev_val varchar(MAX) = '',
@new_val varchar(MAX) = '',
@start_date datetime = '',
@end_date datetime = ''
我立即将它们存储到局部变量中(因为这样做可以显着加快另一个 SP):
declare @iObj_id bigint,
@cUID varchar(50),
@cPrev_val varchar(max),
@cNew_val varchar(max),
@tStart_date datetime,
@tEnd_date datetime
set @iObj_id = @obj_id
set @cUID = @uid
set @cPrev_val = @prev_val
set @cNew_val = @new_val
set @tStart_date = @start_date
set @tEnd_date = @end_date
这是 SP 的那个分支的代码:
create table #r (obj_id int, item_id int, l tinyint)
create clustered index #ri on #r (obj_id, item_id)
insert into #r
select object_id(obj_name), @iObj_id, 0
from dbo.tblSYSChangeHistory_Objects
where obj_type = 'U' and descr = cast(@cPrev_val AS varchar(150))
declare @i tinyint, @cnt int
set @i = 1
while @i <= 4
begin
insert into #r
select obj_id, item_id, @i
from dbo.vSYSChangeHistoryFK a with (nolock)
where exists (select null from #r where obj_id = a.rel_obj_id and item_id = a.rel_item_id and l = @i - 1)
and not exists (select null from #r where obj_id = a.obj_id and item_id = a.item_id)
set @cnt = @@rowcount
insert into #r
select rel_obj_id, rel_item_id, @i
from dbo.vSYSChangeHistoryFK a with (nolock)
where object_name(obj_id) not in (<this is a list of particular tables in the database>)
and exists (select null from #r where obj_id = a.obj_id and item_id = a.item_id and l between @i - 1 and @i)
and not exists (select null from #r where obj_id = a.rel_obj_id and item_id = a.rel_item_id)
set @i = case @cnt + @@rowcount when 0 then 100 else @i + 1 end
end
select date, obj_name, item, [uid], [action],
pkey, item_id, id, key_obj_id into #tCH_R
from dbo.vSYSChangeHistory a with (nolock)
where exists (select null from #r where obj_id = a.obj_id and item_id = a.item_id)
and (@cUID = '' or uid = @cUID)
and (@cNew_val = '' or [action] = @cNew_val)
declare ch_item_cursor cursor for
select distinct pkey, key_obj_id, item_id
from #tCH_R
where item = '' and pkey <> ''
open ch_item_cursor
fetch next from ch_item_cursor
into @cPrev_val, @iObj_id, @iCol_id
while @@fetch_status = 0
begin
set @SQLStr = 'select @val = ' + @cPrev_val +
' from ' + object_name(@iObj_id) + ' with (nolock)' +
' where id = @id'
exec sp_executesql @SQLStr,
N'@val varchar(max) output, @id int',
@cNew_val output, @iCol_id
update #tCH_R
set item = @cNew_val
where key_obj_id = @iObj_id
and item_id = @iCol_id
fetch next from ch_item_cursor
into @cPrev_val, @iObj_id, @iCol_id
end
close ch_item_cursor
deallocate ch_item_cursor
select date, obj_name,
cast(item AS varchar(254)) AS item,
uid, [action],
cast(id AS int) AS id
from #tCH_R
order by id
return
如您所见,代码使用视图。这是该定义:
ALTER VIEW [dbo].[vSYSChangeHistoryFK]
AS
SELECT i.obj_id, i.item_id, c1.parent_object_id AS rel_obj_id, i2.item_id AS rel_item_id
FROM dbo.vSYSChangeHistoryItemsD AS i INNER JOIN
sys.foreign_key_columns AS c1 ON c1.referenced_object_id = i.obj_id AND c1.constraint_column_id = 1 INNER JOIN
dbo.vSYSChangeHistoryItemsD AS i2 ON c1.parent_object_id = i2.obj_id INNER JOIN
dbo.tblSYSChangeHistory_Details AS d1 ON d1.i_id = i.min_id AND d1.col_id = c1.referenced_column_id INNER JOIN
dbo.tblSYSChangeHistory_Details AS d1k ON d1k.i_id = i2.min_id AND d1k.col_id = c1.parent_column_id AND ISNULL(d1.new_val,
ISNULL(d1.prev_val, '')) = ISNULL(d1k.new_val, ISNULL(d1k.prev_val, '')) --LEFT OUTER JOIN
UNION ALL
SELECT i0.obj_id, i0.item_id, c01.parent_object_id AS rel_obj_id, i02.item_id AS rel_item_id
FROM dbo.vSYSChangeHistoryItemsD AS i0 INNER JOIN
sys.foreign_key_columns AS c01 ON c01.referenced_object_id = i0.obj_id AND c01.constraint_column_id = 1 AND col_name(c01.referenced_object_id,
c01.referenced_column_id) = 'ID' INNER JOIN
dbo.vSYSChangeHistoryItemsD AS i02 ON c01.parent_object_id = i02.obj_id INNER JOIN
dbo.tblSYSChangeHistory_Details AS d01k ON i02.min_id = d01k.i_id AND d01k.col_id = c01.parent_column_id AND ISNULL(d01k.new_val,
d01k.prev_val) = CAST(i0.item_id AS varchar(max))
最后,该视图使用了另一种视图:
ALTER VIEW [dbo].[vSYSChangeHistoryItemsD]
AS
SELECT h.obj_id, m.item_id, MIN(m.id) AS min_id
FROM dbo.tblSYSChangeHistory AS h INNER JOIN
dbo.tblSYSChangeHistory_Items AS m ON h.id = m.h_id
GROUP BY h.obj_id, m.item_id
使用 Profiler 时,视图 vSYSChangeHistoryFK 似乎是罪魁祸首,我的测试表明,特定问题在于 vSYSChangeHistoryItemsD 的两个副本和 foreign_key_columns 表之间的连接。
我正在寻找有关如何在这里提供可接受的性能的任何想法。客户报告有时会等待长达 15 分钟而没有得到结果。我已经测试了将近 10 分钟,至少有一个案例没有结果。
如果 2008 年或以后有新的语言元素可以解决这个问题,我认为客户愿意升级。
谢谢。