Instalando Firebird silenciosamente

2 comentarios

Firebird puede ser instalado silenciosamente, es decir sin que se vean mensajes ni pantallas mientras se está instalando, eso puede ser muy útil cuando además del Firebird estamos instalando nuestra aplicación. También cuando queremos que se instale sin intervención del usuario.

Lo primero que debemos hacer, evidentemente, es descargar el ejecutable del Firebird, lo podemos encontrar en este enlace:

http://www.firebirdsql.org/en/firebird-2-5-2-upd1/

Instalación1

 

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

Luego, abrimos la ventana “Símbolo del sistema” con derechos de administrador (para ello buscamos el icono y hacemos clic con el botón derecho):

Instalación2

 

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

Y a continuación escribimos el nombre del ejecutable y sus opciones, por ejemplo:

Instalación3

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

De esta manera luego de esperar unos segundos habremos instalado al Firebird y durante la instalación ningún mensaje fue visible.

Las opciones que podemos utilizar para la instalación son las siguientes:

/? | /h | /HELP
/FORCE
/NOCPL
/NOGDS32
/COPYFBCLIENT
/COMPONENTS=”lista de componentes separados por comas”
/TASKS=”lista de tareas separadas por comas”
/MERGETASKS=”lista de tareas separadas por comas”

/CLEAN

/SP-
/SILENT | /VERYSILENT [/SUPPRESSMSGBOXES]
/LOG[=”Nombre_del_archivo”]
/NOCANCEL
/NORESTART
/RESTARTEXITCODE=código de salida
/LOADINF=”Nombre_del_archivo”
/SAVEINF=”Nombre_del_archivo”
/LANG=lenguaje
/DIR=”x:\Nombre_carpeta”
/GROUP=”Nombre_carpeta”
/NOICONS

Artículo relacionado:

El índice del blog Firebird21

 

Entendiendo la Integridad Referencial

Deja un comentario

La “integridad referencial” es un concepto utilizado en las bases de datos y significa que la relación entre dos tablas siempre es consistente.

¿Qué significa la palabra “integridad”?

Que no pueden sobrar ni faltar datos, deben estar todos, ninguno más y ninguno menos.

¿Qué significa la palabra “referencia”?

Que conociendo algunos datos de una tabla se pueden conocer todos los datos de la otra tabla.

¿Cómo se establece la relación entre las dos tablas?

A través de una restricción Foreign Key.

En una restricción Foreign Key una de las tablas actúa como “padre” y la otra tabla actúa como “hija”. La relación entre ambas tablas está dada por una columna (o más de una columna) cuyos tipos de datos son idénticos. Por ejemplo, se puede establecer entre una columna INTEGER de la tabla “padre” y una columna INTEGER de la tabla “hija”, pero no se puede entre una columna INTEGER de la tabla “padre” y una columna VARCHAR de la tabla “hija”.

  • SMALLINT con SMALLINT, ok
  • INTEGER con INTEGER, ok
  • BIGINT con BIGINT, ok
  • SMALLINT con VARCHAR, error
  • INTEGER con FLOAT, error
  • VARCHAR con DATE, error
  • etc.

¿Qué es una relación consistente?

Aquella en la cual cada fila de la tabla “hija” tiene una fila y solamente una fila correspondiente en la tabla “padre”. Si una fila de la tabla “hija” no tiene una fila (y solamente una fila) correspondiente en la tabla “padre” entonces ya no existe la integridad referencial entre ambas tablas.

  • 1 fila de la tabla “hija” se corresponde con 0 filas de la tabla “padre” —> no hay integridad referencial
  • 1 fila de la tabla “hija” se corresponde con 1 fila de la tabla “padre” —> sí hay integridad referencial
  • 1 fila de la tabla “hija” se corresponde con 2 ó más filas de la tabla “padre” —> no hay integridad referencial

¿Qué debemos tener en cuenta para establecer una integridad referencial?

Que la relación entre la tabla “hija” y la tabla “padre” se haga mediante la Primary Key de la tabla “padre” o de una Unique Key de la tabla “padre”.

