Entendiendo a GSTAT (1)

Deja un comentario

GSTAT es una herramienta que se usa desde la línea de comandos y su misión es mostrarnos estadísticas sobre una Base de Datos. Deber ser ejecutado en la misma computadora donde se encuentra el Servidor del Firebird y solamente puede ser ejecutado por el usuario SYSDBA o por el usuario que creó la Base de Datos. Como el tema es bastante largo, lo trataremos en varios artículos.

Se lo invoca de la siguiente manera:

GSTAT [opciones] MiBaseDatos

donde MiBaseDatos debe constar de la ruta completa y las opciones disponibles son las siguientes (se pueden abreviar con las letras dentro de los paréntesis):

-(a)ll       Analiza las páginas de datos y las páginas de índices
-(d)ata      Analiza solamente las páginas de datos
-(h)eader    Analiza solamente la página cabecera
-(i)ndex     Analiza solamente las páginas de índices
-(s)ystem    Como -(a)ll pero también incluye estadísticas sobre las tablas internas
-(u)ser      Nombre del usuario
-(p)assword  Contraseña del usuario
-(f)etch     Extrae la contraseña de un archivo de texto
-(r)ecord    Muestra el tamaño y estadísticas de la versión
-(t)able     Solamente analiza las tablas que se especifican aquí
-(tr)usted   Usa autentificación de confianza
-(z)         Muestra la versión de GSTAT

Como la información que muestra GSTAT puede ser muy larga, y ocupar inclusive cientos o miles de líneas, lo recomendable es enviar esa información a un archivo de texto para poder analizarla con mayor facilidad.

Entonces, la forma correcta de invocarlo es así:

gstat01

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

Donde hemos usado > NombreArchivo para indicarle que envíe su salida a un archivo de texto.

En nuestro ejemplo ese archivo de texto se llama TRANSC.TXT, y usamos el Bloc de Notas del Windows para ver su contenido.

gstat02

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

En la Captura 2. se ven solamente las primeras líneas del archivo TRANSC.TXT, en realidad ese archivo es muchísimo más grande, tiene varios cientos de líneas.

Veamos ahora con mayor detenimiento el significado y utilidad de cada una de las opciones que tenemos disponibles:

Opción -header

Podemos abreviarla como -h

gstat03

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

En la Captura 3. invocamos a GSTAT con la opción -h para que nos muestre la información que se encuentra en la cabecera de esta Base de Datos.

gstat04

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

Flags (Banderas)

Este número es siempre cero.

Checksum (Suma de comprobación)

Es siempre 12345 y sirve para comprobar si hay algún error físico en el disco duro. Cuando la página de cabecera es guardada en el disco y más tarde leída el checksum de la página de cabecera es comparado con 12345 y si son distintos, entonces un error de checksum es mostrado.

Generation (Generación)

Es un contador que se incrementa cada vez que se escribe algún dato en la página de cabecera.

Page size (tamaño de la página)

Es el tamaño que tiene cada una de las páginas de la Base de Datos, en bytes.

ODS version (versión de on-disk-structure)

El número de versión de on-disk-structure, sirve para conocer con cual versión del Firebird se creó la Base de Datos.

10.0 = 1.0
10.1 = 1.5
11.0 = 2.0
11.1 = 2.1
11.2 = 2.5
12.0 = 3.0

Oldest transaction (la transacción más antigua)

El identificador de la más antigua transacción “interesante”. Una transacción es “interesante” cuando no ha finalizado con un COMMIT. Se le abrevia como OIT.

Oldest active (la más antigua transacción activa)

El identificador de la más antigua transacción activa. Una transacción está activa cuando no ha finalizado ni con un COMMIT ni con un ROLLBACK y no está en el limbo. Se la abrevia como OAT.

Oldest snapshot (la más antigua transacción instantánea)

El identificador de la más antigua transacción cuya basura no puede ser recolectada. La basura no será recolectada de esta transacción ni de las transacciones que tengan un identificador mayor. Se la abrevia como OST. La diferencia entre la Oldest snapshot y la Oldest active determina cuando un sweep automático ocurre. El valor por defecto es 20000.

