14

このクエリが実行されているボックスは、データセンターで実行されている専用サーバーです。

AMD Opteron 1354 クアッドコア 2.20GHz 2GB の RAM Windows Server 2008 x64 (はい、RAM が 2GB しかないことはわかっています。プロジェクトが稼働したら 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

1

さて、私のテストマシンは高速ではありません。実際、それは本当に遅いです。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が追加されます。この列にインデックスを付けると、注文のパフォーマンスが確実に向上します。

また、主キーと外部キーを定義すると、SQLServerが非常に役立ちます

コードにリストされているメソッドは洗練されており、基本的にcdonnerが記述した応答と同じですが、sqlではなくc#を除きます。dbを調整すると、おそらくさらに良い結果が得られます。

-クリス

于 2009-03-04T04:24:25.250 に答える
1

ハードウェア設定でディスクを最大限に活用しています。

データ/ログ/tempDBファイルの配置についてのコメントを考えると、どんな量の調整もバンドエイドになると思います。

250,000行は小さいです。1000万行で問題がどれほどひどくなるか想像してみてください。

tempDBを独自の物理ドライブ(RAID 0が望ましい)に移動することをお勧めします。

于 2009-03-04T03:09:19.720 に答える
1

私の最初の提案は、* を削除し、必要最小限の列に置き換えることです。

第二に、関与するトリガーはありますか?LastActivityAt フィールドを更新するものはありますか?

于 2009-03-04T02:06:52.867 に答える
1

問題のクエリに基づいて、テーブルに組み合わせインデックスを追加してみてくださいStories(CategoryID、UserID、LastActivityAt)

于 2009-03-04T02:17:30.283 に答える
0

SQL Serverをしばらく使用していると、クエリにわずかな変更を加えただけでも、応答時間が大幅に異なる可能性があることがわかります。最初の質問で読んだ内容とクエリプランを見ると、オプティマイザーは、部分的な結果を作成し、それを別のステップとして並べ替えることが最善のアプローチであると判断したと思われます。部分的な結果は、UsersテーブルとStoriesテーブルの複合です。これはtempdbで形成されます。したがって、過剰なディスクアクセスは、この一時テーブルの形成と並べ替えが原因です。

解決策は、Stories.LastActivityAt、Stories.UserId、Stories.CategoryIdに複合インデックスを作成することであることに同意します。順序は非常に重要です。フィールドLastActivityAtが最初である必要があります。

于 2009-03-04T04:35:24.070 に答える