Desarrollando aplicaciones inteligentes

2 comentarios

De vez en cuando me toca evaluar aplicaciones desarrolladas por otras personas y lo que siempre noto es que carecen de “inteligencia”. O tienen algo, pero no mucha, o no toda la que pudieran tener.

¿Cuándo una aplicación es “inteligente”?

Cuando toma las decisiones correctas, en el momento oportuno.

¿Y cómo se consigue eso?

Haciendo que el usuario trabaje lo menos posible.

Para ello, la aplicación debe evitar pedirle al usuario que haga algo que podría evitarse.

Ejemplo 1. Tablas maestras sin filas o con solamente una fila

Un ejemplo muy común es el siguiente:

– La aplicación está preparada para ser multi-moneda, pero esta Empresa utiliza una sola moneda. Entonces, no tiene sentido preguntarle cual es la moneda con la cual se está haciendo la compra, la venta, o lo que sea. Como hay una sola moneda es evidente cual es. La aplicación en ese caso ya debería elegir la moneda correcta (y nunca se equivocará, ya que hay una sola moneda)

Entonces, la aplicación verifica cuantas filas existen en la tabla de MONEDAS. Si esa tabla no tiene filas, entonces debe avisarle al usuario que no puede continuar porque la tabla de MONEDAS está vacía (si se le permite continuar recién en el momento de grabar se percatará del error y se le hará perder tiempo al usuario). Si hay solamente una fila entonces ya la muestra y deshabilita ese campo para que el usuario ni siquiera precise presionar la tecla ENTER.

En ambos casos (la tabla de MONEDAS sin filas o con una sola fila) la aplicación actuó inteligentemente.

Ejemplo 2. Procesos que pueden realizarse automáticamente

Algunos procesos deben realizarse cuando se cumple una condición. Por ejemplo al finalizar un mes se realiza un proceso de cierre.

Supongamos que se necesita guardar un resumen de las ventas de cada mes. En este caso en una tabla de CONFIGURACIÓN se podría tener guardados el mes y el año del último resumen. Si tenemos guardados 08/2014 (mes de agosto del año 2014) y las fechas de las ventas que están ingresando los usuarios corresponden al mes de septiembre de 2014 nada se hace, pero cuando una venta se carga con mes octubre del año 2014 y además la fecha del Servidor nos muestra que estamos en el mes de octubre del año 2014 entonces la aplicación ya puede iniciar automáticamente el proceso de generación del resumen de las ventas del mes de septiembre de 2014. ¿Por qué? porque la aplicación ya sabe que se terminaron de cargar las ventas del mes de septiembre.

A veces, aunque los usuarios sepan que deben ejecutar tal o cual proceso cuando se cumple alguna condición no lo hacen. Porque se olvidaron, porque estaban ocupados con otras cosas, o porque no les importa. Sea como sea, el proceso aún no fue realizado y cuando se necesiten sus resultados deberá ser ejecutado y eso hará perder tiempo valioso a los usuarios, tiempo que podrían haberse ahorrado si la aplicación hubiera actuado inteligentemente.

Conclusión:

Aunque en el primer ejemplo se utilizó una tabla de MONEDAS, ese desde luego no es el único caso. Lo mismo se haría con las tablas de MARCAS, de UNIDADES DE MEDIDA, de PAÍSES, de TIPOS DE ENVASES, de IMPUESTOS, etc., o sea, con cualquier tabla que eventualmente podría tener una sola fila.

Lo mismo con los procesos. Cualquier proceso que debe ejecutarse cuando se cumple una determinada condición es factible de ser lanzado automáticamente, proveyéndole así de “inteligencia” a nuestra aplicación.

Aunque las compras de las aplicaciones las deciden los propietarios o los directivos de la Empresa, quienes luego las usan son quienes determinan si se la seguirá usando o si será desechada y cambiada por algo mejor. Una aplicación desechada es una pésima publicidad para el desarrollador en cambio una aplicación funcionando a full es una buenísima publicidad.

Si una aplicación es “inteligente” entonces los usuarios trabajarán lo mínimo posible. Como dice un viejo dicho informático: “si el usuario trabaja poco es porque el Analista trabajó mucho. Y viceversa”.

