Optimizando SuperServer: poniendo toda la Base de Datos en la memoria caché

5 comentarios

Hay un truco que podemos utilizar para optimizar a SuperServer, funciona también en Classic pero solamente con bases de datos mucho más pequeñas.

Como recordarás, si las filas que devolverá un SELECT ya se encuentran en la memoria caché entonces la velocidad de respuesta será muy alta porque la memoria RAM es muchísimo más rápida que el disco duro. Entonces, la idea es tener a toda la Base de Datos (o al menos a las tablas más utilizadas cuando tener a toda la Base de Datos no es posible) en la memoria caché.

Para poner a toda la Base de Datos en la memoria caché podríamos ejecutar el siguiente stored procedure:

Listado 1.

CREATE PROCEDURE LLENAR_CACHE
AS
   DECLARE VARIABLE lcNombreTabla   VARCHAR(1024);
   DECLARE VARIABLE lcComando       VARCHAR(1024);
   DECLARE VARIABLE lnCantidadFilas INTEGER;
BEGIN
   
   FOR SELECT
      RDB$RELATION_NAME
   FROM
      RDB$RELATIONS
   INTO
      :lcNombreTabla
   DO BEGIN
      lcComando = 'SELECT COUNT(*) FROM ' || lcNombreTabla;
      EXECUTE STATEMENT lcComando INTO :lnCantidadFilas;
   END

END;

Luego de ejecutar el comando EXECUTE PROCEDURE LLENAR_CACHE y el comando COMMIT, toda la Base de Datos se encontrará en la memoria caché (si hay suficiente espacio en la memoria caché, por supuesto). ¿Por qué? Porque cada vez que se lee una página de una tabla esa página es puesta en la memoria caché. La función COUNT(*) lee cada página de una tabla y por consiguiente coloca a cada página de esa tabla en la memoria caché. El stored procedure LLENAR_CACHE realiza esa operación para todas las tablas de la Base de Datos. Como resultado final, todas las páginas de todas las tablas se encontrarán en la memoria caché, si es que hay allí suficiente espacio libre.

¿Y si no hay suficiente espacio libre?

Entonces podríamos poner en la memoria caché a las tablas más utilizadas en los informes.

Listado 2.

CREATE PROCEDURE LLENAR_CACHE
AS
BEGIN
   SELECT COUNT(*) FROM BANCOS;
   SELECT COUNT(*) FROM PRODUCTOS;
   SELECT COUNT(*) FROM CLIENTES;
   SELECT COUNT(*) FROM PROVEEDORES;
   SELECT COUNT(*) FROM STOCK;
   SELECT COUNT(*) FROM MONEDAS;
END; 

Eso hará que todas las operaciones de lectura se realicen súper rápido. Pero durante el trabajo normal diario muchas operaciones de INSERT, UPDATE, y DELETE se irán ejecutando y en la memoria caché ya no se encontrarán las últimas filas insertadas, actualizadas, y borradas.

¿Y entonces?

Entonces lo que debemos hacer es volver a ejecutar el stored procedure LLENAR_CACHE para que nuevamente en la memoria caché se encuentre toda la Base de Datos (como en el caso del Listado 1.) o las tablas más utilizadas (como en el caso del Listado 2.).

Ese proceso podríamos hacerlo manualmente (por ejemplo, cada hora) pero es mucho más inteligente pedirle al Sistema Operativo que realice esa tarea.

Para ello crearemos un archivo de script y un archivo batch.

Listado 3.

CONNECT MiBaseDatos.FDB USER SYSDBA PASSWORD masterkey;

-- Primera vez
EXECUTE PROCEDURE LLENAR_CACHE;
COMMIT;
SHELL PING 192.0.2.2 -n 1 -w 36000 > NUL;

-- Segunda vez
EXECUTE PROCEDURE LLENAR_CACHE;
COMMIT;
SHELL PING 192.0.2.2 -n 1 -w 5000 > NUL;

-- Finalizar
EXIT;

Escribimos el contenido del Listado 3. en el bloc de notas y lo grabamos con el nombre LLENAR_CACHE.SQL

¿Qué hace este archivo de script?

Primero, se conecta a nuestra Base de Datos. Segundo, ejecuta el stored procedure llamado LLENAR_CACHE. Tercero, para finalizar la transacción hace un COMMIT. Cuarto, ejecuta el comando PING del Sistema Operativo donde la dirección IP mostrada es una IP que nunca existe y se usa normalmente para pruebas, la opción -n indica la cantidad de repeticiones, la opción -w indica la cantidad de milisegundos (36.000 milisegundos = 1 hora), y al redirigir a NUL no se ve salida en la pantalla.

En el Listado 3. el stored procedure LLENAR_CACHE se ejecutó 2 veces, con una hora de diferencia entre esas ejecuciones. Si la Base de Datos se usará durante 10 horas seguidas cada día, entonces deberíamos escribir 10 veces el EXECUTE PROCEDURE, el COMMIT, y el SHELL. Para que el Listado 3. no sea muy largo solamente escribí 2 veces esos comandos, en tu caso podrías necesitar escribirlos 8 veces, 10 veces, 14 veces, etc.

Listado 4.

C:
CD "\ARCHIVOS DE PROGRAMA\FIREBIRD\FIREBIRD_2_5\BIN"
ISQL -INPUT C:\USERS\WALTER\DESKTOP\LLENAR_CACHE.SQL

¿Qué hace este archivo batch?

Primero, se ubica en la unidad C:. Segundo, se ubica en la carpeta donde se encuentra el programa ISQL.EXE. Tercero, ejecuta el programa ISQL.EXE con la opción -INPUT y el nombre completo del archivo de script.

En síntesis, ejecuta al programa ISQL.EXE pidiéndole que ejecute los comandos que se encuentran en el archivo de script.

¿Cómo ejecutamos ese archivo batch?

Tenemos dos formas:

  • Manualmente
  • Automáticamente

Manualmente sería haciendo clic en él cada día antes de que el primer usuario empiece a trabajar, por ejemplo a las 7:30 de cada día. El problema es que podríamos olvidarnos de hacer ese clic o llegamos tarde al trabajo o estamos de vacaciones, etc.

Automáticamente sería mediante el Programador de Tareas del Windows. Podríamos programar una tarea que se ejecute cada vez que se enciende el Servidor o cada día a las 7:30. También podríamos hacerlo en nuestra aplicación: si detectamos que nadie está usando la Base de Datos entonces ejecutamos el archivo batch.

