运行此查询的框是在数据中心中运行的专用服务器。
AMD Opteron 1354四核2.20GHz 2GB的RAM Windows Server 2008 x64(是的,我知道我只有2GB的RAM,当项目上线时我升级到8GB)。所以我在一个表中创建了250,000个虚拟行来真正压力测试LINQ to SQL生成的一些查询,并确保它们不会很糟糕,我注意到其中一个是花费了大量时间。< / p>
我使用索引将此查询缩短到17秒,但我为了这个答案而删除了它们从头到尾。只有索引是主键。
Stories table --
[ID] [int] IDENTITY(1,1) NOT NULL,
[UserID] [int] NOT NULL,
[CategoryID] [int] NOT NULL,
[VoteCount] [int] NOT NULL,
[CommentCount] [int] NOT NULL,
[Title] [nvarchar](96) NOT NULL,
[Description] [nvarchar](1024) NOT NULL,
[CreatedAt] [datetime] NOT NULL,
[UniqueName] [nvarchar](96) NOT NULL,
[Url] [nvarchar](512) NOT NULL,
[LastActivityAt] [datetime] NOT NULL,
Categories table --
[ID] [int] IDENTITY(1,1) NOT NULL,
[ShortName] [nvarchar](8) NOT NULL,
[Name] [nvarchar](64) NOT NULL,
Users table --
[ID] [int] IDENTITY(1,1) NOT NULL,
[Username] [nvarchar](32) NOT NULL,
[Password] [nvarchar](64) NOT NULL,
[Email] [nvarchar](320) NOT NULL,
[CreatedAt] [datetime] NOT NULL,
[LastActivityAt] [datetime] NOT NULL,
目前在数据库中有1个用户,1个类别和250,000个故事,我试图运行此查询。
SELECT TOP(10) *
FROM Stories
INNER JOIN Categories ON Categories.ID = Stories.CategoryID
INNER JOIN Users ON Users.ID = Stories.UserID
ORDER BY Stories.LastActivityAt
查询需要52秒才能运行,CPU使用率徘徊在2-3%,Membery是1.1GB,900MB可用,但磁盘使用率似乎失控。它是@ 100MB /秒,其中2/3是写入tempdb.mdf,其余的是从tempdb.mdf读取。
现在有趣的部分......
SELECT TOP(10) *
FROM Stories
INNER JOIN Categories ON Categories.ID = Stories.CategoryID
INNER JOIN Users ON Users.ID = Stories.UserID
SELECT TOP(10) *
FROM Stories
INNER JOIN Users ON Users.ID = Stories.UserID
ORDER BY Stories.LastActivityAt
SELECT TOP(10) *
FROM Stories
INNER JOIN Categories ON Categories.ID = Stories.CategoryID
ORDER BY Stories.LastActivityAt
所有这3个查询都是即时的。
第一次查询的执行计划 http://i43.tinypic.com/xp6gi1.png
Exec计划其他3个查询(按顺序)
http://i43.tinypic.com/30124bp.png
http://i44.tinypic.com/13yjml1.png
http://i43.tinypic.com/33ue7fb.png
非常感谢任何帮助。
添加索引后执行计划(再次降至17秒) http://i39.tinypic.com/2008ytx.png
我从每个人那里得到了很多有用的反馈,我感谢你,我在这里尝试了一个新的角度。我查询我需要的故事,然后在单独的查询中获取类别和用户以及3个查询它只用了250毫秒...我不明白这个问题但是如果它有效并且在250毫秒时暂时不会少于我坚持下去。这是我用来测试它的代码。
DBDataContext db = new DBDataContext();
Console.ReadLine();
Stopwatch sw = Stopwatch.StartNew();
var stories = db.Stories.OrderBy(s => s.LastActivityAt).Take(10).ToList();
var storyIDs = stories.Select(c => c.ID);
var categories = db.Categories.Where(c => storyIDs.Contains(c.ID)).ToList();
var users = db.Users.Where(u => storyIDs.Contains(u.ID)).ToList();
sw.Stop();
Console.WriteLine(sw.ElapsedMilliseconds);
答案 0 :(得分:13)
尝试在Stories.LastActivityAt上添加索引。我认为执行计划中的聚集索引扫描可能是由于排序造成的。
编辑: 由于我的查询在瞬间返回,行只有几个字节长,但已经运行了5分钟,并且在我添加了2K varchar之后仍然存在,我认为Mitch有一个观点。它是无关紧要的数据量,但这可以在查询中修复。
尝试将join,sort和top(10)放在视图或嵌套查询中,然后再与故事表联接,以获取您需要的10行的其余数据。
像这样:
select * from
(
SELECT TOP(10) id, categoryID, userID
FROM Stories
ORDER BY Stories.LastActivityAt
) s
INNER JOIN Stories ON Stories.ID = s.id
INNER JOIN Categories ON Categories.ID = s.CategoryID
INNER JOIN Users ON Users.ID = s.UserID
如果你有LastActivityAt的索引,这应该运行得非常快。
答案 1 :(得分:3)
因此,如果我正确读取第一部分,它会在17秒内用索引作出响应。还有一段时间来突破10条记录。我认为时间是按顺序排列的。我想要一个关于LastActivityAt,UserID,CategoryID的索引。只是为了好玩,删除订单,看看它是否快速返回10条记录。如果是,那么您知道它不在其他表的连接中。另外,将*替换为所需的列会很有帮助,因为正在排序时所有3个表列都在tempdb中 - 正如Neil所提到的那样。
查看执行计划,您会注意到额外的排序 - 我相信这是需要花费一些时间的顺序。我假设你有一个3的索引,它是17秒...所以你可能想要一个索引的连接条件(userid,categoryID)和另一个索引的lastactivityat - 看看它是否表现更好。通过索引调整向导运行查询也是一件好事。
答案 2 :(得分:1)
我的第一个建议是删除*,并将其替换为您需要的最少列。
第二,是否涉及触发器?什么会更新LastActivityAt字段?
答案 3 :(得分:1)
根据您的问题查询,尝试在表Stories
上添加组合索引(CategoryID,UserID,LastActivityAt)
答案 4 :(得分:1)
您正在硬件设置中最大化磁盘。
鉴于您对Data / Log / tempDB文件放置的评论,我认为任何数量的调整都将成为一个绑定。
250,000行很小。想象一下你的问题在1000万行中会有多糟糕。
我建议你将tempDB移到自己的物理驱动器上(最好是RAID 0)。
答案 5 :(得分:1)
好的,所以我的测试机器不快。实际上它真的很慢。它是1.6 ghz,n 1 gb ram,没有多个磁盘,只有一个(读慢)磁盘用于sql server,os和extras。
我创建了定义了主键和外键的表。 插入了2个类别,500个随机用户和250000个随机故事。
运行上面的第一个查询需要16秒(也没有计划缓存)。 如果我索引LastActivityAt列,我会在一秒钟内得到结果(此处也没有计划缓存)。
以下是我过去常用的脚本。
--Categories table --
Create table Categories (
[ID] [int] IDENTITY(1,1) primary key NOT NULL,
[ShortName] [nvarchar](8) NOT NULL,
[Name] [nvarchar](64) NOT NULL)
--Users table --
Create table Users(
[ID] [int] IDENTITY(1,1) primary key NOT NULL,
[Username] [nvarchar](32) NOT NULL,
[Password] [nvarchar](64) NOT NULL,
[Email] [nvarchar](320) NOT NULL,
[CreatedAt] [datetime] NOT NULL,
[LastActivityAt] [datetime] NOT NULL
)
go
-- Stories table --
Create table Stories(
[ID] [int] IDENTITY(1,1) primary key NOT NULL,
[UserID] [int] NOT NULL references Users ,
[CategoryID] [int] NOT NULL references Categories,
[VoteCount] [int] NOT NULL,
[CommentCount] [int] NOT NULL,
[Title] [nvarchar](96) NOT NULL,
[Description] [nvarchar](1024) NOT NULL,
[CreatedAt] [datetime] NOT NULL,
[UniqueName] [nvarchar](96) NOT NULL,
[Url] [nvarchar](512) NOT NULL,
[LastActivityAt] [datetime] NOT NULL)
Insert into Categories (ShortName, Name)
Values ('cat1', 'Test Category One')
Insert into Categories (ShortName, Name)
Values ('cat2', 'Test Category Two')
--Dummy Users
Insert into Users
Select top 500
UserName=left(SO.name+SC.name, 32)
, Password=left(reverse(SC.name+SO.name), 64)
, Email=Left(SO.name, 128)+'@'+left(SC.name, 123)+'.com'
, CreatedAt='1899-12-31'
, LastActivityAt=GETDATE()
from sysobjects SO
Inner Join syscolumns SC on SO.id=SC.id
go
--dummy stories!
-- A Count is given every 10000 record inserts (could be faster)
-- RBAR method!
set nocount on
Declare @count as bigint
Set @count = 0
begin transaction
while @count<=250000
begin
Insert into Stories
Select
USERID=floor(((500 + 1) - 1) * RAND() + 1)
, CategoryID=floor(((2 + 1) - 1) * RAND() + 1)
, votecount=floor(((10 + 1) - 1) * RAND() + 1)
, commentcount=floor(((8 + 1) - 1) * RAND() + 1)
, Title=Cast(NEWID() as VARCHAR(36))+Cast(NEWID() as VARCHAR(36))
, Description=Cast(NEWID() as VARCHAR(36))+Cast(NEWID() as VARCHAR(36))+Cast(NEWID() as VARCHAR(36))
, CreatedAt='1899-12-31'
, UniqueName=Cast(NEWID() as VARCHAR(36))+Cast(NEWID() as VARCHAR(36))
, Url=Cast(NEWID() as VARCHAR(36))+Cast(NEWID() as VARCHAR(36))
, LastActivityAt=Dateadd(day, -floor(((600 + 1) - 1) * RAND() + 1), GETDATE())
If @count % 10000=0
Begin
Print @count
Commit
begin transaction
End
Set @count=@count+1
end
set nocount off
go
--returns in 16 seconds
DBCC DROPCLEANBUFFERS
SELECT TOP(10) *
FROM Stories
INNER JOIN Categories ON Categories.ID = Stories.CategoryID
INNER JOIN Users ON Users.ID = Stories.UserID
ORDER BY Stories.LastActivityAt
go
--Now create an index
Create index IX_LastADate on Stories (LastActivityAt asc)
go
--With an index returns in less than a second
DBCC DROPCLEANBUFFERS
SELECT TOP(10) *
FROM Stories
INNER JOIN Categories ON Categories.ID = Stories.CategoryID
INNER JOIN Users ON Users.ID = Stories.UserID
ORDER BY Stories.LastActivityAt
go
排序肯定是你的减速发生的地方。 排序主要在tempdb中完成,大表将导致添加LOTS。 在此列上建立索引肯定会提高订单的效果。
此外,定义主键和外键可以帮助SQL Server实现
你的代码中列出的方法很优雅,cdonner编写的响应基本上与c#相同,而不是sql。调整数据库可能会产生更好的结果!
- 克里斯
答案 6 :(得分:0)
在运行每个查询之前,您是否清除了SQL Server缓存?
在SQL 2000中,它类似于DBCC DROPCLEANBUFFERS。谷歌获取更多信息的命令。
查看查询,我会得到
的索引Categories.ID Stories.CategoryID Users.ID Stories.UserID
可能 Stories.LastActivityAt
但是,是的,听起来结果可能是虚假的缓存。答案 7 :(得分:0)
当您使用SQL Server一段时间后,您会发现即使对查询进行的最小更改也会导致响应时间大不相同。根据我在初始问题中阅读的内容,并查看查询计划,我怀疑优化器已经确定最佳方法是形成部分结果,然后将其作为单独的步骤进行排序。部分结果是Users和Stories表的组合。这是在tempdb中形成的。因此,过多的磁盘访问是由于此临时表的形成然后排序所致。
我同意解决方案应该是在Stories.LastActivityAt,Stories.UserId,Stories.CategoryId上创建一个复合索引。订单非常重要,LastActivityAt字段必须是第一个。