No importa lo buena que tú creas que es tu aplicación, si a los usuarios de la misma no les gusta entonces no durará, harán comentarios negativos un día sí y el otro también, hasta que quienes tienen el poder de decisión decidirán tirarla a la basura y conseguirse algo mejor.

La mejor forma de evaluar la “inteligencia” de una aplicación es sentándote tú mismo a utilizarla con una gran carga de datos y pidiendo un montón de informes. Si te das cuenta que algo podría mejorarse, entonces debes mejorarlo.

Si tu aplicación le hace perder 2 segundos al usuario por cada fila que carga, y se cargan 1.000 filas por día entonces son 2.000 segundos por día que se perdieron, lo cual en un mes laboral hará 48.000 segundos y en un año 576.000 segundos, lo cual representa 160 horas ó 20 días laborales, que podrían haberse empleado en algo más productivo.

Artículos relacionados:

El índice del blog Firebird21

El foro del blog Firebird21

 

 

Anuncios

¿Qué datos guardar de una Factura?

16 comentarios

Este tema ya ha sido discutido en artículos anteriores pero siempre viene bien darle un breve repaso. Veo que aún hay mucha gente que tiene problemas conceptuales con el mismo.

El punto es muy sencillo: cualquier documento que pueda ser impreso y cuyos datos estaban correctos en el momento de la impresión debe poder ser vuelto a imprimir exactamente igual a la primera vez.

¿Por qué eso?

Porque en el momento de la impresión de un documento esos datos que se ven en él son los correctos (o al menos se supone que los son). Si más tarde por cualquier motivo se necesita reimprimir ese mismo documento entonces la reimpresión debe ser una copia exacta de lo que se imprimió la primera vez. Si eso no es así podrás tener problemas con la autoridad impositiva de tu país, porque a la gente del gobierno no le gusta que los documentos sean distintos.

Y tienen toda la razón del mundo, si a un cliente le diste una Factura por un total de 1.000 dólares y unos meses después al consultar esa Factura se ve que la venta fue de sólo 800 dólares hay un grave problema allí. Y en la mayoría de los países alguien se irá a la cárcel por eso o al menos pagará una fuerte multa, y todo eso podría haberse evitado si la Base de Datos hubiera estado cuidadosamente diseñada.

Entonces, lo que se imprime y sale de la Empresa no se debe normalizar.

En la gran mayoría de los casos, lo mejor y lo recomendable es que las tablas estén normalizadas. En este caso especial lo correcto es que no lo estén.

¿Por qué?

Porque si en la Factura el cliente tiene una dirección y un teléfono, después se muda, se actualizan su dirección y su teléfono en la tabla de CLIENTES, se reimprime la Factura y aparecerán en ella los nuevos datos, y eso está mal. Deberían aparecer los datos originales.

Eso hasta podría ser conversable con la gente del gobierno porque no hubo evasión impositiva ahí, pero ¿y si se cambió la cantidad de productos vendidos o los precios de venta?

Eso ya es otra cosa y un problema gravísimo. Porque eso sí puede provocar evasión impositiva, un delito muy grave en casi todos los países.

Entonces, en nuestra tabla de detalles de ventas tendríamos que guardar el Identificador del Producto vendido y también su código y también su nombre.

¿Y cómo evitamos que se modifiquen la cantidad vendida o el precio de venta o cualquier otro dato de detalle?

Con un trigger que se dispara cuando se quiere actualizar una fila, y envía una excepción.

IF UPDATING THEN
   EXCEPTION E_NO_SE_PUEDE_MODIFICAR_ESTA_FILA

¿Y cómo evitamos que se borre una fila?

Con un trigger que se dispara cuando se quiere borrar una fila y envía una excepción.

IF DELETING THEN
   EXCEPTION E_NO_SE_PUEDE_BORRAR_ESTA_FILA

¿Y si está todo mal y queremos borrar esa Factura?

Para este caso legítimo en nuestra tabla de cabecera tendremos una columna que nos indique que la Factura está anulada. Eso implica que la Factura jamás se borra de la tabla, simplemente se le pone una marca de que sus datos no deben ser utilizados en la mayoría de los informes.