¿Qué implica que entre dos tablas exista una integridad referencial?

  1. Que no puedes agregarle una fila a la tabla “hija” si no existe la fila correspondiente en la tabla “padre”
  2. Que no puedes cambiar la columna usada en la referencia en la tabla “padre” (a menos que hayas establecido “actualización en cascada”)
  3. Que no puedes borrar la fila usada en la referencia en la tabla “padre” (a menos que hayas establecido “borrado en cascada”)

¿Qué significan “actualización en cascada” y “borrado en cascada”?

  • “Actualización en cascada” significa que si cambias el valor de la columna en la tabla “padre” ese mismo cambio se efectuará automáticamente en todas las filas de la tabla “hija” correspondientes
  • “Borrado en cascada” significa que si borras una fila de la tabla “padre” todas las filas correspondientes de la tabla “hija” también serán borradas.

Ejemplo:

Tenemos una tabla llamada PAISES y otra tabla llamada PRODUCTOS. Queremos que la tabla PRODUCTOS referencie a la tabla PAISES, para poder así saber cual es el país del cual proceden nuestros productos.

INTEGRIDAD1

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

La Primary Key de esta tabla está compuesta por las columnas PAI_CODSUC y PAI_IDENTI

INTEGRIDAD2

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

Podemos establecer una restricción “Foreign Key” entre ambas tablas usando las dos columnas que tienen en común: Código de la Sucursal e Identificador del País:

ALTER TABLE PRODUCTOS ADD CONSTRAINT FK_PRODUCTOS3 FOREIGN KEY (PRD_CODSUC,PRD_IDEPAI) REFERENCES PAISES(PAI_CODSUC,PAI_IDENTI);

Como ambas tablas están relacionadas mediante una “Foreign Key”, entonces:

  1. No podemos colocar en la columna PRD_IDEPAI un número que no exista en la columna PAI_IDENTI. O sea que el número que coloquemos en la columna PRD_IDEPAI debe existir en la columna PAI_IDENTI
  2. No podemos colocar NULL en la columna PRD_IDEPAI
  3. No podemos cambiar el número que se encuentra en la columna PAI_IDENTI si ese número existe en alguna columna PRD_IDEPAI
  4. No podemos borrar una fila de la tabla PAISES si el número de su columna PAI_IDENTI existe en la columna PRD_IDEPAI de alguna fila de la tabla PRODUCTOS

El punto 3. podemos hacerlo si establecimos “actualización en cascada”, ejemplo:

ALTER TABLE PRODUCTOS ADD CONSTRAINT FK_PRODUCTOS3 FOREIGN KEY (PRD_CODSUC,PRD_IDEPAI) REFERENCES PAISES(PAI_CODSUC,PAI_IDENTI) ON UPDATE CASCADE;

El punto 4. podemos hacerlo si establecimos “borrado en cascada”, ejemplo:

ALTER TABLE PRODUCTOS ADD CONSTRAINT FK_PRODUCTOS3 FOREIGN KEY (PRD_CODSUC,PRD_IDEPAI) REFERENCES PAISES(PAI_CODSUC,PAI_IDENTI) ON DELETE CASCADE;

Conclusión:

Siempre que sea posible debemos tener tablas con “integridad referencial” porque eso nos asegura de tener datos consistentes en las tablas “hijas”.

Como no se puede guardar en una tabla “hija” un valor que no exista en su tabla “padre” eso nos da la seguridad de que todos los valores de la tabla “hija” existan en la tabla “padre” y evitamos así tener filas “huérfanas” (se llaman así a las que no tienen “padre”). Las filas huérfanas solamente pueden causar problemas y ningún beneficio.

Artículos relacionados:

Entendiendo a las Foreign Keys

El índice del blog Firebird21

Averiguando el tamaño en bytes de una tabla

Deja un comentario

¿Cuántos bytes ocupa en el disco duro una tabla?

Para responder a esta pregunta podemos hacer uso del programa GSTAT.EXE que viene incluido en la instalación del Firebird y que lo encontrarás en su carpeta \BIN

Hay que ejecutar el programa GSTAT con la opción -t y el nombre de la tabla que nos interesa.

Ejemplo:

Queremos averiguar cuantos bytes ocupa en el disco duro la tabla PROVEEDORES, para ello ejecutamos el programa GSTAT con la opción -t PROVEEDORES:

