Supongamos la siguiente situación:

  • Estamos desarrollando una aplicación para un supermercado muy grande, que tiene habilitadas 40 cajas, en las cuales se registran ventas desde las 06:30 hasta las 21:30 de cada día.
  • Todos los días compran productos
  • Hay días en que los gerentes y el personal administrativo necesitan conocer el saldo actualizado de los productos (de todos, o de algunos de ellos) no una ni dos ni diez veces sino cientos de veces.

Entendiendo el problema

Uno podría pensar que en situaciones así la solución sería tener en la tabla de PRODUCTOS una columna llamada CANTIDAD_ACTUAL y cada vez que se compra o se vende un producto:

  • bloquear el registro
  • actualizar el registro
  • desbloquear el registro

de hecho, algo así es lo que normalmente hacen quienes están acostumbrados a usar tablas .DBF

Después de  todo, si actualizar el registro tarda unas milésimas de segundos ¿por qué no hacerlo así?

El problema no es el UPDATE, el problema es la transacción.

Una transacción correctamente diseñada debe ser corta, por lo tanto muy rápida, y terminar siempre con un COMMIT o con un ROLLBACK.

Desde el momento en que escribiste el UPDATE, hasta el momento del COMMIT o el ROLLBACK, el registro involucrado queda bloqueado. Es cierto, normalmente eso debería durar milésimas de segundos.

Pero, lastimosamente no siempre sucede así, un problema de hardware podría hacer que la conexión entre la computadora Cliente y la computadora Servidor se interrumpa, dejando la transacción abierta y por lo tanto impidiendo a todas las demás computadoras actualizar esa fila.

También, sin darte cuenta, podría existir alguna condición que nunca llama a un COMMIT o un ROLLBACK. Algo así no debería suceder pero ya sabes como son las cosas: no debe, pero sucede.

En una empresa medianamente grande no es un solo programador quien desarrolla programas, entonces aunque tú seas súper cuidadoso en este tema, podría ocurrir que otro programador no lo sea tanto y se haya olvidado de un COMMIT o de un ROLLBACK o su stored procedure haya entrado en un ciclo infinito. Y aunque hoy seas el único programador, la semana que viene podrían necesitar a otro para desarrollar algunos programitas, la Base de Datos es propiedad de la empresa, no es tuya, y por lo tanto no siempre puedes impedir que otro la utilice para sus desarrollos.

Es por consiguiente, muy mala práctica en SQL (no solamente en Firebird, en SQL en general) estar haciendo UPDATE de tablas cuyos registros se usan mucho. Y sin olvidarnos del hecho de que cada UPDATE deja basura en la Base de Datos, haciéndola más grande y más lenta.

¿Cuántas veces se necesita consultar el saldo de los productos cada día? ¿una vez, dos veces, diez veces? eso es demasiado poco, y no justifica bloquear las operaciones de venta, que son las que dejan dinero a la empresa, por algo que puede ser evitado y que además muy pocas veces en el día se necesita.

Es distinto si esos saldos necesitan ser consultados cientos de veces por día. ¿Qué hacemos en ese caso?

¿Cómo lo solucionamos?

Estas son algunas posibilidades (hay más, como puedes leer en el artículo: “Actualización de saldos: cuando sí y cuando no” y en los comentarios de ese artículo):

  1. En la tabla PRODUCTOS tenemos una columna llamada CANTIDAD_ACTUAL que es actualizada cada vez que entra un producto (generalmente por una compra) o sale un producto (generalmente por una venta)
  2. Cuando necesitamos conocer la cantidad actual de un producto escribimos un SELECT que sume las entradas y reste las salidas
  3. Usamos una tabla auxiliar para guardar en ella los saldos actualizados de los productos

Solución 1. Con una columna CANTIDAD_ACTUAL

Como vimos en la sección “Entendiendo el problema” esta solución no debemos emplear en SQL porque corremos el gran riesgo de que por culpa de una falla en el hardware o en el software la transacción se quede abierta (activa, en terminología Firebird) y le complique la vida a todos los demás usuarios.