Next transaction (siguiente transacción)

El identificador que se le asignará a la siguiente transacción. Se la abrevia como NT.

Bumped transaction (transacción superada)

 Está obsoleta, ya no se usa. Siempre es 1.

Sequence number (número de secuencia)

Número de secuencia de la página de cabecera. Está obsoleta, ya no se usa. Siempre es 0.

Next attachment ID (siguiente identificador de conexión)

El identificador de la siguiente conexión a esta Base de Datos. Indica cuantas veces se han conectado a esta Base de Datos. Cada vez que una aplicación (cualquier aplicación) se conecta a esta Base de Datos este número aumenta en 1. (La excepción es GSTAT, porque GSTAT no se conecta de la forma normal).

Implementation ID (Identificador de la implementación)

Cuando la Base de Datos fue creada, pudo haber sido creada en una computadora que tenía diferente hardware y sistema operativo que la computadora en la cual se encuentra ahora. El Implementation ID muestra un número que identifica al hardware en el cual la Base de Datos fue creada.

Shadow count (cuenta de espejo)

Aunque en realidad shadow significa sombra, aquí se lo traduce como espejo. Muestra la cantidad de archivos adjuntos a esta Base de Datos o disponibles para ser usados por esta Base de Datos. Este número a veces es incorrecto, por eso es preferible escribir SHOW DATABASE en el programa ISQL para tener la información exacta.

Page buffers (buffers de página)

Es la cantidad de páginas que se pueden almacenar en la memoria caché. Un valor 0 significa que se usa el valor predeterminado, que por defecto es 2048 en SuperServer, y 75 en Classic y en SuperClassic. Se puede cambiar ese valor en el archivo FIREBIRD.CONF, en la entrada DefaultDbCachePages. También puede cambiarse con el programa GFIX.

Next header page (siguiente página de cabecera)

El número de página que tiene la siguiente página de cabecera. En general todas las bases de datos tienen una sola página de cabecera y por lo tanto este número es casi siempre 0.

Database dialect (dialecto de la Base de Datos)

Es siempre 3 en las nuevas versiones de Firebird. Anteriormente también podía ser 1, pero el dialecto 1 ya está obsoleto.

Creation date (fecha de la creación)

La fecha en la cual esta Base de Datos fue creada originalmente o la fecha en la cual fue restaurada por el programa GBAK.

Attributes (atributos)

Atributos que puede tener la Base de Datos.

no reserve. Todas las páginas son rellenadas al 100%, no se deja espacio libre en las páginas para INSERT, UPDATE, o DELETE. Es útil solamente en las bases de datos que son de sólo lectura.

force write. Los datos son escritos en el disco duro en el momento en que se solicita, no se guardan en la memoria caché sino que son directamente escritos en el disco. Esto es más lento que guardar los datos en la memoria caché pero es mucho más seguro, sobre todo en Windows.

shutdown. La Base de Datos ha sido cerrada y no puede ser utilizada.

read only. La Base de Datos es de sólo lectura, nada se puede escribir en ella.

multi-user maintenance. La Base de Datos está cerrada para realizar mantenimiento en ella. Solamente puede ser abierta por el usuario SYSDBA o por el creador de la Base de Datos o por ambos.

single-user maintenance. La Base de Datos está cerrada para realizar mantenimiento en ella. Puede ser abierta o por el usuario SYSDA o por el creador de la Base de Datos, pero solamente por uno de ellos, no por ambos.

Artículos relacionados:

El índice del blog Firebird21

El foro del blog Firebird2

Anuncios

Ejemplo de recursión (4). Actualizando filas recursivamente

3 comentarios

Así como podemos consultar filas de forma recursiva, también podemos actualizar esas filas recursivamente si es lo que necesitamos.

Veamos un caso:

Tenemos una tabla llamada CUENTAS que tiene estos datos:

UPDATE1

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

En la columna CUE_IDENTI guardamos el identificador de cada fila; en la columna CUE_IDEPAD guardamos el identificador de su fila padre; en la columna CUE_TIMEST guardamos la fecha y la hora de la última actualización de una cuenta. Ahora supongamos que queremos que la cuenta CRÉDITOS y todas las cuentas que sean sus descendientes tengan la fecha y la hora actuales.