Conclusión:

Si la arquitectura que usamos es SuperServer entonces podemos conseguir que los resultados de las consultas sean rapidísimos poniendo a toda la Base de Datos en la memoria caché. Si no tenemos suficiente memoria para poner a toda la Base de Datos en la memoria caché, entonces pondremos allí a las tablas más utilizadas.

Dependiendo de nuestro caso crearíamos un stored procedure como el mostrado en el Listado 1. o como el mostrado en el Listado 2.

Luego creamos un archivo de script como el mostrado en el Listado 3., el cual se encargará de llenar la memoria caché. Como los usuarios estarán insertando, actualizando, y borrando filas de las tablas el contenido de la memoria caché se irá quedando desactualizado. Por eso cada hora (o el tiempo que te parezca conveniente) se vuelve a llenar la memoria caché con el contenido actual de cada tabla.

Finalmente, creamos un archivo batch que se encargará de ejecutar al programa ISQL.EXE teniendo como entrada al archivo de script. Ese archivo batch debería ser ejecutado cada día antes de que el primer usuario se conecte a la Base de Datos. Esto podríamos hacerlo manualmente o mucho mejor, automáticamente.

Artículos relacionados:

El índice del blog Firebird21

El foro del blog Firebird21

 

Cambiando el tipo de una columna: de caracter a numérico y viceversa

3 comentarios

Lo ideal es siempre que una vez que has definido la cantidad de columnas de cada tabla y el tipo de cada una de esas columnas ya no necesites modificarlas. Pero lo ideal … no siempre es lo que ocurre en la vida real.

Entonces, ahora te encuentras con una situación en la cual consideras conveniente o imprescindible cambiar el tipo de una columna. Por ejemplo era VARCHAR y la quieres convertir a INTEGER. O era SMALLINT y la quieres convertir a CHAR, cosas así.

El comando deseado

Si tienes una columna que es de tipo CHAR o VARCHAR y la quieres cambiar a INTEGER muchos desearían que existiera algo similar a:

ALTER TABLE
   MiTabla
ALTER COLUMN
   MiColumna
TYPE
   INTEGER

Pero tal comando no existe en Firebird, si lo intentas obtendrás el mensaje: “Cannot change datatype for column MiColumna from a character type to a non-character type.

¿Por qué, cuál es el problema? El problema es que Firebird no hace la conversión en el momento en que escribes un comando como el anterior sino en el momento en que consultas su valor (con un SELECT) o cambias su valor (con un UPDATE). Por lo tanto cuando escribes el comando ALTER TABLE él no puede saber si todos los valores pueden convertirse a INTEGER o no. Y entonces hace lo más seguro: rechaza el cambio.

Claro, tú puedes decir: “pero si solamente hay números enteros en esa columna, debería aceptar el cambio del tipo de datos”. Eso tú lo sabes, pero el Firebird no lo sabe porque no verifica que efectivamente así sea. ¿Y por qué no lo verifica? Porque tu tabla podría tener millones y millones de filas y verificarla podría demorar mucho tiempo, por lo tanto hace la sencilla: impide que modifiques el tipo de datos.

La solución

Ya hemos visto cual es el problema, ahora veremos que hay una solución. No es tan sencilla como escribir un comando ALTER TABLE pero funciona.

Paso 1. Verificar que todos los valores puedan ser convertidos a INTEGER

Evidentemente que el 100% de los valores deben poder convertirse a INTEGER, si hay al menos uno que no puede convertirse entonces primero hay que solucionar eso y luego continuar con los siguientes pasos.

SELECT
   CAST(MiColumna AS INTEGER) AS TodosSonNumerosEnteros
FROM
   MiTabla;

Si el SELECT anterior terminó con errores, y quieres ver cuales son las filas que tienen esos errores (o sea, las filas que no tienen números enteros) el siguiente artículo te muestra lo que puedes hacer para ver a esas filas problemáticas:

Validando que el contenido de un CHAR o VARCHAR sea numérico

Si el SELECT anterior terminó sin errores, entonces seguimos con el:

Paso 2. Renombrar la vieja columna

COMMIT;

ALTER TABLE
   MiTabla
ALTER COLUMN
   MiColumna
TO
   ColumnaVieja;

Si esta columna tiene dependencias (es decir, si ha sido usada en alguna vista, stored procedure o trigger) entonces obtendrás un mensaje de error. Debes solucionar eso antes de poder continuar.

Este paso es muy importante porque te permite descubrir si la columna tiene dependencias.

Paso 3. Agregarle una columna a la tabla con el tipo de datos deseado

COMMIT;

ALTER TABLE
   MiTabla
ADD
   MiNuevaColumna INTEGER;

Paso 4. Agregarle los valores a la nueva columna

COMMIT;

UPDATE
   MiTabla
SET
   MiNuevaColumna = CAST(ColumnaVieja AS INTEGER)
WHERE
   ColumnaVieja IS NOT NULL;

Paso 5. Verificar que la nueva columna tenga los valores correctos

COMMIT;

SELECT
   (MiNuevaColumna + 1)
FROM
   MiTabla;

Si el Firebird te muestra algún mensaje de error (supuestamente todo debería estar ok, pero siempre lo mejor es asegurarse de que así sea) entonces tienes que buscar el motivo y solucionarlo, antes de continuar.

Paso 6. Borrar la columna vieja

Si el SELECT anterior finalizó sin errores, entonces ya podemos borrar con toda seguridad a la columna vieja, porque ya no la necesitaremos más.

COMMIT;

ALTER TABLE
   MiTabla
DROP COLUMN
   ColumnaVieja;

COMMIT;

Este paso es muy conveniente hacerlo para no confundirnos. Si no borramos a la columna vieja más adelante podríamos estar asignándole valores, algo que muy probablemente ya no deberíamos hacer.

Paso 7 (opcional). Renombrar a la nueva columna

Si deseas que la nueva columna tenga el mismo nombre que tenía la columna vieja, entonces:

ALTER TABLE
   MiTabla
ALTER COLUMN
   MiNuevaColumna
TO
   MiColumna

COMMIT;

Paso 8 (opcional). Poner a la nueva columna en la misma posición que tenía la vieja columna

Si quieres que la nueva columna que acabas de crear se encuentre en la misma posición que tenía la columna vieja que acabas de borrar, entonces:

ALTER TABLE
   MiTabla
ALTER
   MiColumna
POSITION
   6;

COMMIT;

