Access
 sql >> Database >  >> RDS >> Access

Rapporti in modo più dettagliato del solito:Microsoft Access

Segnalazione più dettagliata del solito – Microsoft Access

In genere, quando eseguiamo i rapporti, di solito lo facciamo con una granularità maggiore. Ad esempio, i clienti desiderano comunemente un rapporto mensile sulle vendite. Il database memorizzerebbe le singole vendite come un unico record, quindi non è un problema riassumere le cifre per mese ciascuna. Idem con l'anno, o anche passando da una sottocategoria a una categoria.

Ma supponiamo che debbano andare giù ? Più probabilmente, la risposta sarà "il design del database non va bene. rottamare e ricominciare da capo!” Dopotutto, avere la giusta granularità per i tuoi dati è essenziale per un solido database. Ma questo non è stato un caso in cui la normalizzazione non è stata eseguita. Consideriamo la necessità di rendere conto dell'inventario e dei ricavi e trattarli in modo FIFO. Mi farò rapidamente da parte per sottolineare che non sono CBA e qualsiasi affermazione contabile che faccio deve essere trattata con il massimo sospetto. In caso di dubbio, chiama il tuo commercialista.

Con il disclaimer fuori mano, diamo un'occhiata a come archiviamo attualmente i dati. In questo esempio, dobbiamo registrare gli acquisti di prodotti, quindi dobbiamo registrare le vendite degli acquisti che abbiamo appena acquistato.

Supponiamo che per un singolo prodotto abbiamo 3 acquisti:
Date | Qty | Per-Cost
9/03 | 3 | $45
9/08 | 6 | $40
9/09 | 8 | $50

Successivamente vendiamo questi prodotti in diverse occasioni a un prezzo diverso:
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

Tieni presente che la granularità è a livello di transazione:creiamo un unico record per ogni acquisto e per ogni ordine. Questo è molto comune e ha un senso logico:dobbiamo solo inserire la quantità di prodotti che abbiamo venduto, a un prezzo specifico per una particolare transazione.

OK, dov'è la contabilità che hai rifiutato?

Per i rapporti, dobbiamo calcolare le entrate che abbiamo realizzato su ciascuna unità di prodotto. Mi dicono che devono elaborare il prodotto in modo FIFO... cioè la prima unità di prodotto che è stata acquistata dovrebbe essere la prima unità di prodotto da ordinare. Per poi calcolare il margine che abbiamo realizzato su quell'unità di prodotto, dobbiamo cercare il costo di quella particolare unità di prodotto, quindi detrarre dal prezzo per cui è stato ordinato.

Margine lordo =ricavo del prodotto – costo del prodotto

Niente di sconvolgente, ma aspetta, guarda gli acquisti e gli ordini! Abbiamo fatto solo 3 acquisti, con 3 punti di costo diversi, poi abbiamo avuto 6 ordini con 3 punti di prezzo distinti. Quale fascia di costo va a quale fascia di prezzo, allora?

Questa semplice formula di calcolo del margine lordo, in maniera FIFO, richiede ora di andare alla granularità della singola unità di prodotto. Non abbiamo da nessuna parte nel nostro database. Immagino che se suggerissi agli utenti di inserire un record per unità di prodotto, ci sarebbe una protesta abbastanza forte e forse qualche insulto. Allora, cosa fare?

La rottura

Diciamo che ai fini contabili utilizzeremo la data di acquisto per smistare ogni singola unità del prodotto. Ecco come dovrebbe risultare:
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

Se studi la suddivisione, puoi vedere che ci sono sovrapposizioni in cui consumiamo alcuni prodotti da un acquisto per ordini così e così mentre altre volte abbiamo un ordine che viene evaso da acquisti diversi.

Come notato in precedenza, in realtà non abbiamo 17 righe in nessuna parte del database. Abbiamo solo 3 file di acquisti e 6 file di ordini. Come otteniamo 17 righe da una delle due tabelle?

Aggiungere più fango

Ma non abbiamo finito. Ti ho appena fornito un esempio idealizzato in cui ci è capitato di avere un equilibrio perfetto di 17 unità acquistate che è contrastato da 17 unità di ordini per lo stesso prodotto. Nella vita reale, non è così bello. A volte ci rimangono prodotti in eccesso. A seconda del modello di business, potrebbe anche essere possibile tenere più ordini di quelli disponibili nell'inventario. Coloro che giocano in borsa riconoscono come le vendite allo scoperto.

La possibilità di uno squilibrio è anche il motivo per cui non possiamo prendere una scorciatoia semplicemente sommando tutti i costi e i prezzi, quindi sottrarre per ottenere il margine. Se ci rimanessero X unità, dobbiamo sapere a quale punto di costo si trovano per calcolare l'inventario. Allo stesso modo, non possiamo presumere che un ordine non evaso verrà evaso ordinatamente da un singolo acquisto con un punto di costo. Quindi i calcoli che veniamo non devono funzionare solo per l'esempio ideale, ma anche per dove abbiamo scorte in eccesso o ordini non evasi.

Per prima cosa affrontiamo la questione di capire quanti init di prodotto dobbiamo considerare. È ovvio che una semplice SUM() delle quantità di unità ordinate o delle quantità di unità acquistate non sarà sufficiente. No, invece, dobbiamo SOMMA() sia la quantità di prodotti acquistati che la quantità di prodotti ordinati. Quindi confronteremo i SUM() e sceglieremo quello più alto. Potremmo iniziare con questa 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);

Quello che stiamo facendo qui è suddividere in 3 passaggi logici:

