Rapportera mer detaljerat än vanligt – Microsoft Access
Vanligtvis när vi gör rapportering gör vi det vanligtvis med en högre granularitet. Till exempel vill kunder ofta ha en månatlig rapport över försäljningen. Databasen skulle lagra den individuella försäljningen som en enda post, så det är inga problem att summera siffrorna till varje månad. Dito med år, eller till och med att gå från en underkategori till kategori.
Men anta att de behöver gå ned ? Mer troligt kommer svaret att vara "databasdesignen är inte bra. skrota och börja om!" När allt kommer omkring är det viktigt att ha rätt granularitet för dina data för en solid databas. Men detta var inte ett fall där normalisering inte gjordes. Låt oss överväga behovet av att göra en redovisning av inventeringen och intäkterna och hantera dem på ett FIFO-sätt. Jag kommer snabbt att kliva åt sidan för att påpeka att jag inte är CBA, och alla redovisningskrav jag gör ska behandlas med största misstänksamhet. Ring din revisor om du är osäker.
Med ansvarsfriskrivningen ur vägen, låt oss titta på hur vi för närvarande lagrar data. I det här exemplet måste vi registrera inköpen av produkter och sedan måste vi registrera försäljningen av inköpen som vi just köpte.
Anta att vi har tre inköp för en enskild produkt:
Date | Qty | Per-Cost
9/03 | 3 | $45
9/08 | 6 | $40
9/09 | 8 | $50
Vi säljer sedan dessa produkter vid olika tillfällen till ett annat pris:
Date | Qty | Per-Price
9/05 | 2 | $60
9/07 | 1 | $55
9/10 | 4 | $50
9/12 | 3 | $60
9/15 | 3 | $65
9/19 | 4 | $55
Observera att granulariteten är på en transaktionsnivå – vi skapar en enda post för varje köp och för varje beställning. Detta är mycket vanligt och logiskt – vi behöver bara ange mängden produkter vi sålt, till ett specificerat pris för en viss transaktion.
Ok, var är redovisningsgrejerna som du avslog?
För rapporterna måste vi beräkna intäkterna vi gjorde på varje produktenhet. De säger till mig att de måste bearbeta produkten på ett FIFO-sätt... det vill säga, den första produktenheten som köptes ska vara den första produktenheten som ska beställas. För att sedan beräkna marginalen vi gjorde på den produktenheten måste vi slå upp kostnaden för den specifika produkten och sedan dra av från priset den beställdes för.
Bruttomarginal =produktintäkt – produktkostnad
Inget världskrossande, men vänta, titta på inköpen och beställningarna! Vi hade bara 3 köp, med 3 olika kostnadspunkter, sedan hade vi 6 beställningar med 3 distinkta prispunkter. Vilken kostnadspunkt går till vilken prispunkt då?
Denna enkla formel för att beräkna bruttomarginalen på ett FIFO-sätt kräver nu att vi går till granulariteten för individuell produktenhet. Vi har ingenstans i vår databas. Jag föreställer mig att om jag föreslog att användarna skulle ange en post per produktenhet, skulle det bli en ganska högljudd protest och kanske en del namnupprop. Så, vad ska man göra?
Dela upp det
Låt oss säga att vi för redovisningsändamål kommer att använda inköpsdatumet för att sortera varje enskild enhet av produkten. Så här ska det komma ut:
Line # | Purch Date | Order Date | Per-Cost | Per-Price
1 | 9/03 | 9/05 | $45 | $60
2 | 9/03 | 9/05 | $45 | $60
3 | 9/03 | 9/07 | $45 | $55
4 | 9/08 | 9/10 | $40 | $50
5 | 9/08 | 9/10 | $40 | $50
6 | 9/08 | 9/10 | $40 | $50
7 | 9/08 | 9/10 | $40 | $50
8 | 9/08 | 9/12 | $40 | $60
9 | 9/08 | 9/12 | $40 | $60
10 | 9/09 | 9/12 | $50 | $60
11 | 9/09 | 9/15 | $50 | $65
12 | 9/09 | 9/15 | $50 | $65
13 | 9/09 | 9/15 | $50 | $65
14 | 9/09 | 9/19 | $50 | $55
15 | 9/09 | 9/19 | $50 | $55
16 | 9/09 | 9/19 | $50 | $55
17 | 9/09 | 9/19 | $50 | $55
Om du studerar uppdelningen kan du se att det finns överlappningar där vi konsumerar någon produkt från ett köp för si och så beställningar medan vi andra gånger har en beställning som fullföljs av olika köp.
Som nämnts tidigare har vi faktiskt inte de 17 raderna någonstans i databasen. Vi har bara 3 rader med inköp och 6 rader med beställningar. Hur får vi ut 17 rader från någon av tabellerna?
Lägga till mer lera
Men vi är inte klara. Jag gav dig bara ett idealiserat exempel där vi råkade ha en perfekt balans på 17 köpta enheter som motverkas av 17 beställningar för samma produkt. I verkligheten är det inte så vackert. Ibland sitter vi kvar med överflödiga produkter. Beroende på affärsmodell kan det också vara möjligt att hålla fler beställningar än vad som finns tillgängligt i lagret. De som spelar aktiemarknaden känner igen som blankning.
Möjligheten till en obalans är också anledningen till att vi inte kan ta en genväg att helt enkelt summera alla kostnader och priser och sedan subtrahera för att få marginalen. Om vi blev kvar med X enheter måste vi veta vilken kostnadspunkt de är för att beräkna lagret. På samma sätt kan vi inte anta att en ouppfylld beställning kommer att uppfyllas snyggt av ett enda köp med en kostnadspunkt. Så de beräkningar vi kommer måste inte bara fungera för det ideala exemplet utan också för var vi har överskottslager eller ouppfyllda beställningar.
Låt oss först ta itu med frågan om hur många produktstarter vi behöver överväga. Det är uppenbarligen att en enkel SUM() av antalet beställda enheter eller antalet köpta enheter inte kommer att räcka. Nej, snarare måste vi SUM() både antalet köpta produkter och antalet beställda produkter. Vi kommer sedan att jämföra SUM()s och välja den högre. Vi skulle kunna börja med denna fråga:
WITH ProductPurchaseCount AS (
SELECT
p.ProductID,
SUM(p.QtyBought) AS TotalPurchases
FROM dbo.tblProductPurchase AS p
GROUP BY p.ProductID
), ProductOrderCount AS (
SELECT
o.ProductID,
SUM(o.QtySold) AS TotalOrders
FROM dbo.tblProductOrder AS o
GROUP BY o.ProductID
)
SELECT
p.ProductID,
IIF(ISNULL(pc.TotalPurchases, 0) > ISNULL(oc.TotalOrders, 0), pc.TotalPurchases, oc.TotalOrders) AS ProductTransactionCount
FROM dbo.tblProduct AS p
LEFT JOIN ProductPurchaseCount AS pc
ON p.ProductID = pc.ProductID
LEFT JOIN ProductOrderCount AS oc
ON p.ProductID = oc.ProductID
WHERE NOT (pc.TotalPurchases IS NULL AND oc.TotalOrders IS NULL);
Det vi gör här är att vi delar upp i tre logiska steg:
a) få SUM() för de kvantiteter som köpts av produkter
b) få SUM() för de kvantiteter som beställts av produkter
Eftersom vi inte vet om vi kan ha en produkt som kan ha några inköp men inga beställningar eller en produkt som har beställt men vi har inga köpt, kan vi inte gå och gå med varken två bord. Av den anledningen använder vi produkttabellerna som den auktoritativa källan för alla produkt-ID vi vill veta om, vilket för oss till det tredje steget:
c) matcha beloppen med deras produkter, avgöra om produkten har någon transaktion (t.ex. antingen köp eller beställningar som någonsin gjorts) och i så fall välj det högre numret i paret. Det är vårt antal transaktioner som en produkt har haft.
Men varför räknas transaktionen?
Målet här är att ta reda på hur många rader vi behöver generera per produkt för att representera varje enskild enhet av en produkt som har deltagit i antingen ett köp eller en beställning. Kom ihåg att i vårt första idealiska exempel hade vi 3 inköp och 6 beställningar, båda balanserade till totalt 17 enheter av produkten som köptes sedan beställdes. För den specifika produkten måste vi kunna skapa 17 rader för att generera data vi hade i figuren ovan.
Så hur omvandlar vi det enda värdet av 17 i rad till 17 rader? Det är där magin med räkningstabellen inträder.
Om du inte har hört talas om tally table borde du göra det nu. Jag låter andra fylla dig i ämnet räkningstabell; här, här och här. Det räcker med att säga att det är ett fantastiskt verktyg att ha i din SQL-verktygslåda.
Om vi antar att vi reviderar ovanstående fråga så att den sista delen nu är en CTE som heter ProductTransactionCount, kan vi skriva frågan så här:
<the 3 CTEs from previous exampe>
INSERT INTO tblProductTransactionStaging (
ProductID,
TransactionNumber
)
SELECT
c.ProductID,
t.Num AS TransactionNumber
FROM ProductTransactionCount AS c
INNER JOIN dbo.tblTally AS t
ON c.TransactionCount >= t.Num;
Och pesto! Vi har nu så många rader som vi behöver – exakt – för varje produkt som vi behöver för att göra redovisning. Notera uttrycket i ON-satsen – vi gör en triangulär sammanfogning – vi använder inte den vanliga jämlikhetsoperatorn eftersom vi vill generera 17 rader ur tomma intet. Observera att samma sak kan uppnås med en CROSS JOIN och en WHERE-sats. Experimentera med båda för att hitta vilket som fungerar bättre.
Vi får våra transaktioner att räknas
Så vi har vår tillfälliga tabell satt upp rätt antal rader. Nu måste vi fylla i tabellen med data om inköp och beställningar. Som du såg i figuren måste vi kunna beställa inköpen och beställningarna senast det datum de köptes respektive beställdes. Och det är där ROW_NUMBER() och taltabellen kommer till undsättning.
SELECT
p.ProductID,
ROW_NUMBER() OVER (PARTITION BY p.ProductID ORDER BY p.PurchaseDate, p.PurchaseID) AS TransactionNumber,
p.PurchaseDate,
p.CostPer
FROM dbo.tblProductPurchase AS p
INNER JOIN dbo.tblTally AS t
ON p.QtyBought >= t.Num;
Du kanske undrar varför vi behöver ROW_NUMBER() när vi kan använda tally-kolumnen Num. Svaret är att om det finns flera köp, kommer antalet bara att gå så högt som det köpets kvantitet, men vi måste gå högt som 17 – totalt 3 separata inköp av 3, 6 och 8 enheter. Således partitionerar vi med ProductID medan tally's Num kan sägas vara partitionerat av PurchaseID vilket inte är vad vi vill ha.
Om du körde SQL, kommer du nu att få en fin breakout, en rad returneras för varje enhet av produkten som köpts, sorterad efter inköpsdatum. Observera att vi också sorterar efter köp-ID, för att hantera fallet där det gjordes flera köp av samma produkt samma dag, så vi måste bryta bandet på något sätt för att säkerställa att siffrorna per kostnad beräknas konsekvent. Vi kan sedan uppdatera den tillfälliga tabellen med köpet:
WITH PurchaseData AS (
<previous query>
)
MERGE INTO dbo.tblProductTransactionStaging AS t
USING PurchaseData AS p
ON t.ProductID = p.ProductID
AND t.TransactionNumber = p.TransactionNumber
WHEN MATCHED THEN UPDATE SET
t.PurchaseID = p.PurchaseID,
t.PurchaseDate = p.PurchaseDate,
t.CostPer = p.CostPer;
Beställningsdelen är i princip samma sak – ersätt bara "Köp" med "Beställ", så skulle du få bordet fyllt precis som vi hade i originalbilden i början av inlägget.
Och vid det här tillfället är du redo att göra alla andra sorters redovisningsmässiga fördelar nu när du har delat upp produkterna från en transaktionsnivå ner till en enhetsnivå som du behöver för att exakt kartlägga kostnaden för varor till intäkterna för den specifika produktenheten genom att använda FIFO eller LIFO som krävs av din revisor. Beräkningarna är nu elementära.
Granularitet i en OLTP-värld
Begreppet granularitet är ett begrepp som är vanligare i datalager än i OLTP-applikationer, men jag tror att scenariot som diskuteras belyser behovet av att ta ett steg tillbaka och tydligt identifiera vad som är den nuvarande granulariteten i OLTP:s schema. Som vi såg hade vi fel granularitet i början och vi behövde omarbeta så att vi kunde få den granularitet som krävs för att uppnå vår rapportering. Det var en lycklig olycka att vi i det här fallet kan sänka granulariteten exakt eftersom vi redan har all komponentdata närvarande så vi var helt enkelt tvungna att transformera data. Det är inte alltid fallet, och det är mer troligt att om schemat inte är tillräckligt detaljerat kommer det att motivera omdesign av schemat. Icke desto mindre hjälper det att identifiera den granularitet som krävs för att uppfylla kraven att tydligt definiera de logiska steg du måste vidta för att nå det målet.
Komplett SQL-skript för att visa att poängen kan erhållas DemoLowGranularity.sql.