Si la vieja columna se encontraba en la posición 1, entonces después de POSITION escribirías 1. Si se encontraba en la posición 2, escribirías 2, y así sucesivamente. El 6 que se puso arriba es solamente un ejemplo.

Conclusión:

En Firebird no hay un comando que nos permita cambiar el tipo de datos de una columna. Y no lo hay por nuestra propia seguridad, de esa manera se evita que cometamos errores graves como convertir texto a INTEGER.

Sin embargo, si necesitamos hacerlo hay formas de conseguirlo. En este artículo se mostró la forma más segura de todas (no la única, pero sí la más segura y conveniente de utilizar).

Con el Paso 1. verificamos que todos los números sean enteros. Con el Paso 2. verificamos que no hay dependencias. Con el Paso 3. agregamos una columna que tiene el tipo de datos que deseamos tener. Con el Paso 4. le agregamos a la nueva columna los valores que debe tener. Con el Paso 5. verificamos que la nueva columna tenga los valores correctos. Con el Paso 6. borramos a la vieja columna, esto nos evitará confusiones más adelante. Con el Paso 7. renombramos a la nueva columna, para que se llame igual a la columna original. Con el Paso 8. ponemos a la nueva columna en la misma posición que tenía la columna original.

Siguiendo estos pasos te asegurarás que todo se ha realizado correctamente. Todo quedará como debe quedar.

Por lo tanto, si alguna vez necesitas cambiar el tipo de datos de una columna, ya sabes como lograrlo.

Artículos relacionados:

Validando que el contenido de un CHAR o VARCHAR sea numérico

El índice del blog Firebird21

El foro del blog Firebird21

Evitando actualizaciones concurrentes del mismo registro

2 comentarios

Como ya hemos visto, hay técnicas para evitar conflictos entre transacciones, esos conflictos ocurren cuando dos o más de ellas quieren actualizar o borrar el mismo registro al mismo tiempo.

Evitando conflictos en las transacciones

Pero a veces no podemos usar esas técnicas porque debemos estar actualizando a los mismos registros concurrentemente, sí o sí. ¿Cómo resolvemos el problema en ese caso?

Bien, primero veamos de manera simplificada cuales son las tareas que realiza el motor del Firebird cuando una transacción READ WRITE READ COMMITTED quiere actualizar un registro:

  1. Lee el registro
  2. Evalúa los datos que se guardarán en el nuevo registro
  3. Escribe el nuevo registro en una página de datos

Recuerda que en Firebird cada vez que se usa el comando UPDATE o el comando DELETE se le está agregando un nuevo registro a la Base de Datos. Si la transacción finaliza con un COMMIT entonces el nuevo registro será el que vale, el que sirve, el que se usará de ahí en adelante y el registro viejo será basura. En cambio, si la transacción finaliza con un ROLLBACK el registro anterior seguirá siendo usado y el nuevo pasará a engrosar la basura. Como ves, en ambos casos se genera basura, y ese es el motivo por el cual periódicamente hay que eliminarla.

¿Y cuál es el problema con esta forma de proceder del Firebird?

Una transacción que se inició con los parámetros anteriores, en el paso 1. lee el último registro confirmado (o sea, que finalizó con un COMMIT) y al llegar al paso 3. vuelve a leerlo para asegurarse que se trata del mismo registro. Si no lo hiciera entonces podría estar silenciosamente sobreescribiendo el trabajo de otra persona.

Si el registro que leyó en el paso 1. es distinto del registro que leyó en el paso 3. entonces lanzará una excepción que nos informará que hay un error en la actualización. Un deadlock. Un bloqueo mortal. La actualización falló.

El tiempo que el Firebird demora en completar los pasos del 1. al 3. es pequeñísimo, se mide en milisegundos, así que usualmente no tendremos problemas con eso … salvo cuando varios usuarios estén actualizando a muchísimos registros al mismo tiempo, pues allí sí tales circunstancias podrían darse.

El Firebird solamente “bloquea” a un registro cuando lo está actualizando, mientras no lo esté actualizando el registro está desbloqueado. Esto significa que si queremos bloquear a un registro, debemos actualizarlo, aunque sea escribiendo en él lo mismo que ya tenía escrito.

Y eso es lo que hace el comando SELECT … WITH LOCK.

Y es ese comando el que deberíamos usar cuando queremos ejecutar un proceso de actualización y queremos tener la seguridad de que no tendremos conflictos con otras transacciones. Esta es una forma de transacción pesimista, la cual aunque no es recomendada en Firebird a veces puede ser necesario usar y por ese motivo es que existe.

El algoritmo entonces sería el siguiente:

SET TRANSACTION 
READ WRITE 
WAIT 
READ COMMITTED 
NO RECORD_VERSION 
LOCK TIMEOUT 5 ;
SELECT MisColumnas FROM MiTabla WHERE MiCondición WITH LOCK ;
UPDATE MiTabla SET MisColumnas = MisNuevosValores WHERE MiCondición ;

¿Qué se hizo aquí?

Primero: se establecieron los parámetros de la transacción:

READ WRITE: La transacción puede leer y también puede escribir en los registros

WAIT: Si otra transacción está actualizando el registro, esta transacción esperará

READ COMMITTED: Leerá la última versión confirmada del registro, sin importar cuando ese registro fue confirmado. Si antes de que esta transacción empezara, o después.

NO RECORD_VERSION: No podrá leer un registro que otra transacción está actualizando

LOCK TIMEOUT 5: Esperará un máximo de 5 segundos hasta que la otra transacción que está actualizando al registro finalice con un COMMIT o con un ROLLBACK. Desde luego que podrías poner más o menos segundos, como te parezca.

Segundo: se consultaron y se bloquearon los registros. Al usar la cláusula WITH LOCK le estamos diciendo que también haga un UPDATE silencioso, para que ninguna otra transacción pueda actualizar a este registro antes de que nosotros lo actualicemos. Fíjate que este segundo paso puede fallar, fallará si ya otra transacción tiene bloqueado a cualquiera de los registros de MiTabla que cumplen con MiCondición.

Tercero: se actualizaron los registros. Si el segundo paso finalizó con éxito entonces este tercer paso también finalizará con éxito. Para eso justamente sirve el SELECT … WITH LOCK, para asegurarnos de que el UPDATE finalizará con éxito.

Observaciones:

Este método es muy efectivo, aunque raramente necesario. En Firebird lo normal y lo correcto es que las transacciones sean optimistas y este es un ejemplo de transacción pesimista. Por ello lo recomendable es que los registros que son bloqueados por el SELECT … WITH LOCK sean muy pocos, idealmente solamente un registro y no más de uno.