TAMANO1

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

Page size nos indica cual es el tamaño de cada página de la Base de Datos, en bytes. Todas las páginas tienen el mismo tamaño. En este ejemplo el tamaño de cada página es 4096 bytes como podemos ver en (1)

Data pages nos indica cuantas páginas ocupa la tabla, en este ejemplo la tabla se llama PROVEEDORES y ocupa 4 páginas, como podemos ver en (2)

Entonces, para saber cuantos bytes está ocupando en el disco la tabla PROVEEDORES simplemente multiplicamos el tamaño de cada página por la cantidad de páginas (4096 * 4) y obtenemos como  resultado 16384 bytes.

Artículo relacionado:

El índice del blog Firebird21

 

Un stored procedure para recrear todas las vistas

Deja un comentario

En ocasiones necesitamos recrear las vistas de nuestra Base de Datos; a veces ocurre que después de modificar la estructura de una tabla cuando queremos ejecutar una vista obtenemos el mensaje de error: “string truncation”.

Como pueden ser varias las vistas afectadas por ese problema, lo más inteligente es tener un stored procedure que se encargue de recrearlas a todas:

CREATE PROCEDURE RECREAR_VISTAS
AS
   DECLARE VARIABLE lcNombreVista VARCHAR(   32);
   DECLARE VARIABLE lcCodigoVista VARCHAR(32765);
   DECLARE VARIABLE lcColumna     VARCHAR(   32);
   DECLARE VARIABLE lcComando     VARCHAR(32765);
   DECLARE VARIABLE lcFila        VARCHAR( 1024);
BEGIN

   -- Para cada vista de la Base de Datos
   FOR
      SELECT
         TRIM(R.RDB$RELATION_NAME),
         R.RDB$VIEW_SOURCE
      FROM
         RDB$RELATIONS R
      WHERE
         R.RDB$VIEW_SOURCE IS NOT NULL
      ORDER BY
         R.RDB$RELATION_NAME
      INTO
         :lcNombreVista,
         :lcCodigoVista
      DO BEGIN
         lcFila    = '';
         lcColumna = '';
         -- Para cada columna de esta vista
         FOR
            SELECT
               TRIM(F.RDB$FIELD_NAME)
            FROM
               RDB$RELATION_FIELDS F
            WHERE
               F.RDB$RELATION_NAME = :lcNombreVista
            ORDER BY
               F.RDB$FIELD_POSITION
            INTO
               :lcColumna
            DO
               IF (lcFila = '') THEN
                  lcFila = ' ' || :lcColumna;
               ELSE
                  lcFila = :lcFila || ', ' || ASCII_CHAR(13) || ASCII_CHAR(10) || ' ' || :lcColumna;
         -- Se modifica la vista
         lcComando = 'CREATE OR ALTER VIEW ' ||
                   :lcNombreVista || '(' ||
                   ASCII_CHAR(13) || ASCII_CHAR(10) ||
                   :lcFila ||
                   ') ' ||
                   ASCII_CHAR(13) || ASCII_CHAR(10) ||
                   'AS ' ||
                   :lcCodigoVista ;
         EXECUTE STATEMENT lcComando;
      END
END;

Este stored procedure se encargará de recrear a todas las vistas de la Base de Datos, asegurándonos por tanto que todas ellas se encuentran en perfectas condiciones.

¿Cómo funciona este stored procedure?

En primer lugar, obtiene el nombre de cada vista y el código fuente de ella.

En segundo lugar, obtiene el nombre de cada columna involucrada en esa vista.

En tercer lugar, guarda todos esos datos en una variable de tipo carácter.

En cuarto y último lugar, ejecuta el comando que recrea la vista.

Artículo relacionado:

El índice del blog Firebird21

 

Borrando filas cuando las condiciones están en otra tabla

1 comentario

A veces podemos necesitar borrar filas de una tabla, pero las filas que queremos borrar dependen de una o más condiciones de otra tabla.

Por ejemplo, escribimos esta consulta:

SELECT
   E.EMP_NOMBRE,
   E.EMP_APELLD,
   P.PRO_IDENTI,
   P.PRO_NOMBRE
