Nauwkeuriger rapporteren dan normaal – Microsoft Access
Wanneer we rapportage doen, doen we dit meestal met een hogere granulariteit. Klanten willen bijvoorbeeld vaak een maandelijks verkooprapport. De database zou de individuele verkopen als één record opslaan, dus het is geen probleem om de cijfers per maand op te tellen. Idem met jaartal, of zelfs van een subcategorie naar categorie gaan.
Maar stel dat ze naar beneden moeten gaan ? Waarschijnlijker zal het antwoord zijn:"het databaseontwerp is niet goed. weggooien en opnieuw beginnen!” Het hebben van de juiste granulariteit voor uw gegevens is immers essentieel voor een solide database. Maar dit was geen geval waarin normalisatie niet werd uitgevoerd. Laten we eens kijken naar de noodzaak om een boekhouding te maken van de voorraad en inkomsten en deze op een FIFO-manier te behandelen. Ik zal snel een stap opzij zetten om erop te wijzen dat ik geen CBA ben en dat alle boekhoudkundige claims die ik maak met de grootste argwaan moeten worden behandeld. Bel bij twijfel uw accountant.
Laten we, nu de disclaimer uit de weg is, eens kijken naar hoe we de gegevens momenteel opslaan. In dit voorbeeld moeten we de aankopen van producten registreren en vervolgens de verkopen van de aankopen die we net hebben gekocht.
Stel dat we voor een enkel product 3 aankopen hebben:
Date | Qty | Per-Cost
9/03 | 3 | $45
9/08 | 6 | $40
9/09 | 8 | $50
We verkopen die producten dan later bij verschillende gelegenheden tegen een andere prijs:
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
Houd er rekening mee dat de granulariteit op transactieniveau ligt - we creëren één record voor elke aankoop en voor elke bestelling. Dit is heel gebruikelijk en is logisch - we hoeven alleen de hoeveelheid producten in te voeren die we hebben verkocht, tegen een bepaalde prijs voor een bepaalde transactie.
Ok, waar zijn de boekhoudkundige dingen die je hebt afgewezen?
Voor de rapporten moeten we de inkomsten berekenen die we per producteenheid hebben verdiend. Ze vertellen me dat ze het product op een FIFO-manier moeten verwerken... dat wil zeggen dat de eerste producteenheid die is gekocht, de eerste producteenheid moet zijn die moet worden besteld. Om vervolgens de marge te berekenen die we op die producteenheid hebben gemaakt, moeten we de kosten van die specifieke producteenheid opzoeken en vervolgens aftrekken van de prijs waarvoor het is besteld.
Brutomarge =productopbrengst – productkosten
Niets wereldschokkends, maar wacht, kijk naar de aankopen en bestellingen! We hadden slechts 3 aankopen, met 3 verschillende kostenpunten, daarna hadden we 6 bestellingen met 3 verschillende prijspunten. Welk kostenpunt gaat dan naar welk prijspunt?
Deze eenvoudige formule voor het berekenen van de brutomarge, op een FIFO-manier, vereist nu dat we naar de granulariteit van de individuele producteenheid gaan. We hebben nergens in onze database. Ik stel me voor dat als ik zou voorstellen dat de gebruikers één record per eenheid product invoeren, er een vrij luid protest zou zijn en misschien wat scheldwoorden. Dus, wat te doen?
Het opbreken
Laten we zeggen dat we voor boekhoudkundige doeleinden de aankoopdatum zullen gebruiken om elke afzonderlijke eenheid van het product te sorteren. Dit is hoe het eruit zou moeten komen:
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
Als je de uitsplitsing bestudeert, kun je zien dat er overlappingen zijn waarbij we een bepaald product van de ene aankoop consumeren voor zo en zo bestellingen, terwijl we de andere keer een bestelling hebben die wordt vervuld door verschillende aankopen.
Zoals eerder opgemerkt, hebben we die 17 rijen nergens in de database. We hebben alleen de 3 rijen met aankopen en 6 rijen met bestellingen. Hoe krijgen we 17 rijen uit beide tabellen?
Meer modder toevoegen
Maar we zijn nog niet klaar. Ik heb je zojuist een geïdealiseerd voorbeeld gegeven waar we toevallig een perfecte balans hadden van 17 gekochte eenheden die worden gecompenseerd door 17 eenheden bestellingen voor hetzelfde product. In het echte leven is het niet zo mooi. Soms zitten we met overtollige producten. Afhankelijk van het bedrijfsmodel is het misschien ook mogelijk om meer bestellingen aan te houden dan er in de voorraad beschikbaar zijn. Degenen die op de aandelenmarkt spelen, herkennen short-selling.
De mogelijkheid van een onbalans is ook de reden waarom we geen kortere weg kunnen nemen door simpelweg alle kosten en prijzen op te tellen en vervolgens af te trekken om de marge te krijgen. Als we X-eenheden overhouden, moeten we weten welk kostenpunt ze zijn om de voorraad te berekenen. Evenzo kunnen we er niet van uitgaan dat een niet-uitgevoerde bestelling netjes wordt uitgevoerd door een enkele aankoop met één kostenpunt. Dus de berekeningen die we maken, moeten niet alleen voor het ideale voorbeeld werken, maar ook voor waar we overtollige voorraad of niet-uitgevoerde bestellingen hebben.
Laten we eerst kijken naar het aantal inits van het product dat we moeten overwegen. Het is duidelijk dat een simpele SOM() van de bestelde hoeveelheden of de gekochte hoeveelheden niet voldoende is. Nee, we moeten eerder SOM() zowel de hoeveelheid gekochte producten als de hoeveelheid bestelde producten. We zullen dan de SUM()s vergelijken en de hogere kiezen. We zouden kunnen beginnen met deze query:
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);
Wat we hier doen, is dat we in 3 logische stappen opsplitsen:
a) verkrijg de SUM() van de gekochte hoeveelheden per product
b) verkrijg de SUM() van de bestelde hoeveelheden per product
Omdat we niet weten of we een product hebben dat misschien wat aankopen heeft maar geen bestellingen of een product dat wel bestellingen heeft maar we hebben er niets gekocht, kunnen we niet aan beide tafels gaan zitten. Om die reden gebruiken we de producttabellen als de gezaghebbende bron van alle Product-ID's waarover we meer willen weten, wat ons bij de derde stap brengt:
c) de bedragen afstemmen op hun producten, bepalen of het product een transactie heeft (bijvoorbeeld aankopen of bestellingen die ooit zijn gedaan) en zo ja, kies het hogere nummer van het paar. Dat is onze telling van het totale aantal transacties dat een product heeft gehad.
Maar waarom tellen de transacties mee?
Het doel hier is om erachter te komen hoeveel rijen we per product moeten genereren om elke individuele eenheid van een product die heeft deelgenomen aan een aankoop of een bestelling adequaat weer te geven. Onthoud dat we in ons eerste ideale voorbeeld 3 aankopen en 6 bestellingen hadden, die beide uitkwamen op een totaal van 17 eenheden gekocht en besteld product. Voor dat specifieke product moeten we rijen van 17 kunnen maken om de gegevens te genereren die we in de bovenstaande afbeelding hadden.
Dus hoe transformeren we de enkele waarde van 17 op een rij in 17 rijen? Dat is waar de magie van de teltafel haar intrede doet.
Als je nog nooit van de teltafel hebt gehoord, zou je dat nu moeten doen. Ik zal anderen je laten vullen met het onderwerp tally table; hier, hier en hier. Het volstaat te zeggen dat het een formidabele tool is om in je SQL-toolkit te hebben.
Ervan uitgaande dat we de bovenstaande query herzien zodat het laatste deel nu een CTE is met de naam ProductTransactionCount, kunnen we de query als volgt schrijven:
<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;
En pesto! We hebben nu zoveel rijen als we nodig hebben - precies - voor elk product dat we nodig hebben om de boekhouding te doen. Let op de uitdrukking in de ON-clausule - we doen een driehoekige join - we gebruiken niet de gebruikelijke gelijkheidsoperator omdat we 17 rijen uit het niets willen genereren. Merk op dat hetzelfde kan worden bereikt met een CROSS JOIN en een WHERE-clausule. Experimenteer met beide om te ontdekken welke het beste werkt.
Onze transacties laten tellen
Dus we hebben onze tijdelijke tabel ingesteld op het juiste aantal rijen. Nu moeten we de tabel vullen met gegevens over aankopen en bestellingen. Zoals je in de afbeelding hebt gezien, moeten we de aankopen en bestellingen kunnen bestellen op de datum waarop ze respectievelijk zijn gekocht of besteld. En dat is waar ROW_NUMBER() en tally-tabel te hulp komen.
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;
Je vraagt je misschien af waarom we ROW_NUMBER() nodig hebben als we de kolom Num van de telling zouden kunnen gebruiken. Het antwoord is dat als er meerdere aankopen zijn, de Num alleen zo hoog zal gaan als de hoeveelheid van die aankoop, maar we moeten hoog gaan als 17 - het totaal van 3 afzonderlijke aankopen van 3, 6 en 8 eenheden. We partitioneren dus op ProductID, terwijl tally's Num kan worden gepartitioneerd op PurchaseID, wat niet is wat we willen.
Als je de SQL hebt uitgevoerd, krijg je nu een mooie uitsplitsing, een rij die wordt geretourneerd voor elke gekochte eenheid product, gerangschikt op aankoopdatum. Houd er rekening mee dat we ook sorteren op Aankoop-ID, om het geval af te handelen waarin er meerdere aankopen van hetzelfde product op dezelfde dag waren, dus we moeten de band op de een of andere manier verbreken om ervoor te zorgen dat de cijfers per kostprijs consistent worden berekend. We kunnen dan de tijdelijke tabel bijwerken met de aankoop:
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;
Het gedeelte met bestellingen is in principe hetzelfde:vervang gewoon "Aankoop" door "Bestelling", en je krijgt de tabel gevuld zoals we hadden in de originele afbeelding aan het begin van de post.
En op dit punt ben je helemaal klaar om alle andere soorten boekhoudkundige goedheid te doen nu je de producten hebt opgesplitst van een transactieniveau tot een eenheidsniveau dat je nodig hebt om de kosten van het goed nauwkeurig in kaart te brengen met de inkomsten voor die specifieke producteenheid met behulp van FIFO of LIFO zoals vereist door uw accountant. De berekeningen zijn nu elementair.
Nauwkeurigheid in een OLTP-wereld
Het concept van granulariteit is een concept dat vaker voorkomt in datawarehouses dan in OLTP-toepassingen, maar ik denk dat het besproken scenario de noodzaak benadrukt om een stap terug te doen en duidelijk te identificeren wat de huidige granulariteit van het OLTP-schema is. Zoals we zagen, hadden we in het begin de verkeerde granulariteit en moesten we herwerken zodat we de granulariteit konden krijgen die nodig was om onze rapportage te bereiken. Het was een gelukkig toeval dat we in dit geval de granulariteit nauwkeurig kunnen verlagen, omdat we alle componentgegevens al aanwezig hebben, dus we moesten de gegevens gewoon transformeren. Dat is niet altijd het geval, en het is waarschijnlijker dat als het schema niet gedetailleerd genoeg is, het een herontwerp van het schema rechtvaardigt. Desalniettemin helpt het identificeren van de granulariteit die nodig is om aan de vereisten te voldoen, om duidelijk de logische stappen te definiëren die u moet nemen om dat doel te bereiken.
Compleet SQL-script om het punt te demonstreren kan worden verkregen DemoLowGranularity.sql.