14

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

AMD Opteron 1354 四核 2.20GHz 2GB RAM Windows Server 2008 x64(是的,我知道我只有 2GB 的 RAM,当项目上线时我将升级到 8GB)。

因此,我在一个表中创建了 250,000 个虚拟行,以对 LINQ to SQL 生成的一些查询进行真正的压力测试,并确保它们不会太糟糕,我注意到其中一个花费了荒谬的时间。

我用索引将这个查询缩短到 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%,成员为 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

执行其他 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);
4

8 回答 8

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 上有一个索引,它应该运行得非常快。

于 2009-03-04T02:32:58.717 回答
3

因此,如果我正确阅读了第一部分,它会在 17 秒内响应索引。想出 10 条记录还需要一段时间。我在想时间是按子句排序的。我想要一个关于 LastActivityAt、UserID、CategoryID 的索引。只是为了好玩,删除 order by 看看它是否快速返回 10 条记录。如果是这样,那么您就知道它不在与其他表的连接中。此外,将 * 替换为所需的列也会很有帮助,因为在排序时所有 3 个表列都在 tempdb 中 - 正如 Neil 提到的那样。

查看执行计划,您会注意到额外的排序 - 我相信这是需要一些时间的顺序。我假设你有一个 3 的索引,它是 17 秒......所以你可能想要一个索引用于连接条件(用户 ID、类别 ID)和另一个用于 lastactivityat - 看看它是否表现更好。此外,最好通过索引调整向导运行查询。

于 2009-03-04T02:24:56.240 回答
1

我的第一个建议是删除 *,并将其替换为您需要的最少列。

第二,是否有触发因素?会更新 LastActivityAt 字段的东西?

于 2009-03-04T02:06:52.867 回答
1

根据您的问题查询,尝试在表上添加组合索引Stories(CategoryID、UserID、LastActivityAt)

于 2009-03-04T02:17:30.283 回答
1

您正在最大化硬件设置中的磁盘。

鉴于您对 Data/Log/tempDB 文件位置的评论,我认为任何数量的调整都将成为创可贴。

250,000 行很小。想象一下,如果有 1000 万行,您的问题会有多糟糕。

我建议你将 tempDB 移到它自己的物理驱动器上(最好是 RAID 0)。

于 2009-03-04T03:09:19.720 回答
1

好的,所以我的测试机并不快。实际上它真的很慢。它 1.6 ghz,n 1 gb 的内存,没有多个磁盘,只有一个(读取速度慢)磁盘用于 sql server、操作系统和其他功能。

我创建了定义了主键和外键的表。插入了 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。在此列上有一个索引肯定会提高 order by 的性能。

此外,定义主键和外键可以极大地帮助 SQL Server

您的代码中列出的方法很优雅,并且基本上与 cdonner 编写的响应相同,除了在 c# 中而不是 sql 中。调整数据库可能会产生更好的结果!

——克里斯

于 2009-03-04T04:24:25.250 回答
0

您是否在运行每个查询之前清除了 SQL Server 缓存?

在 SQL 2000 中,它类似于 DBCC DROPCLEANBUFFERS。谷歌命令以获取更多信息。

查看查询,我会有一个索引

Categories.ID Stories.CategoryID Users.ID Stories.UserID

可能还有 Stories.LastActivityAt

但是,是的,听起来结果可能是缓存的虚假“cos”。

于 2009-03-04T02:50:07.270 回答
0

当您使用 SQL Server 一段时间后,您会发现即使是对查询的最小更改也会导致完全不同的响应时间。根据我在最初的问题中所读到的内容,并查看查询计划,我怀疑优化器已决定最好的方法是形成部分结果,然后将其作为单独的步骤进行排序。部分结果是用户和故事表的组合。这是在 tempdb 中形成的。所以过多的磁盘访问是由于这个临时表的形成然后排序。

我同意解决方案应该是在 Stories.LastActivityAt、Stories.UserId、Stories.CategoryId 上创建复合索引。顺序非常重要,LastActivityAt 字段必须是第一个。

于 2009-03-04T04:35:24.070 回答