AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • Início
  • system&network
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • Início
  • system&network
    • Recentes
    • Highest score
    • tags
  • Ubuntu
    • Recentes
    • Highest score
    • tags
  • Unix
    • Recentes
    • tags
  • DBA
    • Recentes
    • tags
  • Computer
    • Recentes
    • tags
  • Coding
    • Recentes
    • tags
Início / dba / Perguntas / 157602
Accepted
Jez
Jez
Asked: 2016-12-09 10:09:11 +0800 CST2016-12-09 10:09:11 +0800 CST 2016-12-09 10:09:11 +0800 CST

Como posso tornar essa consulta de agregação mais eficiente?

  • 772

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 é OrganizationNamee cujo valor é TimesDownloaded, criando o equivalente a uma dinâmica PIVOTem 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 Records 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 PIVOTSQL 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

Saída típica: Saída típica

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 PIVOToperaçã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 PIVOToperação para a própria consulta SQL; dessa forma, o SQL Server só precisa gerar o número de linhas na Recordtabela, 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 PIVOTmais 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 JOINa Recordtabela usando o RecordIdPK para obter as informações extras do registro por linha.

optimization sql-server-2014
  • 1 1 respostas
  • 187 Views

1 respostas

  • Voted
  1. Best Answer
    Daniel Hutmacher
    2016-12-09T13:26:12+08:002016-12-09T13:26:12+08:00

    O que provavelmente está levando muito tempo é um grande número de Sortoperaçõ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:

    CREATE INDEX IX_RecordId
        ON dbo.RecordClassificationLink (RecordId);
    
    CREATE INDEX IX_MaintenanceJobHistory_ByUser
        ON dbo.MaintenanceJobHistory (AccessingUser, RecordId);
    
    CREATE UNIQUE INDEX UIX_AdamUser_OrgnName
        ON dbo.AdamUser (OrganizationName, Name);
    

    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:

    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],
        SUM(ISNULL(hist._count, 0)) 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)
    ) AS rec
    CROSS JOIN [dbo].[AdamUser] AS usr
    LEFT JOIN
        (SELECT AccessingUser, RecordId, COUNT(*) AS _count
         FROM [dbo].[MaintenanceJobHistory]
         WHERE [CreatedOn] > '2016-01-01 00:00:00' AND
               [CreatedOn] < '2016-12-01 00:00:00'
         GROUP BY AccessingUser, RecordId
        ) AS hist ON
            usr.Name = hist.AccessingUser AND
            rec.Id = hist.RecordId
    GROUP BY
        rec.Id, usr.OrganizationName;
    

    Eu fiz o seguinte:

    • a histsubconsulta é agregada em AccessingUser, RecordId, e eu criei um arquivo COUNT(*) AS _count. Essa consulta usa o novo índice IX_MaintenanceJobHistory_ByUserpara executar com muita eficiência sem nenhuma concessão de memória ou tabelas de hash.
    • Por causa disso, eu substituí COUNT(hist.Id)porSUM(ISNULL(hist._count, 0)) AS TimesDownloaded
    • O novo índice on dbo.RecordClassificationLinkajuda a executar uma junção suave com a Recordtabela, mas se você adicionar seu WHEREe HAVING, esse índice não o ajudará.
    • O novo índice on dbo.AdamUsertambém melhora o desempenho eliminando um operador Sort - porque você agrega na OrganizationNamecoluna, 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: primeiro plano

    EDIT : Você também pode simplificar recum pouco a parte - provavelmente será um pouco mais fácil de ler:

    SELECT
        rec.[Id] AS RecordId,
        rec.[CreationDate] AS RecordCreationDate,
        rec.[LastModified] AS RecordLastModified,
        rec.[AssetType] AS RecordAssetType,
        rec.[MasterFilename] AS RecordMasterFilename,
        rec.[GameName] AS RecordGameName,
        usr.[OrganizationName],
        SUM(ISNULL(hist._count, 0)) AS TimesDownloaded
    FROM
    (
        SELECT
            Id,
            CreationDate,
            LastModified,
            AssetType,
            MasterFilename,
            GameName
        FROM [dbo].[Record]
        WHERE Id IN (
            SELECT RecordId
            FROM [dbo].[RecordClassificationLink]
            --- WHERE ClassificationId=...
            )
    ) AS rec
    CROSS JOIN [dbo].[AdamUser] AS usr
    LEFT JOIN
        (SELECT AccessingUser, RecordId, COUNT(*) AS _count
         FROM [dbo].[MaintenanceJobHistory]
         WHERE [CreatedOn] > '2016-01-01 00:00:00' AND
               [CreatedOn] < '2016-12-01 00:00:00'
         GROUP BY AccessingUser, RecordId
        ) AS hist ON
            usr.Name = hist.AccessingUser AND
            rec.Id = hist.RecordId
    GROUP BY
        usr.OrganizationName,
        rec.[Id],
        rec.[CreationDate],
        rec.[LastModified],
        rec.[AssetType],
        rec.[MasterFilename],
        rec.[GameName],
        usr.[OrganizationName];
    

    ... e o plano parece um pouco melhor também (procure o vermelho Lazy spoolna parte inferior do plano original, que agora se foi).

    Novo plano

    • 3

