Summen (Summen / Zählungen) über mehrere untergeordnete Tabellen hinweg Abfrageoptimierung

7

Wir haben 12 Arten von Ausgaben in unserer Datenbank, einige haben ziemlich unterschiedliche Daten abzüglich der Betragsfelder. Wir haben mehrere Stellen in der Anwendung und in den Berichten, die einzelne und mehrere Aufwandsummen und Zählungen pro Aufwandsart und Gesamtsummen erfordern. Am Ende möchten wir eine Ansicht für alle diese Aufrufe, können jedoch eine gespeicherte Prozedur verwenden.

Wir haben uns dafür mehrere Alternativen angesehen und festgestellt, dass ein CTE es uns ermöglicht, alle erforderlichen Daten ohne Verwendung von temporären Tabellen abzurufen. Die Verwendung von Joins funktioniert nicht, da wir gesehen haben, dass Datensätze repliziert oder entfernt wurden, unabhängig davon, was wir versucht haben.

Ich habe eine Teilmenge der Kostentabellen und der Abfrage angehängt, die den CTE enthält. Hat jemand eine bessere Alternative als diese? Etwas schneller? Nähern wir uns dieser "Abflachung" angemessen?

Bitte beachten Sie, dass der Ausführungsplan für diese Abfrage gleich ist, unabhängig davon, ob es sich um eine Ansicht oder einen Proc handelt. Die Ausführung des Proc scheint doppelt so lange zu dauern.

Unten ist der Code

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

AKTUALISIEREN:

Hier ist das Datenbankschema mit Einfügungen für diejenigen, die daran interessiert sind, es live zu sehen.

DB-Schema und Einfügungen

Mit SQL Server 2014 Standard habe ich das Datenbankschema / die Einfügungen in eine Datei (hier zu groß) geändert, die mehr Einfügungen sowie einen hochgeladenen Ausführungsplan und Ergebnisse enthält (2 Bilder werden nebeneinander angezeigt, um alle zurückgegebenen Spalten anzuzeigen).

Ausführungsplan

Ergebnisse 1

Ergebnisse fortgesetzt

Zoinky
quelle

Antworten:

2

Hat jemand eine bessere Alternative als diese? Etwas schneller?

Ihre ursprüngliche Abfrage führt Tabellenscans für alle 6 Tabellen durch.

Sie können das entfernen distinct eer.EventRegistrationIdund verwenden GROUP BY eer.EventRegistrationId, alles bleibt gleich.

Im Folgenden Indizes wird Ihnen helfen, das zu vermeiden TABLE SCANund ein tun INDEX SEEK:

create nonclustered index [nc_PettyExpenses] on [dbo].[PettyExpenses] (
    [IsDisputed]
    ,[IsUndisputed]
    ) include (
    EventRegistrationId
    ,AmountPaid
    ,CommercialValueAmount
    )
go

create nonclustered index [nc_HirePremisesExpenses] on [dbo].[HirePremisesExpenses] (
    [IsDisputed]
    ,[IsUndisputed]
    ) include (
    EventRegistrationId
    ,AmountPaid
    ,CommercialValueAmount
    )
go

create nonclustered index [nc_AdvertisingExpenses] on [dbo].[AdvertisingExpenses] (
    [IsDisputed]
    ,[IsUndisputed]
    ) include (
    EventRegistrationId
    ,AmountPaid
    ,CommercialValueAmount
    )
go

create nonclustered index [nc_ServiceExpenses] on [dbo].[ServiceExpenses] (
    [IsDisputed]
    ,[IsUndisputed]
    ) include (
    EventRegistrationId
    ,AmountPaid
    ,CommercialValueAmount
    )
go

create nonclustered index [nc_GoodsSuppliedExpenses] on [dbo].[GoodsSuppliedExpenses] (
    [IsDisputed]
    ,[IsUndisputed]
    ) include (
    EventRegistrationId
    ,AmountPaid
    ,CommercialValueAmount
    )
go

create nonclustered index [nc_TravelHireVehicleExpenses] on [dbo].[TravelHireVehicleExpenses] (
    [IsDisputed]
    ,[IsUndisputed]
    ) include (
    EventRegistrationId
    ,AmountPaid
    ,CommercialValueAmount
    )
