Tenho uma consulta que está demorando muito na minha máquina (7 minutos) para ser executada e gostaria de saber se poderia torná-la (significativamente) mais rápida:
SELECT
rec.[Id] AS RecordId,
MIN(rec.[CreationDate]) AS RecordCreationDate,
MIN(rec.[LastModified]) AS RecordLastModified,
MIN(rec.[AssetType]) AS RecordAssetType,
MIN(rec.[MasterFilename]) AS RecordMasterFilename,
MIN(rec.[GameName]) AS RecordGameName,
usr.[OrganizationName],
COUNT(hist.[Id]) AS TimesDownloaded
FROM
(
SELECT
innerRec.Id,
MIN(innerRec.CreationDate) AS CreationDate,
MIN(innerRec.LastModified) AS LastModified,
MIN(innerRec.AssetType) AS AssetType,
MIN(innerRec.MasterFilename) AS MasterFilename,
MIN(innerRec.GameName) AS GameName
FROM
[dbo].[Record] innerRec INNER JOIN [dbo].[RecordClassificationLink] innerLnk ON innerRec.Id = innerLnk.RecordId
-- WHERE (classification ID is foo or bar)
GROUP BY
innerRec.Id
-- HAVING COUNT(innerLnk.ClassificationId) = (number of specified classifications)
) rec
CROSS JOIN
[dbo].[AdamUser] usr
LEFT JOIN
(SELECT * FROM [dbo].[MaintenanceJobHistory] WHERE [CreatedOn] > '2016-01-01 00:00:00' AND [CreatedOn] < '2016-12-01 00:00:00') hist ON usr.Name = hist.AccessingUser AND rec.Id = hist.RecordId
GROUP BY
rec.Id, usr.OrganizationName
O que está fazendo é extrair dados para serem colocados em um relatório de planilha do Excel (se uma planilha é uma boa apresentação desses dados está fora do escopo desta questão :-))
A primeira subconsulta extrai registros opcionalmente filtrados por uma lista de IDs de classificação. Estes são, então, cruzados com a tabela de usuários, porque cada linha da tabela de usuários realmente contém as informações que realmente precisamos para isso: o nome da organização do usuário. Em seguida, deixei a tabela de histórico de trabalho de manutenção (armazenando uma entrada para cada download de registro) para criar várias linhas se um registro foi acessado várias vezes e, em seguida, agrupe por ID de registro e nome da organização para obter um "número de downloads de registro por organização" contam como TimesDownloaded
.
O código que está lendo essa saÃda preenche uma matriz associativa cuja chave é OrganizationName
e cujo valor é TimesDownloaded
, criando o equivalente a uma dinâmica PIVOT
em que cada linha de registro contém uma coluna por organização, cada uma contendo a contagem do número de downloads de registro.
Como você pode imaginar, isso é executado muito lentamente em um grande conjunto de dados, como eu disse acima; aquele com o qual estou trabalhando tem ~ 38.000 Record
s e ~ 1.000 usuários, o que significa que a junção cruzada resulta em ~ 38.000.000 linhas, mas parece conceitualmente necessário.
Isso pode ser significativamente mais eficiente? Seria melhor se eu fizesse o PIVOT
SQL dinâmico?
O DBMS que estou usando é o SQL Server 2014.
Aqui estão as definições de esquema para as tabelas:
CREATE TABLE [dbo].[AdamUser](
[Id] [uniqueidentifier] NOT NULL,
[Name] [nvarchar](200) NOT NULL,
[UserGroupName] [nvarchar](50) NOT NULL,
[OrganizationName] [nvarchar](50) NOT NULL,
PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
CREATE UNIQUE NONCLUSTERED INDEX [UIX_AdamUser_Name] ON [dbo].[AdamUser]
(
[Name] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
GO
CREATE TABLE [dbo].[MaintenanceJobHistory](
[Id] [uniqueidentifier] NOT NULL,
[Data] [xml] NOT NULL,
[CreatedOn] [datetime] NOT NULL,
[Type] [nvarchar](512) NOT NULL,
[RecordId] [uniqueidentifier] NOT NULL,
[AccessingUser] [nvarchar](200) NOT NULL,
CONSTRAINT [PK_MaintenanceJobHistory] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
CREATE NONCLUSTERED INDEX [IX_MaintenanceJobHistory_CreatedOn] ON [dbo].[MaintenanceJobHistory]
(
[CreatedOn] ASC
)
INCLUDE ( [Id],
[RecordId],
[AccessingUser]) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, SORT_IN_TEMPDB = OFF, DROP_EXISTING = OFF, ONLINE = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
GO
CREATE TABLE [dbo].[Record](
[Id] [uniqueidentifier] NOT NULL,
[CreationDate] [datetime] NOT NULL,
[LastModified] [datetime] NOT NULL,
[AssetType] [nvarchar](max) NULL,
[MasterFilename] [nvarchar](max) NULL,
[GameName] [nvarchar](max) NULL,
CONSTRAINT [PK_Record] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]
GO
CREATE TABLE [dbo].[RecordClassificationLink](
[Id] [uniqueidentifier] NOT NULL,
[RecordId] [uniqueidentifier] NOT NULL,
[ClassificationId] [uniqueidentifier] NOT NULL,
CONSTRAINT [PK_RecordClassificationLink] PRIMARY KEY CLUSTERED
(
[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
GO
Aqui está o plano de execução: https://www.brentozar.com/pastetheplan/?id=Sy6LlXDXg
No entanto, essa saÃda é transformada em uma planilha do Excel de acordo com as linhas a seguir pelo programa de chamada (portanto, é como uma PIVOT
operação):
.----------------------------------------------------------------------.
| Filename | Creation Date | #times downloaded by: CompanyA | CompanyB | ...
| foo.png | 1/2/3 | 0 | 2 |
| bar.png | 1/3/4 | 3 | 1 |
...
ATUALIZAR:
Acabei tornando as coisas significativamente mais eficientes movendo a PIVOT
operação para a própria consulta SQL; dessa forma, o SQL Server só precisa gerar o número de linhas na Record
tabela, em vez do multiplicado pelo número de organizações (não é tão ruim até chegar a centenas de organizações, ponto em que é um número enorme). A operação ainda leva alguns minutos, mas é bem mais suportável. Aqui está a consulta que finalmente decidi usar:
SELECT *
FROM (
SELECT
rec.[Id] AS RecordId,
'Org_' + usr.[OrganizationName] AS OrganizationNamePrefixed,
COUNT(hist.[Id]) AS TimesDownloaded -- To be aggregated by PIVOT
FROM (
SELECT
innerRec.[Id]
FROM
[dbo].[Record] innerRec
INNER JOIN
[dbo].[RecordClassificationLink] innerLnk ON innerLnk.[RecordId] = innerRec.[Id]
-- WHERE (classification ID is foo or bar), for optional classification filtering
GROUP BY
innerRec.[Id]
-- HAVING COUNT(innerLnk.ClassificationId) = (number of specified classifications), for optional classification filtering
) rec
CROSS JOIN [dbo].[AdamUser] usr
LEFT JOIN (
SELECT * FROM [dbo].[MaintenanceJobHistory] WHERE [CreatedOn] > 'eg. 2016-01-01 12:00:00' AND [CreatedOn] < 'eg. 2016-12-01 12:00:00'
) hist ON hist.[AccessingUser] = usr.[Name] AND hist.[RecordId] = rec.[Id]
GROUP BY
rec.[Id], usr.[OrganizationName]
) srcTable
PIVOT -- Pivot around columns outside aggregation fn, eg. heading column [OrganizationNamePrefixed] & all other columns: [RecordId]
(
MIN(srcTable.[TimesDownloaded]) FOR [OrganizationNamePrefixed] IN (...list of ~200 columns dynamically generated...)
) pivotTable
INNER JOIN [dbo].[Record] outerRec ON outerRec.[Id] = pivotTable.[RecordId]
Adicionei vários Ãndices e também tornei o PIVOT
mais eficiente possÃvel selecionando apenas a coluna de agregação, a coluna de tÃtulos e as outras colunas necessárias para girar. Por fim, refiz JOIN
a Record
tabela usando o RecordId
PK para obter as informações extras do registro por linha.
O que provavelmente está levando muito tempo é um grande número de
Sort
operações em seu plano de consulta. Você pode antecipar isso classificando os dados você mesmo, na forma de Ãndices.Aqui estão algumas sugestões de Ãndice que eu acho que ajudariam você a começar:
Em seguida, você pode modificar um pouco sua consulta para ajudar o otimizador a fazer algumas escolhas inteligentes, como agregar certos fluxos de dados antes de serem unidos e criar um produto muito maior que levará mais tempo para agregar:
Eu fiz o seguinte:
hist
subconsulta é agregada emAccessingUser, RecordId
, e eu criei um arquivoCOUNT(*) AS _count
. Essa consulta usa o novo ÃndiceIX_MaintenanceJobHistory_ByUser
para executar com muita eficiência sem nenhuma concessão de memória ou tabelas de hash.COUNT(hist.Id)
porSUM(ISNULL(hist._count, 0)) AS TimesDownloaded
dbo.RecordClassificationLink
ajuda a executar uma junção suave com aRecord
tabela, mas se você adicionar seuWHERE
eHAVING
, esse Ãndice não o ajudará.dbo.AdamUser
também melhora o desempenho eliminando um operador Sort - porque você agrega naOrganizationName
coluna, pode muito bem ter seus dados classificados desde o inÃcio.Na minha opinião, isso deve dar o mesmo resultado, mas é tarde aqui, então você mesmo terá que verificar os resultados. :)
Aqui está o meu plano de consulta:
EDIT : Você também pode simplificar
rec
um pouco a parte - provavelmente será um pouco mais fácil de ler:... e o plano parece um pouco melhor também (procure o vermelho
Lazy spool
na parte inferior do plano original, que agora se foi).