FROM
   EMPLEADOS   E
JOIN
   PROFESIONES P
   ON E.EMP_IDEPRO = P.PRO_IDENTI
WHERE
   P.PRO_NOMBRE = 'Secretaria'

Y obtenemos este resultado, que nos muestra a todas las “Secretarias” que tenemos:

BORRAR1

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

Fíjate que la condición para mostrar las filas (la que se encuentra en el WHERE) depende de la tabla PROFESIONES, no de la tabla EMPLEADOS. Nuestra tabla principal es EMPLEADOS, pero la condición se encuentra en otra tabla, en la tabla de PROFESIONES.

Y ahora queremos borrar a todas las “Secretarias”. Pero si escribimos algo así:

DELETE FROM
   EMPLEADOS   E
JOIN
   PROFESIONES P
   ON E.EMP_IDEPRO = P.PRO_IDENTI
WHERE
   P.PRO_NOMBRE = 'Secretaria'

no funcionará. ¿Por qué no? porque el comando DELETE no tiene una cláusula JOIN que podamos usar. Entonces, ¿cómo hacemos para borrar a todas las “Secretarias”?

Reemplazando la cláusula JOIN por la función EXISTS(), como vemos a continuación:

Solución 1

DELETE FROM
   EMPLEADOS E
WHERE
   EXISTS(SELECT
             PRO_NOMBRE
          FROM
             PROFESIONES P
          WHERE
             E.EMP_IDEPRO = P.PRO_IDENTI AND
             P.PRO_IDENTI = 1)

Solución 2

DELETE FROM
   EMPLEADOS E
WHERE
   EXISTS(SELECT
             PRO_NOMBRE
          FROM
             PROFESIONES P
          WHERE
             E.EMP_IDEPRO = P.PRO_IDENTI AND
             P.PRO_NOMBRE = 'Secretaria')

En la Solución 1 usamos el identificador con el cual se referencian las tablas de EMPLEADOS y de PROFESIONES, mientras que en la Solución 2 usamos el nombre de la Profesión.

Con ambas soluciones se obtiene exactamente el mismo resultado, así que ya dependerá de tí cual de ellas te resulta más conveniente. En general, lo recomendable es que cuando se quiere borrar una fila se use el identificador de esa fila (o sea su Primary Key) y no alguna otra columna, es más seguro así.

Lo que debes notar es que en la cláusula WHERE que se encuentra dentro de la función EXISTS() debes escribir la misma condición que escribiste en el JOIN y además la condición que escribiste en el WHERE de tu SELECT. Es solamente eso, como ves, se soluciona muy fácilmente.

Pero siempre verifica que el resultado obtenido es exactamente el buscado antes de ejecutar tu COMMIT, porque si escribes mal el WHERE podrías llegar a borrar todas las filas de la tabla, no solamente algunas.

Conclusión:

Ni el comando INSERT, ni el comando UPDATE, ni el comando DELETE cuentan con una cláusula JOIN pero siempre podemos simular un INNER JOIN si usamos la función EXISTS().

Lo que debemos recordar es verificar que el resultado obtenido sea exactamente el buscado antes de ejecutar el COMMIT, porque si escribimos una mala condición en el WHERE podríamos estar afectando a muchas más filas de las que deberían, con resultados catrastróficos.

Artículo relacionado:

El índice del blog Firebird21

Usando DISTINCT y EXISTS() eficientemente

Deja un comentario

En muchas consultas podemos obtener los mismos resultados si usamos la cláusula DISTINCT o la función EXISTS(). Pero obtener los mismos resultados no significa que ambas formas sean igual de rápidas.

Por ejemplo, podemos tener una tabla de PROFESIONES con estas filas:

DISTINCT_EXIST_1

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

Y una tabla de EMPLEADOS con estas filas:

DISTINCT_EXIST_2

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

Y lo que nos interesa saber es: ¿cuáles son las profesiones de nuestros empleados?

Consulta 1. Usando DISTINCT

SELECT
   DISTINCT
   P.PRO_IDENTI,
   P.PRO_NOMBRE
FROM
   PROFESIONES P
INNER JOIN
   EMPLEADOS E
      ON P.PRO_CODSUC = E.EMP_CODSUC AND
         P.PRO_IDENTI = E.EMP_IDEPRO