a) ottenere la SUM() delle quantità acquistate per prodotti
b) ottenere la SUM() delle quantità ordinate per prodotti

Poiché non sappiamo se potremmo avere un prodotto che potrebbe avere degli acquisti ma nessun ordine o un prodotto che ha degli ordini effettuati ma non ne abbiamo acquistati, non possiamo lasciare nessuno dei due tavoli. Per questo motivo, utilizziamo le tabelle dei prodotti come fonte autorevole di tutti i ProductID di cui vogliamo essere a conoscenza, il che ci porta al 3° passaggio:

c) abbinare le somme ai loro prodotti, determinare se il prodotto ha qualche transazione (ad es. acquisti o ordini mai effettuati) e, in tal caso, scegliere il numero più alto della coppia. Questo è il nostro conteggio delle transazioni totali che ha avuto un prodotto.

Ma perché la transazione conta?

L'obiettivo qui è capire quante righe dobbiamo generare per prodotto per rappresentare adeguatamente ogni singola unità di un prodotto che ha partecipato a un acquisto oa un ordine. Ricorda nel nostro primo esempio ideale, abbiamo avuto 3 acquisti e 6 ordini, entrambi bilanciati per un totale di 17 unità di prodotto acquistate e poi ordinate. Per quel particolare prodotto, dovremo essere in grado di creare 17 righe per generare i dati che avevamo nella figura sopra.

Quindi, come trasformiamo il singolo valore di 17 di fila in 17 righe? È qui che entra in gioco la magia del tally table.

Se non hai sentito parlare del tavolo di conteggio, dovresti ora. Lascerò che gli altri ti riempiano l'argomento della tabella di conteggio; qui, qui e qui. Basti dire che è uno strumento formidabile da avere nel tuo toolkit SQL.

Supponendo di rivedere la query precedente in modo che l'ultima parte sia ora un CTE denominato ProductTransactionCount, possiamo scrivere la query in questo modo:
<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;

E pesto! Ora abbiamo tutte le righe di cui avremo bisogno, esattamente, per ogni prodotto di cui abbiamo bisogno per fare la contabilità. Nota l'espressione nella clausola ON - stiamo eseguendo un join triangolare - non stiamo usando il solito operatore di uguaglianza perché vogliamo generare 17 righe dal nulla. Si noti che la stessa cosa può essere ottenuta con un CROSS JOIN e una clausola WHERE. Sperimenta con entrambi per trovare quello che funziona meglio.

Fare contare la nostra transazione

Quindi abbiamo la nostra tabella temporanea per impostare il giusto numero di righe. Ora, dobbiamo popolare la tabella con i dati su acquisti e ordini. Come hai visto nella figura, dobbiamo poter ordinare gli acquisti e gli ordini rispettivamente entro la data in cui sono stati acquistati o ordinati. Ed è qui che ROW_NUMBER() e la tabella di conteggio vengono in soccorso.
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;

Potresti chiederti perché abbiamo bisogno di ROW_NUMBER() quando potremmo usare la colonna Num del conteggio. La risposta è che se ci sono più acquisti, il Num andrà solo fino alla quantità di quell'acquisto, ma dobbiamo salire fino a 17, il totale di 3 acquisti separati di 3, 6 e 8 unità. Pertanto, partizioniamo per ProductID mentre si può dire che il numero di tally sia partizionato per PurchaseID che non è quello che vogliamo.

Se hai eseguito l'SQL, ora otterrai un bel breakout, una riga restituita per ogni unità di prodotto acquistata, ordinata per data di acquisto. Tieni presente che ordiniamo anche per PurchaseID, per gestire il caso in cui sono stati effettuati più acquisti dello stesso prodotto lo stesso giorno, quindi dobbiamo rompere il pareggio in qualche modo per garantire che i dati per costo siano calcolati in modo coerente. Possiamo quindi aggiornare la tabella temporanea con l'acquisto:
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;

La parte degli ordini è fondamentalmente la stessa cosa:sostituisci semplicemente "Acquisto" con "Ordine" e otterresti il ​​​​tabellone pieno proprio come avevamo nella figura originale all'inizio del post.

E a questo punto, sei pronto per fare ogni altro tipo di bontà contabile ora che hai suddiviso i prodotti da un livello di transazione fino a un livello di unità di cui hai bisogno per mappare accuratamente il costo del bene alle entrate per quella particolare unità di prodotto utilizzando FIFO o LIFO come richiesto dal tuo commercialista. I calcoli ora sono elementari.

Granularità in un mondo OLTP

Il concetto di granularità è un concetto più comune nel data warehouse che nelle applicazioni OLTP, ma penso che lo scenario discusso evidenzi la necessità di fare un passo indietro e identificare chiaramente qual è l'attuale granularità dello schema OLTP. Come abbiamo visto, all'inizio avevamo la granularità sbagliata e dovevamo rielaborare in modo da poter ottenere la granularità necessaria per ottenere i nostri rapporti. È stato un felice incidente che in questo caso possiamo abbassare con precisione la granularità poiché abbiamo già tutti i dati dei componenti presenti, quindi abbiamo semplicemente dovuto trasformare i dati. Non è sempre così ed è più probabile che se lo schema non è sufficientemente granulare, sarà necessario riprogettare lo schema. Tuttavia, identificare la granularità richiesta per soddisfare i requisiti aiuta a definire chiaramente i passaggi logici che devi intraprendere per raggiungere tale obiettivo.

È possibile ottenere lo script SQL completo per dimostrare il punto DemoLowGranularity.sql.