Сортировка разливов в tempdb из-за varchar (max)

10

На сервере с 32 ГБ мы используем SQL Server 2014 SP2 с максимальной памятью 25 ГБ, у нас есть две таблицы, здесь вы найдете упрощенную структуру обеих таблиц:

CREATE TABLE [dbo].[Settings](
    [id] [int] IDENTITY(1,1) NOT NULL,
    [resourceId] [int] NULL,
    [typeID] [int] NULL,
    [remark] [varchar](max) NULL,
    CONSTRAINT [PK_Settings] PRIMARY KEY CLUSTERED ([id] ASC)
) ON [PRIMARY]
GO

CREATE TABLE [dbo].[Resources](
    [id] [int] IDENTITY(1,1) NOT NULL,
    [resourceUID] [int] NULL,
 CONSTRAINT [PK_Resources] PRIMARY KEY CLUSTERED ([id] ASC)
) ON [PRIMARY]
GO

со следующими некластеризованными индексами:

CREATE NONCLUSTERED INDEX [IX_UID] ON [dbo].[Resources]
(
    [resourceUID] ASC
)

CREATE NONCLUSTERED INDEX [IX_Test] ON [dbo].[Settings]
(
    [resourceId] ASC,
    [typeID] ASC
)

База данных настроена на compatibility level120.

Когда я запускаю этот запрос, есть разливы tempdb. Вот как я выполняю запрос:

exec sp_executesql N'
select r.id,remark
FROM Resources r
inner join Settings on resourceid=r.id
where resourceUID=@UID
ORDER BY typeID',
N'@UID int',
@UID=38

Если не выбрать [remark]поле, разливов не происходит. Моей первой реакцией было то, что разливы произошли из-за небольшого числа предполагаемых строк в операторе вложенного цикла.

Поэтому я добавляю 5 столбцов даты и времени и 5 целочисленных столбцов в таблицу настроек и добавляю их в свой оператор выбора. Когда я выполняю запрос, никаких разливов не происходит.

Почему разливы происходят только тогда, когда [remark]выбрано? Это, вероятно, как-то связано с тем, что это varchar(max). Что я могу сделать, чтобы избежать проливания tempdb?

Добавление OPTION (RECOMPILE)к запросу не имеет значения.

Фредерик Вандерхаген
источник
Может быть, вы можете попробовать select r.id, LEFT(remark, 512)(или любой разумной длины подстроки может быть).
mustaccio
@Forrest: я пытаюсь воспроизвести данные, необходимые для моделирования проблемы. На первый взгляд это связано с низкой оценкой вложенного цикла. По моим фиктивным данным, предполагаемое количество строк намного выше, и
Frederik

Ответы:

10

Здесь будет несколько возможных обходных путей.

Вы можете вручную настроить выделение памяти, хотя я бы, вероятно, не пошел по этому пути.

Вы также можете использовать CTE и TOP, чтобы переместить сортировку ниже, прежде чем захватывать столбец максимальной длины. Это будет выглядеть примерно так:

WITH CTE AS (
SELECT TOP 1000000000 r.ID, s.ID AS ID2, s.typeID
FROM Resources r
inner join Settings s on resourceid=r.id
where resourceUID=@UID
ORDER BY s.typeID
)
SELECT c.ID, ca.remark
FROM CTE c
CROSS APPLY (SELECT remark FROM dbo.Settings s WHERE s.id = c.ID2) ca(remark)
ORDER BY c.typeID

Подтверждение концепции dbfiddle здесь . Пример данных все равно будет оценен!

Если вы хотите прочитать отличный анализ Пола Уайта, читайте здесь.

Форрест
источник
7

Почему разливы происходят только тогда, когда выбрано [замечание]?

Разлив происходит, когда вы включаете этот столбец, потому что у вас недостаточно большой объем памяти для сортировки больших строковых данных.

Вы не получаете достаточно большой доступ к памяти, потому что фактическое количество строк в 10 раз больше, чем предполагаемое количество строк (1302 фактических против 126 предполагаемых).

Почему оценка выключена? Почему SQL Server считает, что в dbo есть только одна строка с resourceid38?

Это может быть проблема статистики, которую вы можете проверить, запустив DBCC SHOW_STATISTICS('dbo.Settings', 'IX_Test')и посмотрев счетчик для этого шага гистограммы. Но план выполнения, кажется, указывает на то, что статистика является настолько полной и актуальной, насколько это возможно.

Поскольку статистика не помогает, ваша лучшая ставка - это, вероятно, переписывание запроса, о котором Форрест рассказал в своем ответе.

Джош Дарнелл
источник
3

