Запрос к 250 тысячам строк занимает 53 секунды
-
03-07-2019 - |
Вопрос
Этот запрос выполняется на выделенном сервере, работающем в центре обработки данных.
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 должно быть первым.