53秒かかる250,000行に対するクエリ
-
03-07-2019 - |
質問
このクエリが実行されているボックスは、データセンターで実行されている専用サーバーです。
AMD Opteron 1354クアッドコア2.20GHz 2GBのRAM Windows Server 2008 x64(はい、RAMが2GBしかないことは知っています。プロジェクトがライブになったら8GBにアップグレードしています)。
だから、テーブルに250,000のダミー行を作成して、LINQ to SQLが生成するいくつかのクエリのストレステストを実際に行い、それらがひどくならないことを確認し、そのうちの1つがとてつもなく時間がかかっていることに気付きました
インデックスを使用してこのクエリを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);
解決
Stories.LastActivityAtにインデックスを追加してみてください。実行計画のクラスター化インデックススキャンは、ソートが原因であると考えられます。
編集: 私のクエリは数バイトの長さの行で瞬時に返されましたが、すでに5分間実行されており、2K varcharを追加した後もまだ続いているので、Mitchにはポイントがあると思います。シャッフルされるのはそのデータの量であり、これはクエリで修正できます。
結合、並べ替え、および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にインデックスがある場合、これは非常に高速に実行されます。
他のヒント
最初の部分を正しく読んだ場合、インデックスで17秒以内に応答します。まだ10枚のレコードを探し出すにはまだ時間がかかります。私は、時間は句による順序であると考えています。 LastActivityAt、UserID、CategoryIDのインデックスが必要です。楽しみのために、注文を削除し、10個のレコードがすぐに返されるかどうかを確認します。存在する場合は、他のテーブルへの結合にないことがわかります。また、Neilが述べたように、3つのテーブル列すべてがソート中にtempdbにあるため、*を必要な列に置き換えると役立ちます。
実行計画を見ると、余分な並べ替えに気づくでしょう-それは時間がかかる順序だと思います。私はあなたが3のインデックスを持っていて17秒だったと仮定しています...したがって、結合基準(userid、categoryID)に1つのインデックスが必要であり、lastactivityatに別のインデックスが必要な場合があります-また、インデックスチューニングウィザードを使用してクエリを実行することをお勧めします。
最初の提案は、*を削除し、必要な最小限の列に置き換えることです。
2番目に、トリガーが関係していますか? LastActivityAtフィールドを更新する何か
問題のクエリに基づいて、テーブル Stories
(CategoryID、UserID、LastActivityAt)に組み合わせインデックスを追加してみてください
ハードウェア設定でディスクを最大化します。
Data / Log / tempDB Fileの配置についてのコメントをいただければ、どんな量のチューニングもバンデイドになると思います。
250,000行は小さいです。 1000万行で問題がどれほどひどくなるか想像してみてください。
tempDBを独自の物理ドライブ(RAID 0が望ましい)に移動することをお勧めします。
わかりました。テストマシンは高速ではありません。実際、本当に遅いです。 1.6 ghz、n 1 GBのRAM、複数のディスクなし、SQLサーバー、OS、およびその他のための単一(読み取りが遅い)ディスク。
主キーと外部キーが定義されたテーブルを作成しました。 2つのカテゴリ、500人のランダムユーザー、250000のランダムストーリーを挿入しました。
上記の最初のクエリの実行には16秒かかります(プランキャッシュもありません)。 LastActivityAt列にインデックスを付けると、1秒以内に結果が得られます(ここにはプランキャッシュもありません)。
これをすべて行うために使用したスクリプトを次に示します。
--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がsqlではなくc#で記述したのと同じ応答です。データベースをチューニングすると、おそらくさらに良い結果が得られます!
-クリス
各クエリを実行する前に、SQL Serverキャッシュをクリアしましたか?
SQL 2000では、DBCC DROPCLEANBUFFERSのようなものです。 Googleで詳細を確認してください。
クエリを見ると、インデックスがあります
Categories.ID Stories.CategoryID Users.ID Stories.UserID
そして場合によっては Stories.LastActivityAt
しかし、結果は偽の「キャッシュ」だと思われます。
SQL Serverをしばらく使用していると、クエリへのわずかな変更でも、応答時間が大幅に異なる可能性があることがわかります。最初の質問で読んだことと、クエリプランを見ると、オプティマイザーは、最良のアプローチは部分的な結果を形成し、それを別のステップとしてソートすることであると判断したと思われます。部分的な結果は、UsersテーブルとStoriesテーブルの複合です。これはtempdbで形成されます。したがって、過度のディスクアクセスは、この一時テーブルの形成と並べ替えによるものです。
私は、ソリューションがStories.LastActivityAt、Stories.UserId、Stories.CategoryIdに複合インデックスを作成することであることに同意します。順序は非常に重要であり、LastActivityAtフィールドが最初でなければなりません。