Отчитане по-подробно от обикновено – Microsoft Access
Обикновено, когато правим отчети, обикновено го правим с по-висока детайлност. Например клиентите обикновено искат месечен отчет за продажбите. Базата данни ще съхранява отделните продажби като един запис, така че не е проблем да се сумират цифрите за всеки месец. Същото с годината или дори преминаване от подкатегория в категория.
Но да предположим, че трябва да слязат надолу ? По-вероятно отговорът ще бъде „дизайнът на базата данни не е добър. изхвърляй и започвай отначало!” В крайна сметка, наличието на правилната детайлност за вашите данни е от съществено значение за солидна база данни. Но това не беше случай, когато нормализирането не беше извършено. Нека разгледаме необходимостта да направим отчет за инвентара и приходите и да ги третираме по FIFO начин. Бързо ще се отдръпна, за да отбележа, че не съм CBA и всички счетоводни претенции, които правя, трябва да бъдат третирани с най-голямо подозрение. Когато се съмнявате, обадете се на вашия счетоводител.
Като изключим отказа от отговорност, нека да разгледаме как съхраняваме данните в момента. В този пример трябва да запишем покупките на продукти и след това трябва да запишем продажбите на покупките, които току-що купихме.
Да предположим, че за един продукт имаме 3 покупки:
Date | Qty | Per-Cost
9/03 | 3 | $45
9/08 | 6 | $40
9/09 | 8 | $50
След това по-късно продаваме тези продукти по различни поводи на различна цена:
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
Имайте предвид, че детайлността е на ниво транзакция — създаваме един запис за всяка покупка и за всяка поръчка. Това е много често срещано и има логичен смисъл – трябва само да въведем количеството продукти, които сме продали, на определена цена за конкретна транзакция.
Добре, къде са счетоводните неща, които отказахте?
За отчетите трябва да изчислим приходите, които сме направили от всяка единица продукт. Казват ми, че трябва да обработват продукта по FIFO начин... тоест първата закупена единица продукт трябва да бъде първата единица продукт, която трябва да бъде поръчана. За да изчислим след това маржа, който направихме за тази единица продукт, трябва да потърсим цената на тази конкретна единица продукт, след което да извадим от цената, за която е поръчана.
Брутен марж =приход от продукта – себестойност на продукта
Нищо разтърсващо, но чакайте, вижте покупките и поръчките! Имахме само 3 покупки с 3 различни ценови точки, след това имахме 6 поръчки с 3 различни ценови точки. Тогава коя ценова точка отива към коя ценова точка?
Тази проста формула за изчисляване на брутния марж по FIFO начин сега изисква от нас да преминем към детайлността на отделната единица продукт. Нямаме никъде в нашата база данни. Предполагам, че ако предложа на потребителите да въвеждат по един запис за единица продукт, ще има доста силен протест и може би някакво обаждане. И така, какво да правя?
Разбиване
Да кажем, че за счетоводни цели ще използваме датата на покупка, за да сортираме всяка отделна единица от продукта. Ето как трябва да излезе:
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
Ако проучите разбивката, можете да видите, че има припокривания, при които консумираме някакъв продукт от една покупка за толкова и така поръчки, докато друг път имаме поръчка, която се изпълнява от различни покупки.
Както беше отбелязано по-рано, ние всъщност нямаме тези 17 реда никъде в базата данни. Имаме само 3 реда покупки и 6 реда поръчки. Как да получим 17 реда от двете таблици?
Добавяне на още кал
Но ние не сме готови. Току-що ви дадох идеализиран пример, при който случайно имаме перфектен баланс от закупени 17 единици, който се компенсира от 17 единици поръчки за същия продукт. В реалния живот не е толкова красиво. Понякога ни остават излишни продукти. В зависимост от бизнес модела може също да е възможно да се задържат повече поръчки от това, което е налично в инвентара. Тези, които играят на фондовия пазар, разпознават такива като къси продажби.
Възможността за дисбаланс също е причината, поради която не можем да вземем пряк път да просто сумираме всички разходи и цени, след което да извадим, за да получим маржа. Ако сме останали с X единици, трябва да знаем коя разходна точка са те, за да изчислим инвентара. По същия начин не можем да приемем, че една неизпълнена поръчка ще бъде изрядно изпълнена от една покупка с една точка на разходите. Така че изчисленията, които идваме, трябва да работят не само за идеалния пример, но и за това, където имаме излишни запаси или неизпълнени поръчки.
Нека първо да се заемем с въпроса за това колко инициативи на продукта трябва да вземем предвид. Очевидно е, че обикновен SUM() на количествата поръчани единици или количествата закупени единици няма да е достатъчен. Не, по-скоро трябва да SUM() както количеството закупени продукти, така и количеството поръчани продукти. След това ще сравним SUM()s и ще изберем по-високия. Можем да започнем с тази заявка:
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);
Това, което правим тук, е да се разделим на 3 логически стъпки:
a) вземете SUM() на количествата, закупени от продукти
b) вземете SUM() на количествата, поръчани по продукти
Тъй като не знаем дали може да имаме продукт, който може да има някои покупки, но няма поръчки, или продукт, който има направени поръчки, но ние нямаме закупени, не можем да напуснем да се присъединим към двете маси. Поради тази причина ние използваме продуктовите таблици като авторитетен източник на всички ProductID, за които искаме да знаем, което ни отвежда до 3-та стъпка:
в) съпоставете сумите с техните продукти, определете дали продуктът има някаква транзакция (например покупки или поръчки, правени някога) и ако е така, изберете по-големия номер от двойката. Това е нашият брой общи транзакции, които е имал продукт.
Но защо транзакциите се отчитат?
Целта тук е да разберем колко реда трябва да генерираме на продукт, за да представим адекватно всяка отделна единица от продукт, която е участвала в покупка или поръчка. Не забравяйте, че в първия ни идеален пример имахме 3 покупки и 6 поръчки, като и двете балансираха до общо 17 единици продукт, закупен след това поръчан. За този конкретен продукт ще трябва да можем да създадем 17 реда, за да генерираме данните, които имахме на фигурата по-горе.
И така, как да трансформираме единичната стойност от 17 на ред в 17 реда? Тук влиза магията на таблицата за изчисления.
Ако не сте чували за таблицата за изчисление, сега трябва. Ще позволя на другите да ви попълнят темата за таблицата за изчисления; тук, тук и тук. Достатъчно е да се каже, че това е страхотен инструмент, който да имате във вашия SQL инструментариум.
Ако приемем, че ревизираме горната заявка, така че последната част вече е CTE с име ProductTransactionCount, можем да напишем заявката по следния начин:
<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;
И песто! Вече имаме толкова редове, колкото ще ни трябват — точно — за всеки продукт, който трябва да направим счетоводство. Обърнете внимание на израза в клаузата ON – ние правим триъгълно свързване – ние не използваме обичайния оператор за равенство, защото искаме да генерираме 17 реда от нищото. Обърнете внимание, че същото може да се постигне с CROSS JOIN и клауза WHERE. Експериментирайте и с двете, за да намерите кое работи по-добре.
Отчитане на нашите транзакции
Така че нашата временна таблица е настроила правилния брой редове. Сега трябва да попълним таблицата с данни за покупки и поръчки. Както видяхте на фигурата, ние трябва да можем да поръчаме покупките и поръчките съответно до датата, на която са закупени или поръчани. И това е мястото, където ROW_NUMBER() и таблицата за изчисление идват на помощ.
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;
Може да се чудите защо се нуждаем от ROW_NUMBER(), когато можем да използваме колоната Num на броя. Отговорът е, че ако има множество покупки, Num ще достигне само количеството на тази покупка, но ние трябва да достигнем до 17 - общо 3 отделни покупки от 3, 6 и 8 единици. По този начин, ние разделяме по ProductID, докато tally’s Num може да се каже, че е разделен по PurchaseID, което не е това, което искаме.
Ако сте стартирали SQL, сега ще получите хубава разбивка, връщан ред за всяка закупена единица продукт, подредена по дата на покупка. Имайте предвид, че ние също сортираме по PurchaseID, за да се справим със случая, когато е имало множество покупки на един и същи продукт в един и същи ден, така че трябва да прекъснем връзката по някакъв начин, за да гарантираме, че стойностите на Per-Cost се изчисляват последователно. След това можем да актуализираме временната таблица с покупката:
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;
Частта с поръчките е по същество едно и също нещо – просто заменете „Покупка“ с „Поръчка“ и ще получите попълнена таблица точно както имахме в оригиналната фигура в началото на публикацията.
И в този момент сте готови да правите всички други видове счетоводни добрини, сега, след като сте разбили продуктите от ниво на транзакция надолу до ниво на единица, което ви е необходимо, за да съпоставите точно цената на стоките с приходите за тази конкретна единица продукт, използвайки FIFO или LIFO, както се изисква от вашия счетоводител. Изчисленията вече са елементарни.
Драйналност в света на OLTP
Концепцията за детайлност е концепция, по-често срещана в хранилището на данни, отколкото в OLTP приложенията, но мисля, че обсъжданият сценарий подчертава необходимостта да се отстъпи назад и ясно да се идентифицира каква е текущата детайлност на схемата на OLTP. Както видяхме, имахме грешна детайлност в началото и трябваше да преработим, за да можем да получим детайлността, необходима за постигане на нашето отчитане. Беше щастлив случай, че в този случай можем точно да намалим детайлността, тъй като вече разполагаме с всички данни за компонентите, така че просто трябваше да трансформираме данните. Това не винаги е така и е по-вероятно, ако схемата не е достатъчно детайлна, това ще наложи препроектиране на схемата. Независимо от това, идентифицирането на детайлността, необходима за удовлетворяване на изискванията, помага ясно да дефинирате логическите стъпки, които трябва да предприемете, за да стигнете до тази цел.
Пълен SQL скрипт за демонстриране на точката може да бъде получен DemoLowGranularity.sql.