Вопрос

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

AMD Opteron 1354 четырехъядерный 2,20 ГГц 2 ГБ RAM Windows Server 2008 X64 (да, я знаю, что у меня есть только 2 ГБ оперативной памяти, я обновлен до 8 ГБ, когда проект выйдет в эфир).

Итак, я создал 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 секунды, загрузка ЦП колеблется на уровне 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

Все три запроса выполняются практически мгновенно.

План выполнения для первого запроса.
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

Любая помощь приветствуется.

План выполнения после добавления индексов (снова до 17 секунд).
http://i39.tinypic.com/2008ytx.png

Я получил много полезных отзывов от всех и благодарю вас, я попробовал по-новому взглянуть на это.Я запрашиваю нужные мне истории, затем отдельными запросами получаю категории и пользователей, а для трех запросов это заняло всего 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 минут и продолжает работать после того, как я добавил 2-килобайтный varchar, я думаю, что Митч прав.Это объем этих данных, который бесполезно перемешивается, но это можно исправить в запросе.

Попробуйте поместить соединение, сортировку и 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 записей.Если да, то вы знаете, что его нет в соединениях с другими таблицами.Также было бы полезно заменить * необходимыми столбцами, поскольку все три столбца таблицы находятся в базе данных tempdb во время сортировки, как упомянул Нил.

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

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

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

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

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

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

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

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

Итак, моя тестовая машина не быстрая.На самом деле это очень медленно.Это 1,6 ГГц, 1 ГБ оперативной памяти. Никаких нескольких дисков, только один диск (медленное чтение) для сервера sql, операционной системы и дополнительных возможностей.

Я создал ваши таблицы с определенными первичными и внешними ключами.Добавлено 2 категории, 500 случайных пользователей и 250 000 случайных историй.

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

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

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

--Крис

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

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

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

Категории.

и, возможно, Stories.lastactivityat

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

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

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top