Temos 12 tipos de despesas em nosso banco de dados, algumas com dados bastante diferentes menos os campos Valor. Temos vários locais no aplicativo e relatórios que exigem totais de despesas únicas e múltiplas e contagens por tipo de despesa e totais gerais. No final, queremos uma View para todas essas chamadas, mas estamos abertos para usar um procedimento armazenado.
Analisamos várias alternativas para isso e descobrimos que um CTE nos permite obter todos os dados necessários sem o uso de tabelas temporárias. O uso de junções não funciona, pois vimos registros sendo replicados ou removidos, não importa o que tentamos.
Anexei um subconjunto das tabelas de despesas e a consulta que inclui o CTE. Alguém tem uma alternativa melhor que essa? Algo mais rápido? Estamos abordando esse 'achatamento' apropriadamente?
Observe que o plano de execução é o mesmo para esta consulta, seja uma View ou um Proc, e o Proc parece levar o dobro do tempo para ser executado.
Abaixo está o código
WITH pe AS
(
SELECT
EventRegistrationId
,sum(AmountPaid) as AmountPaidTotal
,sum(CommercialValueAmount) as CommercialValueAmountTotal
,count(1) as ExpenseCount
FROM PettyExpenses
WHERE IsDisputed = 0 AND IsUndisputed = 0
group by EventRegistrationId
),hpe AS
(
SELECT
EventRegistrationId
,sum(AmountPaid) as AmountPaidTotal
,sum(CommercialValueAmount) as CommercialValueAmountTotal
,count(1) as ExpenseCount
FROM HirePremisesExpenses
WHERE IsDisputed = 0 AND IsUndisputed = 0
group by EventRegistrationId
), ae AS
(
SELECT
EventRegistrationId
,sum(AmountPaid) as AmountPaidTotal
,sum(CommercialValueAmount) as CommercialValueAmountTotal
,count(1) as ExpenseCount
FROM AdvertisingExpenses
WHERE IsDisputed = 0 AND IsUndisputed = 0
group by EventRegistrationId
), se AS
(
SELECT
EventRegistrationId
,sum(AmountPaid) as AmountPaidTotal
,sum(CommercialValueAmount) as CommercialValueAmountTotal
,count(1) as ExpenseCount
FROM ServiceExpenses
WHERE IsDisputed = 0 AND IsUndisputed = 0
group by EventRegistrationId
), gse AS
(
SELECT
EventRegistrationId
,sum(AmountPaid) as AmountPaidTotal
,sum(CommercialValueAmount) as CommercialValueAmountTotal
,count(1) as ExpenseCount
FROM GoodsSuppliedExpenses
WHERE IsDisputed = 0 AND IsUndisputed = 0
group by EventRegistrationId
), thve AS
(
SELECT
EventRegistrationId
,sum(AmountPaid) as AmountPaidTotal
,sum(CommercialValueAmount) as CommercialValueAmountTotal
,count(1) as ExpenseCount
FROM TravelHireVehicleExpenses
WHERE IsDisputed = 0 AND
IsUndisputed = 0
group by EventRegistrationId
)
select
distinct eer.EventRegistrationId
--Petty Expense
,ISNULL(pe.AmountPaidTotal,0) as PettyExpenseAmountPaid
,ISNULL(pe.CommercialValueAmountTotal,0) as PettyExpenseCommercial
,ISNULL(pe.ExpenseCount,0) as PettyExpenseCount
--Hire On Premise Expense
,ISNULL(hpe.AmountPaidTotal,0) as HireOnPremisesExpenseAmountPaid
,ISNULL(hpe.CommercialValueAmountTotal,0) as HireOnPremisesExpenseCommercial
,ISNULL(hpe.ExpenseCount,0) as HireOnPremisesExpenseCount
--Advertising Expense
,ISNULL(ae.AmountPaidTotal,0) as AdvertisingExpenseAmountPaid
,ISNULL(ae.CommercialValueAmountTotal,0) as AdvertisingExpenseCommercial
,ISNULL(ae.ExpenseCount,0) as AdvertisingExpenseExpenseCount
--Services Expense
,ISNULL(se.AmountPaidTotal,0) as ServiceExpenseAmountPaid
,ISNULL(se.CommercialValueAmountTotal,0) as ServiceExpenseCommercial
,ISNULL(se.ExpenseCount,0) as ServiceExpenseExpenseCount
--Goods Supplied Expense
,ISNULL(gse.AmountPaidTotal,0) as GoodsSuppliedExpenseAmountPaid
,ISNULL(gse.CommercialValueAmountTotal,0) as GoodsSuppliedExpenseCommercial
,ISNULL(gse.ExpenseCount,0) as GoodsSuppliedExpenseExpenseCount
--Travel and Vehicle Expense
,ISNULL(thve.AmountPaidTotal,0) as TravelVehicleExpenseAmountPaid
,ISNULL(thve.CommercialValueAmountTotal,0) as TravelVehicleExpenseCommercial
,ISNULL(thve.ExpenseCount,0) as TravelVehicleExpenseExpenseCount
--All Expenses
,ISNULL(pe.AmountPaidTotal,0)
+ ISNULL(hpe.AmountPaidTotal,0)
+ ISNULL(ae.AmountPaidTotal,0)
+ ISNULL(se.AmountPaidTotal,0)
+ ISNULL(gse.AmountPaidTotal,0)
+ ISNULL(thve.AmountPaidTotal,0) as AllExpenseAmountPaidTotal
,ISNULL(pe.CommercialValueAmountTotal,0)
+ ISNULL(hpe.CommercialValueAmountTotal,0)
+ ISNULL(ae.CommercialValueAmountTotal,0)
+ ISNULL(se.CommercialValueAmountTotal,0)
+ ISNULL(gse.CommercialValueAmountTotal,0)
+ ISNULL(thve.CommercialValueAmountTotal,0) as AllExpenseCommercialValueTotal
,ISNULL(pe.ExpenseCount,0)
+ ISNULL(hpe.ExpenseCount,0)
+ ISNULL(ae.ExpenseCount,0)
+ ISNULL(se.ExpenseCount,0)
+ ISNULL(gse.ExpenseCount,0)
+ ISNULL(thve.ExpenseCount,0) as AllExpenseCount
from EventRegistrations eer
left join pe on pe.EventRegistrationId = eer.EventRegistrationId
left join hpe on hpe.EventRegistrationId = eer.EventRegistrationId
left join ae on ae.EventRegistrationId = eer.EventRegistrationId
left join se on se.EventRegistrationId = eer.EventRegistrationId
left join gse on gse.EventRegistrationId = eer.EventRegistrationId
left join thve on thve.EventRegistrationId = eer.EventRegistrationId
ATUALIZAR:
Aqui está o esquema db com inserções para aqueles que estão interessados em vê-lo ao vivo.
Esquema de banco de dados e inserções
Usando o SQL Server 2014 Standard, alterei o esquema/inserções db para um arquivo (muito grande para aqui) que contém mais inserções, bem como plano de execução e resultados carregados (2 imagens ficam lado a lado para mostrar todas as colunas retornadas)
Preenchi o banco de dados com MUITOS dados e fiz algumas descobertas interessantes.
O uso desses índices filtrados retorna resultados em cerca de 7 segundos em meu sistema, em comparação com mais de 1 minuto com os índices não filtrados sugeridos por Kin.
Mais o índice recomendado por Kin (que você provavelmente já possui, pois parece ser a chave primária da tabela):
Acontece que não existe um vencedor claro em termos de tempo decorrido.
Sua forma de consulta obtém um plano paralelo muito bom usando
MERGE JOIN
s.A consulta usando
UNION ALL
s obtém um plano serial muito mais simples.No que diz respeito às estatísticas de IO, parece que o plano serial é um pouco mais eficiente que o plano paralelo:
Estatísticas IO do plano paralelo
Estatísticas de E/S do plano serial
Os tempos decorridos são muito semelhantes, mas o tempo de CPU é maior para o plano paralelo.
Lembre-se de que os índices filtrados têm algumas limitações , portanto, podem não ser a melhor escolha para você.
Dependendo de suas necessidades, a resposta pode ser converter cada um
CTE
em seu exemplo em uma exibição indexada . Isso economizaria as agregações em cada consulta às custas de maior armazenamento e operações CRUD um pouco mais lentas. O formato básico seria algo como:Repita o mesmo padrão acima para cada um
CTE
. Então você teria apenas umVIEW
gosto em seu exemplo, mas vocêLEFT JOIN
para o novoINDEXED VIEW
's em vez doCTE
's.Como você mencionou que está usando a Standard Edition, vale a pena saber que você terá que usar a dica de consulta
WITH (NOEXPAND)
se quiser que as exibições indexadas se comportem como exibições indexadas em vez de exibições normais. Aliás, isso também é verdade atualmente com os bancos de dados Azure Sql Server.Sua consulta original fará varreduras de tabela para todas as 6 tabelas.
Você pode remover o
distinct eer.EventRegistrationId
e usarGROUP BY eer.EventRegistrationId
, resto tudo permanece o mesmo.Os índices abaixo ajudarão você a evitar
TABLE SCAN
e farão umINDEX SEEK
:COM
OPTION (MAXDOP 1)
+ Índices Acimaplano de consulta
saída de E/S de estatísticas
SEM
OPTION (MAXDOP 1)
+ Índices AcimaPlano de consulta :
Estatísticas de saída IO
Abaixo está o código para completar:
Observação: você pode falsificar contagens de linhas e contagens de páginas <-- APENAS POR MOTIVOS EDUCACIONAIS