Raportowanie bardziej szczegółowe niż zwykle — Microsoft Access
Zazwyczaj, gdy robimy raportowanie, zwykle robimy to z większą szczegółowością. Na przykład klienci często chcą mieć miesięczny raport sprzedaży. Baza danych przechowuje poszczególne sprzedaże jako pojedynczy rekord, więc nie ma problemu z sumowaniem danych z każdego miesiąca. To samo z rokiem, a nawet przejściem z podkategorii do kategorii.
Ale załóżmy, że muszą zejść w dół ? Bardziej prawdopodobne, że odpowiedź będzie brzmiała:„projekt bazy danych nie jest dobry. złom i zacznij od nowa!” W końcu posiadanie odpowiedniej szczegółowości danych jest niezbędne dla solidnej bazy danych. Ale to nie był przypadek, w którym nie dokonano normalizacji. Rozważmy potrzebę rozliczenia zapasów i przychodów oraz rozliczenia ich w sposób FIFO. Odsunę się szybko, aby zaznaczyć, że nie jestem CBA, a wszelkie roszczenia księgowe, które zgłaszam, należy traktować z najwyższą podejrzliwością. W razie wątpliwości zadzwoń do księgowego.
Po usunięciu zastrzeżenia spójrzmy, jak obecnie przechowujemy dane. W tym przykładzie musimy rejestrować zakupy produktów, a następnie rejestrować sprzedaż zakupów, które właśnie kupiliśmy.
Załóżmy, że dla pojedynczego produktu mamy 3 zakupy:
Date | Qty | Per-Cost
9/03 | 3 | $45
9/08 | 6 | $40
9/09 | 8 | $50
Następnie sprzedajemy te produkty przy różnych okazjach po innej cenie:
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
Zwróć uwagę, że szczegółowość jest na poziomie transakcji — tworzymy jeden rekord dla każdego zakupu i dla każdego zamówienia. Jest to bardzo powszechne i ma logiczny sens – wystarczy podać ilość sprzedanych produktów po określonej cenie dla konkretnej transakcji.
OK, gdzie są kwestie księgowe, których odrzuciłeś?
W przypadku raportów musimy obliczyć przychód, jaki uzyskaliśmy z każdej jednostki produktu. Mówią mi, że muszą przetworzyć produkt w sposób FIFO… to znaczy, że pierwsza zakupiona jednostka produktu powinna być pierwszą jednostką zamawianego produktu. Aby następnie obliczyć marżę, jaką uzyskaliśmy na tej jednostce produktu, musimy sprawdzić koszt tej konkretnej jednostki produktu, a następnie odjąć od ceny, za którą została zamówiona.
Marża brutto =przychód produktu – koszt produktu
Nic wstrząsającego, ale czekaj, spójrz na zakupy i zamówienia! Mieliśmy tylko 3 zakupy z 3 różnymi punktami kosztowymi, potem mieliśmy 6 zamówień z 3 różnymi punktami cenowymi. Który punkt kosztowy przechodzi do którego poziomu cenowego?
Ta prosta formuła obliczania marży brutto, w sposób FIFO, wymaga teraz przejścia do granulacji poszczególnych jednostek produktu. Nie mamy nigdzie w naszej bazie danych. Wyobrażam sobie, że gdybym zasugerował użytkownikom wpisanie jednego rekordu na jednostkę produktu, byłby dość głośny protest i może jakieś wyzwiska. Więc co robić?
Rozbijanie tego
Załóżmy, że dla celów księgowych będziemy używać daty zakupu do sortowania poszczególnych jednostek produktu. Tak powinno wyglądać:
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
Jeśli przeanalizujesz podział, możesz zauważyć, że zdarzają się nakładania się sytuacji, w których konsumujemy niektóre produkty z jednego zakupu w przypadku różnych zamówień, podczas gdy innym razem mamy zamówienie, które jest realizowane przez różne zakupy.
Jak wspomniano wcześniej, w rzeczywistości nie mamy tych 17 wierszy nigdzie w bazie danych. Mamy tylko 3 rzędy zakupów i 6 rzędów zamówień. Jak uzyskać 17 wierszy z obu tabel?
Dodawanie więcej błota
Ale jeszcze nie skończyliśmy. Właśnie podałem wyidealizowany przykład, w którym mieliśmy idealną równowagę 17 zakupionych jednostek, która jest skontrowana przez 17 jednostek zamówień na ten sam produkt. W prawdziwym życiu to nie jest takie ładne. Czasami zostaje nam nadmiar produktów. W zależności od modelu biznesowego może być również możliwe przechowywanie większej liczby zamówień niż jest dostępne w magazynie. Ci, którzy grają na giełdzie, rozpoznają na przykład krótką sprzedaż.
Możliwość braku równowagi jest również powodem, dla którego nie możemy iść na skróty i po prostu zsumować wszystkie koszty i ceny, a następnie odjąć, aby uzyskać marżę. Jeśli zostało nam X jednostek, musimy wiedzieć, jaki mają one koszt, aby obliczyć zapasy. Podobnie nie możemy zakładać, że niezrealizowane zamówienie zostanie zgrabnie zrealizowane poprzez jednorazowy zakup z jednym punktem kosztowym. Tak więc obliczenia, które otrzymujemy, muszą nie tylko sprawdzać się w idealnym przykładzie, ale także w przypadku nadmiernych zapasów lub niezrealizowanych zamówień.
Zajmijmy się najpierw kwestią obliczenia, ile initów produktu musimy wziąć pod uwagę. Oczywiście zwykłe SUMA() ilości zamówionych jednostek lub ilości zakupionych jednostek nie wystarczy. Nie, raczej musimy SUM() zarówno ilość zakupionych produktów, jak i ilość zamówionych produktów. Następnie porównamy SUM() i wybierzemy wyższą. Moglibyśmy zacząć od tego zapytania:
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);
To, co tutaj robimy, to podział na 3 logiczne kroki:
a) uzyskaj SUM() ilości zakupionych przez produkty
b) uzyskaj SUM() ilości zamówionych przez produkty
Ponieważ nie wiemy, czy możemy mieć produkt, który może mieć jakieś zakupy, ale nie ma zamówień, lub produkt, który ma złożone zamówienia, ale nie mamy żadnego zakupionego, nie możemy dołączyć do dwóch stołów. Z tego powodu używamy tabel produktów jako autorytatywnego źródła wszystkich identyfikatorów produktów, o których chcemy wiedzieć, co prowadzi nas do trzeciego kroku:
c) dopasuj kwoty do swoich produktów, określ, czy produkt ma jakąkolwiek transakcję (np. zakupy lub zamówienia, które kiedykolwiek zostały dokonane), a jeśli tak, wybierz wyższą liczbę pary. To jest nasza całkowita liczba transakcji, które miał produkt.
Ale dlaczego liczy się transakcja?
Celem jest tutaj ustalenie, ile wierszy musimy wygenerować dla każdego produktu, aby odpowiednio reprezentować każdą pojedynczą jednostkę produktu, która brała udział w zakupie lub zamówieniu. Pamiętaj, że w naszym pierwszym idealnym przykładzie mieliśmy 3 zakupy i 6 zamówień, które równoważyły się w sumie 17 sztuk produktu kupionego, a następnie zamówionego. Dla tego konkretnego produktu musimy być w stanie utworzyć 17 wierszy, aby wygenerować dane, które mieliśmy na powyższym rysunku.
Jak więc przekształcić pojedynczą wartość 17 z rzędu w 17 wierszy? W tym miejscu wkracza magia tabeli tally.
Jeśli nie słyszałeś o tabelach liczbowych, powinieneś teraz. Pozwolę innym wpisać ci temat tabeli rachunkowej; tu, tu i tu. Wystarczy powiedzieć, że to potężne narzędzie, które możesz mieć w swoim zestawie narzędzi SQL.
Zakładając, że zmienimy powyższe zapytanie tak, że ostatnią częścią jest teraz CTE o nazwie ProductTransactionCount, możemy napisać zapytanie w ten sposób:
<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;
I pesto! Mamy teraz tyle wierszy, ile będziemy potrzebować — dokładnie — dla każdego produktu, którego potrzebujemy do księgowości. Zwróć uwagę na wyrażenie w klauzuli ON — robimy złączenie trójkątne — nie używamy zwykłego operatora równości, ponieważ chcemy wygenerować 17 wierszy z powietrza. Zauważ, że to samo można osiągnąć za pomocą klauzuli CROSS JOIN i WHERE. Eksperymentuj z obydwoma, aby znaleźć lepsze rozwiązanie.
Zliczanie naszych transakcji
Mamy więc naszą tabelę tymczasową, ustawioną odpowiednią liczbę wierszy. Teraz musimy wypełnić tabelę danymi o zakupach i zamówieniach. Jak widać na rysunku, musimy być w stanie zamówić zakupy i zamówienia odpowiednio do daty ich zakupu lub zamówienia. I tu z pomocą przychodzą ROW_NUMBER() i tabela tally.
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;
Możesz się zastanawiać, po co nam ROW_NUMBER(), skoro możemy użyć kolumny Num. Odpowiedź jest taka, że jeśli jest wiele zakupów, Num wzrośnie tylko do wielkości tego zakupu, ale musimy osiągnąć 17 - łącznie 3 oddzielne zakupy po 3, 6 i 8 jednostek. W ten sposób dzielimy na partycje według ProductID, podczas gdy można powiedzieć, że tally's Num jest podzielona według PurchaseID, co nie jest tym, czego chcemy.
Jeśli uruchomiłeś SQL, otrzymasz teraz ładny podział, wiersz zwracany dla każdej zakupionej jednostki produktu, uporządkowany według daty zakupu. Zwróć uwagę, że sortujemy również według identyfikatora zakupu, aby poradzić sobie z przypadkiem, w którym tego samego dnia dokonano wielu zakupów tego samego produktu, więc musimy jakoś przełamać remis, aby upewnić się, że liczby na koszt są obliczane spójnie. Następnie możemy zaktualizować tabelę tymczasową za pomocą zakupu:
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;
Część dotycząca zamówień to w zasadzie to samo – po prostu zamień „Zakup” na „Zamówienie”, a tabela zostanie wypełniona tak, jak na oryginalnym rysunku na początku postu.
W tym momencie wszyscy jesteście gotowi do wszystkich innych działań księgowych, teraz, gdy podzieliliście produkty z poziomu transakcji na poziom jednostki, który jest potrzebny do dokładnego odwzorowania kosztu towaru na przychód dla tej konkretnej jednostki produktu przy użyciu FIFO lub LIFO zgodnie z wymaganiami Twojego księgowego. Obliczenia są teraz elementarne.
Dokładność w świecie OLTP
Pojęcie granulacji jest pojęciem bardziej powszechnym w hurtowni danych niż w aplikacjach OLTP, ale myślę, że omawiany scenariusz podkreśla potrzebę cofnięcia się i jasnego określenia, jaka jest aktualna granulacja schematu OLTP. Jak widzieliśmy, na początku mieliśmy niewłaściwą szczegółowość i musieliśmy przepracować, aby uzyskać szczegółowość wymaganą do osiągnięcia naszego raportowania. To był szczęśliwy przypadek, że w tym przypadku możemy dokładnie obniżyć ziarnistość, ponieważ mamy już wszystkie dane składowe, więc po prostu musieliśmy je przekształcić. Nie zawsze tak jest i bardziej prawdopodobne jest, że jeśli schemat nie jest wystarczająco szczegółowy, uzasadni to przeprojektowanie schematu. Niemniej jednak określenie szczegółowości wymaganej do spełnienia wymagań pomaga jasno określić logiczne kroki, które należy podjąć, aby osiągnąć ten cel.
Kompletny skrypt SQL, aby zademonstrować, że punkt można uzyskać DemoLowGranularity.sql.