Запрос по 250 тыс. Строк занимает 53 секунды - PullRequest
14 голосов
/ 04 марта 2009

Поле, на котором выполняется этот запрос, - это выделенный сервер, работающий в центре данных.

AMD Opteron 1354 Quad-Core 2,20 ГГц 2 ГБ ОЗУ Windows Server 2008 x64 (Да, я знаю, что у меня только 2 ГБ ОЗУ, я обновляю до 8 ГБ, когда проект будет запущен).

Итак, я прошел и создал 250 000 фиктивных строк в таблице, чтобы провести стресс-тестирование некоторых запросов, которые генерирует LINQ to SQL, и убедиться, что они не ужасные, и я заметил, что один из них занимал абсурдное количество времени. 1005 *

У меня был этот запрос до 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 секунды, загрузка ЦП колеблется на 2-3%, Membery составляет 1,1 ГБ, свободно 900 МБ, но использование диска кажется неуправляемым. Это @ 100 МБ / с, из которых 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

Планы Exec для других 3 запросов (по порядку).
http://i43.tinypic.com/30124bp.png
http://i44.tinypic.com/13yjml1.png
http://i43.tinypic.com/33ue7fb.png

Любая помощь будет принята с благодарностью.

План Exec после добавления индексов (снова до 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);

Ответы [ 8 ]

13 голосов
/ 04 марта 2009

Попробуйте добавить индекс на Stories.LastActivityAt. Я думаю, что сканирование кластерного индекса в плане выполнения может быть связано с сортировкой.

Edit: Поскольку мой запрос мгновенно возвращался со строками длиной всего в несколько байтов, но выполнялся уже 5 минут и все еще выполняется после того, как я добавил 2K varchar, я думаю, что Митч имеет смысл. Это объем этих данных, который перетасовывается даром, но это можно исправить в запросе.

Попробуйте поместить объединение, сортировку и вершину (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, он должен работать очень быстро.

3 голосов
/ 04 марта 2009

Так что, если я правильно прочитал первую часть, она ответит через 17 секунд индексом. Который еще время, чтобы выпить 10 записей. Я думаю, что время в порядке по пунктам. Я хотел бы индекс на LastActivityAt, UserID, CategoryID. Просто для удовольствия, удалите заказ и посмотрите, быстро ли он возвращает 10 записей. Если это так, то вы знаете, что его нет в соединениях с другими таблицами. Также было бы полезно заменить * на необходимые столбцы, так как все 3 столбца таблицы находятся в базе данных tempdb во время сортировки - как упоминал Нил.

Глядя на планы выполнения, вы заметите дополнительную сортировку - я полагаю, что это порядок, на который уйдет некоторое время. Я предполагаю, что у вас был индекс с 3, и он составлял 17 секунд ... так что вы можете захотеть один индекс для критериев соединения (userid, categoryID) и другой для lastactivityat - посмотрите, работает ли он лучше. Также было бы хорошо выполнить запрос с помощью мастера настройки индекса.

1 голос
/ 04 марта 2009

Хорошо, значит, мой тестовый компьютер не быстрый. На самом деле это очень медленно. Это 1,6 ГГц, n 1 ГБ оперативной памяти, Нет нескольких дисков, только один диск (медленное чтение) для сервера SQL, ОС и дополнительные функции.

Я создал ваши таблицы с определением первичного и внешнего ключей. Вставлено 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. Наличие индекса в этом столбце, безусловно, улучшит производительность заказа.

Кроме того, определение ваших первичных и внешних ключей помогает SQL Server безмерно

Ваш метод, который указан в вашем коде, элегантен и в основном тот же ответ, который написал cdonner, кроме как на c #, а не в sql. Настройка БД, вероятно, даст еще лучшие результаты!

- Kris

1 голос
/ 04 марта 2009

Вы максимально используете диски в настройках оборудования.

Учитывая ваши комментарии по поводу вашего размещения в файле Data / Log / tempDB, я думаю, что любое количество настроек будет бандой.

250000 строк мало. Представьте, насколько серьезными будут ваши проблемы с 10 миллионами строк.

Я предлагаю вам переместить tempDB на свой физический диск (предпочтительно RAID 0).

1 голос
/ 04 марта 2009

Исходя из вашего проблемного запроса, попробуйте добавить индекс комбинации в таблицу Stories (CategoryID, UserID, LastActivityAt)

1 голос
/ 04 марта 2009

Мое первое предложение - удалить * и заменить его минимальным необходимым количеством столбцов.

секунда, задействован ли триггер? Что-то, что обновит поле LastActivityAt?

0 голосов
/ 04 марта 2009

Если вы поработали с SQL Server в течение некоторого времени, вы обнаружите, что даже самые маленькие изменения в запросе могут привести к совершенно разному времени отклика. Из того, что я прочитал в первоначальном вопросе и, глядя на план запроса, я подозреваю, что оптимизатор решил, что лучший подход - сформировать частичный результат, а затем отсортировать его как отдельный шаг. Частичным результатом является составная часть таблиц Users и Stories. Это формируется в tempdb. Таким образом, избыточный доступ к диску связан с формированием и последующей сортировкой этой временной таблицы.

Я согласен с тем, что решение должно заключаться в создании составного индекса для Stories.LastActivityAt, Stories.UserId, Stories.CategoryId. Порядок ОЧЕНЬ важен, поле LastActivityAt должно быть первым.

0 голосов
/ 04 марта 2009

Вы очистили кэш SQL Server перед выполнением каждого запроса?

В SQL 2000 это что-то вроде DBCC DROPCLEANBUFFERS. Google команда для получения дополнительной информации.

Глядя на запрос, у меня был бы индекс для

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

и возможно Stories.LastActivityAt

Но да, похоже, что результатом может быть фиктивный случай кэширования.

...