我有这个评论表(超过1百万行),每天大约有10,000个插入和大约100,000次查询,以及轻微的删除和更新。获取注释的查询会导致性能问题,有时它会锁定整个数据库,并且我会收到很多超时。请帮我调整我的索引和其他任何东西,以便它表现更好。下面我包含了有关它的信息,如果你需要更多,请询问。我每天重建所有索引并在2008服务器上运行sql server 2008网络版。
谢谢你:)结构:
id (int, identity)
profile_id (int)
owner_id (int)
added_date (datetime)
comments varchar(4000)
logical_delete (datetime)
索引:
id (PK, clustered)
profile_id (70% fill)
owner_id (70% fill)
added_date (70% fill)
profile_id + logical_delete (70%)
查询:
select
c.id, c.owner_id, c.comments, c.is_public, c.added_date,
u.first_name, u.last_name, c.profile_id
from [profile_comment] c with(nolock)
inner join [user] u with(nolock) on u.id = c.owner_id
where c.profile_id = @profile_id and c.logical_delete is null
order by c.added_date desc
执行计划:
|--Nested Loops(Inner Join, OUTER REFERENCES:([c].[owner_id], [Expr1005]) WITH ORDERED PREFETCH)
|--Sort(ORDER BY:([c].[added_date] DESC)) **[5%]**
| |--Nested Loops(Inner Join, OUTER REFERENCES:([c].[id], [Expr1004]) WITH UNORDERED PREFETCH) **[0%]**
| |--Index Seek(OBJECT:([DB].[dbo].[profile_comment].[IX_profile_comment_combined1] AS [c]), SEEK:([c].[profile_id]=(1) AND [c].[logical_delete]=NULL) ORDERED FORWARD) **[1%]**
| |--Clustered Index Seek(OBJECT:([JakLeci].[dbo].[profile_comment].[PK__profile_comment__primary] AS [c]), SEEK:([c].[id]=[JakLeci].[dbo].[profile_comment].[id] as [c].[id]) LOOKUP ORDERED FORWARD) **[47%]**
|--Clustered Index Seek(OBJECT:([DB].[dbo].[user].[PK__user__id] AS [u]), SEEK:([u].[id]=[DB].[dbo].[profile_comment].[owner_id] as [c].[owner_id]) ORDERED FORWARD) **[47%]**
答案 0 :(得分:1)
(profile_id,added_date DESC)上的聚簇索引应该可以解决问题。这将通过profile_id快速查找,已经按added_date排序。剩下的唯一操作是对logical_delete进行过滤,对用户进行循环连接(应该在user_id上进行集群)。
根据返回的行数,您仍然可以读取相当多的磁盘。您的评论专栏非常广泛。您可能需要考虑限制added_date(或TOP)返回的行数,或者缓存结果。
我无法想象这会导致高CPU使用率,并且您正在使用NOLOCK,因此您不应该阻止其他查询。如果这确实是导致超时的原因,那么它必须是I / O.您可能想要检查内存使用情况和磁盘子系统,以确保您获得了不错的性能。检查前后的逻辑读取和CPU时间,以确定您是否正在帮助。
您也可以删除一些索引来加速插入。除了浪费空间之外,我不确定70%的填充量是多少,但我可能错了。
答案 1 :(得分:1)
Nested Loops(Inner Join, OUTER REFERENCES:([c].[owner_id], [Expr1005]) WITH ORDERED PREFETCH)
|--Sort(ORDER BY:([c].[added_date] DESC)) **[5%]**
| |--Nested Loops(Inner Join, OUTER REFERENCES:([c].[id], [Expr1004]) WITH UNORDERED PREFETCH) **[0%]**
| |--Index Seek(OBJECT:([DB].[dbo].[profile_comment].[IX_profile_comment_combined1] AS [c]), SEEK:([c].[profile_id]=(1) AND [c].[logical_delete]=NULL) ORDERED FORWARD) **[1%]**
| |--Clustered Index Seek(OBJECT:([JakLeci].[dbo].[profile_comment].[PK__profile_comment__primary] AS [c]), SEEK:([c].[id]=[JakLeci].[dbo].[profile_comment].[id] as [c].[id]) LOOKUP ORDERED FORWARD) **[47%]**
|--Clustered Index Seek(OBJECT:([DB].[dbo].[user].[PK__user__id] AS [u]), SEEK:([u].[id]=[DB].[dbo].[profile_comment].[owner_id] as [c].[owner_id]) ORDERED FORWARD) **[47%]**
以下是我阅读此计划的方法:查询以查找profile_id = @profile_id开始,逻辑_deleted在IX_Profile_comment_combined上为空,然后在聚簇索引上执行嵌套连接循环,它按added_date对结果进行排序,然后它在用户上做了一个嵌套循环。
您可以迅速消除的一件事是SORT,通过将IX_profile_combined的定义更改为:
CREATE INDEX IX_profile_combined
ON profile_comment(logical_deleted, profile_id, added_date)
因为logical_deleted是一个非常低的选择性列,所以它应该是索引中最左边的键。因为特定的@profile_id的输出必须按added_date排序,所以added_date必须在密钥顺序中是profile_id的权利。
除了排序,查询计划对我来说很好。但我很好奇为什么一个被认为是系统中最大的生猪的查询在获得所有候选行时仅消耗1%,然后在两个聚簇索引查找上徘徊到93%的时间而在排序上只有5%。这不会成为问题查询的症状。 @profile_id是一个非常低的选择性密钥吗?您发布的计划是从非代表性的运行中收集的,运行良好吗?
答案 2 :(得分:0)
为什么不在管理工作室中运行查询并让它向您显示实际的执行计划,尝试查看问题所在?
如果没有这个,我唯一可以建议的是不要让你的主要身份密钥成为聚集索引,除非你经常查询。你可以尝试在profile_id和added_date上添加一个索引(或者使其成为唯一的其他东西)并将其作为聚簇索引,因为你正在通过profile_id进行查询而你很可能很少在身份字段上查询。
我需要实际访问数据库以查看实际发生的事情(按照我在顶部推荐的那样)来找出真正的罪魁祸首。它可能在连接上,但我非常怀疑它,因为我猜测用户表中的id列也是主要的聚簇索引。
答案 3 :(得分:0)
通过搜索列上的聚集索引,您将获得最佳性能:
(profile_id, is_deleted, owner_id, added_date desc)
缺点是只需按id查找行就需要使用较慢的非聚集索引,因为你只能有一个聚簇索引。
如果这不是一个选项,那么下一个最好的事情是创建一个多列覆盖索引,包括:
(profile_id, is_deleted, owner_id, added_date desc, id, comments, is_public)
因为注释相当大,您可以使用INCLUDE语法将注释包含为非键列:
create index idx on c (profile_id, is_deleted, owner_id, added_date desc, id,
is_public) include (comments)
评论不会被编入索引,但是当其他密钥匹配时,您将能够快速检索它。
更多细节:
http://www.sql-server-performance.com/tips/covering_indexes_p1.aspx
当然u.id应该是用户的主键。
最好使is_deleted为非null(tinyint默认为0)。
使用NOLOCK / READUNCOMMITTED,select不会创建任何锁定或阻止。