Является ли правило WHERE-JOIN-ORDER- (SELECT) для порядка столбцов индекса неправильным?

9

Я пытаюсь улучшить этот (под) запрос, являясь частью более крупного запроса:

select SUM(isnull(IP.Q, 0)) as Q, 
        IP.OPID 
    from IP
        inner join I
        on I.ID = IP.IID
    where 
        IP.Deleted=0 and
        (I.Status > 0 AND I.Status <= 19) 
    group by IP.OPID

Sentry Plan Explorer указал на несколько относительно дорогих поисков ключей для таблицы dbo. [I], выполненных по запросу выше.

Таблица dbo.I

    CREATE TABLE [dbo].[I] (
  [ID]  UNIQUEIDENTIFIER NOT NULL,
  [OID]  UNIQUEIDENTIFIER NOT NULL,
  []  UNIQUEIDENTIFIER NOT NULL,
  []  UNIQUEIDENTIFIER NOT NULL,
  [] UNIQUEIDENTIFIER NULL,
  []  UNIQUEIDENTIFIER NOT NULL,
  []  CHAR (3) NOT NULL,
  []  CHAR (3)  DEFAULT ('EUR') NOT NULL,
  []  DECIMAL (18, 8) DEFAULT ((1)) NOT NULL,
  [] CHAR (10)  NOT NULL,
  []  DECIMAL (18, 8) DEFAULT ((1)) NOT NULL,
  []  DATETIME  DEFAULT (getdate()) NOT NULL,
  []  VARCHAR (35) NULL,
  [] NVARCHAR (100) NOT NULL,
  []  NVARCHAR (100) NULL,
  []  NTEXT  NULL,
  []  NTEXT  NULL,
  []  NTEXT  NULL,
  []  NTEXT  NULL,
  [Status]  INT DEFAULT ((0)) NOT NULL,
  []  DECIMAL (18, 2)  NOT NULL,
  [] DECIMAL (18, 2)  NOT NULL,
  [] DECIMAL (18, 2)  NOT NULL,
  [] DATETIME DEFAULT (getdate()) NULL,
  []  DATETIME NULL,
  []  NTEXT  NULL,
  []  NTEXT  NULL,
  [] TINYINT  DEFAULT ((0)) NOT NULL,
  []  DATETIME NULL,
  []  VARCHAR (50) NULL,
  []  DATETIME  DEFAULT (getdate()) NOT NULL,
  []  VARCHAR (50) NOT NULL,
  []  DATETIME NULL,
  []  VARCHAR (50) NULL,
  []  ROWVERSION NOT NULL,
  []  DATETIME NULL,
  []  INT  NULL,
  [] TINYINT DEFAULT ((0)) NOT NULL,
  [] UNIQUEIDENTIFIER NULL,
  []  TINYINT DEFAULT ((0)) NOT NULL,
  []  TINYINT  DEFAULT ((0)) NOT NULL,
  [] NVARCHAR (50)  NULL,
  [] TINYINT DEFAULT ((0)) NOT NULL,
  []  UNIQUEIDENTIFIER NULL,
  [] UNIQUEIDENTIFIER NULL,
  []  TINYINT  DEFAULT ((0)) NOT NULL,
  []  TINYINT DEFAULT ((0)) NOT NULL,
  []  UNIQUEIDENTIFIER NULL,
  []  DECIMAL (18, 2)  NULL,
  []  DECIMAL (18, 2)  NULL,
  [] DECIMAL (18, 2)  DEFAULT ((0)) NOT NULL,
  [] UNIQUEIDENTIFIER NULL,
  [] DATETIME NULL,
  [] DATETIME NULL,
  []  VARCHAR (35) NULL,
  [] DECIMAL (18, 2)  DEFAULT ((0)) NOT NULL,
  CONSTRAINT [PK_I] PRIMARY KEY NONCLUSTERED ([ID] ASC) WITH (FILLFACTOR = 90),
  CONSTRAINT [FK_I_O] FOREIGN KEY ([OID]) REFERENCES [dbo].[O] ([ID]),
  CONSTRAINT [FK_I_Status] FOREIGN KEY ([Status]) REFERENCES [dbo].[T_Status] ([Status])
);                  


GO
CREATE CLUSTERED INDEX [CIX_Invoice]
  ON [dbo].[I]([OID] ASC) WITH (FILLFACTOR = 90);

Таблица dbo.IP