go

create nonclustered index [nc_EventRegistrations] on dbo.EventRegistrations (EventRegistrationId);

MIT OPTION (MAXDOP 1)+ über Indizes

Abfrageplan

Geben Sie hier die Bildbeschreibung ein

Statistik E / A-Ausgabe

Geben Sie hier die Bildbeschreibung ein


OHNE OPTION (MAXDOP 1)+ Über Indizes

Abfrageplan :

Geben Sie hier die Bildbeschreibung ein

Statistik-E / A-Ausgabe

Geben Sie hier die Bildbeschreibung ein

╔═════════════════════════════════╦═════╦═════════════╗
             Option               CPU  ElapsedTime 
╠═════════════════════════════════╬═════╬═════════════╣
 Plain - As you posted your code   16           12 
 Without MAXDOP 1 + Indexes        62           11 
 With MAXDOP 1 + Indexes            0            7  <== Winner !!
╚═════════════════════════════════╩═════╩═════════════╝

Unten finden Sie den Code für die Vollständigkeit:

/*
dbcc freeproccache
dbcc dropcleanbuffers

*/
set nocount on
set statistics io on
set statistics time on;

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 eer.EventRegistrationId
    --Petty Expense
    ,ISNULL(SUM(case e.src
                when 'pe'
                    then e.AmountPaidTotal
                end), 0) as PettyExpenseAmountPaid
    ,ISNULL(SUM(case e.src
                when 'pe'
                    then e.CommercialValueAmountTotal
                end), 0) as PettyExpenseCommercial
    ,ISNULL(SUM(case e.src
                when 'pe'
                    then e.ExpenseCount
                end), 0) as PettyExpenseCount
    --Hire On Premise Expense
    ,ISNULL(SUM(case e.src
                when 'hpe'
                    then e.AmountPaidTotal
                end), 0) as HireOnPremisesExpenseAmountPaid
    ,ISNULL(SUM(case e.src
                when 'hpe'
                    then e.CommercialValueAmountTotal
                end), 0) as HireOnPremisesExpenseCommercial
    ,ISNULL(SUM(case e.src
                when 'hpe'
                    then e.ExpenseCount
                end), 0) as HireOnPremisesExpenseCount
    --Advertising Expense
    ,ISNULL(SUM(case e.src
                when 'ae'
                    then e.AmountPaidTotal
                end), 0) as AdvertisingExpenseAmountPaid
    ,ISNULL(SUM(case e.src
                when 'ae'
                    then e.CommercialValueAmountTotal
                end), 0) as AdvertisingExpenseCommercial
    ,ISNULL(SUM(case e.src
                when 'ae'
                    then e.ExpenseCount
                end), 0) as AdvertisingExpenseExpenseCount
    --Services Expense
    ,ISNULL(SUM(case e.src
                when 'se'
                    then e.AmountPaidTotal
                end), 0) as ServiceExpenseAmountPaid
    ,ISNULL(SUM(case e.src
                when 'se'
                    then e.CommercialValueAmountTotal
                end), 0) as ServiceExpenseCommercial
    ,ISNULL(SUM(case e.src
                when 'se'
                    then e.ExpenseCount
                end), 0) as ServiceExpenseExpenseCount
    --Goods Supplied Expense
    ,ISNULL(SUM(case e.src
                when 'gse'
                    then e.AmountPaidTotal
                end), 0) as GoodsSuppliedExpenseAmountPaid
    ,ISNULL(SUM(case e.src
                when 'gse'
                    then e.CommercialValueAmountTotal
                end), 0) as GoodsSuppliedExpenseCommercial
    ,ISNULL(SUM(case e.src
                when 'gse'
                    then e.ExpenseCount
                end), 0) as GoodsSuppliedExpenseExpenseCount
    --Travel and Vehicle Expense
    ,ISNULL(SUM(case e.src
                when 'thve'
                    then e.AmountPaidTotal
                end), 0) as TravelVehicleExpenseAmountPaid
    ,ISNULL(SUM(case e.src
                when 'thve'
                    then e.CommercialValueAmountTotal
                end), 0) as TravelVehicleExpenseCommercial
    ,ISNULL(SUM(case e.src
                when 'thve'
                    then e.ExpenseCount
                end), 0) as TravelVehicleExpenseExpenseCount
    --All Expenses
    ,ISNULL(SUM(e.AmountPaidTotal), 0) as AllExpenseAmountPaidTotal
    ,ISNULL(SUM(e.CommercialValueAmountTotal), 0) as AllExpenseCommercialValueTotal
    ,ISNULL(SUM(e.ExpenseCount), 0) as AllExpenseCount
