sql >> Base de Datos >  >> RDS >> Access

Informes más granulares de lo habitual:Microsoft Access

Informes más granulares de lo habitual:Microsoft Access

Por lo general, cuando hacemos informes, generalmente lo hacemos con una granularidad más alta. Por ejemplo, los clientes comúnmente quieren un informe mensual de ventas. La base de datos almacenaría las ventas individuales como un solo registro, por lo que no hay problema para resumir las cifras de cada mes. Lo mismo ocurre con el año, o incluso pasar de una subcategoría a otra.

Pero supongamos que necesitan ir abajo ? Lo más probable es que la respuesta sea “el diseño de la base de datos no es bueno. desechar y empezar de nuevo!” Después de todo, tener la granularidad adecuada para sus datos es esencial para una base de datos sólida. Pero este no fue un caso en el que no se hizo la normalización. Consideremos la necesidad de contabilizar el inventario y los ingresos, y tratarlos de manera FIFO. Me haré a un lado rápidamente para señalar que no soy CBA, y cualquier reclamo contable que haga debe ser tratado con la máxima sospecha. En caso de duda, llame a su contador.

Con el descargo de responsabilidad fuera del camino, veamos cómo almacenamos los datos actualmente. En este ejemplo, necesitamos registrar las compras de productos, y luego tenemos que registrar las ventas de las compras que acabamos de comprar.

Supongamos que para un solo producto tenemos 3 compras:
Date | Qty | Per-Cost
9/03 | 3 | $45
9/08 | 6 | $40
9/09 | 8 | $50

Luego vendemos esos productos en diferentes ocasiones a un precio diferente:
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

Tenga en cuenta que la granularidad es a nivel de transacción:creamos un solo registro para cada compra y para cada pedido. Esto es muy común y tiene sentido lógico:solo necesitamos ingresar la cantidad de productos que vendimos, a un precio específico para una transacción en particular.

Bien, ¿dónde están las cosas de contabilidad que renunciaste?

Para los informes, debemos calcular los ingresos que hicimos en cada unidad de producto. Me dicen que deben procesar el producto en forma FIFO… es decir, la primera unidad de producto que se compre debe ser la primera unidad de producto que se ordene. Para luego calcular el margen que obtuvimos en esa unidad de producto, debemos buscar el costo de esa unidad de producto en particular y luego deducirlo del precio por el que se ordenó.

Margen bruto =ingresos del producto – costo del producto

Nada espectacular, pero espera, ¡mira las compras y los pedidos! Tuvimos solo 3 compras, con 3 puntos de costo diferentes, luego tuvimos 6 pedidos con 3 puntos de precio distintos. Entonces, ¿qué punto de costo corresponde a qué punto de precio?

Esta fórmula simple de calcular el margen bruto, de manera FIFO ahora requiere que vayamos a la granularidad de la unidad individual de producto. No tenemos ningún lugar en nuestra base de datos. Me imagino que si sugiero que los usuarios ingresen un registro por unidad de producto, habrá una protesta bastante fuerte y tal vez algunos insultos. Entonces, ¿qué hacer?

Dividirlo

Digamos que, a efectos contables, utilizaremos la fecha de compra para clasificar cada unidad individual del producto. Así es como debería salir:
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

Si estudia el desglose, puede ver que hay superposiciones en las que consumimos algún producto de una compra para tal y tal pedido, mientras que otras veces tenemos un pedido que se completa con diferentes compras.

Como se señaló anteriormente, en realidad no tenemos esas 17 filas en ninguna parte de la base de datos. Solo tenemos las 3 filas de compras y las 6 filas de pedidos. ¿Cómo obtenemos 17 filas de cualquiera de las tablas?

Agregar más barro

Pero no hemos terminado. Acabo de darle un ejemplo idealizado en el que tenemos un saldo perfecto de 17 unidades compradas que se contrarresta con 17 unidades de pedidos del mismo producto. En la vida real, no es tan bonito. A veces nos quedamos con exceso de productos. Según el modelo de negocio, también es posible mantener más pedidos de los que están disponibles en el inventario. Aquellos que juegan en el mercado de valores reconocen como ventas en corto.

La posibilidad de un desequilibrio también es la razón por la que no podemos tomar el atajo de simplemente sumar todos los costos y precios, y luego restarlos para obtener el margen. Si nos quedamos con X unidades, necesitamos saber en qué punto de costo se encuentran para calcular el inventario. Del mismo modo, no podemos suponer que un pedido no completado se cumplirá perfectamente con una sola compra con un punto de costo. Así que los cálculos a los que venimos no solo deben funcionar para el ejemplo ideal sino también para donde tenemos exceso de inventario o pedidos no cumplidos.

Primero tratemos el asunto de calcular cuántas unidades de producto debemos considerar. Obviamente, una simple SUMA() de las cantidades de unidades pedidas o las cantidades de unidades compradas no será suficiente. No, más bien debemos SUMAR() tanto la cantidad de productos comprados como la cantidad de productos pedidos. Luego compararemos los SUM() y elegiremos el más alto. Podríamos comenzar con esta consulta:
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);

Lo que estamos haciendo aquí es dividirnos en 3 pasos lógicos:

