查询250k行需要53秒

时间:2009-03-04 01:59:43

标签: sql linq-to-sql query-optimization

运行此查询的框是在数据中心中运行的专用服务器。

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);

8 个答案:

答案 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字段必须是第一个。