Si por ejemplo, en la Caja 7 se estaba registrando la venta de 20 productos y ocurrió una falla en el hardware o en el software que impidió a la transacción finalizar con un COMMIT o con un ROLLBACK, ninguna de las demás Cajas podrá vender a alguno de esos 20  productos. Inclusive, si una Caja lo intenta y la transacción está en modo WAIT, esa Caja también se quedará congelada. Y si está en modo NO WAIT, no se quedará congelada pero no se podrá vender ese producto, perdiéndose por lo tanto una venta (y los dueños del supermercado no se acordarán muy bien ni del programador ni de la madre del programador).

Solución 2. Con un SELECT

Esto es perfectamente factible cuando la cantidad de consultas diarias es poca, los movimientos diarios no son muchos, y las computadoras son rápidas.

Es la solución más sencilla porque con un simple SELECT solucionamos el problema.

Si la transacción es SNAPSHOT, READ ONLY, rápidamente obtendremos los resultados.

Solución 3. Con una tabla auxiliar

Esta es la mejor solución cuando la cantidad de consultas diarias es grande (cientos o miles de veces) o los movimientos diarios son muchos (miles o millones).

Funciona de la siguiente manera:

En una tabla llamada SALDOS_PRODUCTOS se guardan el Identificador del Producto, la fecha y hora en que se obtuvo la cantidad, y la cantidad obtenida.

SALDOS1

Captura 1. Si haces clic en la imagen la verás más grande

Los registros de esta tabla son actualizados cada vez que un SELECT los involucra. Cuidado con esto: no se actualizan cuando se compra o se vende un producto sino cuando alguien quiere conocer la cantidad actual de ese producto.

Por lo tanto, la tabla PRODUCTOS jamás es actualizada cuando se hace una compra o una venta. De esa manera, aunque ocurra un problema de hardware o de software en una Caja las demás Cajas ni se enterarán y seguirán vendiendo normalmente.

La que se actualiza es la tabla SALDOS_PRODUCTOS, pero no cuando ocurre una compra o una venta sino cuando se hace un SELECT a esa tabla.

Para cada producto involucrado en un SELECT a la tabla SALDOS_PRODUCTOS se le suman todas las entradas y se le restan todas las salidas ocurridas desde la última actualización. Y solamente ellas. Si por ejemplo hace 20 minutos se consultó el saldo del producto “Coca Cola de 1 litro” y ahora se vuelve a consultar el saldo de ese producto, se suman las entradas y se restan las salidas ocurridas durante estos últimos 20 minutos, nada más.

¿Qué se consigue con esta técnica?

  1. Que las ventas de las Cajas jamás se interrumpan por culpa de un fallo en el hardware o en el software de otra Caja o de otra computadora. Cada Caja solamente hace INSERTs, jamás hace un UPDATE o un DELETE, y por lo tanto jamás podría bloquear a un registro (los INSERT nunca bloquean registros, solamente los UPDATE y los DELETE pueden bloquearlos).
  2. Que los SELECTs sean rapidísimos porque no procesan a todos los movimientos ocurridos desde hace meses o años, sino solamente a los ocurridos desde el SELECT anterior, lo cual pudo haber sido hace pocos minutos.

¿Hay algún problema con esta técnica?

Sí, una falla en el hardware o en el software también podría causar que queden registros bloqueados en la tabla SALDOS_PRODUCTOS pero eso solamente afectará a quienes quieren consultar los datos de los productos, no afectará la registración ni de las compras ni de las ventas.

Además, cada vez que se quiere consultar el saldo de un producto se deben procesar todas las entradas y todas las salidas ocurridas desde la consulta anterior. Y si no ocurrieron ni entradas ni salidas entonces se está perdiendo tiempo en comprobar que no ocurrieron ni entradas ni salidas.

Conclusión:

Usar la técnica adecuada puede hacer una gran diferencia en cuanto a la “bondad” de tu aplicación. Si te equivocas, podrías hacerles perder mucho tiempo a tus usuarios y también podrías hacerles perder ventas.

La Solución 3. es la que requiere más trabajo de tu parte pero en contrapartida es, de lejos, la mejor cuando hay muchos movimientos o muchas consultas.

Artículos relacionados

Actualización de saldos: cuando sí y cuando no (lee también los comentarios)

El índice del blog Firebird21