Listado 1.

UPDATE
   CUENTAS
SET
   CUE_TIMEST = CURRENT_TIMESTAMP
WHERE
   CUE_IDENTI IN (
      WITH RECURSIVE MiCTE AS (
         
         SELECT
            CUE_IDENTI
         FROM
            CUENTAS
         WHERE
            CUE_IDENTI = 6
         
         UNION ALL
         
         SELECT
            T1.CUE_IDENTI
         FROM
            CUENTAS T1
         JOIN
            MiCTE 
               ON T1.CUE_IDEPAD = MiCTE.CUE_IDENTI
         
      )
      
      SELECT CUE_IDENTI FROM MiCTE

   );

UPDATE2

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

Tal y como podemos ver en la Captura 2. se actualizó la columna CUE_TIMEST de la cuenta que tiene CUE_IDENTI =6 y también de todos sus descendientes. En el Listado 1. podemos ver que la primera cuenta a ser actualizada es la que tiene CUE_IDENTI = 6, a continuación se actualizan las demás filas.

Desde luego que no solamente podemos cambiar la fecha y la hora al actualizar, podemos cambiar cualquier dato que necesitemos.

Explicación:

Como en la cláusula WHERE del UPDATE estamos usando IN eso significa que queremos actualizar varias filas. ¿Qué filas actualizaremos? Aquellas cuya columna CUE_IDENTI se encuentre en la tabla virtual que crearemos. En nuestra tabla virtual la primera fila es la que tiene CUE_IDENTI = 6, así que esa fila sí o sí será actualizada. A continuación hallamos el CUE_IDENTI de las demás filas que queremos actualizar. Como la tabla virtual es recursiva entonces se le irán agregando filas hasta que la condición del JOIN deje de cumplirse. El último SELECT es el que devuelve todas las filas de la tabla virtual, y todas esas filas tienen una sola columna, llamada CUE_IDENTI.

Entonces, nuestro UPDATE recursivo sería el equivalente a este UPDATE no recursivo.

Listado 2.

UPDATE
   CUENTAS
SET
   CUE_TIMEST = CURRENT_TIMESTAMP
WHERE
   CUE_IDENTI IN (6, 7, 8, 9, 10)

¿Cuál es la diferencia? Que en el Listado 2. debemos conocer  todos los identificadores que deseamos actualizar, en cambio en el Listado 1., no. Allí solamente necesitamos conocer el identificador de la primera cuenta, nada más. Todas las cuentas que sean sus descendientes serán actualizadas, sean 4 como en este caso o sean 500 o sean 1000. La única limitación es que pueden ser como máximo 1024 porque el Firebird permite hasta 1024 recursiones.

El Listado 2. es más sencillo, pero requiere conocer los identificadores de todas las cuentas que queremos actualizar y si los usuarios están constantemente agregando cuentas tendremos que estar cambiando nuestro UPDATE a cada rato. Por el contrario con el Listado 1. lo único que debemos conocer es el identificador de la primera cuenta que deseamos actualizar y todas las cuentas que sean sus descendientes se actualizarán también.

Artículos relacionados:

Stored procedures recursivos

Entendiendo a las tablas autoreferenciadas

Usando CTE (Common Table Expression)

Otro ejemplo de CTE: ventas semanales

Usando varias CTE en una vista o en un stored procedure

FOR SELECT y tablas CTE

Usando recursividad con CTE

Ejemplo de recursión (1). Filas salteadas

Ejemplo de recursión (2). Numerar filas

Ejemplo de recursión (3). Fechas consecutivas

El índice del blog Firebird21

El foro del blog Firebird21

Usando IDENTIFICADORES y CÓDIGOS en nuestras tablas

Deja un comentario

Aunque un Identificador es distinto que un Código mucha gente los confunde, inclusive aún teniendo muchos años de experiencia informática siguen tratando a los Códigos como si fueran Identificadores y eso es incorrecto porque deberían usarse para tareas distintas.

