Recursos para Profesionales de TI >
Página principal de foros
>
Foros de Servidores: Productos y Tecnologías
>
SQL Server
>
La mejor forma de hacer running totals - correr totales - acomular totales
La mejor forma de hacer running totals - correr totales - acomular totales
- Hola que tal es mi primer post por aqui un gusto haber encontrado este sitio.
Apreciaria mucho un poco de su ayuda para resolver mi problema.Uso sql server 2005.Dejenme explicar lo que quiero hacer:
Tengo una tabla con estas caracteristicas:
Id | Entradas | Salidas | Saldo
1 | 100.00 | 0.00 | 100.00 ---> La idea es que me sume las entradas y reste las salidas pero que vaya
1 | 100.00 | 0.00 | 200.00 ---> acomulando los totales en este caso 2 entradas de 100.00 el saldo serian 200
1 | 0.00 | 100.00| 100.00 ---> En este caso hay una salida de 100.00 ahora el nuevo saldo es 100.00
1 | 100.00 | 0.00 | 200.00 ---> Y asi sucesivamente.....
1 | 100.00 | 0.00 | 300.00
1 | 200.00 | 0.00 | 500.00
2 | 100.00 | 0.00 | 100.00 ---> Pero necesito que si el ID es diferente el saldo para este ID no
2 | 100.00 | 0.00 | 200.00 ---> se mezcle con el anterior en este caso que el saldo sea independiente al ID 1
2 | 100.00 | 0.00 | 300.00 ---> Es decir que el saldo de ID 2 empieze desde cero.
2 | 100.00 | 0.00 | 400.00
2 | 0.00 | 100.00 | 300.00
El saldo de ID 1 termina en 500.00 y el saldo de ID 2 termina en 300.00 en mi tabla ya tengo los campos id,entradas y salidas lo unico que necesito generar es el campo saldo, mi pregunta es cual la mejor forma de hacer esto considerando que esto tengo que hacerlo muchas veces en un dia, y que afecta a mas de 1 millon de filas en mi tabla.
Si hay alguna forma establecida que sea la mejor de todas y que sea la que la mayoria de la gente usa , algun procedimiento almacenado, algo para que lo haga en automatico conforme se vayan registrando nuevos datos en la tabla o simplemente como lo hacen ustedes o como me recomendarian que lo hiciera, acepto cualquier tipo de sugerencia.
Podria agarrar el ultimo registro insertado en mi tabla de algun id para generar el nuevo saldo es decir en el caso del ID 1 agarrar este registro 1 | 200.00 | 0.00 | 500.00 que fue el ultimo para generar mi nuevo saldo que quedaria mas o menos asi 1 | 500.00 | 0.00 |1000.00 siendo asi seria muy facil pero el problema es que a veces los registros necesitan ser intercalados.
Aqui lo explico mejor:
Id | Entradas | Salidas | Saldo
1 | 500.00 | 0.00 | 0.00 ---> Este es el nuevo registro que entrara a mi tabla no tengo saldo por que lo desconozco.
Asi termino mi tabla
Id | Entradas | Salidas | Saldo
1 | 100.00 | 0.00 | 100.00
1 | 100.00 | 0.00 | 200.00
1 | 0.00 | 100.00| 100.00
1 | 100.00 | 0.00 | 200.00
1 | 100.00 | 0.00 | 300.00
1 | 200.00 | 0.00 | 500.00
Insertando Nuevo registro:
Id | Entradas | Salidas | Saldo
1 | 100.00 | 0.00 | 100.00
1 | 100.00 | 0.00 | 200.00
1 | 500.00 | 0.00 | 0.00 ---> El nuevo registro tiene que ser mostrado en ese lugar asi que necesito otra vez
1 | 0.00 | 100.00| 100.00 ---> correr los totales
1 | 100.00 | 0.00 | 200.00
1 | 100.00 | 0.00 | 300.00
1 | 200.00 | 0.00 | 500.00
Debe quedar asi:
Id | Entradas | Salidas | Saldo
1 | 100.00 | 0.00 | 100.00
1 | 100.00 | 0.00 | 200.00
1 | 500.00 | 0.00 | 700.00
1 | 0.00 | 100.00| 600.00
1 | 100.00 | 0.00 | 700.00
1 | 100.00 | 0.00 | 800.00
1 | 200.00 | 0.00 |1000.00
Espero haberme dado a entender, si necesitan mas informacion o no fui muy claro en mi explicacion agradeceria que me lo hicieran saber. Disculpen el haberme alargado tanto.
De antemano agradezco cualquier comentario,sugerencia o respuesta.
Gracias
Salu2
Respuestas
- SQL Server no ha dado soporte completo a la clausula OVER, lo cual ayudaria a tener un metodo relacional con buen performance para solucionar este tipo de problemas.
Aqui tienes una serie de sugerencias hechas por Itzik Ben-Gan y Sujata Metha. Tomense su tiempo, por favor, y voten si estan interesados en tener estas facilidades en la proxima version de sql server.
Este es el documento original:
http://www.insidetsql.com/OVER_Clause_and_Ordered_Calculations.doc.
Aqui algunas entradas directas en connect.microsoft.com:
Progressive Ordered Calculations:
https://connect.microsoft.com/SQLServer/feedback/ViewFeedback.aspx?FeedbackID=254397
FIRST_VALUE, LAST_VALUE functions:
https://connect.microsoft.com/SQLServer/feedback/ViewFeedback.aspx?FeedbackID=254395
DISTINCT clause for aggregates:
https://connect.microsoft.com/SQLServer/feedback/ViewFeedback.aspx?FeedbackID=254393
ROWS and RANGE window subclauses:
https://connect.microsoft.com/SQLServer/feedback/ViewFeedback.aspx?FeedbackID=254392
Vector expressions:
https://connect.microsoft.com/SQLServer/feedback/ViewFeedback.aspx?FeedbackID=254391
TOP OVER:
https://connect.microsoft.com/SQLServer/feedback/ViewFeedback.aspx?FeedbackID=254390
LAG and LEAD functions:
https://connect.microsoft.com/SQLServer/feedback/ViewFeedback.aspx?FeedbackID=254388
ORDER BY for aggregates:
https://connect.microsoft.com/SQLServer/feedback/ViewFeedback.aspx?FeedbackID=254387
Para resolver este problema con las herramientas actuales, podemos usar lo sgte:
- cursor
- query correlacionado
- funcion CLR
- Tabla de totales que se actualiza mediante un trigger cada vez que executamos una sentencia en las tablas base.
No existe un metodo especial que brinde simplicidad y buen desempenio . Debes probar los diferentes metodos y escojer el que mejor se adapte a tu ambiente.
El maestro Itzik Ben-Gan tiene una serie de articulos donde compara los distintos metodos. Para ver los articulos necesitas estar subscrito a la revista, pero puedo decir que vale la pena.
Set-Based vs. Cursor-Based Solutions for Running Aggregates
http://www.sqlmag.com/article/articleid/101736
Subqueries and Joins for Running Aggregates
http://www.sqlmag.com/article/articleid/101623
CLR-Based Solution for Running Aggregates
http://www.sqlmag.com/articles/index.cfm?articleid=102097
No importa que metodo usemos, es importante que cada transaccion pueda ser identificada unicamente y que podamos establecer un orden cronologico de ocurrencia. Como vez, en tu ejemplo no podemos identificar cada transaccion unicamente, y mucho menos saber el orden en que ocurrio la transaccion.
Ejemplo (usando query correlacionado):
DECLARE @t TABLE ( sk int NOT NULL IDENTITY(1, 1) UNIQUE, Id int, Entradas numeric(6, 2), Salidas numeric(6, 2), fecha_trans datetime NOT NULL DEFAULT (GETDATE()) ); INSERT INTO @t(id, Entradas, Salidas) VALUES(1, 100.00, 0.00); INSERT INTO @t(id, Entradas, Salidas) VALUES(1, 100.00, 0.00); INSERT INTO @t(id, Entradas, Salidas) VALUES(1, 0.00, 100.00); INSERT INTO @t(id, Entradas, Salidas) VALUES(1, 100.00, 0.00); INSERT INTO @t(id, Entradas, Salidas) VALUES(1, 100.00, 0.00); INSERT INTO @t(id, Entradas, Salidas) VALUES(1, 200.00, 0.00); SELECT T1.*, ( SELECT SUM(Entradas) + SUM(-1 * Salidas) FROM @t AS T2 WHERE T2.id = T1.id AND (T2.fecha_trans < T1.fecha_trans OR (T2.fecha_trans = T1.fecha_trans AND T2.sk <= T1.sk)) ) AS Saldo FROM @t AS T1 ORDER BY T1.id, T1.fecha_trans, T1.sk; GO
AMB- Marcado como respuestaXhester Rodriguez martes, 24 de noviembre de 2009 19:32
- Xhester,
Gracias por tu voto. Estoy seguro que la introduccion de esas facilidades seran de mucha ayuda.
> La que no probé fue la funcion CLR esto por que no encontre mucha información al respecto si me pudieras dar algun ejemplo o link
> con ejemplos para probarla te lo agradeceria mucho.
Aqui tienes un magnifico ejemplo.
Running sums yet again: SQLCLR saves the day!
http://sqlblog.com/blogs/adam_machanic/archive/2006/07/12/running-sums-yet-again-sqlclr-saves-the-day.aspx
> Hasta el momento me estoy inclinando por la opcion del trigger que se encargaria de crear/actualizar los saldos en mi tabla al momento insertar alguna fila
Dejame aclarar una cosa antes de continuar. Mencione una tabla de saldos, y me referi a casos donde tenemos por ejemplo una cuenta y necesitamos saber el saldo a la fecha, por lo que mantener el saldo en una segunda tabla y no tener que recorrer las filas constantemente para saber el saldo, nos ahorraria un poco de tiemepo. Ahora, este no es el caso pues deseas el saldo por transaccion y no necesariamente el ultimo a la fecha.
Usemos el mismo ejemplo que atache a mi primer post. Vamos a adicionar la columna [Saldo] y crear un trigger "after insert". Fijate que solo voy a crear el trigger para despues de insercion, asi que queda de tu parte escribir uno que maneje los casos de actualizacion y borrado, en caso de que esto sea permitido.
USE tempdb; GO CREATE TABLE dbo.T ( sk int NOT NULL IDENTITY(1, 1) UNIQUE, Id int, Entradas numeric(6, 2), Salidas numeric(6, 2), fecha_trans datetime NOT NULL DEFAULT (GETDATE()), Saldo numeric(10, 2) ); GO SET ANSI_NULLS ON; GO SET QUOTED_IDENTIFIER ON; GO CREATE TRIGGER dbo.tr_T_ins ON dbo.T AFTER INSERT AS IF @@ROWCOUNT = 0 RETURN; SET NOCOUNT ON; BEGIN TRY ;WITH r_set AS ( SELECT T1.sk, T1.Id, T1.fecha_trans, ( SELECT ISNULL(SUM(Entradas), 0) + ISNULL(SUM(-1 * Salidas), 0) FROM dbo.T AS T2 WHERE T2.id = T1.id AND (T2.fecha_trans < T1.fecha_trans OR (T2.fecha_trans = T1.fecha_trans AND T2.sk <= T1.sk)) ) AS Saldo FROM dbo.T AS T1 INNER JOIN INSERTED AS I ON T1.sk = I.sk ) UPDATE T1 SET T1.Saldo = R.Saldo FROM dbo.T AS T1 INNER JOIN r_set AS R ON T1.sk = R.sk ; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION; SELECT ERROR_NUMBER() AS ERROR_NUMBER, ERROR_MESSAGE() AS [ERROR_MESSAGE]; END CATCH GO INSERT INTO dbo.T(id, Entradas, Salidas) VALUES(1, 100.00, 0.00); GO SELECT * FROM dbo.T ORDER BY Id, sk; GO INSERT INTO dbo.T(id, Entradas, Salidas) VALUES(1, 100.00, 0.00); GO SELECT * FROM dbo.T ORDER BY Id, sk; GO INSERT INTO dbo.T(id, Entradas, Salidas) VALUES(1, 0.00, 100.00); GO SELECT * FROM dbo.T ORDER BY Id, sk; GO INSERT INTO dbo.T(id, Entradas, Salidas) VALUES(1, 100.00, 0.00); GO SELECT * FROM dbo.T ORDER BY Id, sk; GO INSERT INTO dbo.T(id, Entradas, Salidas) VALUES(1, 100.00, 0.00); GO SELECT * FROM dbo.T ORDER BY Id, sk; GO INSERT INTO dbo.T(id, Entradas, Salidas) VALUES(1, 200.00, 0.00); GO SELECT * FROM dbo.T ORDER BY Id, sk; GO DROP TABLE dbo.T; GO
Ten en cuenta que los triggers se ejecutan bajo la misma transaccion donde la sentencia que los dispara participa. Se prudente porque estos influyen en cuanto durara la transaccion.
AMB- Marcado como respuestaGustavo LarrieraMVP, Moderadormartes, 24 de noviembre de 2009 19:33
Todas las respuestas
- SQL Server no ha dado soporte completo a la clausula OVER, lo cual ayudaria a tener un metodo relacional con buen performance para solucionar este tipo de problemas.
Aqui tienes una serie de sugerencias hechas por Itzik Ben-Gan y Sujata Metha. Tomense su tiempo, por favor, y voten si estan interesados en tener estas facilidades en la proxima version de sql server.
Este es el documento original:
http://www.insidetsql.com/OVER_Clause_and_Ordered_Calculations.doc.
Aqui algunas entradas directas en connect.microsoft.com:
Progressive Ordered Calculations:
https://connect.microsoft.com/SQLServer/feedback/ViewFeedback.aspx?FeedbackID=254397
FIRST_VALUE, LAST_VALUE functions:
https://connect.microsoft.com/SQLServer/feedback/ViewFeedback.aspx?FeedbackID=254395
DISTINCT clause for aggregates:
https://connect.microsoft.com/SQLServer/feedback/ViewFeedback.aspx?FeedbackID=254393
ROWS and RANGE window subclauses:
https://connect.microsoft.com/SQLServer/feedback/ViewFeedback.aspx?FeedbackID=254392
Vector expressions:
https://connect.microsoft.com/SQLServer/feedback/ViewFeedback.aspx?FeedbackID=254391
TOP OVER:
https://connect.microsoft.com/SQLServer/feedback/ViewFeedback.aspx?FeedbackID=254390
LAG and LEAD functions:
https://connect.microsoft.com/SQLServer/feedback/ViewFeedback.aspx?FeedbackID=254388
ORDER BY for aggregates:
https://connect.microsoft.com/SQLServer/feedback/ViewFeedback.aspx?FeedbackID=254387
Para resolver este problema con las herramientas actuales, podemos usar lo sgte:
- cursor
- query correlacionado
- funcion CLR
- Tabla de totales que se actualiza mediante un trigger cada vez que executamos una sentencia en las tablas base.
No existe un metodo especial que brinde simplicidad y buen desempenio . Debes probar los diferentes metodos y escojer el que mejor se adapte a tu ambiente.
El maestro Itzik Ben-Gan tiene una serie de articulos donde compara los distintos metodos. Para ver los articulos necesitas estar subscrito a la revista, pero puedo decir que vale la pena.
Set-Based vs. Cursor-Based Solutions for Running Aggregates
http://www.sqlmag.com/article/articleid/101736
Subqueries and Joins for Running Aggregates
http://www.sqlmag.com/article/articleid/101623
CLR-Based Solution for Running Aggregates
http://www.sqlmag.com/articles/index.cfm?articleid=102097
No importa que metodo usemos, es importante que cada transaccion pueda ser identificada unicamente y que podamos establecer un orden cronologico de ocurrencia. Como vez, en tu ejemplo no podemos identificar cada transaccion unicamente, y mucho menos saber el orden en que ocurrio la transaccion.
Ejemplo (usando query correlacionado):
DECLARE @t TABLE ( sk int NOT NULL IDENTITY(1, 1) UNIQUE, Id int, Entradas numeric(6, 2), Salidas numeric(6, 2), fecha_trans datetime NOT NULL DEFAULT (GETDATE()) ); INSERT INTO @t(id, Entradas, Salidas) VALUES(1, 100.00, 0.00); INSERT INTO @t(id, Entradas, Salidas) VALUES(1, 100.00, 0.00); INSERT INTO @t(id, Entradas, Salidas) VALUES(1, 0.00, 100.00); INSERT INTO @t(id, Entradas, Salidas) VALUES(1, 100.00, 0.00); INSERT INTO @t(id, Entradas, Salidas) VALUES(1, 100.00, 0.00); INSERT INTO @t(id, Entradas, Salidas) VALUES(1, 200.00, 0.00); SELECT T1.*, ( SELECT SUM(Entradas) + SUM(-1 * Salidas) FROM @t AS T2 WHERE T2.id = T1.id AND (T2.fecha_trans < T1.fecha_trans OR (T2.fecha_trans = T1.fecha_trans AND T2.sk <= T1.sk)) ) AS Saldo FROM @t AS T1 ORDER BY T1.id, T1.fecha_trans, T1.sk; GO
AMB- Marcado como respuestaXhester Rodriguez martes, 24 de noviembre de 2009 19:32
- Primero que nada muchisimas gracias por responder te lo agradezco mucho.
Tu respuesta es excelente, muy completa y precisa. Me ayudó a despejar muchas dudas que tenia, ya he probado estas tres formas:
- cursor
- query correlacionado
- Tabla de totales que se actualiza mediante un trigger cada vez que executamos una sentencia en las tablas base.
Segun lo que entendi en el documento la del cursor es un poco mas rapida pero mas larga y complicada de crear pero al parecer vale la pena, es una lastima que SQL Server tenga un soporte tan pobre en la funcion OVER ya que leyendo me di cuenta que en otras bases de datos como Oracle la funcion OVER hace esto de una forma limpia y sencilla solo haciendo esto:
SELECT empid, ordermonth, qty,
SUM(qty) OVER(PARTITION BY empid
ORDER BY ordermonth) AS cumulativeqty
FROM EmpOrders;
Por lo pronto ya vote para tener estas facilidades en la proxima version de sql "Ya que me imagino que en las versiones mas nuevas 'actuales' de SQL aun no estan implementadas".
La que no probé fue la funcion CLR esto por que no encontre mucha información al respecto si me pudieras dar algun ejemplo o link con ejemplos para probarla te lo agradeceria mucho.
Hasta el momento me estoy inclinando por la opcion del trigger que se encargaria de crear/actualizar los saldos en mi tabla al momento insertar alguna fila, solo que no se me aclara muy bien el panorama de como tendria que hacer el update para que me actualize o genere los saldos(Hize un update sencillo y funciono a la perfeccion el detalle esta en hacer el update que actualize los saldos en forma acomulada ahi es donde no me queda muy claro). Te estaria muy agradecido si me ayudaras con un pequeño ejemplo de como tendria que hacer el update en el trigger para que crear/actualizar los saldos. Nada mas como sería el update el tema del trigger me queda muy claro.
Espero no abusar de tu buena voluntad, la verdad que con lo que ya me ayudaste es mas que suficiente, pero si me vendria bien un breve ejemplo. XD
Nuevamente muchisisimas gracias.
Salu2
Xhester - Xhester,
Gracias por tu voto. Estoy seguro que la introduccion de esas facilidades seran de mucha ayuda.
> La que no probé fue la funcion CLR esto por que no encontre mucha información al respecto si me pudieras dar algun ejemplo o link
> con ejemplos para probarla te lo agradeceria mucho.
Aqui tienes un magnifico ejemplo.
Running sums yet again: SQLCLR saves the day!
http://sqlblog.com/blogs/adam_machanic/archive/2006/07/12/running-sums-yet-again-sqlclr-saves-the-day.aspx
> Hasta el momento me estoy inclinando por la opcion del trigger que se encargaria de crear/actualizar los saldos en mi tabla al momento insertar alguna fila
Dejame aclarar una cosa antes de continuar. Mencione una tabla de saldos, y me referi a casos donde tenemos por ejemplo una cuenta y necesitamos saber el saldo a la fecha, por lo que mantener el saldo en una segunda tabla y no tener que recorrer las filas constantemente para saber el saldo, nos ahorraria un poco de tiemepo. Ahora, este no es el caso pues deseas el saldo por transaccion y no necesariamente el ultimo a la fecha.
Usemos el mismo ejemplo que atache a mi primer post. Vamos a adicionar la columna [Saldo] y crear un trigger "after insert". Fijate que solo voy a crear el trigger para despues de insercion, asi que queda de tu parte escribir uno que maneje los casos de actualizacion y borrado, en caso de que esto sea permitido.
USE tempdb; GO CREATE TABLE dbo.T ( sk int NOT NULL IDENTITY(1, 1) UNIQUE, Id int, Entradas numeric(6, 2), Salidas numeric(6, 2), fecha_trans datetime NOT NULL DEFAULT (GETDATE()), Saldo numeric(10, 2) ); GO SET ANSI_NULLS ON; GO SET QUOTED_IDENTIFIER ON; GO CREATE TRIGGER dbo.tr_T_ins ON dbo.T AFTER INSERT AS IF @@ROWCOUNT = 0 RETURN; SET NOCOUNT ON; BEGIN TRY ;WITH r_set AS ( SELECT T1.sk, T1.Id, T1.fecha_trans, ( SELECT ISNULL(SUM(Entradas), 0) + ISNULL(SUM(-1 * Salidas), 0) FROM dbo.T AS T2 WHERE T2.id = T1.id AND (T2.fecha_trans < T1.fecha_trans OR (T2.fecha_trans = T1.fecha_trans AND T2.sk <= T1.sk)) ) AS Saldo FROM dbo.T AS T1 INNER JOIN INSERTED AS I ON T1.sk = I.sk ) UPDATE T1 SET T1.Saldo = R.Saldo FROM dbo.T AS T1 INNER JOIN r_set AS R ON T1.sk = R.sk ; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION; SELECT ERROR_NUMBER() AS ERROR_NUMBER, ERROR_MESSAGE() AS [ERROR_MESSAGE]; END CATCH GO INSERT INTO dbo.T(id, Entradas, Salidas) VALUES(1, 100.00, 0.00); GO SELECT * FROM dbo.T ORDER BY Id, sk; GO INSERT INTO dbo.T(id, Entradas, Salidas) VALUES(1, 100.00, 0.00); GO SELECT * FROM dbo.T ORDER BY Id, sk; GO INSERT INTO dbo.T(id, Entradas, Salidas) VALUES(1, 0.00, 100.00); GO SELECT * FROM dbo.T ORDER BY Id, sk; GO INSERT INTO dbo.T(id, Entradas, Salidas) VALUES(1, 100.00, 0.00); GO SELECT * FROM dbo.T ORDER BY Id, sk; GO INSERT INTO dbo.T(id, Entradas, Salidas) VALUES(1, 100.00, 0.00); GO SELECT * FROM dbo.T ORDER BY Id, sk; GO INSERT INTO dbo.T(id, Entradas, Salidas) VALUES(1, 200.00, 0.00); GO SELECT * FROM dbo.T ORDER BY Id, sk; GO DROP TABLE dbo.T; GO
Ten en cuenta que los triggers se ejecutan bajo la misma transaccion donde la sentencia que los dispara participa. Se prudente porque estos influyen en cuanto durara la transaccion.
AMB- Marcado como respuestaGustavo LarrieraMVP, Moderadormartes, 24 de noviembre de 2009 19:33
- Nuevamente una respuesta excelente y un claro y sencillo ejemplo, muchisisimas gracias por tu ayuda ya optimizé el trigger con mi aplicación y parece estar funcionando correctamente. Nuevamente agradezco mucho tu ayuda y tu tiempo. Pero como no me canso de molestarte XD ahora estoy pensando en implementar esto mismo en SQL Server Compact y me queda la duda de cual es la mejor forma de hacerlo , ya que SQL Server Compact no acepta triggers y esta mas limitado.
O si hubiera alguna otra forma de usar una base de datos sql que la forma de conexion fuera tan simple como se hace con access es decir un solo archivo al cual conectarse si necesidad de instalar nada, como en el caso de sql que se tiene que instalar minimo el SQL Server Express que sirva como servidor en alguna maquina, yo quiero una aplicación que dependa solo de un archivo como en el caso de SQL Server Compact que se crea solo el archivo .sdf y no se necesita instalar nada mas. Pero SQL Server Compact esta muy limitado, por eso busco otra alternativa sin instalar todo el Server SQL.
De verdad muchas gracias.
Saludos
Xhester - Xhester,
Me temo que esta vez no te pueda ser de ayuda, pues no he tenido oportunidad de trabajar con la version compacta.
AMB - No hay problema, ya realize una pregunta en el foro acerca de esto.
Nuevamente muchas gracias por todo.