2009-03-04 2 views
14

이 쿼리가 실행되는 상자는 데이터 센터에서 실행되는 전용 서버입니다.53 초가 걸리는 250k 행에 대한 쿼리

RAM 윈도우 서버 2008의 x64의 AMD 옵테론 1354 쿼드 코어 2.20GHz 2기가바이트 (네,이 프로젝트가 라이브가되면 내가 8 기가 바이트 업그레이드하는거야, 난 단지 2GB의 RAM을 알고).

그래서 나는 LINQ to SQL이 생성하는 몇 가지 쿼리를 테스트하고 그들이 끔찍한 것이 아니며 그 중 하나가 터무니없는 시간을 보냈다는 것을 확인하기 위해 테이블에 250,000 개의 더미 행을 만들었습니다.

색인을 사용하여 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 명과 카테고리 25 만개가 있으며이 쿼리를 실행하려고했습니다.

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 

질의, CPU 사용량 2 ~ 3 %로 가져갈 실행 52초 소요 Membery는 1.1GB, 900메가바이트 무료이지만 디스크의 사용은 통제 불능 보인다. @ 100MB/sec이고 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/33ue7fb.png

어떤 도움 http://i43.tinypic.com/30124bp.png
http://i44.tinypic.com/13yjml1.png
이 많이 주시면 감사하겠습니다.

인덱스 추가 후 실행 계획 (최대 17 초).
http://i39.tinypic.com/2008ytx.png

많은 도움이되는 의견을 보내 주신 모든 분들께 감사 드리며, 새로운 각도로 시도해 보았습니다. 내가 필요한 이야기를 쿼리 한 다음 별도의 쿼리를 통해 범주와 사용자를 얻고 3 가지 쿼리만으로도 250ms 만 소요되었습니다 ... 문제는 이해할 수 없지만 작동한다면 250ms 이하로 당분간은 그걸로 붙어있어. 이것을 테스트 할 때 사용한 코드는 다음과 같습니다.

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

당신이 실행 계획을 게시 할 것을 제안합니다 ... –

+0

당신의 tempDB는 어디에 있습니까? 그것은 별도의 물리적 스핀들에 있습니까? –

+0

데이터, 로그 파일은 어디에 있습니까? 별도의 물리적 드라이브에 있습니까? –

답변

13

Stories.LastActivityAt에 색인을 추가해보세요. 실행 계획에서 클러스터 된 인덱스 스캔은 정렬로 인한 것일 수 있습니다.

편집 : 내 쿼리는 몇 바이트 길이의 행이있는 순간에 반환되었지만 이미 5 분 동안 실행 중이며 2K varchar를 추가 한 후에도 계속 발생합니다. 미치가 요점을 가지고 있다고 생각합니다. 아무 것도하지 않고 섞인 데이터의 볼륨이지만 쿼리에서 수정할 수 있습니다.

뷰 또는 중첩 된 쿼리에 조인, 정렬 및 상단 (10)을 넣은 다음 스토리 테이블에 다시 조인하여 필요한 10 개의 행에 대한 나머지 데이터를 얻으십시오. 이처럼

: 당신이 LastActivityAt에 인덱스가있는 경우

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 

이 매우 빠르게 실행해야합니다.

+0

총 실행 시간 \t 25.0000 당신은 그 사람입니다! –

+0

좋은 것. 최적화가 최적화를 수행 할 수 없다는 사실에 놀랐습니다. –

+0

+1. 아이러니하게도 SQL 2008에 대한 쿼리와 관련하여 최근에 매우 비슷한 것을 한 적이 있습니다.하지만 같은 문제 일 수는 없습니다. –

1

첫 번째 제안은 *를 제거하고 필요한 최소 열로 대체하는 것입니다.

초, 트리거가 관련되어 있습니까? LastActivityAt 필드를 업데이트하는 것이 있습니까?

+0

LINQ to SQL을 사용하면 선택적으로 열을로드하는 것이 쉽지 않습니다. 나는 그것이 가능하다는 것을 충분히 이해하지만 수백만 행이있는 테이블에서 SELECT *를 사용한 이전 작업에서 ORM을 사용했습니다. 나쁘다고 생각하지만 이런 종류의 성능 저하는 본 적이 없습니다. –

+0

예, 열을 제한하는 것은 어둠 속에서 찌르는듯한 느낌이었습니다. 그 외에는 색인을 가지고 놀지 않고 그다지 많지 않았습니다. –

1

문제 쿼리를 기반으로 테이블 Stories (카테고리 ID, 사용자 ID, LastActivityAt) 내가 처음 부분을 제대로 읽으면

+0

SELECT * –

+0

으로 인해 사용하지 못할 수도 있습니다. 예, 최대 1 분 2 초까지 부딪 혔습니다. –

3

