Granulärere Berichte als üblich – Microsoft Access
Wenn wir Berichte erstellen, tun wir dies in der Regel mit einer höheren Granularität. Beispielsweise möchten Kunden häufig einen monatlichen Umsatzbericht. Die Datenbank würde die einzelnen Verkäufe als einen einzigen Datensatz speichern, sodass es kein Problem ist, die Zahlen auf den Monat aufzusummieren. Dito mit Jahr, oder sogar von einer Unterkategorie zu einer Kategorie.
Aber angenommen, sie müssen nach unten gehen ? Wahrscheinlicher ist die Antwort:„Das Datenbankdesign ist nicht gut. Schrott und fang von vorne an!“ Schließlich ist die richtige Granularität Ihrer Daten für eine solide Datenbank unerlässlich. Dies war jedoch kein Fall, in dem keine Normalisierung durchgeführt wurde. Betrachten wir die Notwendigkeit, den Bestand und die Einnahmen zu berücksichtigen und sie nach dem FIFO-Prinzip zu behandeln. Ich werde schnell zur Seite treten, um darauf hinzuweisen, dass ich kein CBA bin und alle von mir erhobenen Bilanzierungsansprüche mit größtem Misstrauen zu behandeln sind. Rufen Sie im Zweifelsfall Ihren Buchhalter an.
Nachdem der Haftungsausschluss aus dem Weg geräumt wurde, schauen wir uns an, wie wir die Daten derzeit speichern. In diesem Beispiel müssen wir die Käufe von Produkten aufzeichnen, und dann müssen wir die Verkäufe der Einkäufe aufzeichnen, die wir gerade gekauft haben.
Angenommen, wir haben für ein einzelnes Produkt 3 Käufe:
Date | Qty | Per-Cost
9/03 | 3 | $45
9/08 | 6 | $40
9/09 | 8 | $50
Wir verkaufen diese Produkte dann später zu verschiedenen Anlässen zu einem anderen Preis:
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
Beachten Sie, dass die Granularität auf Transaktionsebene liegt – wir erstellen einen einzigen Datensatz für jeden Kauf und für jede Bestellung. Dies kommt sehr häufig vor und ist logisch sinnvoll – wir müssen nur die Menge der von uns verkauften Produkte zu einem bestimmten Preis für eine bestimmte Transaktion eingeben.
OK, wo ist das Buchhaltungsmaterial, das Sie abgelehnt haben?
Für die Berichte müssen wir die Einnahmen berechnen, die wir mit jeder Produkteinheit erzielt haben. Sie sagen mir, dass sie das Produkt auf FIFO-Weise verarbeiten müssen … das heißt, die erste gekaufte Produkteinheit sollte die erste bestellte Produkteinheit sein. Um dann die Marge zu berechnen, die wir für diese Produkteinheit erzielt haben, müssen wir die Kosten für diese bestimmte Produkteinheit nachschlagen und dann von dem Preis abziehen, für den sie bestellt wurde.
Bruttomarge =Produktumsatz – Produktkosten
Nichts Weltbewegendes, aber warte, schau dir die Einkäufe und Bestellungen an! Wir hatten nur 3 Einkäufe mit 3 verschiedenen Kostenpunkten, dann hatten wir 6 Bestellungen mit 3 unterschiedlichen Preispunkten. Welcher Kostenpunkt geht dann zu welchem Preispunkt?
Diese einfache Formel zur Berechnung der Bruttomarge auf FIFO-Weise erfordert nun, dass wir uns der Granularität der einzelnen Produkteinheit zuwenden. Wir haben nirgendwo in unserer Datenbank. Ich stelle mir vor, wenn ich vorschlagen würde, dass die Benutzer einen Datensatz pro Produkteinheit eingeben, würde es einen ziemlich lauten Protest und vielleicht einige Beschimpfungen geben. Also, was tun?
Aufbrechen
Nehmen wir an, dass wir für Buchhaltungszwecke das Kaufdatum verwenden, um jede einzelne Einheit des Produkts zu sortieren. So sollte es herauskommen:
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
Wenn Sie die Aufschlüsselung studieren, können Sie sehen, dass es Überschneidungen gibt, wo wir einige Produkte aus einem Kauf für so und so Bestellungen verbrauchen, während wir ein anderes Mal eine Bestellung haben, die durch verschiedene Käufe erfüllt wird.
Wie bereits erwähnt, haben wir diese 17 Zeilen eigentlich nirgendwo in der Datenbank. Wir haben nur die 3 Reihen mit Einkäufen und 6 Reihen mit Bestellungen. Wie erhalten wir 17 Zeilen aus beiden Tabellen?
Mehr Schlamm hinzufügen
Aber wir sind noch nicht fertig. Ich habe Ihnen gerade ein idealisiertes Beispiel gegeben, bei dem wir zufällig ein perfektes Gleichgewicht von 17 gekauften Einheiten hatten, dem 17 Einheiten von Bestellungen für dasselbe Produkt gegenüberstehen. Im wirklichen Leben ist es nicht so schön. Manchmal bleiben uns überschüssige Produkte übrig. Je nach Geschäftsmodell kann es auch möglich sein, mehr Bestellungen zu halten, als im Bestand verfügbar sind. Diejenigen, die an der Börse spielen, erkennen solche Leerverkäufe.
Die Möglichkeit eines Ungleichgewichts ist auch der Grund, warum wir nicht einfach alle Kosten und Preise summieren und dann subtrahieren können, um die Marge zu erhalten. Wenn wir mit X Einheiten übrig bleiben, müssen wir wissen, welcher Kostenpunkt sie sind, um den Bestand zu berechnen. Ebenso können wir nicht davon ausgehen, dass eine unerfüllte Bestellung durch einen einzigen Kauf mit einem Kostenpunkt sauber erfüllt wird. Daher müssen die Berechnungen, zu denen wir kommen, nicht nur für das ideale Beispiel funktionieren, sondern auch für Fälle, in denen wir überschüssige Lagerbestände oder nicht ausgeführte Bestellungen haben.
Beschäftigen wir uns zunächst mit der Frage, wie viele Produkt-Inits wir berücksichtigen müssen. Es ist offensichtlich, dass eine einfache SUM() der bestellten Stückzahlen oder der gekauften Stückzahlen nicht ausreicht. Nein, vielmehr müssen wir sowohl die Menge der gekauften Produkte als auch die Menge der bestellten Produkte SUM(). Wir vergleichen dann die SUM(s) und wählen die höhere aus. Wir könnten mit dieser Abfrage beginnen:
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);
Was wir hier tun, ist, dass wir in 3 logische Schritte aufteilen:
a) Erhalten Sie die SUM() der gekauften Mengen nach Produkten
b) Erhalten Sie die SUM() der bestellten Mengen nach Produkten
Da wir nicht wissen, ob wir ein Produkt haben, für das möglicherweise einige Käufe, aber keine Bestellungen vorliegen, oder ein Produkt, für das Bestellungen aufgegeben, aber keine gekauft wurden, können wir keinen der beiden Tische beitreten. Aus diesem Grund verwenden wir die Produkttabellen als maßgebliche Quelle für alle Produkt-IDs, über die wir etwas wissen möchten, was uns zum dritten Schritt bringt:
c) Ordne die Summen ihren Produkten zu, bestimme, ob das Produkt irgendeine Transaktion hat (z. B. entweder Käufe oder jemals getätigte Bestellungen) und wenn ja, wähle die höhere Zahl des Paares. Das ist unsere Gesamtzahl der Transaktionen, die ein Produkt hatte.
Aber warum die Transaktionszählung?
Das Ziel hier ist herauszufinden, wie viele Zeilen wir pro Produkt generieren müssen, um jede einzelne Einheit eines Produkts, das entweder an einem Kauf oder einer Bestellung teilgenommen hat, angemessen darzustellen. Denken Sie daran, dass wir in unserem ersten idealen Beispiel 3 Käufe und 6 Bestellungen hatten, die beide zu insgesamt 17 gekauften und dann bestellten Produkteinheiten führten. Für dieses spezielle Produkt müssen wir in der Lage sein, 17 Zeilen zu erstellen, um die Daten zu generieren, die wir in der obigen Abbildung hatten.
Wie transformieren wir also den einzelnen Wert von 17 in einer Reihe in 17 Reihen? Hier kommt die Magie der Strichliste ins Spiel.
Wenn Sie noch nie von einem Tally-Tisch gehört haben, sollten Sie es jetzt tun. Ich lasse Sie von anderen über das Thema Zähltabelle informieren; hier, hier und hier. Es genügt zu sagen, dass es ein beeindruckendes Tool ist, das Sie in Ihrem SQL-Toolkit haben sollten.
Angenommen, wir überarbeiten die obige Abfrage so, dass der letzte Teil jetzt ein CTE namens ProductTransactionCount ist, können wir die Abfrage folgendermaßen schreiben:
<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;
Und Pesto! Wir haben jetzt so viele Zeilen, wie wir brauchen – genau – für jedes Produkt, das wir für die Buchhaltung benötigen. Beachten Sie den Ausdruck in der ON-Klausel – wir machen eine Dreiecksverknüpfung – wir verwenden nicht den üblichen Gleichheitsoperator, weil wir 17 Zeilen aus dem Nichts generieren wollen. Beachten Sie, dass dasselbe mit einem CROSS JOIN und einer WHERE-Klausel erreicht werden kann. Experimentieren Sie mit beiden, um herauszufinden, was besser funktioniert.
Damit unsere Transaktionen zählen
Also haben wir unsere temporäre Tabelle mit der richtigen Anzahl von Zeilen eingerichtet. Jetzt müssen wir die Tabelle mit Daten zu Käufen und Bestellungen füllen. Wie Sie in der Abbildung gesehen haben, müssen wir in der Lage sein, die Käufe und Bestellungen bis zum Kauf- bzw. Bestelldatum zu bestellen. Und hier kommen ROW_NUMBER() und Zähltabellen zur Hilfe.
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;
Sie fragen sich vielleicht, warum wir ROW_NUMBER() brauchen, wenn wir die Num-Spalte des Tally verwenden könnten. Die Antwort ist, dass bei mehreren Käufen die Anzahl nur so hoch ist wie die Menge dieses Kaufs, aber wir müssen 17 hoch gehen – die Summe von 3 separaten Käufen von 3, 6 und 8 Einheiten. Daher partitionieren wir nach ProductID, während man sagen kann, dass die Num von Tally nach PurchaseID partitioniert ist, was nicht das ist, was wir wollen.
Wenn Sie die SQL ausgeführt haben, erhalten Sie jetzt eine schöne Aufschlüsselung, eine Zeile, die für jede gekaufte Produkteinheit zurückgegeben wird, geordnet nach Kaufdatum. Beachten Sie, dass wir auch nach PurchaseID sortieren, um den Fall zu behandeln, in dem es mehrere Käufe desselben Produkts am selben Tag gab, sodass wir die Bindung irgendwie lösen müssen, um sicherzustellen, dass die Pro-Cost-Zahlen konsistent berechnet werden. Wir können dann die temporäre Tabelle mit dem Kauf aktualisieren:
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;
Der Bestellteil ist im Grunde dasselbe – ersetzen Sie einfach „Kauf“ durch „Bestellung“, und Sie würden die Tabelle so auffüllen, wie wir es in der ursprünglichen Abbildung am Anfang des Beitrags getan hatten.
Und an diesem Punkt sind Sie bereit, alle anderen Arten von Buchhaltungsleistungen zu erbringen, nachdem Sie die Produkte von einer Transaktionsebene auf eine Einheitenebene heruntergebrochen haben, die Sie benötigen, um die Warenkosten den Einnahmen genau zuzuordnen für diese bestimmte Produkteinheit unter Verwendung von FIFO oder LIFO, wie von Ihrem Buchhalter gefordert. Die Berechnungen sind jetzt elementar.
Granularität in einer OLTP-Welt
Das Konzept der Granularität ist ein Konzept, das in Data Warehouse-Anwendungen häufiger vorkommt als in OLTP-Anwendungen, aber ich denke, das besprochene Szenario unterstreicht die Notwendigkeit, einen Schritt zurückzutreten und die aktuelle Granularität des OLTP-Schemas klar zu identifizieren. Wie wir gesehen haben, hatten wir am Anfang die falsche Granularität und mussten nacharbeiten, damit wir die für unsere Berichterstellung erforderliche Granularität erreichen konnten. Es war ein glücklicher Zufall, dass wir in diesem Fall die Granularität genau verringern können, da wir bereits über alle Komponentendaten verfügen, sodass wir die Daten einfach transformieren mussten. Das ist nicht immer der Fall, und es ist wahrscheinlicher, dass ein Schema, das nicht granular genug ist, eine Neugestaltung des Schemas rechtfertigt. Nichtsdestotrotz hilft die Identifizierung der zur Erfüllung der Anforderungen erforderlichen Granularität dabei, die logischen Schritte klar zu definieren, die Sie unternehmen müssen, um dieses Ziel zu erreichen.
Ein vollständiges SQL-Skript zur Demonstration des Punktes kann unter DemoLowGranularity.sql abgerufen werden.