La ventaja de este método es que si el SELECT … WITH LOCK finalizó con éxito entonces tendremos una seguridad del 100% de que el UPDATE también finalizará con éxito. Desde luego que para que eso sea verdad, MiCondición debe ser la misma tanto en el SELECT como en el UPDATE. Si ponemos condiciones distintas, nada podremos asegurar.

Conclusión:

En general, y como regla, deberíamos evitar usar los comandos UPDATE y DELETE ya que los conflictos entre transacciones solamente pueden ocurrir cuando se usan esos comandos.

Sin embargo, a veces es necesario usar esos comandos, no podemos evitarlo. En esos casos casi siempre es preferible que nuestras transacciones sean optimistas, porque el Firebird está optimizado para usar transacciones optimistas. Solamente cuando no tenemos alternativa deberíamos usar transacciones pesimistas, y en este artículo se vio una técnica de ello.

Artículos relacionados:

Entendiendo a las transacciones

Modos de bloqueo de las transacciones

Transacciones optimistas y transacciones pesimistas

Algo más sobre transacciones optimistas y transacciones pesimistas

¿Por qué en Firebird es preferible que las transacciones sean optimistas?

Evitando conflictos en las transacciones

Entendiendo las páginas de la Base de Datos

El índice del blog Firebird21

El foro del blog Firebird21

 

Evitando conflictos en las transacciones

2 comentarios

A las bases de datos de Firebird normalmente se conectan muchos usuarios y en ocasiones quieren actualizar o borrar la misma fila y al mismo tiempo y eso puede causar conflictos. ¿Cómo los evitamos?

Primero, debemos tener presente que es normal que existan conflictos y que debemos prepararnos para lidiar con ellos.

Segundo, en muchos casos es posible evitar esos conflictos si diseñamos inteligentemente la Base de Datos.

Evitando los conflictos

Los conflictos solamente pueden ocurrir cuando dos o más usuarios quieren actualizar o borrar la misma fila y al mismo tiempo. O sea, cuando se usa el comando UPDATE o el comando DELETE.

Por lo tanto, lo mejor es evitar usar esos comandos y solamente usar el comando INSERT.

¿Y cómo evitamos usar el comando UPDATE o el comando DELETE?

Hay dos técnicas:

  1. Guardar los movimientos. Sería el caso si por ejemplo tenemos una tabla de PRODUCTOS y se hacen compras y ventas de esos productos. Mientras se están haciendo las compras y las ventas jamás actualizamos la tabla de PRODUCTOS, solamente insertamos filas a nuestra tabla de movimientos.
  2. Usando una tabla auxiliar. Sería el caso cuando no guardamos los datos en una tabla de movimientos. Por ejemplo, cuando queremos cambiar el precio de venta de un producto.

En ambos casos, esperamos que nadie esté conectado a la Base de Datos (o al menos, que nadie esté usando la tabla de PRODUCTOS) y mediante un stored procedure hacemos las actualizaciones correspondientes.

En otras palabras, sí usaremos el comando UPDATE o el comando DELETE pero los usaremos cuando estamos 100% seguros de que no pueden ocurrir conflictos.

Ventaja de evitar los conflictos:

  • El trabajo de los usuarios que registran movimientos o actualizaciones es más rápido, porque nunca se encontrarán con una situación de tener que esperar o de deadlock (o sea, una actualización o borrado que falló).

Desventaja de evitar los conflictos:

  • El usuario que debe consultar los datos (mediante el comando SELECT) obtendrá más lentamente los resultados porque los datos se encontrarán en dos o más tablas, no solamente en una tabla.

Conclusión:

Lo normal es que sea preferible evitar los conflictos, ya que los usuarios que registran movimientos o actualizaciones constantemente están trabajando y los conflictos los afectarán negativamente. Además generalmente son muchos más que los usuarios que solamente consultan datos. Quienes necesitan consultar datos generalmente son muy pocas personas y además lo hacen esporádicamente, y con la gran velocidad de las computadoras actuales solamente podrían tardar unos segundos más en obtener un informe, así que no vale la pena preocuparse demasiado por ellos.

Por lo tanto, en el trabajo del día a día es conveniente usar solamente el comando INSERT, y usar el comando UPDATE o el comando DELETE en procesos de actualización que se realizan cuando nadie está usando la Base de Datos (o cuando nadie está usando la tabla que será actualizada).

Si un usuario no puede grabar una fila porque otro usuario tiene bloqueada la fila, eso es muy molesto y debemos hacer lo posible por evitar ese tipo de situaciones.

Artículos relacionados:

Bloqueos mortales

Modos de bloqueo de las transacciones

Transacciones optimistas y transacciones pesimistas

Usando transacciones con acceso exclusivo a tablas

Algo más sobre transacciones optimistas y transacciones pesimistas

¿Por qué en Firebird es preferible que las transacciones sean optimistas?

El índice del blog Firebird21

El foro del blog Firebird21

 

 

Usando Firebird durante 24/7/365

5 comentarios

Algunas aplicaciones requieren acceso a las bases de datos durante 24/7/365, o sea durante las 24 horas del día, los 7 días de la semana y los 365 días del año. En otras palabras: sin interrupción, por ningún motivo.

Algunos ejemplos típicos son:

  • Sanatorios, hospitales, centros de salud
  • Farmacias que trabajan las 24 horas
  • Estaciones de servicio (expendio de combustibles)
  • Policía
  • Fuerzas Armadas
  • etc.

Una aplicación desarrollada para ser utilizada por ellos tiene la obligación de poder funcionar sin interrupción en cualquier momento del día. No le puedes decir al Director de un hospital que entre las 22:00 y las 23:00 no pueden registrarse los datos de los pacientes porque hay que hacerle un mantenimiento a la Base de Datos. Ni decirle a un General que si mañana el país es atacado por el enemigo entre las 02:00 y las 03:00 entonces no podrá usar la Base de Datos porque ya tiene mucha basura y que hiciste un proceso para que elimine esa basura pero requieres que nadie esté conectado a la Base de Datos.

¿Y cuál es el problema?

El problema radica en que a las bases de datos de Firebird debe hacérsele una tarea de mantenimiento llamada sweep de vez en cuando. El sweep (barrido, en castellano) tiene como finalidad eliminar a todas las versiones inútiles de las filas que fueron modificadas o borradas. Cada vez que una fila es modificada o borrada se crea una versión de esa fila llamada delta. La parte positiva de esto es que puede realizarse un ROLLBACK y dejar a la fila como se encontraba anteriormente si el usuario cambió de idea. La parte negativa es que se va acumulando basura y todas las operaciones se van volviendo más lentas y por lo tanto esa basura debe ser eliminada alguna vez.

 Y el sweep puede demorar mucho tiempo.

¿Cuándo se inicia el sweep?

El sweep puede iniciarse automáticamente (que es su valor por defecto) cuando la diferencia entre la OST (Oldest Snapshot Transaction) y la OAT (Oldest Active Transaction) es mayor que el intervalo predefinido del sweep. Ese intervalo es de 20.000 por defecto pero puede modificarse.

Si el intervalo del sweep es de 0, entonces el sweep nunca se iniciará automáticamente. Eso implica que hay que iniciarlo manualmente. Para ello se usa el programa GFIX con la opción –sweep. En este caso lo recomendable es que nadie esté usando la Base de Datos mientras se realiza el sweep.

 ¿Qué ocurre cuándo se inicia el sweep?

Que la transacción que hizo iniciar al sweep no empezará hasta que el sweep finalice. Supongamos que el intervalo del sweep es de 20.000, entonces el desafortunado usuario que quiso iniciar una transacción cuando OST – OAT fue de 20.001 tendrá que esperar hasta que finalice el sweep antes de que su transacción pueda empezar. A quienes iniciaron sus transacciones antes que él no les afectará, pero a él sí. Y si justo es un General de muy malas pulgas, entonces…

 ¿Se puede evitar que se inicie el sweep sin perjudicar el rendimiento de la Base de Datos?

Sí.

La buena noticia es que si las aplicaciones que usan la Base de Datos están correctamente diseñadas entonces nunca será necesario realizar el sweep.

La basura que se encuentra dentro de una Base de Datos puede deberse a los siguientes motivos:

  • Una operación de UPDATE que finalizó con un COMMIT
  • Una operación de DELETE que finalizó con un COMMIT
  • Una operación de UPDATE que finalizó con un ROLLBACK
  • Una operación de DELETE que finalizó con un ROLLBACK

La basura dejada por las transacciones que finalizaron con un COMMIT es recolectada (garbage collection) automáticamente cuando se hace un SELECT a esas filas. Por lo tanto, para eliminar toda la basura de una tabla que fue dejada por los COMMIT se puede escribir un simple comando: SELECT * FROM MiTabla y listo: esa tabla ya no tendrá basura dejada por los COMMIT.

La basura dejada por el ROLLBACK sin embargo, solamente puede ser eliminada con un sweep.

Por lo tanto la solución es muy simple y muy sencilla: No usar ROLLBACK.

¿Y cómo se puede evitar usar ROLLBACK?

Hay varias alternativas posibles, una posible solución sería esta:

  • Haces un SELECT a la fila que te interesa, por supuesto finalizando ese SELECT con un COMMIT. El resultado de ese SELECT lo guardas en una tabla temporal que no es de Firebird, por ejemplo en una tabla .DBF
  • Le muestras la fila de la tabla .DBF al usuario
  • Le permites que cambie los campos o que borre la fila
  • Si decide Guardar los cambios, entonces haces un UPDATE a la tabla de Firebird con los valores que se ven en la pantalla y luego el COMMIT correspondiente
  • Si decide Cancelar los cambios, entonces allí termina todo, no tocas la tabla de Firebird para nada

Como ves, en estos casos se trabaja un poco más pero te aseguras que todas las transacciones terminen con un COMMIT.

Recolectando la basura

Adicionalmente, en tu aplicación deberías tener un proceso que se encargue de hacer un SELECT * a todas las tablas de tu Base de Datos. Cuando el usuario ejecuta ese proceso entonces se hace un SELECT * FROM MiTabla1, SELECT * FROM MiTabla2, SELECT * FROM MiTabla3, etc.

Recordatorio:

La técnica mostrada en este artículo es necesaria solamente para las bases de datos que deben estar activas durante 24/7/365. En los demás casos se puede seguir el procedimiento normal de hacer el sweep automáticamente o manualmente.

Conclusión:

Las bases de datos de Firebird pueden perfectamente ser usadas en entornos que las requieren abiertas durante las 24 horas, los 7 días de la semana y los 365 días del año. Lo que debemos tener en cuenta en esos casos es que deberíamos evitar el sweep porque el sweep puede demorar mucho tiempo en bases de datos muy grandes. La forma de evitar el sweep es nunca terminar las transacciones con ROLLBACK porque las transacciones que terminan con ROLLBACK dejan basura que solamente puede ser eliminada con un sweep, en cambio la basura dejada por las transacciones que terminan con un COMMIT puede ser eliminada con un comando SELECT * FROM MiTabla.

 Entonces, el problema es causado por la forma en que Firebird actúa cuando se hace un UPDATE o un DELETE que finalizaron con un ROLLBACK pero puede ser solucionado si la aplicación finaliza todas las transacciones con un COMMIT.

Artículos relacionados:

La arquitectura MGA

Entendiendo a las transacciones

Entendiendo a los identificadores de las transacciones

Entendiendo sweep y garbage collection

El índice del blog Firebird21

El foro del blog Firebird21

 

¿Por qué en Firebird es preferible que las transacciones sean optimistas?

4 comentarios

Para entenderlo, debes tener bien en claro cuando se puede actualizar (UPDATE) o borrar (DELETE) una fila en Firebird.

En Firebird, todo lo que hagamos estará siempre dentro de una transacción, siempre, sin excepción. Todas las operaciones (INSERT, UPDATE, DELETE, SELECT, FETCH) siempre se encuentran dentro de una transacción.

Y una transacción puede actualizar (UPDATE) o borrar (DELETE) una fila solamente si la tiene bloqueada. Si una transacción no ha bloqueado a una fila entonces no podrá actualizarla ni borrarla, le será imposible hacerlo.

En un determinado momento solamente puede haber una transacción bloqueando a una fila. Es imposible que dos o más transacciones tengan a una fila bloqueada al mismo tiempo. No se puede. Solamente una transacción la puede estar bloqueando.

Y solamente la transacción que la tiene bloqueada la puede actualizar (UPDATE) o borrar (DELETE), las demás transacciones no podrán hacerlo hasta que la transacción que la tiene bloqueada finalice (con un COMMIT o con un ROLLBACK, pero debe finalizar antes de que esa fila pueda ser actualizada o borrada por otra transacción).

