【问题标题】:Query against 250k rows taking 53 seconds查询 250k 行需要 53 秒
【发布时间】:2010-10-11 04:11:07
【问题描述】:

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

AMD Opteron 1354 四核 2.20GHz 2GB 内存 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);

【问题讨论】:

  • 建议您发布执行计划...
  • 您的 tempDB 位于何处?它在单独的物理主轴上吗?
  • 您的数据、日志文件在哪里?在单独的物理驱动器上?
  • 因为它需要 17 秒,并且发生了相同的 HDD 锤击。 TempDB 与所有其他数据库文件/日志位于第二个 HDD 上。
  • 考虑到以前的工作,25 万条记录似乎并不多

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


【解决方案1】:

尝试在 Stories.LastActivityAt 上添加索引。我认为执行计划中的聚集索引扫描可能是由于排序的原因。

编辑: 由于我的查询立即返回,行只有几个字节长,但已经运行了 5 分钟,并且在我添加了一个 2K varchar 之后仍在继续,我认为 Mitch 有一点。数据量是无用的,但可以在查询中修复。

尝试将 join、sort 和 top(10) 放在视图或嵌套查询中,然后针对故事表返回 join 以获取您需要的 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。具有讽刺意味的是,我最近针对 SQL 2008 的查询做了一些非常相似的事情,但我从未想过这可能是同样的问题!
  • @Chad Moran:话虽如此,您仍然需要正确设置硬件!
  • @Mitch 是的,你是对的。当需要进行生产时,我会确保 SoftLayer 连接 3 个其他 HDD,并且我会对其进行 RAID 0,感谢您的提示! :)
【解决方案2】:

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

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

【讨论】:

  • 我同意这一点。如果没有索引,order by 必须花费时间。不过,我在执行计划中没有看到这一点。很奇怪。
  • 是的,我通过调优向导运行了它,在添加了建议后,它把它缩短到了 17 秒。
  • 是的,删除 ORDER BY 使其非常即时。有趣的是,如果我删除其中一个 INNER JOINS 并留下 ORDER BY,它的速度也一样快。
  • 那很好,到时候在 LastActivityAt 上添加一个额外的索引对订单有帮助吗?
【解决方案3】:

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

其次,是否涉及触发器?会更新 LastActivityAt 字段的内容?

【讨论】:

  • 对,使用 LINQ to SQL 选择性地加载列真的很痛苦。我完全理解这是可能的,但我在以前的工作中使用过 ORM,这些工作在具有数百万行的表上使用 SELECT *。我知道这很糟糕,但我从未见过这种对性能的损害。
  • 是的,限制列有点像在黑暗中刺伤,除了玩索引之外我想不出什么。
【解决方案4】:

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

【讨论】:

  • 可能无法使用,因为 SELECT *
  • 是的,将它提高到 1 分 2 秒。
【解决方案5】:

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

鉴于您的 cmets 关于您的 Data/Log/tempDB 文件放置,我认为任何数量的调整都将是一个创可贴。

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

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

【讨论】:

  • 是的,我认为你是对的,但我可以使用 3 个单独的查询在不到 250 毫秒的时间内获取相同的数据似乎并不正确。听起来查询计划搞砸了,今晚会碰到一些 MS 的人。
  • @Chad Moran:讨厌成为悲观主义者,但我认为 MS 的人会给你同样的建议。一旦任何查询计划需要使用 tempDB,您几乎肯定会遇到瓶颈
【解决方案6】:

好的,所以我的测试机并不快。实际上它真的很慢。它是 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。调整数据库可能会得到更好的结果!

--克里斯

【讨论】:

    【解决方案7】:

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

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

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

    类别.ID Stories.CategoryID 用户ID Stories.UserID

    并且可能 Stories.LastActivityAt

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

    【讨论】:

    • @Robo:其实是 DBCC FREEPROCCACHE
    【解决方案8】:

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

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

    【讨论】:

      猜你喜欢
      • 2019-08-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多