注:本文仅针对 DiscuzNT3.0, sqlserver 2000版本,其他版本请勿对号入座.
在前两篇已经对看帖,发帖回帖做了优化,做过优化之后论坛访问起来果然顺畅多了。随着论坛帖子数的不断增加,论坛出现了一个新的问题,管理员在删帖的时候经常报错,超时,删除失败。
由于有了之前的两次经验,现在找起问题来比之前要快多了,现在先找到删帖的存储过程 dnt_deletetopicbytidlist3,脚本如下:
@tidlist AS VARCHAR ( 2000 ),
@chanageposts AS BIT
AS
DECLARE @postcount int
DECLARE @topiccount int
DECLARE @todaycount int
DECLARE @sqlstr nvarchar ( 4000 )
DECLARE @fid varchar ( 2000 )
DECLARE @posterid varchar ( 200 )
DECLARE @tempFid int
DECLARE @tempPosterid int
DECLARE @tempLayer int
DECLARE @temppostdatetime datetime
DECLARE @tempfidlist AS VARCHAR ( 1000 )
SET @fid = ''
SET @posterid = ''
SET @postcount = 0
SET @topiccount = 0
SET @todaycount = 0
SET @tempfidlist = '' ;
IF @tidlist <> ''
BEGIN
-- 问题:列运算
DECLARE cu_dnt_posts CURSOR FOR SELECT [ fid ] , [ posterid ] , [ layer ] , [ postdatetime ]
FROM [ dnt_posts3 ]
WHERE CHARINDEX ( ' , ' + RTRIM ( [ dnt_posts3 ] . [ tid ] ) + ' , ' , ' , ' + @tidlist + ' , ' ) > 0
OPEN cu_dnt_posts
FETCH NEXT FROM cu_dnt_posts into @tempFid , @tempPosterid , @tempLayer , @temppostdatetime
WHILE @@FETCH_STATUS = 0
BEGIN
SET @postcount = @postcount + 1
IF @tempLayer = 0
BEGIN
SET @topiccount = @topiccount + 1
END
IF DATEDIFF (d, @temppostdatetime , GETDATE ()) = 0
BEGIN
SET @todaycount = @todaycount + 1
END
IF CHARINDEX ( ' , ' + LTRIM ( STR ( @tempFid )) + ' , ' , @fid + ' , ' ) = 0
BEGIN
-- 把所有关联的论坛id保存到变量 @tempfidlist,后面会用到
SELECT @tempfidlist = ISNULL ( [ parentidlist ] , '' )
FROM [ dnt_forums ] WHERE [ fid ] = @tempFid
IF RTRIM ( @tempfidlist ) <> ''
BEGIN
SET @fid = RTRIM ( @fid ) + ' , ' + RTRIM ( @tempfidlist ) + ' , ' + CAST ( @tempFid AS VARCHAR ( 10 ))
END
ELSE
BEGIN
SET @fid = RTRIM ( @fid ) + ' , ' + CAST ( @tempFid AS VARCHAR ( 10 ))
END
END
IF @chanageposts = 1
BEGIN
-- 更新用户发帖数量
UPDATE [ dnt_users ] SET [ posts ] = [ posts ] - 1 WHERE [ uid ] = @tempPosterid
END
FETCH NEXT FROM cu_dnt_posts into @tempFid , @tempPosterid , @tempLayer , @temppostdatetime
END
CLOSE cu_dnt_posts
DEALLOCATE cu_dnt_posts
IF LEN ( @fid ) > 0
BEGIN
SET @fid = SUBSTRING ( @fid , 2 , LEN ( @fid ) - 1 )
IF @chanageposts = 1
BEGIN
UPDATE [ dnt_statistics ] SET [ totaltopic ] = [ totaltopic ] - @topiccount , [ totalpost ] = [ totalpost ] - @postcount
-- 问题:列运算
UPDATE [ dnt_forums ]
SET [ posts ] = [ posts ] - @postcount ,
[ topics ] = [ topics ] - @topiccount ,
[ todayposts ] = [ todayposts ] - @todaycount
WHERE CHARINDEX ( ' , ' + RTRIM ( [ fid ] ) + ' , ' , ' , ' + @fid + ' , ' ) > 0
END
-- 问题:列运算
DELETE FROM [ dnt_favorites ] WHERE CHARINDEX ( ' , ' + RTRIM ( [ tid ] ) + ' , ' , ' , ' + @tidlist + ' , ' ) > 0 AND [ typeid ] = 0
-- 问题:列运算
DELETE FROM [ dnt_polls ] WHERE CHARINDEX ( ' , ' + RTRIM ( [ tid ] ) + ' , ' , ' , ' + @tidlist + ' , ' ) > 0
-- 问题:列运算
DELETE FROM [ dnt_posts3 ] WHERE CHARINDEX ( ' , ' + RTRIM ( [ tid ] ) + ' , ' , ' , ' + @tidlist + ' , ' ) > 0
-- 问题:列运算
DELETE FROM [ dnt_mytopics ] WHERE CHARINDEX ( ' , ' + RTRIM ( [ tid ] ) + ' , ' , ' , ' + @tidlist + ' , ' ) > 0
END
-- 问题:列运算
DELETE FROM [ dnt_topics ] WHERE CHARINDEX (( ' , ' + RTRIM ( [ closed ] ) + ' , ' ), ' , ' + @tidlist + ' , ' ) > 0
OR CHARINDEX ( ' , ' + RTRIM ( [ tid ] ) + ' , ' , ' , ' + @tidlist + ' , ' ) > 0
-- 问题:列运算
UPDATE [ dnt_tags ] SET [ count ] = [ count ] - 1 , [ fcount ] = [ fcount ] - 1
WHERE [ tagid ] IN ( SELECT [ tagid ] FROM [ dnt_topictags ]
WHERE CHARINDEX ( ' , ' + RTRIM ( [ tid ] ) + ' , ' , ' , ' + @tidlist + ' , ' ) > 0 )
-- 问题:列运算
DELETE FROM [ dnt_topictags ] WHERE CHARINDEX ( ' , ' + RTRIM ( [ tid ] ) + ' , ' , ' , ' + @tidlist + ' , ' ) > 0
-- 问题:列运算
DELETE FROM [ dnt_topictagcaches ] WHERE CHARINDEX ( ' , ' + RTRIM ( [ tid ] ) + ' , ' , ' , ' + @tidlist + ' , ' ) > 0
OR CHARINDEX (( ' , ' + RTRIM ( [ linktid ] ) + ' , ' ), ' , ' + @tidlist + ' , ' ) > 0
end
go
删帖的操作流程是这样的:
1)传入@tidlist, 用逗号把帖子id隔开,比如“1,2,3,4,5” 这样的格式;
2)用游标 cu_dnt_posts 循环读取 dnt_posts3 表(此处用到了列运算,已标出);
3)找到该贴子id关联的所有论坛id,放到@tempfidlist变量,后面会用到这个变量,这个语句
SELECT @tempfidlist = ISNULL([parentidlist],'') FROM [dnt_forums] WHERE [fid] = @tempFid
同时更新用户的发帖数量,这一句 UPDATE [dnt_users] SET [posts] = [posts] - 1 WHERE [uid] = @tempPosterid
4)关闭游标
5)更新统计信息,这句 UPDATE [dnt_statistics] SET [totaltopic]=[totaltopic] - @topiccount, [totalpost]=[totalpost] - @postcount
6)更新论坛发帖数量,这句 UPDATE [dnt_forums]
7)删除收藏夹,删除回复表,删除用户发帖信息表 等等
8)删除主贴表,删除标签表 等等
这个过程代码量很大,逻辑稍微的有些复杂, 更糟糕的是里面大半的操作都做了列运算,charindex()这个函数就是,oh,my god,我们论坛目前的主贴表是 320万,其他回复表,最少的也有400万,最多有1000万,我可以分负责任的说,只要一运行这个过程,就死定了。不信?我们还是请profiler来帮我们看看吧(当然这是在测试环境,生产环境请勿尝试,否则后果自负)。
后台管理删帖界面:
点击提交,等了N久,得到下图(超时了):
看看profiler跟踪的结果,如下图:
看看这几个性能参数 cpu 27360,reads 71290,duration 30016 ,看到这里应该了然了,不超时是不可能的。
优化的方案有很多种,我这里只提供一种, 修改他的存储过程,不修改他的业务逻辑,我的原理是,用in代替charindex()函数,到目前为止dnt所有的数据库性能问题都和charindex()这个函数有关,而我也都是改成in,这次比之前稍微有难度,因为他涉及到了游标,修改要谨慎,改过之后的过程脚本如下:
@tidlist AS VARCHAR ( 2000 ),
@chanageposts AS BIT
AS
DECLARE @postcount int
DECLARE @topiccount int
DECLARE @todaycount int
DECLARE @sqlstr nvarchar ( 4000 )
DECLARE @fid varchar ( 2000 )
DECLARE @posterid varchar ( 200 )
declare @sql nvarchar ( 4000 )
SET @fid = ''
SET @posterid = ''
SET @postcount = 0
SET @topiccount = 0
SET @todaycount = 0
IF @tidlist = ''
return
set @sql =
' DECLARE @tempFid int ' + char ( 13 ) + char ( 10 ) +
' DECLARE @tempPosterid int ' + char ( 13 ) + char ( 10 ) +
' DECLARE @tempLayer int ' + char ( 13 ) + char ( 10 ) +
' DECLARE @temppostdatetime datetime ' + char ( 13 ) + char ( 10 ) +
' DECLARE @tempfidlist AS VARCHAR(1000) ' + char ( 13 ) + char ( 10 ) +
' SET @tempfidlist = '''' ' + char ( 13 ) + char ( 10 ) +
' declare @cu_dnt_posts cursor ' + char ( 13 ) + char ( 10 ) +
' set @cu_dnt_posts = CURSOR read_only forward_only FOR
SELECT [fid],[posterid],[layer],[postdatetime] FROM [dnt_posts3] WHERE tid in( '
+ @tidlist + ' ) ' + char ( 13 ) + char ( 10 ) +
' OPEN @cu_dnt_posts ' + char ( 13 ) + char ( 10 ) +
' FETCH NEXT FROM @cu_dnt_posts into @tempFid,@tempPosterid,@tempLayer,@temppostdatetime '
+ char ( 13 ) + char ( 10 ) +
' WHILE @@FETCH_STATUS = 0 ' + char ( 13 ) + char ( 10 ) +
' BEGIN ' + char ( 13 ) + char ( 10 ) +
' SET @postcount = @postcount + 1 ' + char ( 13 ) + char ( 10 ) +
' IF @tempLayer = 0 ' + char ( 13 ) + char ( 10 ) +
' SET @topiccount = @topiccount + 1 ' + char ( 13 ) + char ( 10 ) +
' IF DATEDIFF(d,@temppostdatetime,GETDATE()) = 0 ' + char ( 13 ) + char ( 10 ) +
' SET @todaycount = @todaycount + 1 ' + char ( 13 ) + char ( 10 ) +
' IF CHARINDEX( '' , '' + LTRIM(STR(@tempFid)) + '' , '' ,@fid + '' , '' ) = 0 '
+ char ( 13 ) + char ( 10 ) +
' BEGIN ' + char ( 13 ) + char ( 10 ) +
' SELECT @tempfidlist = ISNULL([parentidlist], '''' ) FROM [dnt_forums] WHERE [fid] = @tempFid '
+ char ( 13 ) + char ( 10 ) +
' IF RTRIM(@tempfidlist)<> '''' ' + char ( 13 ) + char ( 10 ) +
' SET @fid = RTRIM(@fid) + '' , '' + RTRIM(@tempfidlist) + '' , '' + CAST(@tempFid AS VARCHAR(10)) '
+ char ( 13 ) + char ( 10 ) +
' ELSE ' + char ( 13 ) + char ( 10 ) +
' SET @fid =RTRIM(@fid) + '' , '' + CAST(@tempFid AS VARCHAR(10)) ' + char ( 13 ) + char ( 10 ) +
' END ' + char ( 13 ) + char ( 10 ) +
' IF @chanageposts = 1 ' + char ( 13 ) + char ( 10 ) +
' UPDATE [dnt_users] SET [posts] = [posts] - 1 WHERE [uid] = @tempPosterid '
+ char ( 13 ) + char ( 10 ) +
' FETCH NEXT FROM @cu_dnt_posts into @tempFid,@tempPosterid,@tempLayer,@temppostdatetime '
+ char ( 13 ) + char ( 10 ) +
' END ' + char ( 13 ) + char ( 10 ) +
' CLOSE @cu_dnt_posts ' + char ( 13 ) + char ( 10 ) +
' DEALLOCATE @cu_dnt_posts '
exec sp_executesql @sql ,N ' @postcount int output,@topiccount int output,
@todaycount int output,@fid varchar(2000) output,@chanageposts int ' ,
@postcount output, @topiccount output, @todaycount output, @fid output, @chanageposts
IF LEN ( @fid ) > 0
BEGIN
SET @fid = SUBSTRING ( @fid , 2 , LEN ( @fid ) - 1 )
IF @chanageposts = 1
BEGIN
UPDATE [ dnt_statistics ] SET [ totaltopic ] = [ totaltopic ] - @topiccount , [ totalpost ] = [ totalpost ] - @postcount
set @sql =
' UPDATE [dnt_forums]
SET [posts]=[posts] - @postcount,
[topics]=[topics] - @topiccount,
[todayposts] = [todayposts] - @todaycount
where fid in ( ' + @fid + ' ) '
exec sp_executesql @sql ,N ' @postcount int,@topiccount int,@todaycount int ' ,
@postcount , @topiccount , @todaycount
END
set @sql = ' DELETE FROM [dnt_favorites] WHERE tid in ( ' + @tidlist + ' ) AND [typeid]=0 '
exec sp_executesql @sql
print @sql
set @sql = ' DELETE FROM [dnt_polls] WHERE tid in ( ' + @tidlist + ' ) '
exec sp_executesql @sql
print @sql
set @sql = ' DELETE FROM [dnt_posts3] WHERE tid in ( ' + @tidlist + ' ) '
exec sp_executesql @sql
print @sql
set @sql = ' DELETE FROM [dnt_mytopics] WHERE tid in ( ' + @tidlist + ' ) '
exec sp_executesql @sql
print @sql
END
set @sql = ' DELETE FROM [dnt_topics] WHERE [closed] in ( ' + @tidlist + ' ) or [tid] in ( ' + @tidlist + ' ) '
exec sp_executesql @sql
print @sql
set @sql = ' UPDATE [dnt_tags] SET [count]=[count]-1, [fcount]=[fcount]-1 WHERE [tagid] IN (SELECT [tagid] FROM [dnt_topictags] WHERE tid in ( ' + @tidlist + ' ) ) '
exec sp_executesql @sql
print @sql
set @sql = ' DELETE FROM [dnt_topictags] WHERE tid in ( ' + @tidlist + ' ) '
exec sp_executesql @sql
print @sql
set @sql = ' DELETE FROM [dnt_topictagcaches] WHERE tid in ( ' + @tidlist + ' ) OR linktid in ( ' + @tidlist + ' ) '
exec sp_executesql @sql
GO
感兴趣的童鞋可以把@sql打印出来,看看sql脚本,修改时需谨慎,优化后的过程如何呢,我们来看看profiler的效果:
看性能参数:cpu 62,reads 2684,duration 420;KO。
如果有童鞋正在使用dnt3.0,并且你希望你的论坛真正的跑起来,不妨试一试。
到目前为止,本系列已经出了3篇随笔,都是针对dnt的数据库性能问题提供了一些基本的思路和解决方案,以后数据库不再是重点;下一篇会针对dnt的asp.net端,如有兴趣敬请期待。
下篇预告:优化你的DiscuzNT,让它跑起来(4)asp.net 缓存和死锁