Por lo tanto, lo que ocurre es lo siguiente:

  • Se bloquea la fila
  • Se la actualiza (con el comando UPDATE) o se la borra (con el comando DELETE)
  • Finaliza la transacción

Entonces, la pregunta es ¿en qué momento se bloquea la fila?

La transacción optimista piensa: “Cuando necesite actualizar la fila la encontraré desbloqueada y entonces yo podré bloquearla”

La transacción pesimista piensa: “No sé si cuando necesite actualizar la fila la encontraré desbloqueada por eso lo mejor es que la bloquee ahora mismo para que cuando necesite actualizarla ya la tenga bloqueada”.

Ya ves por que a una se le llama “optimista” y a la otra se le llama “pesimista”.

¿Cómo actúa una transacción optimista?

  1. Inicia la transacción
  2. Realiza ochopotecientas tareas
  3. Bloquea la fila
  4. Actualiza la fila
  5. Realiza ochopotecientas tareas más
  6. Finaliza la transacción

¿Cómo actúa una transacción pesimista?

  1. Inicia la transacción
  2. Bloquea la fila
  3. Realiza ochoptecientas tareas
  4. Actualiza la fila
  5. Realiza ochopotecientas tareas más
  6. Finaliza la transacción

¿Cuál es la diferencia?

Que la transacción optimista bloquea a la fila inmediatamente antes de actualizarla, un microsegundo antes; en cambio la transacción pesimista la bloquea mucho antes de utilizarla. Como puedes ver, los pasos 2. y 3. están invertidos, los demás son iguales.

¿Y esto qué implica?

Que las transacciones pesimistas mantienen a la fila bloqueada durante mucho más tiempo, porque realizar esas “ochopotecientas tareas” puede demorar bastante.

Pero además hay otro problema con las transacciones pesimistas y es que pueden estar bloqueando a muchas filas que jamás usarán. Si una transacción pesimista bloquea a una tabla está bloqueando a todas las filas de esa tabla, aunque solamente necesite actualizar algunas. Si una transacción pesimista bloquea a una tabla que tiene 2500 filas aunque solamente necesite actualizar 200 de esas filas, ninguna de las 2500 filas podrán ser actualizadas por otras transacciones, no solamente las 200 filas que necesita estarán bloqueadas, las 2500 filas estarán bloqueadas.

¿Qué ocurre si una transacción quiere bloquear una fila que ya está bloqueada por otra transacción?

Que no podrá bloquearla. Eso porque en un determinado momento una fila puede estar bloqueada por una sola transacción, no pueden dos transacciones bloquear a la misma fila en el mismo momento. El intento de bloquear a una fila que está bloqueada por otra transacción se conoce como conflicto o colisión. La transacción que intentó bloquear a la fila y falló tiene dos alternativas:

a) Espera hasta que la transacción que tiene a la fila bloqueada finalice con un COMMIT o con un ROLLBACK, para después de eso volver a intentar bloquear dicha fila. Eso ocurre cuando el modo de bloqueo de la transacción es WAIT (esperar)

b) Sale inmediatamente, con un mensaje de error. Eso ocurre cuando el modo de bloqueo de la transacción es NO WAIT (no esperar)

¿Cuáles son las ventajas de usar transacciones optimistas?

  1. Las filas quedan bloqueadas durante muy poco tiempo
  2. La probabilidad de que haya conflictos (colisiones) con otras transacciones es muy pequeña

¿Cuáles son las ventajas de usar una transacción pesimista?

En Firebird es extremadamente raro necesitar usar transacciones pesimistas, en general casi todo puede hacerse con transacciones optimistas. Si requieres de muchas transacciones pesimistas entonces es muy probable que aún no entiendas como funciona Firebird. Pero también usarlas tiene algunas ventajas, ellas son:

  1. Rápidamente, se puede numerar secuencialmente y sin que haya números faltantes y sin provocar deadlocks
  2. Se pueden ejecutar procesos que requieren que otras transacciones no estén insertando, actualizando, o borrando filas

Resumiendo:

En Firebird todo se hace dentro de una transacción, ninguna operación (INSERT, UPDATE, DELETE, SELECT, FETCH) puede hacerse afuera de una transacción. Las transacciones pueden ser optimistas o pesimistas. Las operaciones de UPDATE y de DELETE requieren que la fila sea bloqueada antes de ser actualizada o borrada, no se puede actualizar ni borrar una fila que previamente no fue bloqueada. Las transacciones optimistas bloquean a la fila durante muy poco tiempo, las transacciones pesimistas bloquean a la fila durante más tiempo. Cuanto menos tiempo una fila esté bloqueada mucho mejor, porque eso reduce el riesgo de colisiones. Ocurre una “colisión” cuando una transacción quiere actualizar o borrar una fila que otra transacción tiene bloqueada. Cuando ocurre una colisión la transacción que no pudo bloquear a la fila solamente tiene dos posibilidades: a) espera hasta que la transacción que tiene a la fila bloqueada termine, o b) sale inmediatamente con un mensaje de error.

En aplicaciones multi-usuario es común y es normal que existan colisiones porque dos o más transacciones a veces querrán actualizar la misma fila al mismo tiempo. Eso a veces es inevitable. Pero un buen profesional siempre tratará de que esas colisiones ocurran la menor cantidad de veces posibles y por eso en Firebird hay que tratar de usar siempre transacciones optimistas, porque son las que durante menor tiempo tienen a las filas bloqueadas.

Artículos relacionados:

Entendiendo a las transacciones

Transacciones optimistas y transacciones pesimistas

Algo más sobre transacciones optimistas y transacciones pesimistas

El índice del blog Firebird21

El foro del blog Firebird21

 

Algo más sobre transacciones optimistas y transacciones pesimistas

2 comentarios

En Firebird todo lo que hagamos ocurrirá dentro de una transacción y esas transacciones pueden ser de dos tipos: optimistas y pesimistas.

  • La ventaja de las transacciones optimistas es que terminan más rápido que las transacciones pesimistas equivalentes y que ocasionan muchos menos conflictos (o colisiones)
  • La ventaja de las transacciones pesimistas es que nos permiten numerar sin tener huecos (o sea: números faltantes) y que podemos ejecutar procesos que requieren acceso exclusivo a tablas.

Si una sola persona se encarga de insertar, de actualizar y de borrar las filas de una tabla entonces jamás tendremos problemas, y por lo tanto podríamos usar transacciones optimistas en el 100% de los casos.

Pero si dos o más personas insertan, actualizan o borran filas de una misma tabla entonces allí sí pueden surgir problemas. Y cuantas más personas puedan hacerlo, mayor será la probabilidad de que ocurran problemas.

Este tema ya fue tratado en el artículo:

Transacciones optimistas y transacciones pesimistas

pero aquí lo trataremos con un poco más de profundidad.

En Firebird lo normal, lo correcto, lo recomendable es tener (casi) siempre transacciones optimistas. Una excepción es cuando necesitamos numerar en forma secuencial y consecutiva. Otra excepción es cuando necesitamos ejecutar un proceso de actualización que no debe ser “molestado” por otra transacción.

Caso 1. Si podemos numerar: 1, 2, 15, 32, 39, … entonces deberíamos usar una transacción optimista. Esos números los obtendremos con la ayuda de un generador (también se le llama secuencia). También usaríamos transacciones optimistas en casi todos los INSERT, UPDATE, DELETE, SELECT, FETCH, que hagamos.

Caso 2. Si la numeración debe ser: 1, 2, 3, 4, 5, 6, … y no puede faltar ningún número entonces nuestra transacción deberá ser pesimista.

Mostrando el problema

Supongamos que tenemos una tabla de ALUMNOS y necesitamos que cada uno de esos alumnos tenga un CÓDIGO que debe ser secuencial y consecutivo. El CÓDIGO del último alumno registrado es el 617 y por lo tanto el siguiente CÓDIGO deberá ser el 618.

¿Qué puede pasar si nuestra transacción es optimista?

La transacción T1 hace un SELECT para hallar el código del último alumno, encuentra que es 617, le suma 1 y obtiene 618. Después hace un INSERT y un COMMIT para insertarle una fila a la tabla de ALUMNOS, la cual tendrá el número 618 en la columna CÓDIGO.

El problema es que si la transacción T2 antes del COMMIT de la transacción T1 también hizo un SELECT a la tabla de ALUMNOS también encontrará que el último código es el 617, también le sumará 1, y también obtendrá 618 y también querrá insertar una fila a la tabla de ALUMNOS que en su columna CÓDIGO tenga el número 618.

Como ya existe un CÓDIGO con el número 618 (porque lo grabó la transacción T1) entonces el intento de COMMIT de la transacción T2 será rechazado (o al menos debería ser rechazado, porque los códigos no deberían estar duplicados y por lo tanto tendría que haber una Unique Key sobre la columna CÓDIGO).

Esta situación le ha creado un conflicto a la transacción T2. No puede insertar el número de CÓDIGO que ella legítimamente halló (o sea, el número 618) porque la transacción T1 también lo había hallado e hizo un COMMIT.

Entonces, lo único que le queda por hacer a la transacción T2 es terminar con un ROLLBACK, o sea sin penas ni gloria. ¿Qué pasó aquí? que la transacción T2 halló que el CÓDIGO debía ser 618, hizo un INSERT, hizo un COMMIT, su COMMIT falló porque ya existía un CÓDIGO con el número 618 y entonces debe terminar con un ROLLBACK.

Pero el usuario que ejecutó la transacción T2 no está conforme, él quiere que se inserten los datos del nuevo alumno así que inicia otra transacción para conseguir su objetivo. Pero entre el ROLLBACK y el inicio de su nueva transacción ocurrió un lapso de tiempo durante el cual otro usuario inició la transacción T3. Y entonces la transacción T4 (heredera de la transacción T2) tendrá un nuevo conflicto, porque hallará que el CÓDIGO del nuevo alumno debe ser 619 y cuando quiera guardarlo encontrará que no puede porque ese número ya fue utilizado por la transacción T3.

En un entorno de muchos usuarios concurrentes algo así podría ocurrir frecuentemente y ser harto frustrante para los usuarios. Podrías mostrarles un mensaje diciendo algo como: “La grabación falló, vuelve a intentarlo”, pero después de unas cuantas veces la mayoría de los usuarios estarán hartos.

Haciendo que una transacción sea pesimista

Como veremos a continuación, la solución es que la transacción sea pesimista, entonces la pregunta es: ¿cómo conseguimos que una transacción sea pesimista?

Bien, esto lo podemos hacer de dos formas:

  1. Con SET TRANSACTION
  2. Con el comando UPDATE

Para que la transacción sea pesimista con SET TRANSACTION escribimos algo como:

SET TRANSACTION
   [SNAPSHOT TABLE STABILITY]
   [RESERVING MiTabla1, MiTabla2, MiTabla3, ...
   FOR [SHARED | PROTECTED] {READ | WRITE}]

Si usamos el aislamiento SNAPSHOT TABLE STABILITY entonces tendremos acceso exclusivo a cada tabla que la transacción utilice. Si esas tablas están listadas después de la cláusula RESERVING entonces serán bloqueadas en el mismo momento en que la transacción empiece y liberadas cuando la transacción finalice. Esta es la forma más restrictiva de todas y por lo tanto la menos recomendable en Firebird.

Si no usamos el aislamiento SNAPSHOT TABLE STABILITY y especificamos la cláusula RESERVING entonces las tablas MiTabla1, MiTabla2, MiTabla3, etc. estarán reservadas para ser usadas por esta transacción. Eso no significa que otras transacciones no podrán usar esas tablas sino que no podrán impedir que esta transacción las utilice. Cuidado con eso porque es un error de concepto muy común, mucha gente cree que al reservar una tabla las demás transacciones no podrán usarlas, eso es falso. Lo que se hace al reservar una tabla es asegurarse de que ninguna otra transacción impedirá que esta transacción tenga acceso a esa tabla.

Ejemplo: Si la transacción T1 reservó a la tabla VENTAS entonces la transacción T2 no le podrá impedir a la transacción T1 acceder a la tabla VENTAS.

Las cuatro combinaciones posibles son:

SHARED READ. Permite a cualquier transacción leer datos y a cualquier transacción que tiene el modo de acceso WRITE actualizar filas. Es la menos restrictiva.

SHARED WRITE. Permite a cualquier transacción con modo de acceso WRITE y cuyo aislamiento sea SNAPSHOT o READ COMMITTED leer y actualizar filas. Las transacciones con modo de acceso READ y cuyo aislamiento sea SNAPSHOT o READ COMMITTED podrán leer filas, pero no actualizarlas.

