1

让我提前为这个问题的长度道歉。如果不给出所有定义,我看不出如何问它。

我继承了一个 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 年或以后有新的语言元素可以解决这个问题,我认为客户愿意升级。

谢谢。

4

1 回答 1

1

哇,真是一团糟。您的最大收获应该是删除光标。我看到“存在的地方”——这是很好且高效的 b/c,只要它找到一个匹配它就会中止。而且我看到“不存在的地方”——根据定义,必须扫描所有内容。找到前四了吗?使用 ROW_NUMBER() OVER (PARTITON BY [whatever makes it unique] ORDER BY [whatever your id is] 可以做得更好。很难说。select object_id(obj_name), @iObj_id, 0 使它看起来像只有 @ i=1 循环实际上做任何事情(?)

如果这就是它正在做的事情,你可以把它写成

SELECT * from
(
select ROW_NUMBER() OVER (PARTITION BY obj_id ORDER BY item_id desc) as Row, 
     obj_id, item_id
FROM bo.vSYSChangeHistoryFK a with (nolock) 
where obj_type = 'U' and descr = cast(@cPrev_val AS varchar(150))
   ) paged
where Row between 1 and 4 
ORDER BY Row

可能会有所帮助的 DBA 级别更改是设置基于日期的分区方案。每隔一段时间滚动到一个新分区。将旧分区放在不同的磁盘上。大多数查询可能只需要访问最近的分区,这将是它以前大小的 1/5,在不更改任何其他内容的情况下使其速度更快。

没有完整的答案,抱歉。那个烂摊子需要几个小时才能解析

于 2012-06-26T20:43:04.430 回答