Un Identificador como su nombre lo indica sirve para identificar a una fila, de tal forma que no exista confusión posible con alguna otra fila de la misma tabla. En general lo aconsejable es que sea numérico y autoincremental (1, 2, 3, 4, 5, 6, etc.)

Un Código también sirve para identificar a una fila, de tal forma que no exista confusión posible con alguna otra fila de la misma tabla. Puede ser numérico o alfanumérico (AB123, MMK01, SVB68, 575701238, etc.)

Como ves, ambos sirven para identificar a una fila de forma unívoca, o sea que no pueda confundirse con otra fila.

¿Cuál es la diferencia entre un Identificador y un Código?

Que el Identificador se usa de forma interna en la Base de Datos, para identificar unívocamente a cada fila de una tabla y para relacionar a dos (o más) tablas entre sí. Como son de uso interno los usuarios ni siquiera necesitan saber de que existen. Jamás deberías cambiar el valor de un Identificador, por ningún motivo.

El Código, en cambio, no debería usarse para relacionar a dos (o más) tablas entre sí, esa es la tarea de los Identificadores, no es la tarea de los Códigos. Esto implica que un Código:

a) Solamente existe en una tabla

b) Puede ser cambiado, no hay problema si cambia su valor (desde luego que si debe ser único no podrá estar repetido, pero esa es la única restricción)

En las pantallas y en los informes los usuarios no necesitan ver a los identificadores, recuerda que son para uso interno dentro de la Base de Datos. Los códigos sí pueden verlos, justamente para eso sirven, para ser visualizados por los usuarios.

IDENTI1

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

En la Captura 1 vemos las primeras columnas de una tabla llamada PRODUCTOS. En ella tenemos un Identificador llamado PRD_IDENTI y dos Códigos llamados PRD_CODIGO y PRD_CODBAR respectivamente. Este último significa “código de barras”.

También tenemos una tabla llamada MOVIMDET (detalles de los movimientos de los productos) cuyas primeras columnas son las siguientes:

IDENTI2

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

Para relacionar a ambas tablas se usa la columna MOV_IDEPRD (Identificador del producto), no se usa ni el Código del producto ni el Código de barras del producto, lo que se usa es su Identificador.

Si al consultar la tabla MOVIMDET queremos ver el Código o el Código de barras del producto, pues es muy fácil:

SELECT
   P.PRD_CODIGO,
   P.PRD_CODBAR,
   P.PRD_NOMBRE,
   D.MOV_CANTID,
   D.MOV_PRECIO,
   D.MOV_CANTID * D.MOV_PRECIO AS TOTAL
FROM
   MOVIMDET D
JOIN
   PRODUCTOS P
      ON D.MOV_CODSUC = P.PRD_CODSUC AND
         D.MOV_IDEPRD = P.PRD_IDENTI

Y listo, eso es todo, al relacionar a ambas tablas con un JOIN todas las columnas de la tabla PRODUCTOS están disponibles para ser mostradas o utilizadas en el SELECT.

Conclusión:

Aunque puedes usar a un Código como si fuera un Identificador eso no es lo correcto, los identificadores son para uso interno, los códigos son para uso externo. Los usuarios solamente necesitan ver a los códigos, ellos ni siquiera necesitan saber que existen los identificadores. El valor de un Código debería existir en solamente una tabla, en cambio el valor de un Identificador existirá en todas las tablas relacionadas. Jamás y por ningún motivo deberías cambiar el valor de un Identificador, en cambio puedes cambiar sin problemas el valor de un Código, todas las veces que quieras. En las pantallas y en los informes a los usuarios se les pueden mostrar los Códigos, pues para eso sirven, para ser vistos por los usuarios.

Como los identificadores son para uso interno no hay problema si tienen valores salteados (1, 2, 8, 24, 25, 39, etc.). Esto está muy bien, si cumplen con su misión de identificar a cada fila de la tabla de forma unívoca entonces no importa que falten algunos valores.

Como los códigos son para uso externo entonces quizás los usuarios prefieran verlos en secuencia correlativa (1, 2, 3, 4, 5, 6, 7, 8, etc.). A la Base de Datos no le afecta que estén en secuencia o que falten algunos números, porque dentro de la Base de Datos no se los utiliza, entonces podrías cambiar los códigos para que siempre se muestren en secuencia y jamás haya números faltantes. Pero cuidado con esto, a la Base de Datos no le importará que cambies o no cambies los valores de los códigos, porque nunca los usa. Pero a los usuarios sí que puede importarles porque si un Producto ayer tenía el código 123 y hoy ese mismo producto tiene el código 87, podrías complicarles la vida. Por lo tanto, el hecho de que se pueda cambiar los códigos para que siempre estén en secuencia no implica que se deba hacer eso.

En una Base de Datos bien diseñada jamás hay motivo para cambiar el valor de un Identificador. Aunque sí puede haber motivos para cambiar el valor de un Código (normalmente no debería cambiarse, pero si se cambia no le afecta a la Base de Datos, solamente le afecta a los usuarios).

Artículos relacionados:

El índice del blog Firebird21

El foro del blog Firebird21

Maestro/detalle en Visual FoxPro y Firebird

9 comentarios

Es normal que en nuestras aplicaciones tengamos programas donde los usuarios introducen datos en tablas maestro/detalle.

El proceso es el siguiente:

Inicio de la transacción
   Grabación de la cabecera, retornando la Primary Key de esa cabecera
   Si la grabación de la cabecera estuvo ok
      Grabación de los detalles, guardando en una columna la Primary Key de la cabecera
   Fin si
   Si todo estuvo ok
      COMMIT
   Fin si
   Si hubo algún problema (con la grabación o con el COMMIT)
      ROLLBACK
   Fin si
Fin de la transacción

No siempre un COMMIT finaliza exitosamente, hay varios motivos por los cuales puede fallar (por ejemplo: problemas con la red o una transacción diferente tiene bloqueada una fila) y por lo tanto debemos verificar si tuvo éxito o no.

Primero, creamos un stored procedure para grabar la cabecera:

CREATE PROCEDURE GRABAR_COMPRASCAB(
   Identi TYPE OF COLUMN COMPRASCAB.CMC_IDENTI,
   NroDoc TYPE OF COLUMN COMPRASCAB.CMC_NRODOC)
RETURNS(
   tnIdenti TYPE OF COLUMN COMPRASCAB.CMC_IDENTI)
AS
BEGIN

   UPDATE OR INSERT INTO COMPRASCAB
                   (CMC_IDENTI, CMC_NRODOC)
             VALUES(:Identi   , :NroDoc)
   RETURNING
      CMC_IDENTI
   INTO
      tnIdenti;

END

Para simplificar y no hacerlo muy largo este stored procedure tiene solamente dos columnas, aunque lo normal es que tenga muchas más. Fíjate que retorna el valor de la columna CMC_IDENTI en un parámetro de retorno llamado tnIdenti. Eso significa que desde otros programas o stored procedures podremos consultar el valor de tnIdenti. Como se usa el comando UPDATE OR INSERT eso requiere que alguna de las columnas sea la Primary Key. En este ejemplo la Primary Key es la columna CMC_IDENTI, cuyo valor se retorna en el parámetro tnIdenti.

Luego, creamos el stored procedure para grabar los detalles:

CREATE PROCEDURE GRABAR_COMPRASDET(
   Identi TYPE OF COLUMN COMPRASDET.COM_IDENTI,
   IdeCab TYPE OF COLUMN COMPRASDET.COM_IDECAB,
   IdePrd TYPE OF COLUMN COMPRASDET.COM_IDEPRD,
   Cantid TYPE OF COLUMN COMPRASDET.COM_CANTID,
   Precio TYPE OF COLUMN COMPRASDET.COM_PRECIO)
AS
BEGIN

   UPDATE OR INSERT INTO COMPRASDET
                   (COM_IDENTI, COM_IDECAB, COM_IDEPRD, COM_CANTID, COM_PRECIO)
            VALUES (:Identi   , :IdeCab   , :IdePrd   , :Cantid   , : Precio   );

END

Fíjate que en la segunda columna (COM_IDECAB) se guarda el identificador del maestro (o cabecera).

Todos los identificadores son asignados en triggers before insert, similares al siguiente:

CREATE TRIGGER BI_COMPRASCAB_CMC_IDENTI FOR COMPRASCAB
   ACTIVE BEFORE INSERT
   POSITION 0
AS
BEGIN

   IF (NEW.CMC_IDENTI IS NULL OR NEW.CMC_IDENTI = 0) THEN
      NEW.CMC_IDENTI = GEN_ID(COMPRASCAB_CMC_IDENTI_GEN, 1);

END

O sea que si el nuevo identificador es NULL o es cero entonces se aumenta el valor del generador en 1 y ese valor se asigna al nuevo identificador.

En nuestro programa Visual FoxPro escribiríamos:

M.CMC_IDENTI = 0     && Ponemos cero cuando queremos grabar una nueva compra para que el trigger le asigne el valor
M.CMC_NRODOC = ThisForm.Text1.Value     && Le asignamos el valor al número del documento

lcComando = "EXECUTE PROCEDURE GRABAR_COMPRASCAB(?M.CMC_IDENTI, ?M.CMC_NRODOC)"

lnResultado = SQLExec(gnHandle, lcComando)

IF lnResultado > 0         && Si se ejecutó el stored procedure GRABAR_COMPRASCAB exitosamente
   lnIdeCab = tnIdenti     && Le asignamos a la variable lnIdeCab el valor retornado por el stored procedure GRABAR_COMPRASCAB
   select TEMP             && En la tabla temporal TEMP tenemos los valores que queremos grabar en COMPRASDET
   Locate                  && Se ubica en el primer registro de TEMP. Es lo mismo que escribir GO TOP pero más rápido
   SCAN WHILE lnResultado > 0     && Recorrerá los registros de TEMP mientras lnResultado sea mayor que cero y no se llegue a EOF()
      M.COM_IDENTI = 0              && Ponemos cero para que el trigger le asigne su valor
      M.COM_IDECAB = lnIdeCab       && En lnIdeCab tenemos el valor del identificador del maestro (cabecera)
      M.COM_IDEPRD = TEM_IDEPRD     && En TEM_IDEPRD tenemos el identificador del producto
      M.COM_CANTID = TEM_CANTID     && En TEM_CANTID tenemos la cantidad comprada
      M.COM_PRECIO = TEM_PRECIO     && En TEM_PRECIO tenemos el precio de compra unitario
      lcComando = "EXECUTE PROCEDURE GRABAR_COMPRASDET(?M.COM_IDENTI, ?M.COM_IDECAB, ?M.COM_IDEPRD, ?M.COM_CANTID, ?M.COM_PRECIO)"
      lnResultado = SQLExec(gnHandle, lcComando)
   ENDSCAN
ENDIF

IF lnResultado > 0     && Si todo fue grabado exitosamente
   lnResultado = SQLExec(gnHandle, "COMMIT")     && Se ejecuta el COMMIT
ENDIF

IF lnResultado < 0     && Si ocurrió un error al grabar o con el COMMIT
   =AError(laError)     && Guardamos en el vector (array) laError los detalles del error ocurrido
   =SQLExec(gnHandle, "ROLLBACK")
   =MessageBox("Ocurrió algún error. La compra no fue grabada. " + laError[2])
ENDIF

En la variable lnResultado tenemos el resultado de la ejecución de cada comando SQL. Si su valor es mayor que cero entonces se ejecutó exitosamente, si es menor que cero entonces ocurrió algún error. En este caso usamos la función AERROR() para obtener los datos del error ocurrido; en el segundo elemento del vector que recibió como parámetro tenemos la descripción del error, la cual mostramos al usuario.

Si la variable lnResultado tiene el valor cero significa que el comando aún está ejecutándose. Se lo utiliza en procesamientos asincrónicos, como veremos en otro artículo.

Artículos relacionados:

Maestro/Detalle. Como averiguar el identificador del Maestro

El índice del blog Firebird21

El foro del blog Firebird21