Conclusión:

Los datos que se imprimen en documentos que salen de la Empresa deben ser siempre los mismos, no importa cuando ni cuantas veces se reimprima ese documento. Si eso no se hace así entonces alguna vez se podrán tener graves problemas con los clientes, con los proveedores o mucho peor, con el gobierno.

Para asegurar de que los datos sean siempre los mismos, las tablas donde se encuentran los datos que se imprimirán no deben estar normalizadas. En general, en la gran mayoría de los casos lo correcto es que las tablas sí estén normalizadas, pero en este caso particular (documentos que salen de la Empresa) lo correcto y lo recomendable es que no lo estén.

Para evitar que una fila se modifique podemos usar un trigger que impedirá las modificaciones. Para evitar que una fila sea borrada podemos usar un trigger que impedirá el borrado. Por supuesto que podremos usar un solo trigger para realizar ambas tareas.

Haciendo así nos aseguraremos de que siempre, sin importar cuanto tiempo haya pasado, nuestra Base de Datos mostrará información consistente.

Artículos relacionados:

El índice del blog Firebird21

El foro del blog Firebird21

 

Acelerando los SORT

1 comentario

Ya sabemos que en ocasiones el Firebird necesita ordenar las filas creando archivos temporales, a esa operación se la llama SORT (ordenar, en inglés) y puede ser lenta en tablas grandes. Entonces ¿qué podemos hacer para acelerar los SORT?

  1. Pedirle que los realice en un disco rápido
  2. Pedirle que los realice en un disco RAM
  3. Asegurándonos que el espacio libre en el disco temporario sea más grande que la Base de Datos
  4. Creando los archivos temporales en un disco distinto al de la Base de Datos

Los discos tienen distintas velocidades de lectura/escritura (y distintos precios también, claro). Los más rápidos suelen ser los SSD (Solid State Disk), aunque el problema es que son más caros que los discos duros magnéticos y también tienen una menor vida útil (aunque esto último está cambiando mucho, ya que cada vez duran más tiempo).

Como los SORT requieren crear archivos temporales, y muchos de esos archivos temporales pueden llegar a ser inmensos entonces debemos buscar por todos los medios acelerar el proceso.

Usar un disco RAM es una muy buena alternativa, aunque por supuesto eso requiere que la computadora tenga mucha memoria disponible. Para saber como crear un disco RAM y que programa usar para ello, puedes leer este artículo:

Usando un disco RAM para aumentar la velocidad

 ¿Y cuánto espacio libre debe tener el disco dónde se realizarán los SORT?

Evidentemente, cuanto más, mejor. Como no podemos saber de antemano cuanto será el espacio libre que necesitará el Firebird en general se toma como parámetro el tamaño de la Base de Datos. Es casi seguro que siempre necesitará menos que eso.

Entonces, si nuestra Base de Datos tiene un tamaño de 2 Gb, con tener 2 Gb en el disco donde se crearán los archivos temporales será suficiente. Pero no olvides que las bases de datos siempre crecen entonces hay que prever más, en ese caso un disco (duro o RAM) con 4 Gb sería muy bueno.

¿Y cómo se especifica dónde se crearán los archivos temporales?

En la carpeta donde instalaste el Firebird encontrarás un archivo cuyo nombre es FIREBIRD.CONF, dentro de ese archivo hay una entrada llamada “TempDirectories”. Debes borrar el símbolo de numeral que está a la izquierda y escribir los nombres de las carpetas donde se crearán los archivos temporales. Si usarás más de una carpeta deberás separarlas con punto y coma, por ejemplo:

TempDirectories=G:\TEMP
TempDirectories=G:\TEMP;F:\FIREBIRDTEMPS

¿Qué significan esas entradas?

La primera, que todos los archivos temporales se crearán en la carpeta G:\TEMP

La segunda, que los archivos temporales se crearán en G:\TEMP y que si alguna vez no hay espacio suficiente entonces se los continuará creando en F:\FIREBIRDTEMPS

Es decir, la segunda carpeta solamente se usa si la primera carpeta se quedó sin espacio.

¿Cómo se deben especificar los nombres de las carpetas en TempDirectories?

Como el Firebird primero usará la primera carpeta, y si se queda sin espacio allí usará la segunda carpeta, entonces en primer lugar deberías escribir el nombre de la carpeta que se encuentra en el disco más rápido. Si tienes un disco RAM entonces esa primera carpeta evidentemente será la del disco RAM.

En el ejemplo de arriba si el disco G: es un disco RAM y el disco F: es un disco SSD, entonces estará perfecto, porque primero especificamos el disco más rápido.

Desde luego que puedes tener más de dos carpetas, aunque en este ejemplo se mostraron 2 carpetas tú podrías tener 3, 4, 5, las que necesites. Solamente recuerda que siempre los discos más rápidos deben ser escritos primero.

¿Por qué los discos donde se encuentran las bases de datos y los archivos temporales debe ser distinto?

Si una sola persona está usando la Base de Datos, no hay problema, porque o está creando archivos temporales o no los está creando. En ambos casos estará trabajando a la máxima velocidad posible. Pero lo normal es que a una Base de Datos estén conectadas varias personas al mismo tiempo, cada una de ellas realizando alguna operación y ahí sí el tema se complica y mucho.

¿Por qué se complica?

Tenemos dos posibilidades:

  1. Están en un disco duro magnético
  2. Están en un disco no magnético (por ejemplo, un disco SSD)

Si están en un disco duro magnético, entonces hay un cabezal que debe moverse. Ese es un movimiento mecánico y por lo tanto siempre será lento. Si un usuario hace un SORT el cabezal debe moverse a un sector del disco duro. Otro usuario hace un INSERT, el cabezal debe moverse a otro sector del disco duro. Otro usuario hace un INSERT a otra tabla y nuevamente el cabezal debe moverse. Si los usuarios están ejecutando las operaciones de INSERT, UPDATE, DELETE, SELECT, FETCH, entonces los sectores estarán contiguos o muy cerca unos de otros, porque todos ocurren dentro de la misma Base de Datos. Pero si un usuario está haciendo un SORT esos sectores estarán muy alejados de la Base de Datos y ese constante ir y venir del cabezal hará que tanto las operaciones en la Base de Datos como el SORT sean mucho más lentos de lo que deberían. Si has leído algo sobre el diseño de Sistemas Operativos recordarás que usan semáforos: le dan unos milisegundos a un proceso para que haga lo que quiere hacer, luego otros milisegundos a un segundo proceso, luego otros milisegundos a un tercer proceso, y así sucesivamente hasta regresar al primer proceso. Para los usuarios esto es transparente, creen que cada uno de sus procesos se está ejecutando simultáneamente con los demás procesos pero eso no es así (bueno, salvo que la computadora tenga varios procesadores, claro). De todas maneras, sea la computadora multi-procesador o no, el cabezal del disco duro tendrá un constante ir y venir entre los sectores usados en el SORT y los sectores usados para las otras operaciones. Eso es lento y siempre será lento, podrá aumentarse la velocidad con discos duros más veloces, pero siempre será lento.

Si están en un disco no magnético (por ejemplo, un disco SSD), entonces no hay un cabezal que se mueve, en realidad nada se mueve, pero los discos tienen una vida útil limitada que aunque va aumentando porque la tecnología mejora, siempre tiene un límite. Entonces, si además de realizar las operaciones normales (INSERT, UPDATE, DELETE, SELECT, FETCH) también le pedimos que use a ese mismo disco SSD para los SORT, los backups y los restores, la vida útil de nuestro disco SSD será mucho más corta de lo que debería ser.

Por lo tanto la conclusión es muy sencilla: la Base de Datos debe encontrarse en un disco y los archivos temporales en un disco distinto.

Ese disco debe ser físicamente distinto, no sirve que esté particionado. Si está particionado se trata del mismo disco aunque para el Sistema Operativo sean dos discos diferentes, la realidad es que sigue siendo un mismo disco físico, y eso es justamente lo que no queremos.

Artículos relacionados:

Entendiendo el contenido de un PLAN

Usando un disco RAM para aumentar la velocidad

El índice del blog Firebird21

El foro del blog Firebird21

 

Un error de concepto en la cláusula WHERE

4 comentarios

A veces, escribimos un SELECT, nos parece que está todo bien, pero sin embargo no es así. Lo miramos de arriba a abajo, de abajo a arriba y no encontramos el problema pero sin embargo … el SELECT no funciona.

Lo más probable es que ya te haya ocurrido algo así, y si ese no es el caso, no te apures … ya te ocurrirá.

Un caso de esos ví hace unos días y me pareció instructivo para mostrarlo aquí en el blog. Veamos:

Consulta 1. Un SELECT que sí funciona

SELECT
   PER_NOMBRE,
   PER_APELLD,
   CASE
      WHEN PER_ESTCIV = 1 THEN 'Soltero'
      WHEN PER_ESTCIV = 2 THEN 'Casado'
      WHEN PER_ESTCIV = 3 THEN 'Separado'
      WHEN PER_ESTCIV = 4 THEN 'Divorciado'
      WHEN PER_ESTCIV = 5 THEN 'Viudo'
   END
FROM
   PERSONAS

Este SELECT está todo ok, funcionará perfectamente, nos mostrará el nombre de una persona, su apellido, y su estado civil. El estado civil está codificado con un número que va del 1 al 5 pero lo que veremos será una palabra y eso está ok.

Consulta 2. Un SELECT que no funciona

SELECT
   PER_NOMBRE,
   PER_APELLD
FROM
   PERSONAS
WHERE
   CASE
      WHEN PER_ESTCIV = 1 THEN 'Soltero'
      WHEN PER_ESTCIV = 2 THEN 'Casado'
      WHEN PER_ESTCIV = 3 THEN 'Separado'
      WHEN PER_ESTCIV = 4 THEN 'Divorciado'
      WHEN PER_ESTCIV = 5 THEN 'Viudo'
   END

Este SELECT es muy parecido al anterior, pero sin embargo no funcionará. Lo que se hizo fue pasar el CASE en el WHERE. Eso no es un problema en sí porque podemos tener un CASE en el WHERE, sin embargo este SELECT no funciona.

¿Por qué?

Ese SELECT fue visto por gente con bastante experiencia en Firebird pero sin embargo no detectaron el problema. O tardaron en detectarlo. El autor de este blog detectó el problema al instante pero le extrañó mucho que gente experimentada con Firebird tardara tanto tiempo en encontrar el error (el SELECT problemático en realidad era otro, con otras columnas, otra tabla, muchas más columnas, pero la idea es la misma, y aquí se la muestra simplificada).

¿Dónde está el problema? ¿ya lo encontraste?

Si no es así, vuelve a mirar el SELECT y trata de encontrarlo antes de mirar la solución, porque si miras la solución no estarás usando tu cerebro.

¿Y?

¿Por qué el SELECT mostrado en la Consulta 2. no funciona?

Bien, la respuesta correcta es “porque no está completo”. Le está faltando algo. ¿Qué le está faltando?

Para saber lo que le está faltando debemos pensar en el WHERE. ¿Qué escribimos en el WHERE? Una condición para filtrar las filas que serán mostradas. Ok, todo bien hasta ahí. ¿Y cuáles son los valores posibles de una condición? que se cumple o no se cumple. En SQL eso podríamos traducir como: debe devolver verdadero o falso. Y también NULL es posible.

¿Y entonces?

Entonces, como puedes ver, al SELECT de la Consulta 2. le está faltando una comparación. El valor devuelto por el CASE debe ser comparado con algo, no puede quedarse así como está, porque está en el aire y eso está mal. Sea lo que sea que escribamos en el WHERE nos debe devolver verdadero o falso o NULL. Si devuelve cualquier otra cosa (o no devuelve algo) entonces está mal, no funcionará.

Y ese es el error de concepto. Se escribió un WHERE que no devuelve verdadero ni falso ni NULL.

Por lo tanto, un SELECT corregido y sí funcionando sería similar al siguiente:

SELECT
   PER_NOMBRE,
   PER_APELLD
FROM
   PERSONAS
WHERE
   CASE
      WHEN PER_ESTCIV = 1 THEN 'Soltero'
      WHEN PER_ESTCIV = 2 THEN 'Casado'
      WHEN PER_ESTCIV = 3 THEN 'Separado'
      WHEN PER_ESTCIV = 4 THEN 'Divorciado'
      WHEN PER_ESTCIV = 5 THEN 'Viudo'
   END
   = 'Casado'

Que nos devolverá los nombres y apellidos de las personas casadas. Ahora sí el SELECT está completo porque comparamos el valor devuelto por el CASE con algo, entonces ya está todo ok, y funcionará a la perfección.

Artículos relacionados:

El índice del blog Firebird21

El foro del blog Firebird21

 

Entendiendo el contenido de un PLAN

6 comentarios

En Firebird todos los SELECTs que ejecutemos (manualmente o dentro de una vista) sí o sí usarán un PLAN. Ese PLAN puede ser elegido por el Firebird o por nosotros mismos. En general el Firebird hace un excelente trabajo en hallar el PLAN más adecuado pero en ocasiones no es así y allí nosotros podemos especificar uno mejor.

NOTA: Estos conocimientos no son necesarios para quienes son principiantes en Firebird, tales personas lo mejor que pueden hacer es dejarle al Firebird que especifique el PLAN a ser usado. Pero para quienes tienen un nivel más avanzado sí es importante, porque en algunos casos podrán mejorar drásticamente la velocidad de sus SELECTs.

Para entender mejor lo que es un PLAN puedes leer estos artículos:

Usando un PLAN

Algo más sobre PLAN

 En un PLAN lo que se especifica son los índices que serán usados. Las posibilidades son:

  • No hay un índice y no se usa un índice. Verás la palabra NATURAL.
  • No hay un índice y se necesita usar un índice. Verás la palabra SORT.
  • Hay un índice y no se usa ese índice. Verás la palabra NATURAL.
  • Hay un índice y se usa ese índice. Verás la palabra INDEX.

Si el SELECT tiene un JOIN (o más de uno, claro) entonces las posibilidades son:

  • No se necesita ordenar los conjuntos de resultados usados. Verás la palabra JOIN
  • Se necesita ordenar los conjuntos de resultados usados. Verás la palabra MERGE o más comúnmente las palabras SORT MERGE

Si se quiere mostrar las filas ordenadas:

  • Si hay un índice verás la palabra ORDER
  • Si no hay un índice verás la palabra SORT

El significado de NATURAL

Si ves esta palabra en el PLAN significa que no existe un índice o que sí existe pero no se lo utiliza. Si no se lo utiliza en general es por la pobre selectividad del mismo. Para saber más sobre la selectividad puedes leer estos artículos:

Selectividad de los índices

Recreando índices y calculando estadísticas

NATURAL significa que las filas son extraídas con el mismo orden en el cual fueron guardadas. Esto implica que todas las páginas deben ser leídas antes de que pueda ejecutarse el filtro (la condición que hayas puesto en la cláusula WHERE). Eso puede ser muy lento en tablas grandes.

Sin embargo, NATURAL no siempre es malo, si no te interesa mostrar a las filas con algún orden en particular entonces NATURAL es lo correcto porque es más rápido que usar un índice. Si se usa un índice primero se busca la clave en el índice y luego se extrae la fila correspondiente, si se usa NATURAL se ahorra el primer paso y por lo tanto es más rápido; la diferencia es muy notoria en tablas muy grandes.

El significado de SORT

La palabra SORT significa “ordenar” en inglés y eso es lo que hace el Firebird. Ordena el conjunto de resultados creando archivos temporales. Este proceso puede ser muy lento en tablas grandes porque crear esos archivos temporales puede demorar mucho tiempo. Si encuentras la palabra SORT eso es muy malo para el SELECT. Pero cuidado con esto, que sea malo para el SELECT no implica que necesariamente será malo para la Base de Datos. ¿Por qué? porque para evitar el SORT tendríamos que crear un índice, pero el mantenimiento de los índices es costoso, ya que cada vez que se hace un INSERT, un UPDATE, o un DELETE el índice debe ser modificado, eso toma tiempo y en tablas que constantemente están cambiando le provocarán un sobrecosto sustancial. Por lo tanto, la regla es crear un índice solamente si se lo usará frecuentemente. Por ejemplo, si tu SELECT se ejecutará una sola vez al año (o muy pocas veces en el año) y las filas involucradas se cuentan por millones entonces probablemente lo mejor sea no tener un índice y que el Firebird haga el SORT cuando lo necesite. Es cierto que el SELECT será lento, pero los INSERT, UPDATE, y DELETE serán más rápidos y como las filas se cuentan por millones entonces valdrá la pena no crear un índice.

El significado de INDEX

Esta palabra significa que existe un índice y que se lo utiliza.

El significado de JOIN

La palabra JOIN significa “juntar” en inglés. Si la ves en un PLAN significa que no se requiere ordenar ambos conjuntos de resultados porque ya se encuentran en el orden correcto.

El significado de MERGE o SORT MERGE

La palabra MERGE significa “combinar” o “mezclar” en inglés. MERGE y SORT MERGE hacen exactamente lo mismo, pero se suele escribir SORT MERGE para que quede más claro lo que hacen.

Lo que hacen es lo siguiente: un SORT al primer conjunto de resultados (como vimos más arriba), un SORT al segundo conjunto de resultados (como vimos más arriba) y luego combinar ambos conjuntos de resultados, ya ordenados.

Fíjate que hay dos SORT y una combinación de resultados. Cuidado con eso, algo así puede ser lentísimo en tablas grandes.

Un SORT MERGE es lo peor que puedes tener en un PLAN.

El significado de ORDER

La palabra ORDER significa “ordenar” en inglés, aunque en este caso la podríamos traducir “ordenar por el índice” y a continuación el nombre del índice que usa.

Analizando el PLAN

Para que todo esto quede más claro a continuación veremos varios ejemplos de SELECT con sus respectivos planes de ejecución; y explicaremos cada uno de ellos.

Ejemplo 1:

SELECT
   *
FROM
   BANCOS

PLAN (BANCOS NATURAL)

¿Qué significa este PLAN? que como no le dijimos en cual orden queremos ver a los Bancos entonces los mostró en el mismo orden conque los guardó en la tabla. Eso está muy bien, es lo correcto, porque es lo más rápido.

Ejemplo 2:

SELECT
   *
FROM
   BANCOS
ORDER BY
   BAN_CODSUC,
   BAN_IDENTI

PLAN (BANCOS ORDER PK_BANCOS)

¿Qué significa este PLAN? que como le dijimos que queremos ver a las filas ordenadas por Código de la Sucursal y por Identificador y tenemos un índice con esas columnas entonces usó a ese índice, cuyo nombre es PK_BANCOS.

Esto es más lento que el Ejemplo 1., pero si es lo que necesitamos mostrar entonces está ok.

Ejemplo 3:

SELECT
   *
FROM
   BANCOS
ORDER BY
   BAN_NOMBRE

PLAN SORT ((BANCOS NATURAL))

¿Qué significa este PLAN? que la tabla de BANCOS no estaba ordenada por la columna BAN_NOMBRE (por eso la palabra NATURAL) y que el Firebird tuvo que ordenarla (por eso la palabra SORT) antes de mostrar las filas. Ese ordenamiento crea archivos temporarios (en la memoria caché o en el disco duro) y puede ser muy lento en tablas muy grandes.

Ejemplo 4:

SELECT
   *
FROM
   BANCOS
WHERE
   BAN_CODSUC = 0 AND
   BAN_IDENTI > 10

PLAN (BANCOS INDEX (PK_BANCOS))

¿Qué significa este PLAN? que la tabla BANCOS tiene un índice según las columnas BAN_CODSUC y BAN_IDENTI (que son las usadas para filtrar con la cláusula WHERE) y que el Firebird usó ese índice.

En los WHERE hay siempre que tratar de usar índices porque eso aumenta muchísimo la velocidad.

Ejemplo 5:

SELECT
   *
FROM
   BANCOS
WHERE
   BAN_NOMBRE LIKE 'BAN%'

PLAN (BANCOS NATURAL)

¿Qué significa este PLAN? que la tabla BANCOS no tiene un índice según la columna BAN_NOMBRE y que como no le pedimos que muestre las filas con algún orden en particular las mostró como fueron guardadas.

Ejemplo 6:

SELECT
   *
FROM
   BANCOS
WHERE
   BAN_NOMBRE LIKE 'BAN%'
ORDER BY
   BAN_NOMBRE

PLAN SORT ((BANCOS NATURAL))

¿Qué significa este PLAN? que la tabla BANCOS no tiene un orden según la columna BAN_NOMBRE pero como se necesita mostrar a las filas ordenadas según esa columna entonces se hizo un SORT (es decir, se creó un archivo temporal para poder mostrar a las filas ordenadas).

No te olvides que hacer un SORT es más lento que usar un índice, a veces muchísimo más lento, porque las filas deben ser ordenadas y guardadas en un archivo temporal antes de ser mostradas y eso toma su tiempo.

Ejemplo 7:

SELECT
   B.BAN_CODSUC,
   S.SUC_NOMBRE,
   B.BAN_NOMBRE
FROM
   BANCOS     B
JOIN
   SUCURSALES S
      ON B.BAN_CODSUC = S.SUC_CODIGO

PLAN JOIN (B NATURAL, S INDEX (UQ_SUCURSALES))

¿Qué significa este PLAN? que la tabla S tiene un índice llamado UQ_SUCURSALES según su columna SUC_CODIGO, la tabla BANCOS no tiene un índice sobre su columna BAN_CODSUC y que se hará el JOIN sin ordenar las filas de la tabla B, así como están serán mostradas.

Ejemplo 8:

SELECT
   B.BAN_CODSUC,
   S.SUC_NOMBRE,
   B.BAN_NOMBRE
FROM
   BANCOS     B
JOIN
   SUCURSALES S
      ON B.BAN_CODSUC = S.SUC_CODIGO
ORDER BY
   B.BAN_NOMBRE

PLAN SORT (JOIN (B NATURAL, S INDEX (UQ_SUCURSALES)))

¿Qué significa este PLAN? que la tabla S tiene un índice llamado UQ_SUCURSALES según su columna SUC_CODIGO, la tabla BANCOS no tiene un índice sobre su columna BAN_CODSUC, que se hará el JOIN de esas filas (como vimos en el Ejemplo 7.) y posteriormente se hará un SORT para mostrar a las filas ordenadas según la columna BAN_NOMBRE.

Conclusión:

Entender el contenido del PLAN es muy útil por dos cosas: a) porque sabremos lo que está haciendo el Firebird para mostrar las filas del SELECT, y b) porque podremos analizarlo y quizás descubrir una mejor alternativa. Claro que esto último lleva su buen tiempo, no es algo que conseguiremos hacer enseguida pero vale la pena el esfuerzo porque un PLAN adecuado muestra a las filas muy rápidamente.

Si en el PLAN vemos la palabra SORT eso es algo malo y si vemos las palabras SORT MERGE eso es mucho más malo. Pero son malos para el SELECT, quizás no lo sean para la Base de Datos. Eso porque para evitar los SORT hay que crear índices, pero los índices deben ser mantenidos cada vez que se realiza un INSERT, un UPDATE o un DELETE, y ese mantenimiento no es instantáneo, toma su tiempo, que en tablas muy grandes y con muchas operaciones de inserción, actualización y borrado puede ser muy grande.

Por lo tanto debemos estudiar bien el caso y decidir si creamos el índice o si dejamos que se haga el SORT. En general, si el SELECT será ejecutado muchas veces, frecuentemente, deberíamos crear el índice. En cambio, si se lo ejecutará raramente lo mejor puede ser no crear el índice.

Otro aspecto muy importante a tener en cuenta es que las tablas son dinámicas, constantemente están cambiando, entonces un PLAN que hoy puede ser perfecto podría no serlo dentro de algunos meses o años. Si vamos a escribir el PLAN manualmente debemos tener bien presente que eso implicará más trabajo para nosotros porque de vez en cuando tendremos que verificar que siga funcionando muy bien, y si no es así entonces buscar y escribir un mejor PLAN. Pero encontrar un mejor PLAN no es algo que haremos en dos segundos, puede hacernos perder muchos minutos o horas inclusive.

Artículos relacionados:

Usando un PLAN

Algo más sobre PLAN

Selectividad de los índices

Recreando índices y calculando estadísticas

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