그래서,이 인덱스 17 초 응답에 조합 인덱스를 추가하려고합니다. 어느 쪽이 10 개의 레코드를 뒤죽박죽으로 만드는 지 아직도 어느 정도입니다. 나는 시간이 by by 절에 있다고 생각하고있다.LastActivityAt, UserID, CategoryID에 대한 색인이 필요합니다. 그냥 재미로, 주문을 제거하고 10 레코드를 신속하게 반환하는지 확인하십시오. 그렇다면 다른 테이블에 조인되지 않음을 알 수 있습니다. Neil이 언급했듯이, 정렬하는 동안 tempdb에있는 모든 3 개의 테이블 열이 필요하므로 *를 필요한 열로 대체하는 것도 도움이됩니다.

실행 계획을 보면 추가 정렬을 알 수 있습니다. 나는 그 순서에 따라 시간이 좀 걸릴 것이라고 생각합니다. 나는 당신이 3을 가진 인덱스를 가지고 있다고 가정하고 17 초였습니다 ... 그래서 당신은 조인 기준 (userid, categoryID)에 대한 하나의 인덱스와 lastactivity에 대한 하나의 인덱스를 원할 수 있습니다 - 그것이 더 잘 수행되는지보십시오. 또한 인덱스 튜닝 마법사를 통해 쿼리를 실행하는 것이 좋습니다.

+0

나는 그것에 동의 할 것이다. 인덱스가 없으면 오더를 보내야합니다. 나는 실행 계획에서 그것을 보지 못한다. 기묘한. – cdonner

+0

그래, 내가 튜닝 마법사를 통해 그것을 실행하고 권장 사항을 추가 한 후 17 초 그것을 얻었다. –

+0

ORDER BY를 제거하면 거의 즉시 처리됩니다. 재미있는 것은 내가 내부 조인 중 하나를 제거하고 ORDER BY를 그대로 두는 것입니다. –

0

각 쿼리를 실행하기 전에 SQL Server 캐시를 지우셨습니까?

SQL 2000에서는 DBCC DROPCLEANBUFFERS와 비슷합니다. Google은 더 많은 정보를 제공합니다. 쿼리를 보면

, 나는

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

에 대한 인덱스 가능성이 Stories.LastActivityAt

을 가지고 있지만 그래 것 , 그 결과와 같은 소리는 캐싱의 가짜가 될 수 있습니다.

+0

@ 로보 : 실제로는 DBCC FREEPROCCACHE –

+0

아무 소용이 없다. –

1

하드웨어 설정에서 디스크를 최대한 활용하고 있습니다.

데이터/로그/tempDB 파일 배치에 대한 귀하의 의견을 감안할 때 어느 정도의 튜닝은 bandaid가 될 것이라고 생각합니다.

250,000 행이 작습니다. 문제가 1 천만 행으로 얼마나 나빠질 지 상상해보십시오.

tempDB를 자체 물리적 드라이브 (바람직하게는 RAID 0)로 옮기는 것이 좋습니다.

+0

네가 맞다고 생각하지만, 250ms 이내에 동일한 데이터를 얻기 위해 3 가지 별도의 쿼리를 사용할 수 있다고 생각하지 않는다. 쿼리 계획이 엉망이되어 오늘 밤 MS 사람들을 공격 할 것 같은데. –

+0

@Chad Moran : 비관론자는 싫지만, MS 사람들은 당신에게 똑같은 충고를 줄 것입니다. 쿼리 계획에서 tempDB를 사용해야하는 경우 거의 확실하게 병목 현상이 발생할 것입니다 –

1

제 테스트 시스템이 빠르지 않습니다. 사실 그것은 정말로 느립니다. 1.6 ghz, 1 gb 램, 다중 디스크 없음, SQL Server, 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 @[email protected]+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를하는 데 도움이 immensly

코드에 나열되어

귀하의 방법은 우아하고 cdonner는 SQL C#에서하지를 제외하고 쓴 기본적으로 같은 반응이다. db를 조정하면 더 나은 결과를 얻을 수 있습니다!

--Kris

당신이 얼마 동안 SQL Server와 함께 일한
0

, 당신은 쿼리에 아주 작은 변화가 격렬하게 서로 다른 응답 시간이 발생할 수 있음을 발견 할 것입니다. 초기 질문에서 읽은 쿼리 계획을 살펴보면 최적화 프로그램이 최선의 방법은 부분 결과를 만들어서 별도의 단계로 정렬하는 것이라고 판단한 것 같습니다. 부분적인 결과는 Users 테이블과 Stories 테이블의 합성입니다. 이것은 tempdb에서 형성됩니다. 따라서 과도한 디스크 액세스는이 임시 테이블을 형성 한 다음 정렬하기 때문입니다.

솔루션은 Stories.LastActivityAt, Stories.UserId, Stories.CategoryId에 복합 인덱스를 작성해야한다는 데 동의합니다. 순서는 매우 중요합니다. 필드 LastActivityAt가 가장 먼저 나와야합니다.

관련 문제