WHERE
   P.PRO_CODSUC >= 0

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

Consulta 2. Usando la función EXISTS()

SELECT
   P.PRO_IDENTI,
   P.PRO_NOMBRE
FROM
   PROFESIONES P
WHERE
   P.PRO_CODSUC >= 0 AND
   EXISTS (SELECT E.EMP_IDENTI FROM EMPLEADOS E WHERE E.EMP_CODSUC = P.PRO_CODSUC AND E.EMP_IDEPRO = P.PRO_IDENTI)

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

Conclusión:

Como puedes observar viendo la Captura 3 y la Captura 4, en ambos casos el resultado obtenido es el mismo, pero los tiempos y los consumos de memoria son distintos. En general, la función EXISTS() es más rápida que la cláusula DISTINCT … si hay un índice que pueda usar la función EXISTS() ¿por qué eso? porque si hay un índice, verifica que exista el dato buscado y listo, ya está; en cambio DISTINCT por cada fila que recorre debe verificar que ya no se encuentre en el resultado obtenido hasta ese momento, y eso lleva más tiempo.

Así que ¿en tu consulta has utilizado la cláusula DISTINCT? Cámbiala usando la función EXISTS() y verifica si obtienes más rápido el resultado.

Artículo relacionado:

El índice del blog Firebird21

Creando y usando tablas temporales

7 comentarios

En ocasiones necesitamos guardar datos en forma temporal, esos datos solamente nos son útiles durante un rato y luego ya son desechables. Para esas ocasiones es que podemos usar las tablas temporales.

¿Y por qué no usar una tabla normal y escribir un “DELETE FROM MiTabla” antes de insertarle datos? De esa manera obtendríamos los mismos resultados que usando una tabla temporal, ¿verdad?

Sí y no.

Si un solo usuario necesita de esos datos entonces sí, usar una tabla temporal o usar una tabla normal a la cual le borramos todas las filas antes de empezar el proceso sería exactamente lo mismo.

Pero la cuestión se complica cuando son dos o más los usuarios que necesitan usar esa tabla, por ejemplo si usamos tablas normales podría ocurrir algo como esto:

  • El usuario 1 borró todas las filas de la tabla
  • El usuario 1 le insertó algunas filas a la tabla
  • El usuario 1 hizo un COMMIT
  • El usuario 2 borró todas las filas de la tabla
  • El usuario 2 le insertó algunas filas a la tabla
  • El usuario 2 hizo un COMMIT
  • El usuario 1 quiere consultar las filas de la tabla …. y no encuentra las filas que él insertó, sino las filas que insertó el usuario 2

En cambio, si se usan tablas temporales algo como lo anterior jamás podría ocurrir porque cada usuario tiene su propia versión de la tabla temporal. Es decir que lo que hace el usuario 1 es totalmente independiente de lo que hace el usuario 2. El usuario 1 puede hacer lo que se le antoje en la tabla temporal que el usuario 2 jamás se enterará. Y viceversa.

A las tablas temporales en Firebird se las conoce como GTT (Global Temporary Table) y pueden ser de dos clases:

  • Confinadas a la transacción
  • Confinadas a la conexión

“Confinadas a la transacción” significa que la tabla temporal solamente puede tener filas mientras está siendo usada en una transacción. Cuando la transacción termina todas esas filas son automáticamente borradas definitivamente, sin importar como haya finalizado la transacción (con un COMMIT o con un ROLLBACK) porque en ambos casos todas las filas de la tabla temporal son borradas.

“Confinadas a la conexión” significa que la tabla temporal solamente puede tener filas mientras dure la conexión actual. Cuando la actual conexión con la Base de Datos termina, todas las filas de la tabla temporal son borradas definitivamente.

¿Cómo se crea una tabla temporal?

Escribiendo el comando “CREATE GLOBAL TEMPORARY TABLE MiTabla”, definiendo las columnas y luego finalizando con la cláusula “ON COMMIT DELETE ROWS” (si queremos una tabla temporal confinada a la transacción) o con la cláusula “ON COMMIT PRESERVE ROWS” (si queremos una tabla temporal confinada a la conexión).