Мне кажется, что whereпредложение в запросе дает проблему, и является причиной низких оценок, даже если OPTION(RECOMPILE)используется.

Я создал несколько тестовых данных и в конце концов предложил два решения, сохраняя IDполе resourcesв переменной (если оно всегда уникально) или во временной таблице, если у нас может быть больше одного ID.

База тестовых записей

SET NOCOUNT ON
DECLARE @i int= 1;
WHILE @i <= 10000
BEGIN
INSERT INTO [dbo].[Settings]([resourceId],[typeID],remark)
VALUES(@i,@i,'KEPT THESE VALUES OUT BECAUSE IT WOULD CLUTTER THE EXAMPLES, VALUES OVER 8000 Chars entered here'); -- 23254 character length on each value
INSERT INTO  [dbo].[Resources](resourceUID)
VALUES(@i);
SET @i += 1;
END

Вставьте значения «Поиск», чтобы получить тот же приблизительный набор результатов, что и OP (1300 записей)

INSERT INTO  [dbo].[Settings]([resourceId],[typeID],remark)
VALUES(38,38,'KEPT THESE VALUES OUT BECAUSE IT WOULD CLUTTER THE EXAMPLES, VALUES OVER 8000 Chars entered here')
GO 1300

Изменить статистику для сравнения и обновления, чтобы соответствовать OP

ALTER DATABASE StackOverflow SET COMPATIBILITY_LEVEL = 120;
UPDATE STATISTICS settings WITH FULLSCAN;
UPDATE STATISTICS resources WITH FULLSCAN;

Оригинальный запрос

exec sp_executesql N'
select r.id
FROM Resources r
inner join Settings on resourceid=r.id
where resourceUID=@UID
ORDER BY typeID',
N'@UID int',
@UID=38

Мои оценки еще хуже , с одной оценочной строкой, а 1300 возвращаются. И, как указано в ОП, не имеет значения, если я добавлюOPTION(RECOMPILE)

Важно отметить, что когда мы избавляемся от предложения where, оценки оказываются на 100% правильными, что ожидается, так как мы используем все данные в обеих таблицах.

Я заставил индексы просто убедиться, что мы используем те же, что и в предыдущем запросе, чтобы доказать

exec sp_executesql N'
select r.id,remark
FROM Resources r with(index([IX_UID]))
inner join Settings WITH(INDEX([IX_Test])) 
on resourceid=r.id
ORDER BY typeID',
N'@UID int',
@UID=38

Как и следовало ожидать, хорошие оценки.

Итак, что мы можем изменить, чтобы получить более точные оценки, но при этом стремиться к нашим ценностям?

Если @UID уникален, как в примере, приведенном OP, мы можем поместить сингл id, возвращенный из, resourcesв переменную, а затем выполнить поиск по этой переменной с помощью OPTION (RECOMPILE)

DECLARE @UID int =38 , @RID int;
SELECT @RID=r.id from 
Resources r where resourceUID = @UID;

SELECT @uid, remark 
from Settings 
where resourceId = @uid 
Order by typeID
OPTION(RECOMPILE);

Что дает 100% точные оценки

Но что если в ресурсах есть несколько resourceUID?

добавить некоторые тестовые данные

INSERT INTO Resources(ResourceUID)
VALUES (38);
go 50

Это может быть решено с помощью временной таблицы

CREATE TABLE #RID (id int)
DECLARE @UID int =38 
INSERT INTO #RID
SELECT r.id 
from 
Resources r where resourceUID = @UID

SELECT @uid, remark 
from Settings  s
INNER JOIN #RID r
ON r.id =s.resourceId
Order by typeID
OPTION(RECOMPILE)

DROP TABLE #RID

Опять с точными оценками .

Это было сделано с моим собственным набором данных, YMMV.


Написано с помощью sp_executesql

С переменной

exec sp_executesql N'
DECLARE  @RID int;
    SELECT @RID=r.id from 
    Resources r where resourceUID = @UID;

    SELECT @uid, remark 
    from Settings 
    where resourceId = @uid 
    Order by typeID
    OPTION(RECOMPILE);',
N'@UID int',
@UID=38

С временной таблицей

exec sp_executesql N'

CREATE TABLE #RID (id int)

INSERT INTO #RID
SELECT r.id 
from 
Resources r where resourceUID = @UID

SELECT @uid, remark 
from Settings  s
INNER JOIN #RID r
ON r.id =s.resourceId
Order by typeID
OPTION(RECOMPILE)

DROP TABLE #RID',
N'@UID int',
@UID=38

Все еще 100% правильные оценки на моем тесте

Рэнди Вертонген
источник