relate perguntas

  • Como o Yelp calcula com eficiência a distância no banco de dados?

  • Otimização de consultas

  • Como devo otimizar o armazenamento para esta tabela?

  • DBMS_REDEFINITION vs EXCHANGE PARTITION no oracle

  • Existe uma boa "regra de ouro" para traduzir o custo EXPLAIN para o tempo de execução (relógio de parede)?

Sidebar

Stats

  • Perguntas 205573
  • respostas 270741
  • best respostas 135370
  • utilizador 68524
  • Highest score
  • respostas
  • Marko Smith

    conectar ao servidor PostgreSQL: FATAL: nenhuma entrada pg_hba.conf para o host

    • 12 respostas
  • Marko Smith

    Como fazer a saída do sqlplus aparecer em uma linha?

    • 3 respostas
  • Marko Smith

    Selecione qual tem data máxima ou data mais recente

    • 3 respostas
  • Marko Smith

    Como faço para listar todos os esquemas no PostgreSQL?

    • 4 respostas
  • Marko Smith

    Listar todas as colunas de uma tabela especificada

    • 5 respostas
  • Marko Smith

    Como usar o sqlplus para se conectar a um banco de dados Oracle localizado em outro host sem modificar meu próprio tnsnames.ora

    • 4 respostas
  • Marko Smith

    Como você mysqldump tabela (s) específica (s)?

    • 4 respostas
  • Marko Smith

    Listar os privilégios do banco de dados usando o psql

    • 10 respostas
  • Marko Smith

    Como inserir valores em uma tabela de uma consulta de seleção no PostgreSQL?

    • 4 respostas
  • Marko Smith

    Como faço para listar todos os bancos de dados e tabelas usando o psql?

    • 7 respostas
  • Martin Hope
    Jin conectar ao servidor PostgreSQL: FATAL: nenhuma entrada pg_hba.conf para o host 2014-12-02 02:54:58 +0800 CST
  • Martin Hope
    Stéphane Como faço para listar todos os esquemas no PostgreSQL? 2013-04-16 11:19:16 +0800 CST
  • Martin Hope
    Mike Walsh Por que o log de transações continua crescendo ou fica sem espaço? 2012-12-05 18:11:22 +0800 CST
  • Martin Hope
    Stephane Rolland Listar todas as colunas de uma tabela especificada 2012-08-14 04:44:44 +0800 CST
  • Martin Hope
    haxney O MySQL pode realizar consultas razoavelmente em bilhões de linhas? 2012-07-03 11:36:13 +0800 CST
  • Martin Hope
    qazwsx Como posso monitorar o andamento de uma importação de um arquivo .sql grande? 2012-05-03 08:54:41 +0800 CST
  • Martin Hope
    markdorison Como você mysqldump tabela (s) específica (s)? 2011-12-17 12:39:37 +0800 CST
  • Martin Hope
    Jonas Como posso cronometrar consultas SQL usando psql? 2011-06-04 02:22:54 +0800 CST
  • Martin Hope
    Jonas Como inserir valores em uma tabela de uma consulta de seleção no PostgreSQL? 2011-05-28 00:33:05 +0800 CST
  • Martin Hope
    Jonas Como faço para listar todos os bancos de dados e tabelas usando o psql? 2011-02-18 00:45:49 +0800 CST

Hot tag

sql-server mysql postgresql sql-server-2014 sql-server-2016 oracle sql-server-2008 database-design query-performance sql-server-2017

Explore

  • Início
  • Perguntas
    • Recentes
    • Highest score
  • tag
  • help

Footer

AskOverflow.Dev

About Us

  • About Us
  • Contact Us

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve