Como una transacción que terminó anormalmente es desechada

2 comentarios

Si una transacción terminó normalmente entonces es seguro que fue finalizada con un COMMIT o con un ROLLBACK.

Pero ¿y si terminó anormalmente?

Supongamos que ocurrió un corte de la energía eléctrica que apagó al Servidor mientras había una transacción Activa.

¿Cómo se terminará esa transacción?

Como la transacción no finalizó con un COMMIT ni con un ROLLBACK entonces continuará marcada como Activa aunque en realidad ya está muerta porque será imposible finalizarla con un COMMIT y ya nada se puede hacer con ella.

Pero en la Base de Datos sigue estando marcada como Activa.

Hay 2 formas de desechar (marcar como RolledBack)  a una transacción que finalizó anormalmente, y para entender esas dos formas debemos conocer que ocurre cuando una transacción inicia.

  1. Cada vez que una transacción inicia bloquea a su propio Identificador de transacción. O sea, obtiene acceso exclusivo a él. Por lo tanto, cuando se inicia la transacción T1 bloquea al identificador de la transacción T1. Si luego una transacción T2 quiere actualizar o borrar una fila primero verifica si la última versión de esa fila fue creada por una transacción que aún está Activa.  Si ése es el caso entonces trata de bloquear al Identificador de la transacción T1. Si lo consigue es porque la transacción T1 en realidad ya no está Activa, sino que está muerta. Entonces, la transacción T2 cambia el estado de la transacción T1 y lo marca como RolledBack.
  2. Cada vez que una transacción inicia, trata de obtener un acceso exclusivo a la Base de Datos. Si lo consigue es porque ninguna otra transacción está Activa. Y en ese caso cambia el estado de todas las transacciones que estaban marcadas como Activa y les coloca RolledBack. Tanto si consiguió el acceso exclusivo como si no lo consiguió, a continuación obtiene un acceso compartido a la Base de Datos. Por definición, si una transacción tiene acceso compartido entonces ninguna otra transacción podrá obtener un acceso exclusivo, aunque sí podrá tener un acceso compartido.

El método 1. tiene la ventaja de ser muy rápido, pero tiene la desventaja de que la transacción T1 (que finalizó anormalmente) puede estar marcada como Activa durante muchísimo tiempo si ninguna transacción T2 quiere actualizar o borrar las mismas filas que actualizó o borró la transacción T1.

El método 2. tiene la ventaja de que marca como RolledBack a todas las transacciones que finalizaron anormalmente, y lo hace de una sola vez. Su desventaja es que en bases de datos muy grandes y que tienen muchas transacciones terminadas anormalmente se demora bastante más tiempo.

Entonces ¿qué ocurre cuando se inicia el Servidor del Firebird?

Que la primera transacción intentará obtener un acceso exclusivo a la Base de Datos. Si lo consigue (siempre lo debería conseguir), marcará a todas las transacciones que estaban Activa como RolledBack. Y por lo tanto en la Base de Datos ninguna transacción continuará estando marcada incorrectamente como Activa. Eso está muy bien, así debe ser.

El corolario es que para asegurarnos de no tener transacciones muertas (o sea, transacciones marcadas como Activa pero que en realidad no pueden ser finalizadas con un COMMIT) debemos detener y luego reiniciar al Servidor del Firebird.

Conclusión:

Cuando una transacción se inicia su estado siempre es Activa. Si luego finaliza anormalmente (por un corte de la energía eléctrica, por ejemplo) continúa marcada como Activa aunque en realidad está muerta ya que no puede finalizar con un COMMIT.

Está mal que esté marcada como Activa, eso no es lo correcto, y debe corregirse.

Para corregir el estado de la transacción existen dos métodos que emplea el Firebird:

Método 1. Cuando una transacción T2 quiere actualizar o borrar una fila cuya última versión fue creada por una transacción T1 que figura como Activa aunque sin estar Activa, actualiza el estado de la transacción T1 y le pone RolledBack, tal y como debe ser.

Método 2. Cuando una transacción se inicia, trata de obtener acceso exclusivo a la Base de Datos. Si lo consigue es porque no existe otra transacción Activa y entonces cambia el estado de todas las transacciones que estaban marcadas como Activa (incorrectamente, desde luego) a RolledBack, tal y como debe ser.

La desventaja del Método 1. es que una transacción T1 puede continuar marcada como Activa durante mucho tiempo, si ninguna otra transacción T2 quiere actualizar o borrar esas mismas filas. La desventaja del Método 2. es que solamente funciona cuando no hay otras transacciones activas, por lo tanto es inaplicable en Servidores que están encendidos 24/7/365.

La mejor manera de asegurarnos de no tener transacciones cuyo estado está marcado como Activa aunque en realidad están muertas, es detener y luego reiniciar el Servidor del Firebird.

Artículos relacionados:

Entendiendo a las transacciones

Entendiendo los identificadores de las transacciones

Entendiendo las páginas de la Base de Datos

El índice del blog Firebird21

El foro del blog Firebird21

 

 

 

Escribiendo un trigger condicional

2 comentarios

Como recordarás, los triggers son ejecutados automáticamente por el Firebird cuando se inserta, actualiza, o elimina una fila de una tabla.

Pero ¿Y si deseas tener un trigger que se ejecuta solamente algunas veces, no siempre?

En ese caso deberías escribir un trigger condicional.

¿Cómo?

Muy fácilmente:

  1. Asegurándote que las operaciones de inserción, actualización y borrado sean ejecutadas solamente por usuarios comunes, no por el usuario SYSDBA ni por el creador de la Base de Datos.
  2. Realizando las tareas solamente cuando se cumple la condición que has impuesto, algo como:

TRIGGER_1

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

Artículos relacionados:

Entendiendo a los triggers

Escribiendo un trigger

El índice del blog Firebird21

El foro del blog Firebird21

 

 

¿Usar subconsultas o usar joins?

4 comentarios

En muchas consultas se puede obtener exactamente el mismo resultado si usamos una subconsulta o si usamos un JOIN, entonces ¿cuál es preferible usar?

Veamos un ejemplo.

Listado 1.


SELECT
   VTC_IDENTI,
   VTC_FECHAX,
   VTC_IDECLI,
   CLI_NOMBRE
FROM
   VENTASCAB
JOIN
   CLIENTES
      ON VTC_CODSUC = CLI_CODSUC AND
         VTC_IDECLI = CLI_IDENTI
WHERE
   VTC_CODSUC = 0 AND
   VTC_IDENTI BETWEEN 31 AND 39
ORDER BY
   VTC_IDENTI

Listado 2.

SELECT
   VTC_IDENTI,
   VTC_FECHAX,
   VTC_IDECLI,
   (SELECT CLI_NOMBRE FROM CLIENTES WHERE VTC_CODSUC = CLI_CODSUC AND    VTC_IDECLI = CLI_IDENTI)
FROM
   VENTASCAB
WHERE
   VTC_CODSUC = 0 AND
   VTC_IDENTI BETWEEN 31 AND 39
ORDER BY
   VTC_IDENTI

JOIN1.png

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

Tanto si ejecutamos el Listado 1. como si ejecutamos el Listado 2. el resultado obtenido será exactamente el mismo y el rendimiento también será exactamente el mismo, ninguno será preferible al otro.

Por lo tanto, desde ese punto de vista, ejecutar a uno o al otro es indiferente, cuestión de gustos nomás.

Sin embargo, hay que tener en cuenta que las subconsultas le cuestan más al Firebird optimizar. En consultas sencillas como las mostradas arriba no habrá diferencias pero en subconsultas complicadas ya será otro tema.

Por lo tanto, lo recomendable es usar JOIN y no subconsultas. Si usamos subconsultas podríamos obtener el mismo rendimiento que si usamos JOIN pero jamás obtendríamos un mejor rendimiento. En cambio si usamos JOIN a veces podríamos obtener un mejor rendimiento que si usamos subconsultas.

Artículos relacionados:

El índice del blog Firebird21

El foro del blog Firebird21

 

 

Un stored procedure que retorna una cantidad variable de valores

Deja un comentario

Un stored procedure puede devolver cero valores, un valor, o muchos valores.

La forma más sencilla de hacer que nos devuelva solamente algunos valores, no todos, es llamarlo mediante un SELECT.

O sea que nuestro stored procedure deberá ser seleccionable.

Lo cual se consigue poniendo la instrucción SUSPEND dentro de él.

Entonces, si nuestro stored procedure devuelve los valores ftnValor1, ftnValor2, ftnValor3, ftnValor4 y solamente nos interesa obtener ftnValor1 y ftnValor3, escribiríamos algo como:

SELECT
   ftnValor1,
   ftnValor3
FROM
   MiStoredProcedureSeleccionable
WHERE
   MiCondición

Artículos relacionados:

Entendiendo a los stored procedures

¿Por qué usar stored procedures?

Usando un stored procedure como una función

Escribiendo un stored procedure

Enviando y recibiendo una cantidad variable de parámetros en los stored procedures

El índice del blog Firebird21

El foro del blog Firebird21

 

 

 

 

Obteniendo la primera fila de cada grupo

Deja un comentario