from EventRegistrations eer
left join (
    select 'pe' as src
        ,EventRegistrationId
        ,AmountPaidTotal
        ,CommercialValueAmountTotal
        ,ExpenseCount
    from pe

    union all

    select 'hpe' as src
        ,EventRegistrationId
        ,AmountPaidTotal
        ,CommercialValueAmountTotal
        ,ExpenseCount
    from hpe

    union all

    select 'ae' as src
        ,EventRegistrationId
        ,AmountPaidTotal
        ,CommercialValueAmountTotal
        ,ExpenseCount
    from ae

    union all

    select 'se' as src
        ,EventRegistrationId
        ,AmountPaidTotal
        ,CommercialValueAmountTotal
        ,ExpenseCount
    from se

    union all

    select 'gse' as src
        ,EventRegistrationId
        ,AmountPaidTotal
        ,CommercialValueAmountTotal
        ,ExpenseCount
    from gse

    union all

    select 'thve' as src
        ,EventRegistrationId
        ,AmountPaidTotal
        ,CommercialValueAmountTotal
        ,ExpenseCount
    from thve
    ) as e on eer.EventRegistrationId = e.EventRegistrationId
group by eer.EventRegistrationId --- we removed the distinct and added a group by clause 
option (maxdop 1)

set statistics io off
set statistics time off

Hinweis: Sie können Zeilen- und Seitenzahlen <- NUR AUS BILDUNGSGRÜNDEN fälschen

UPDATE STATISTICS [dbo].[PettyExpenses]             WITH ROWCOUNT = 10000000, pagecount = 10000000
UPDATE STATISTICS [dbo].[HirePremisesExpenses]      WITH ROWCOUNT = 10000000, pagecount = 10000000
UPDATE STATISTICS [dbo].[AdvertisingExpenses]       WITH ROWCOUNT = 10000000, pagecount = 10000000
UPDATE STATISTICS [dbo].[ServiceExpenses]           WITH ROWCOUNT = 10000000, pagecount = 10000000
UPDATE STATISTICS [dbo].[GoodsSuppliedExpenses]     WITH ROWCOUNT = 10000000, pagecount = 10000000
UPDATE STATISTICS [dbo].[TravelHireVehicleExpenses] WITH ROWCOUNT = 10000000, pagecount = 10000000
Kin Shah
quelle
3

Ich habe die Datenbank mit VIELEN Daten gefüllt und einige interessante Ergebnisse erzielt .

Die Verwendung dieser gefilterten Indizes liefert in meinem System Ergebnisse in etwa 7 Sekunden, verglichen mit mehr als 1 Minute bei den von Kin vorgeschlagenen nicht gefilterten Indizes.

create nonclustered index [fnc_PettyExpenses] on [dbo].[PettyExpenses] (
        EventRegistrationId
    ) include (
     AmountPaid
    ,CommercialValueAmount
    )
WHERE [IsDisputed] = 0 AND [IsUndisputed] = 0;
go

create nonclustered index [fnc_HirePremisesExpenses] on [dbo].[HirePremisesExpenses] (
        EventRegistrationId
    ) include (
    AmountPaid
    ,CommercialValueAmount
    )
WHERE [IsDisputed] = 0 AND [IsUndisputed] = 0;
go

create nonclustered index [fnc_AdvertisingExpenses] on [dbo].[AdvertisingExpenses] (
        EventRegistrationId
    ) include (
     AmountPaid
    ,CommercialValueAmount
    )