a) obtener el SUM() de las cantidades compradas por productos
b) obtener el SUM() de las cantidades ordenadas por productos

Como no sabemos si podemos tener un producto que puede tener algunas compras pero ningún pedido o un producto que tiene pedidos realizados pero no hemos comprado ninguno, no podemos dejar unir ninguna de las 2 tablas. Por ese motivo, usamos las tablas de productos como la fuente autorizada de todos los ProductID que queremos conocer, lo que nos lleva al tercer paso:

c) hacer coincidir las sumas con sus productos, determinar si el producto tiene alguna transacción (por ejemplo, compras o pedidos realizados alguna vez) y, de ser así, elegir el número más alto del par. Ese es nuestro conteo de transacciones totales que ha tenido un producto.

¿Pero por qué cuenta la transacción?

El objetivo aquí es averiguar cuántas filas necesitamos generar por producto para representar adecuadamente cada unidad individual de un producto que haya participado en una compra o en un pedido. Recuerde que en nuestro primer ejemplo ideal, tuvimos 3 compras y 6 pedidos, que se equilibraron en un total de 17 unidades de producto compradas y luego ordenadas. Para ese producto en particular, necesitaremos poder crear 17 filas para generar los datos que teníamos en la figura anterior.

Entonces, ¿cómo transformamos el valor único de 17 en una fila en 17 filas? Ahí es donde entra la magia de la tabla de conteo.

Si no has oído hablar de la tabla de conteo, deberías hacerlo ahora. Dejaré que otros le informen sobre el tema de la tabla de conteo; aquí, aquí y aquí. Baste decir que es una herramienta formidable para tener en su kit de herramientas SQL.

Suponiendo que revisamos la consulta anterior para que la última parte sea ahora un CTE denominado ProductTransactionCount, podemos escribir la consulta de la siguiente manera:
<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;

¡Y pesto! Ahora tenemos tantas filas como necesitemos, exactamente, para cada producto que necesitamos para llevar la contabilidad. Tenga en cuenta la expresión en la cláusula ON:estamos haciendo una unión triangular, no estamos usando el operador de igualdad habitual porque queremos generar 17 filas de la nada. Tenga en cuenta que se puede lograr lo mismo con una cláusula CROSS JOIN y WHERE. Experimente con ambos para encontrar cuál funciona mejor.

Hacer que nuestra transacción cuente

Entonces tenemos nuestra tabla temporal configurada con el número correcto de filas. Ahora, necesitamos llenar la tabla con datos sobre compras y pedidos. Como vio en la figura, necesitamos poder ordenar las compras y los pedidos por la fecha en que se compraron o ordenaron, respectivamente. Y ahí es donde ROW_NUMBER() y la tabla de conteo vienen al rescate.
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;

Quizás se pregunte por qué necesitamos ROW_NUMBER() cuando podríamos usar la columna Num de la cuenta. La respuesta es que si hay compras múltiples, el Núm. solo será tan alto como la cantidad de esa compra, pero debemos llegar a 17:el total de 3 compras separadas de 3, 6 y 8 unidades. Por lo tanto, particionamos por ID de producto, mientras que se puede decir que el Num de tally está particionado por ID de compra, que no es lo que queremos.

Si ejecutó el SQL, ahora obtendrá un buen desglose, una fila devuelta por cada unidad de producto comprada, ordenada por fecha de compra. Tenga en cuenta que también ordenamos por ID de compra, para manejar el caso en el que hubo varias compras del mismo producto el mismo día, por lo que tenemos que desempatar de alguna manera para garantizar que las cifras por costo se calculen de manera consistente. A continuación, podemos actualizar la tabla temporal con la compra:
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 de los pedidos es básicamente lo mismo:simplemente reemplace "Comprar" con "Pedido" y obtendrá la tabla llena tal como lo teníamos en la figura original al comienzo de la publicación.

Y en este punto, ya está todo listo para hacer todo otro tipo de bondad contable ahora que ha dividido los productos desde un nivel de transacción hasta un nivel de unidad que necesita para mapear con precisión el costo del bien a los ingresos. para esa unidad particular de producto usando FIFO o LIFO según lo requiera su contador. Los cálculos ahora son elementales.

Granularidad en un mundo OLTP

El concepto de granularidad es un concepto más común en el almacén de datos que en las aplicaciones OLTP, pero creo que el escenario discutido destaca la necesidad de dar un paso atrás e identificar claramente cuál es la granularidad actual del esquema de OLTP. Como vimos, teníamos la granularidad incorrecta al principio y necesitábamos volver a trabajar para poder obtener la granularidad requerida para lograr nuestros informes. Fue un feliz accidente que, en este caso, podamos reducir la granularidad con precisión, ya que ya tenemos todos los datos de los componentes presentes, por lo que simplemente tuvimos que transformar los datos. Ese no es siempre el caso, y es más probable que si el esquema no es lo suficientemente granular, justifique el rediseño del esquema. No obstante, identificar la granularidad requerida para satisfacer los requisitos ayuda a definir claramente los pasos lógicos que debe seguir para llegar a ese objetivo.

Se puede obtener el script SQL completo para demostrar el punto DemoLowGranularity.sql.