CREATE TABLE [dbo].[IP] (
 [ID] UNIQUEIDENTIFIER DEFAULT (newid()) NOT NULL,
 [IID] UNIQUEIDENTIFIER NOT NULL,
 [OID] UNIQUEIDENTIFIER NOT NULL,
 [Deleted] TINYINT DEFAULT ((0)) NOT NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 []UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] INT NOT NULL,
 [] VARCHAR (35) NULL,
 [] NVARCHAR (100) NOT NULL,
 [] NTEXT NULL,
 [] DECIMAL (18, 4) DEFAULT ((0)) NOT NULL,
 [] NTEXT NULL,
 [] NTEXT NULL,
 [] DECIMAL (18, 4) DEFAULT ((0)) NOT NULL,
 [] DECIMAL (18, 4) DEFAULT ((0)) NOT NULL,
 [] DECIMAL (4, 2) NOT NULL,
 [] INT DEFAULT ((1)) NOT NULL,
 [] DATETIME DEFAULT (getdate()) NOT NULL,
 [] VARCHAR (50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
 [] DATETIME NULL,
 [] VARCHAR (50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
 [] ROWVERSION NOT NULL,
 [] INT DEFAULT ((1)) NOT NULL,
 [] DATETIME NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] DECIMAL (18, 4) DEFAULT ((1)) NOT NULL,
 [] DECIMAL (18, 4) DEFAULT ((1)) NOT NULL,
 [] INT DEFAULT ((0)) NOT NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 []UNIQUEIDENTIFIER NULL,
 []NVARCHAR (35) NULL,
 [] VARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] NVARCHAR (35) NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] VARCHAR (12) NULL,
 [] VARCHAR (4) NULL,
 [] NVARCHAR (50) NULL,
 [] NVARCHAR (50) NULL,
 [] VARCHAR (35) NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] TINYINT DEFAULT ((0)) NOT NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] TINYINT DEFAULT ((0)) NOT NULL,
 [] TINYINT DEFAULT ((0)) NOT NULL,
 [] NVARCHAR (50) NULL,
 [] TINYINT DEFAULT ((0)) NOT NULL,
 [] DECIMAL (18, 2) NULL,
 []TINYINT DEFAULT ((1)) NOT NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] UNIQUEIDENTIFIER NULL,
 [] TINYINT DEFAULT ((1)) NOT NULL,
 CONSTRAINT [PK_IP] PRIMARY KEY NONCLUSTERED ([ID] ASC) WITH (FILLFACTOR = 90),
 CONSTRAINT [FK_IP_I] FOREIGN KEY ([IID]) REFERENCES [dbo].[I] ([ID]) ON DELETE CASCADE NOT FOR REPLICATION,
 CONSTRAINT [FK_IP_XType] FOREIGN KEY ([XType]) REFERENCES [dbo].[xTYPE] ([Value]) NOT FOR REPLICATION
);

GO
CREATE CLUSTERED INDEX [IX_IP_CLUST]
 ON [dbo].[IP]([IID] ASC) WITH (FILLFACTOR = 90);

Таблица «I» имеет около 100 000 строк, кластерный индекс - 9 386 страниц.
Таблица IP является дочерней таблицей I и имеет около 175 000 строк.

Я попытался добавить новый индекс, следуя правилу порядка столбцов индекса: "WHERE-JOIN-ORDER- (SELECT)"

https://www.mssqltips.com/sqlservertutorial/3208/use-where-join-orderby-select-column-order-when-creating-indexes/

чтобы обратиться к ключевым поискам и создать поиск по индексу:

CREATE NONCLUSTERED INDEX [IX_I_Status_1]
    ON [dbo].[Invoice]([Status], [ID])

Извлеченный запрос сразу использовал этот индекс. Но оригинальный большой запрос, частью которого он является, не сделал. Он даже не использовал его, когда я заставил его использовать WITH (INDEX (IX_I_Status_1)).

Через некоторое время я решил попробовать другой новый индекс и изменил порядок индексированных столбцов:

CREATE NONCLUSTERED INDEX [IX_I_Status_2]
    ON [dbo].[Invoice]([ID], [Status])

WOHA! Этот индекс использовался извлеченным запросом, а также большим запросом!

Затем я сравнил статистику ввода-вывода извлеченных запросов, заставив ее использовать [IX_I_Status_1] и [IX_I_Status_2]:

Результаты [IX_I_Status_1]:

Table 'I'. Scan count 5, logical reads 636, physical reads 16, read-ahead reads 574
Table 'IP'. Scan count 5, logical reads 1134, physical reads 11, read-ahead reads 1040
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0

Результаты [IX_I_Status_2]:

Table 'I'. Scan count 1, logical reads 615, physical reads 6, read-ahead reads 631
Table 'IP'. Scan count 1, logical reads 1024, physical reads 5, read-ahead reads 1040
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0,  read-ahead reads 0

Хорошо, я мог понять, что запрос мега-большого монстра может быть слишком сложным, чтобы SQL-сервер улавливал идеальный план выполнения, и может пропустить мой новый индекс. Но я не понимаю, почему индекс [IX_I_Status_2] кажется более подходящим и более эффективным для запроса.

Поскольку запрос прежде всего фильтрует таблицу I по столбцу STATUS, а затем соединяется с таблицей IP, я не понимаю, почему [IX_I_Status_2] лучше и используется Sql Server вместо [IX_I_Status_1]?