WHERE [IsDisputed] = 0 AND [IsUndisputed] = 0;
go

create nonclustered index [fnc_ServiceExpenses] on [dbo].[ServiceExpenses] (
        EventRegistrationId
    ) include (
    AmountPaid
    ,CommercialValueAmount
    )
WHERE [IsDisputed] = 0 AND [IsUndisputed] = 0;
go

create nonclustered index [fnc_GoodsSuppliedExpenses] on [dbo].[GoodsSuppliedExpenses] (
        EventRegistrationId
    ) include (
     AmountPaid
    ,CommercialValueAmount
    )
WHERE [IsDisputed] = 0 AND [IsUndisputed] = 0;
go

create nonclustered index [fnc_TravelHireVehicleExpenses] on [dbo].[TravelHireVehicleExpenses] (
        EventRegistrationId
    ) include (
     AmountPaid
    ,CommercialValueAmount
    )
WHERE [IsDisputed] = 0 AND [IsUndisputed] = 0;
go

Plus den von Kin empfohlenen Index (den Sie wahrscheinlich bereits haben, da er der Primärschlüssel der Tabelle zu sein scheint):

create nonclustered index [nc_EventRegistrations] on dbo.EventRegistrations (EventRegistrationId);

Es stellt sich heraus, dass es in Bezug auf die verstrichene Zeit keinen klaren Gewinner gibt.

Ihre Form der Abfrage erhält mit MERGE JOINs einen sehr schönen Parallelplan .

Paralleler Plan

Die Abfrage mit UNION ALLs erhält einen viel einfacheren seriellen Plan.

Serienplan

In Bezug auf die E / A-Statistiken sieht es so aus, als ob der serielle Plan etwas effizienter ist als der parallele Plan:

Parallelplan-E / A-Statistiken Parallelplan-E / A-Statistiken

Serienplan-E / A-Statistiken Serienplan-E / A-Statistiken

Die verstrichenen Zeiten sind sehr ähnlich, aber die CPU-Zeit ist für den parallelen Plan höher.

Denken Sie daran, dass gefilterte Indizes einige Einschränkungen aufweisen , sodass sie möglicherweise nicht die beste Wahl für Sie sind.

spaghettidba
quelle
2

Abhängig von Ihren Anforderungen besteht die Antwort möglicherweise darin, jedes CTEin Ihrem Beispiel in eine indizierte Ansicht zu konvertieren . Dies würde die Aggregationen bei jeder Abfrage auf Kosten eines erhöhten Speichers und einer Berührung langsamerer CRUD-Operationen speichern. Das Grundformat wäre ungefähr so:

CREATE VIEW dbo.AggregatedPettyExpenses 
    WITH SCHEMABINDING AS
    SELECT 
        EventRegistrationId
        ,SUM(AmountPaid)            as AmountPaidTotal
        ,SUM(CommercialValueAmount) as CommercialValueAmountTotal
        ,SUM(CASE WHEN AmountPaid IS NOT NULL THEN 1 ELSE 0 END) as ExpenseCount -- Hack for your count logic
        ,COUNT_BIG(*) AS COUNT
    FROM PettyExpenses 
    WHERE IsDisputed = 0 AND IsUndisputed = 0
    GROUP BY EventRegistrationId

    CREATE UNIQUE CLUSTERED INDEX [IDX_dbo_AggregatedPettyExpenses_EventRegistrationId] 
        ON dbo.AggregatedPettyExpenses(EventRegistrationId);

Wiederholen Sie das gleiche Muster oben für jedes CTE. Dann hätten Sie nur ein VIEWLike in Ihrem Beispiel, aber Sie LEFT JOINzu den neuen INDEXED VIEWanstelle der CTE.

Da Sie erwähnt , dass Sie Standard Edition verwenden ist es wert , zu wissen , werden Sie die verwenden, um Abfrage - Hinweis vonWITH (NOEXPAND) , wenn Sie die indizierte Sichten wollen wie indizierte Sichten anstelle der normalen Ansichten verhalten. Dies gilt übrigens derzeit auch für Azure Sql Server-Datenbanken.

Erik
quelle