¿Las tablas temporales pueden tener Primary Key?

¿Las tablas temporales pueden tener Foreign Key?

Sí, pero limitadamente. Una tabla temporal no puede referenciar a una tabla normal. Tampoco una tabla temporal confinada a la conexión puede referenciar a una tabla temporal confinada a la transacción, por lo tanto las Foreign Keys posibles son:

  • De una tabla temporal confinada a la transacción a una tabla temporal confinada a la transacción
  • De una tabla temporal confinada a la conexión a una tabla temporal confinada a la conexión
  • De una tabla temporal confinada a la transacción a una tabla temporal confinada a la conexión

¿Las tablas temporales pueden tener Unique Key?

¿Las tablas temporales pueden tener índices?

 Sí

Ejemplo 1:

CREATE GLOBAL TEMPORARY TABLE TEMP (
   TEM_IDENTI D_IDENTIFICADOR NOT NULL,
   TEM_NOMBRE D_NOMBRE40)
ON COMMIT DELETE ROWS;

ALTER TABLE TEMP ADD CONSTRAINT PK_TEMP PRIMARY KEY (TEM_IDENTI);

Aquí, creamos una tabla temporal llamada TEMP la cual estará confinada a la transacción. O sea que cuando la transacción finalice con un COMMIT o con un ROLLBACK todas las filas de esta tabla desaparecerán. También le agregamos una Primary Key.

Ejemplo 2:

CREATE GLOBAL TEMPORARY TABLE TEMP2 (
   TEM_IDENTI D_IDENTIFICADOR NOT NULL,
   TEM_NOMBRE D_NOMBRE40)
ON COMMIT PRESERVE ROWS;

ALTER TABLE TEMP2 ADD CONSTRAINT PK_TEMP2 PRIMARY KEY (TEM_IDENTI);

Aquí, creamos una tabla temporal llamada TEMP2, la cual estará confinada a la conexión. O sea que cuando la conexión con la Base de Datos finalice todas las filas de la tabla TEMP2 serán eliminadas.

Verificando que las filas de las tablas temporales confinadas a la transacción son eliminadas

Para verificar que tanto un COMMIT como un ROLLBACK eliminan a todas las filas de la tabla temporal confinada a la transacción escribimos lo siguiente:

GTT1

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

Como puedes ver, el primer SELECT mostró todas las filas insertadas pero el segundo SELECT no, ¿por qué? porque se escribió un COMMIT antes de él y con eso se finalizó la transacción. O sea que el segundo SELECT ya está en otra transacción.

GTT2

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

En este caso se escribió un ROLLBACK antes del segundo SELECT y el resultado fue el mismo que obtuvimos al escribir el COMMIT, o sea: ninguna fila mostrada. Lo cual significa que tanto si la transacción finaliza con un COMMIT o con un ROLLBACK todas las filas de la tabla temporal son eliminadas. El segundo SELECT ya está en otra transacción porque la transacción anterior finalizó con el ROLLBACK.

¿En qué casos es recomendable usar tablas temporales?

Cuando te facilitan la vida. El caso típico es cuando debes realizar una consulta que involucra a varias tablas normales pero esa consulta es demasiado complicada, tiene un montón de condiciones en el WHERE o en el HAVING, y muchos JOINs o UNIONs y te cuesta llegar al resultado deseado porque tu SELECT nunca te muestra lo que te debería mostrar, por más que lo intentas y lo intentas, nunca obtienes lo que deberías obtener y ya te da dolor de cabeza. Entonces, en lugar de estar lidiando con un SELECT inmenso, que te marea de solo mirarlo, es mucho más conveniente filtrar el contenido de las tablas involucradas, colocarlos en algunas tablas temporales y luego con unos pocos JOINs obtienes lo que buscabas.

Al usar tablas temporales seguramente escribirás más, pero todo lo que escribas será fácilmente entendible y entonces obtener las filas deseadas será muy rápido.

Entonces ¿tu SELECT involucra a varias tablas normales, las condiciones del WHERE o del HAVING son muchas y no obtienes lo que deseas? Empieza a pensar en usar tablas temporales.

Artículo relacionado:

El índice del blog Firebird21

Older Entries