Magier
источник
Да, он использует этот индекс в случае соответствия критериям фильтра. Он выполняет сканирование индекса (так же, как с IX_I_Status_2) и по сравнению с этим сохраняет 1 физическое чтение. но мне пришлось «включить (статус)» в этот индекс, потому что статус находится в выходных данных и снова был просмотрен раньше.
Magier
Забавное примечание: после того, как я теперь применил к лучшему индексу, я смог выяснить ([IX_I_Status_2]) и снова выполнить запрос, теперь я получаю предложение об отсутствующем индексе: CREATE NONCLUSTERED INDEX [<Имя отсутствующего индекса, sysname,>] ON [ dbo]. [I] ([Status]) INCLUDE ([ID]) Это плохое предложение и снижает производительность запроса. Сервер TY Sql :)
Magier

Ответы:

19

Является ли правило WHERE-JOIN-ORDER- (SELECT) для порядка столбцов индекса неправильным?

По крайней мере, это неполный и потенциально вводящий в заблуждение совет (я не удосужился прочитать всю статью). Если вы собираетесь читать материалы в Интернете (включая это), вам следует скорректировать уровень доверия в соответствии с тем, насколько хорошо вы уже знаете и доверяете автору, но всегда проверяйте это для себя.

Существует несколько практических правил для создания индексов, в зависимости от конкретного сценария, но ни один из них не является хорошей заменой для понимания основных проблем для себя. Ознакомьтесь с реализацией индексов и операторов плана выполнения в SQL Server, пройдитесь по некоторым упражнениям и получите хорошее представление о том, как индексы можно использовать для повышения эффективности планов выполнения. Нет эффективного пути к получению этих знаний и опыта.

В целом, я могу сказать, что в ваших индексах чаще всего должны быть столбцы, используемые в первую очередь для тестов на равенство, с последними неравенствами и / или предоставленными фильтром индекса. Это не полный оператор, потому что индексы могут также обеспечивать порядок, который может быть более полезным, чем прямой поиск одного или нескольких ключей в некоторых ситуациях. Например, упорядочение можно использовать, чтобы избежать сортировки, снизить стоимость такого физического соединения, как объединение слиянием, включить агрегат потока, быстро найти первые несколько подходящих строк ... и так далее.

Здесь я немного расплывчат, потому что выбор идеального индекса (ов) для запроса зависит от многих факторов - это очень широкая тема.

В любом случае, нет ничего необычного в том, чтобы находить противоречивые сигналы для «лучших» индексов в запросе. Например, ваш предикат объединения хотел бы, чтобы строки были упорядочены одним способом для объединения слиянием, группа по хотела бы, чтобы строки были отсортированы другим способом для агрегата потока, а поиск подходящих строк с использованием предикатов предложения where предложил бы другие индексы.

Причиной индексации является искусство и наука, потому что идеальная комбинация не всегда логически возможна. Выбор лучших компромиссных индексов для рабочей нагрузки (а не просто одного запроса) требует аналитических навыков, опыта и специфических системных знаний. Если бы это было просто , автоматизированные инструменты были бы идеальными, а консультанты по настройке производительности были бы гораздо менее востребованы.

Что касается отсутствующих указателей индекса: они являются оппортунистическими. Оптимизатор привлекает их внимание, когда пытается сопоставить предикаты и требуемый порядок сортировки с несуществующим индексом. Поэтому предложения основаны на конкретных попытках сопоставления в конкретном контексте конкретного варианта подплана, который он рассматривал в то время.

В контексте, предложения всегда имеют смысл, с точки зрения снижения предполагаемой стоимости доступа к данным, в соответствии с моделью оптимизатора. Он не выполняет более широкий анализ запроса в целом (тем более, более широкую рабочую нагрузку), поэтому вы должны рассматривать эти предложения как нежный намек на то, что квалифицированному специалисту необходимо взглянуть на доступные индексы, с предложениями в качестве отправной точки. точка (и обычно не более того).

В вашем случае (Status) INCLUDE (ID)предложение, вероятно, появилось, когда он рассматривал возможность хеш-соединения или объединения слиянием (пример позже). В этом узком контексте предложение имеет смысл. Для запроса в целом, возможно, нет. Индекс (ID, Status)включает соединение с вложенным циклом IDв качестве внешней ссылки: поиск по равенству IDи неравенство по Statusкаждой итерации.

Один из возможных вариантов выбора индексов:

CREATE INDEX i1 ON dbo.I (ID, [Status]);
CREATE INDEX i1 ON dbo.IP (Deleted, OPID, IID) INCLUDE (Q);

... который производит план как:

Возможный план

Я не говорю, что эти показатели оптимальны для вас; они работают над тем, чтобы создать для меня разумный план, не имея возможности увидеть статистику по задействованным таблицам или полные определения и существующую индексацию. Кроме того, я ничего не знаю о более широкой рабочей нагрузке или реальном запросе.

В качестве альтернативы (просто чтобы показать одну из множества дополнительных возможностей):

CREATE INDEX i1 ON dbo.I ([Status]) INCLUDE (ID);
CREATE INDEX i1 ON dbo.IP (Deleted, IID, OPID) INCLUDE (Q);

дает:

Альтернативный план

Планы выполнения были сгенерированы с использованием SQL Sentry Plan Explorer .

Пол Уайт 9
источник