PROTECTED WRITE. Impide que las otras transacciones puedan actualizar filas, las transacciones SNAPSHOT y READ COMMITED podrán leer filas, pero solamente esta transacción podrá actualizar filas.

PROTECTED READ. Ninguna transacción podrá actualizar filas, ni siquiera esta transacción, pero todas las transacciones podrán leer filas.

Desde luego que la primera transacción que reservó una tabla es la que tiene preferencia. Por ejemplo si la transacción T1 reservó a una tabla para PROTECTED READ y la transacción T2 quiere reservar a esa misma tabla para PROTECTED WRITE entonces la transacción T2 no podrá iniciar, será rechazada.

 Para que la transacción sea pesimista con UPDATE hacemos lo siguiente:

  • Hacemos un UPDATE a una fila de la tabla MiTabla1
  • Realizamos operaciones de INSERT, UPDATE, DELETE, SELECT, FETCH en otras tablas (y también en MiTabla1, si queremos)
  • Todas las transacciones que pueden entrar en conflicto con esta transacción también deben comenzar con un UPDATE a la tabla MiTabla1

¿Cuándo iniciar una transacción pesimista con SET TRANSACTION?

En general usamos SET TRANSACTION PROTECTED WRITE cuando queremos ejecutar un stored procedure que no debe ser “molestado” por otras transacciones.

Veamos lo que podría ocurrir si nuestra transacción no es PROTECTED WRITE: queremos guardar en una tabla el saldo actual de todos nuestros clientes (una fila por cada cliente, desde luego) y para eso al saldo inicial de cada cliente le sumamos todas las ventas que le hicimos a crédito y le restamos todas las cobranzas. Pero ¿y si mientras estamos realizando ese proceso alguien guarda una venta a crédito o una cobranza del cliente que estamos procesando? entonces el saldo que hallemos podría ser incorrecto. Desde luego que algo así podría ocurrir y sería desastroso. No deberíamos permitir que ocurra un error tan catastrófico. Para evitarlo nuestra transacción debe ser PROTECTED WRITE, de esa manera solamente nuestra transacción podrá actualizar las tablas MiTabla1, MiTabla2, MiTabla3, etc. Las demás transacciones podrán leer el contenido de esas tablas, pero no cambiar ese contenido.

Fíjate que usamos PROTECTED WRITE para hacer a la transacción pesimista porque las filas que queremos actualizar son muchas, si queremos actualizar una sola fila entonces lo correcto es usar UPDATE para hacer a la transacción pesimista.

¿Cuándo iniciar una transacción pesimista con UPDATE?

Cuando queremos hallar el siguiente número y no queremos que haya huecos (o sea, números faltantes) lo correcto es usar una transacción pesimista con UPDATE.

En el caso de la tabla de ALUMNOS que vimos más arriba el proceso a realizar sería el siguiente:

Abrimos una transacción como READ COMMITTED y WAIT.

En una tabla AUXILIAR tenemos guardado el código del último alumno. En este ejemplo sería el número 617.

Hacemos un UPDATE a esa fila de la tabla AUXILIAR, escribiendo algo como: UPDATE AUXILIAR SET ULTIMO_CODIGO =ULTIMO_CODIGO + 1, por lo tanto en ULTIMO_CODIGO tendremos 618.

Hacemos un SELECT a la tabla AUXILIAR para conocer el valor de ULTIMO_CODIGO, escribiendo algo como: SELECT ULTIMO_CODIGO FROM AUXILIAR, y obtendremos 618.

Le hacemos un INSERT a la tabla de ALUMNOS, poniendo en su columna CODIGO el número que se encuentra en la columna ULTIMO_CODIGO de la tabla AUXILIAR, es decir, el número 618

Terminamos la transacción

¿Qué ocurrirá?

Que la transacción T1 actualizó una fila de la tabla AUXILIAR y por lo tanto esa fila quedará bloqueada hasta que la transacción T1 finalice (con un COMMIT o con un ROLLBACK).

En la tabla ALUMNOS se guardó el CÓDIGO que le corresponde al nuevo alumno, en este caso el 618

La transacción T2 (que debe ser READ COMMITTED y WAIT) también quiso insertarle una fila a la tabla ALUMNOS pero primero debe actualizar una fila de la tabla AUXILIAR. No podrá hacerlo hasta que la transacción T1 finalice. Después de finalizar la transacción T1 recién entonces la transacción T2 podrá actualizar la fila de la tabla AUXILIAR, le pondrá el valor 619 y ese será el CÓDIGO que guardará en la tabla ALUMNOS.

Como puedes ver, la solución es muy sencilla. La transacción T2 debe ser READ COMMITTED para que pueda leer el valor actualizado de la columna ULTIMO_CODIGO después que la transacción T1 finalice. Es cierto que hay una espera de la transacción T2 porque deberá esperar hasta que la transacción T1 finalice, pero eso es todo. Desde luego que eso implica que el modo de bloqueo de las transacciones debe ser WAIT, es decir que la transacción T2 debe quedarse esperando hasta que la transacción T1 finalice, ya que si el modo de bloqueo es NO WAIT la transacción T2 terminará inmediatamente cuando no pueda actualizar la fila de la tabla AUXILIAR. Y no es eso lo que queremos.

Lo que queremos es que si la transacción T2 no puede actualizar una fila de la tabla AUXILIAR se quede esperando hasta que pueda actualizarla.

Resumiendo:

Las transacciones en Firebird pueden ser optimistas o pesimistas. Lo normal, lo más frecuente, lo más recomendable, es que las transacciones sean optimistas, hay muy pocos casos en que se necesitan transacciones pesimistas. Si usas muchas transacciones pesimistas entonces aún no sabes usar a Firebird correctamente. Hay dos formas de conseguir que una transacción sea pesimista: a) usando SET TRANSACTION, b) usando UPDATE. En general se la hace pesimista con SET TRANSACTION cuando las filas a procesar son muchas porque cuando se reserva una tabla se reservan todas las filas de esa tabla. Y se usa UPDATE cuando las filas son muy pocas (lo más frecuente es que sea una sola fila). Si queremos numerar en forma consecutiva y sin que falten números entonces debemos usar una transacción pesimista con UPDATE, cuyo aislamiento sea READ COMMITTED y el modo de bloqueo sea WAIT.

Artículos relacionados:

Entendiendo a las transacciones

Transacciones optimistas y transacciones pesimistas

El índice del blog Firebird21

El foro del blog Firebird21

Older Entries