Como sabes, podemos usar la cláusula GROUP BY para agrupar las filas de una tabla. Sin embargo, a veces podríamos desear obtener solamente una fila de cada grupo.

Veamos un ejemplo.

PRIMERA_FILA_01

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

Como puedes ver, varias filas tienen el mismo identificador de la cabecera. No queremos obtener los totales de cada grupo, sino una fila por cada grupo, algo como:

PRIMERA_FILA_02

Captura 2. Si haces click en la imagen la verás más grande

En la Captura 2. podemos ver el resultado que queremos conseguir, ¿cómo lo logramos?

Listado 1.

SELECT
   ASI_IDENTI,
   ASI_IDECAB,
   ASI_NUMCUE,
   ASI_MONTOX
FROM
   ASIENTOSDET D1
WHERE
   NOT EXISTS (SELECT
                  ASI_IDENTI
               FROM
                  ASIENTOSDET D2
               WHERE
                  D1.ASI_IDECAB = D2.ASI_IDECAB AND
                  D1.ASI_IDENTI > D2.ASI_IDENTI)

Para que la consulta mostrada en el Listado 1. sea rápida, debemos tener un índice según la columna ASI_IDECAB y otro índice según la columna ASI_IDENTI. Si no tenemos esos índices, esta consulta será lentísima en tablas que tienen muchas filas.

Listado 2.

SELECT
   T1.ASI_IDENTI,
   T1.ASI_IDECAB,
   T2.ASI_NUMCUE,
   T2.ASI_MONTOX
FROM
   (SELECT
       MIN(ASI_IDENTI) AS ASI_IDENTI,
       ASI_IDECAB
    FROM
       ASIENTOSDET
    GROUP BY
       ASI_IDECAB
   ) T1
JOIN
   ASIENTOSDET T2
      ON T1.ASI_IDENTI = T2.ASI_IDENTI

Haciendo pruebas el autor de este blog descubrió que la solución del Listado 1. es más eficiente que la solución del Listado 2. pero que en tablas no muy grandes (menos de 1.000.000 de filas) esa diferencia es imperceptible.

Artículos relacionados:

El índice del blog Firebird21

El foro del blog Firebird21

 

Todas las columnas que se usan como Primary Key

4 comentarios

Cuando tenemos muchas tablas, verificar si cada tabla tiene la Primary Key correcta puede ser muy tedioso si debemos estar mirando una por una. La siguiente consulta nos facilitará esa tarea:

Listado 1.

SELECT
   I.RDB$FIELD_NAME      AS NOMBRE_COLUMNA,
   R.RDB$RELATION_NAME   AS NOMBRE_TABLA,
   R.RDB$CONSTRAINT_TYPE AS TIPO_RESTRICCION,
   I.RDB$INDEX_NAME      AS NOMBRE_RESTRICCION
FROM
   RDB$RELATION_CONSTRAINTS R
JOIN
   RDB$INDEX_SEGMENTS       I
      ON R.RDB$CONSTRAINT_NAME = I.RDB$INDEX_NAME
WHERE
   R.RDB$CONSTRAINT_TYPE = 'PRIMARY KEY'

PRIMARYS01

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

En la Captura 1. podemos ver los nombres de cada tabla, los nombres de las columnas que están incluidas en la Primary Key, y los nombres de las Primary Key.

De esta manera nos resultará fácil saber si todas las tablas tienen las Primary Key que deberían tener, o si a alguna de ellas le está sobrando o faltando alguna columna.

Artículos relacionados:

El índice del blog Firebird21

El foro del blog Firebird21

 

Poniendo ceros a la izquierda

5 comentarios

A veces necesitamos completar un string para que tenga una longitud fija de caracteres, algo muy común es agregarle ceros a la izquierda.

Por ejemplo, tenemos el string ‘123’ y queremos convertirlo a ‘0000123’

¿Cómo podemos hacerlo?

Usando la función LPAD(), como vemos a continuación:

Listado 1.

SELECT
   LPAD('123', 7, '0')
FROM
   RDB$DATABASE

Donde ‘123’ es el string que queremos modificar, 7 es la cantidad total de caracteres que tendrá el nuevo string, y ‘0’ es el carácter que se le pondrá a la izquierda (por supuesto que aquí podríamos elegir cualquier otro carácter, ponerle ‘0’ es solamente un ejemplo).

Listado 2.

UPDATE
   PRODUCTOS
SET
   PRD_CODIGO = LPAD(PRD_CODIGO, 9, '0')

En el Listado 2. nos aseguramos que todos los códigos de productos tengan exactamente 9 dígitos, rellenando con ceros a la izquierda cuando sea necesario.

Artículos relacionados:

El índice del blog Firebird21

El foro del blog Firebird21