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

5 comentarios

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

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

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

Listado 1.

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

END;

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

¿Y si no hay suficiente espacio libre?

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

Listado 2.

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

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

¿Y entonces?

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

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

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

Listado 3.

CONNECT MiBaseDatos.FDB USER SYSDBA PASSWORD masterkey;

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

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

-- Finalizar
EXIT;

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

¿Qué hace este archivo de script?

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

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

Listado 4.

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

¿Qué hace este archivo batch?

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

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

¿Cómo ejecutamos ese archivo batch?

Tenemos dos formas:

  • Manualmente
  • Automáticamente

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

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

Conclusión:

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

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

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

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

Artículos relacionados:

El índice del blog Firebird21

El foro del blog Firebird21

 

Respuestas a más preguntas sobre transacciones (4)

1 comentario

¿Estás respondiendo las preguntas sobre transacciones?

Si lo estás haciendo, felicitaciones, si no lo estás haciendo, …. deberías, para aprender más.

Veamos cuales son las respuestas correctas del artículo: Más preguntas sobre transacciones (4)

Pregunta 1. Si una transacción solamente hará SELECTs ¿cuál debe ser su acceso?

[READ ONLY]     [READ WRITE]     [Es indiferente]

La respuesta correcta: Es indiferente. ¿Por qué? porque en ambos casos podrá hacer los SELECTs, sin embargo es preferible que sea READ ONLY porque las transacciones READ ONLY terminan más rápido que las transacciones READ WRITE debido a que el Firebird realiza menos tareas, pero no es obligatorio que sea READ ONLY.

Pregunta 2. Si una transacción solamente hará INSERTs ¿cuál debe ser su acceso?

[READ ONLY]     [READ WRITE]     [Es indiferente]

En este caso la transacción debe ser READ WRITE porque no se puede realizar un INSERT (ni un UPDATE, ni un DELETE) en las transacciones READ ONLY.

Pregunta 3. Si una transacción hará SELECTs e INSERTs, ¿cuál debe ser su acceso?

[READ ONLY]     [READ WRITE]     [Es indiferente]

Debe ser READ WRITE sí o sí, porque no se puede realizar un INSERT (ni un UPDATE, ni un DELETE) en una transacción READ ONLY.

Pregunta 4. Si una transacción quiere conocer lo que otras transacciones que finalizaron con COMMIT hicieron. ¿Cuál debe ser su aislamiento?

[READ COMMITTED]     [SNAPSHOT]     [Es imposible, nunca puede conocer lo que otras transacciones hicieron]

El aislamiento READ COMMITTED permite conocer lo que otras transacciones que ya finalizaron con un COMMIT hicieron. El aislamiento SNAPSHOT no permite tal cosa.

Pregunta 5. Si una transacción T1 ha bloqueado a una fila y una transacción T2 quiere actualizar a esa misma fila. ¿Cuál debe ser el modo de bloqueo de la transacción T2 para que pueda hacerlo?

[WAIT]     [NO WAIT]     [Cualquiera, es indiferente]     [Nunca la transacción T2 podrá actualizar esa fila]

Debe ser WAIT y entonces la transacción T2 esperará hasta que la transacción T1 finalice. Si el modo de bloqueo de la transacción T2 es NO WAIT entonces inmediatamente obtendrá una excepción y nunca podrá actualizar a esa fila.

Pregunta 6. Si una transacción T1 ha bloqueado a una fila, y quieres que la transacción T2 pueda ver la última versión confirmada de esa fila. ¿Cuál versión de registro debe tener la transacción T2?

[RECORD_VERSION]     [NO RECORD_VERSION]     [Es indiferente]

Debes usar RECORD_VERSION, porque en este caso la transacción T2 podrá ver la última versión confirmada de la fila. Si usas NO RECORD_VERSION entonces la fila que actualizó la transacción T1 será inaccesible para la transacción T2, inclusive para lectura.

Pregunta 7. Si una transacción T1 ha bloqueado una fila y no quieres que la transacción T2 pueda ni siquiera ver la última versión confirmada de esa fila ¿Cuál versión de registro debe tener la transacción T2?

[RECORD_VERSION]     [NO RECORD_VERSION]     [Es indiferente]

Debe ser NO RECORD_VERSION porque en este caso si una fila está bloqueada por una transacción las demás transacciones no pueden acceder a esa fila, ni siquiera para lectura.

Pregunta 8. Si una transacción quiere tener acceso exclusivo a todas las tablas que utiliza ¿puede hacerlo?

[Sí]     [No]

La respuesta es . En ese caso su aislamiento debe ser SNAPSHOT TABLE STABILITY. Si la transacción pudo iniciarse (no siempre podrá, otras transacciones podrían impedírselo) entonces tendrá acceso exclusivo.

Pregunta 9. Si una transacción quiere tener acceso exclusivo solamente a algunas de las tablas que utiliza ¿puede hacerlo?

[Sí]     [No]

La respuesta es . En ese caso debe escribir RESERVING y la lista de las tablas a las que quiere acceder de forma exclusiva.

Artículos relacionados:

Entendiendo a las transacciones

3 preguntas sobre transacciones

Respuestas a las 3 preguntas sobre transacciones

Más preguntas sobre transacciones (2)

Respuestas a más preguntas sobre transacciones (2)

Más preguntas sobre transacciones (3)

Respuestas a más preguntas sobre transacciones (3)

Más preguntas sobre transacciones (4)

El índice del blog Firebird21

El foro del blog Firebird21

Evitando conflictos en las transacciones

2 comentarios

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

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

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

Evitando los conflictos

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

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

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

Hay dos técnicas:

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

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

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

Ventaja de evitar los conflictos:

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

Desventaja de evitar los conflictos:

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

Conclusión:

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

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

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

Artículos relacionados:

Bloqueos mortales

Modos de bloqueo de las transacciones

Transacciones optimistas y transacciones pesimistas

Usando transacciones con acceso exclusivo a tablas

Algo más sobre transacciones optimistas y transacciones pesimistas

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

El índice del blog Firebird21

El foro del blog Firebird21

 

 

Usando vistas para insertar, actualizar, y borrar filas

7 comentarios

Probablemente ya has usado vistas para consultar datos y si no lo has hecho seguro que pronto lo harás, ya que eso es lo correcto. Tener las consultas precompiladas aumenta la velocidad de respuesta.

Sin embargo, ¿sabías que también puedes usar las vistas para insertar, actualizar, o borrar filas?

En un entorno multiusuario, donde muchas personas pueden estar al mismo tiempo conectadas a una Base de Datos es importantísimo limitar lo que cada una de esas personas puede hacer. De lo contrario, alguien podría estar haciendo algo que debería tener prohibido. Por ejemplo, un vendedor no debería tener la posibilidad de cambiar el precio de venta de un producto porque eso se presta a corrupción. Ni tampoco poder cambiar el porcentaje de comisión que cobra por cada venta.

Si necesitas que un usuario pueda insertar, actualizar, o borrar filas de una tabla puedes hacerlo de dos formas:

  1. Otorgándole derechos sobre esa tabla
  2. Otorgándole derechos sobre una vista, la cual se encargará de realizar las operaciones sobre dicha tabla

El problema de hacerlo con la primera forma es que el usuario podrá también ver y cambiar columnas que no debería poder ver o cambiar. Si le das el derecho de consulta sobre la tabla PRODUCTOS hace un SELECT * FROM PRODUCTOS y ve todas las filas y todas las columnas de esa tabla.

Mediante la segunda forma, en cambio, puedes controlar muchísimo mejor lo que puede y lo que no puede hacer. Si le das el derecho de consulta sobre la vista V_PRODUCTOS  la cual contiene un SELECT PRD_CODIGO, PRD_NOMBRE FROM PRODUCTOS, entonces solamente podrá ver el código y el nombre de cada producto y nada más.

Desde el punto de vista del Firebird puedes tratar a una vista como si fuera un usuario. Es decir que a una vista puedes otorgarle derechos (privilegios, permisos) sobre una tabla.

Y a los usuarios humanos les otorgas derechos sobre la vista, no sobre la tabla.

Por lo tanto, en lugar de escribir esto:

GRANT ALL ON PRODUCTOS TO ALICIA

GRANT ALL ON PRODUCTOS TO CLAUDIA

GRANT ALL ON PRODUCTOS TO MIRTHA

Sería mucho mejor que escribas esto:

GRANT ALL ON PRODUCTOS TO V_PRODUCTOS

GRANT ALL ON V_PRODUCTOS TO ALICIA

GRANT ALL ON V_PRODUCTOS TO CLAUDIA

GRANT ALL ON V_PRODUCTOS TO MIRTHA

Y fíjate en la primera línea, allí se trata a la vista V_PRODUCTOS como si fuera una persona, ¡¡¡se le están otorgando derechos sobre una tabla!!!. Los derechos se le otorgan a la vista, no a una persona.

¿Qué significa todo esto?

Que ni ALICIA, ni CLAUDIA, ni MIRTHA podrán hacer algo con la tabla PRODUCTOS, no tienen derechos ahí, ni siquiera para consultarla. Pero sí tienen derechos sobre la vista V_PRODUCTOS y por lo tanto estarán limitadas a lo que la vista haga.

Hasta ahí todo bien pero ¿cómo hago para insertar, actualizar, o borrar filas usando vistas?

Muy simple, las vistas también tienen triggers. Entonces es en esos triggers donde escribirás el código que insertará, actualizará, o borrará filas de la/s tabla/s subyacentes. Aunque en realidad no es obligatorio que uses triggers pero sí es conveniente porque te da más control.

VISTAS1

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

VISTAS2

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

VISTAS3

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

Como puedes ver en la Captura 3, esta vista solamente nos permite insertar o actualizar las columnas PRD_IDENTI, PRD_CODIGO, y PRD_NOMBRE, pero no podemos hacer lo mismo con las columnas PRD_PRECTO y PRD_PREVTA. ¿Por qué no? porque en el trigger solamente podemos usar las columnas que pertenecen a la vista. Ni la columna PRD_PRECTO ni la columna PRD_PREVTA pertenecen a la vista, por lo tanto no las podemos usar en el trigger. Si lo intentas, obtendrás un mensaje de error.

En otras palabras, si a un usuario le otorgamos derechos sobre la vista V_PRODUCTOS él solamente podrá ver, insertar, y actualizar las columnas PRD_IDENTI, PRD_CODIGO, y PRD_NOMBRE, y ninguna columna más, porque ninguna otra columna pertenece a la vista.

Por lo tanto, si necesitamos que un usuario pueda acceder a las columnas PRD_PRECTO y PRD_PREVTA entonces deberemos crear otra vista, que le otorgue acceso a esas columnas.

¿Pero hacerlo así no nos obliga a trabajar más?

Pues sí, pero no puedes tenerlo todo fácil en la vida. Se trabaja más pero en contrapartida tu aplicación será mucho más segura y mucho más confiable. Si cualquiera puede meter las pezuñas en tus tablas eso tarde o temprano te provocará grandes problemas. Así que mejor evitarlos desde el principio.

UPDATE OR INSERT INTO V_PRODUCTOS
         (PRD_IDENTI, PRD_CODIGO, PRD_NOMBRE)
  VALUES (0         , '1234'    , 'Producto xxx');

Como puedes ver en el código fuente de arriba, la inserción, actualización, o borrado se hace de la forma normal, las diferencias son:

  1. Se escribe el nombre de una vista, no el nombre de una tabla
  2. Solamente se pueden usar las columnas que pertenecen a la vista

Conclusión:

Usar vistas para realizar todas las operaciones de manipulación de tablas (inserción, actualización, borrado, consulta) es lo mejor, es lo correcto, es lo ideal. ¿Por qué? porque eso nos da un control total, 100%, sobre lo que cada usuario puede hacer o no hacer.

De esta manera los usuarios nunca, y por ningún motivo, acceden directamente a las tablas. Todos los accesos lo hacen a través de las vistas. Y en nuestras vistas podemos usar la cláusula WHERE para establecer condiciones. Por lo tanto, solamente podrán hacer lo que específicamente queremos que puedan hacer. Y nada más.

Artículo relacionado:

El índice del blog Firebird21

 

Conociendo la aplicación que insertó filas a una tabla

2 comentarios

Cuando una Base de Datos puede ser accedida desde varias aplicaciones es muy importante saber cual aplicación insertó las filas que tiene cada tabla.

¿Por qué?

Porque solamente la aplicación que insertó la fila debería tener el derecho de modificar o de borrar esa fila. Si otra aplicación pudiera hacerlo, eso solamente acarreará problemas. Imagínate que la aplicación de VENTAS registró una venta y cuando se la quiere consultar no se la encuentra por ningún lado porque CONTABILIDAD borró esa venta. Eso es inadmisible.

¿Qué necesitaremos?

  1. Una tabla APLICACIONES, para guardar los nombres de las aplicaciones que pueden usarse
  2. Una tabla GTT, para guardar los datos de la conexión actual, incluyendo el Identificador de la aplicación usada para el acceso
  3. Un trigger de Base de Datos que le inserte una fila a la tabla GTT
  4. Desde nuestra aplicación actualizar la tabla GTT para que tenga el Identificador de aplicación correcto
  5. Agregarle una columna a todas las tablas que pueden usarse por más de una aplicación
  6. Dos excepciones, para mostrar los mensajes de error
  7. Agregarle un trigger a cada tabla que puede usarse por más de una aplicación, para decidir si la fila puede ser modificada o eliminada

Tabla APLICACIONES

APLICACIÓN1

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

CREATE TABLE APLICACIONES (
   APL_IDENTI D_IDENTIFICADOR NOT NULL,
   APL_NOMBRE D_NOMBRE40);

   ALTER TABLE APLICACIONES ADD CONSTRAINT PK_APLICACIONES PRIMARY KEY (APL_IDENTI);

   ALTER TABLE APLICACIONES ADD CONSTRAINT UQ_APLICACIONES UNIQUE (APL_NOMBRE);

SET TERM ^ ;

CREATE TRIGGER BI_APLICACIONES_APL_IDENTI FOR APLICACIONES
   ACTIVE BEFORE INSERT
   POSITION 0
AS
BEGIN
   IF (NEW.APL_IDENTI IS NULL OR NEW.APL_IDENTI = 0) THEN
      NEW.APL_IDENTI = GEN_ID(APLICACIONES_APL_IDENTI_GEN, 1);
END^

SET TERM ; ^

El contenido de esta tabla podría ser algo así:

APLICACIÓN2

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

Tabla GTT: CONEXION_ACTUAL

En una tabla GTT guardamos los datos que necesitamos conocer de la conexión que actualmente está usando el usuario.

APLICACIÓN5

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

Trigger de Base de Datos para insertarle una fila a la tabla GTT

CREATE TRIGGER NEW_DBTRIGGER_C
   ACTIVE ON CONNECT
      POSITION 0
AS
BEGIN

   INSERT INTO
      CONEXION_ACTUAL
      (CON_IDENTI, CON_USUARI, CON_TIMEST)
   VALUES
      (CURRENT_CONNECTION, CURRENT_USER, CURRENT_TIMESTAMP) ;

END;

El valor de la columna faltante (CON_APLICA) debería ser puesto por la aplicación correspondiente.

Contenido de la tabla CONEXION_ACTUAL

APLICACIÓN6Captura 4. Si haces clic en la imagen la verás más grande

Ejemplo:

A la tabla CLIENTES pueden insertarle filas las aplicaciones de Contabilidad, Facturación, y Ventas. Si la fila fue insertada por Contabilidad entonces Facturación y Ventas no deberían tener el derecho de modificar esa fila ni de borrarla. De la misma manera, si la fila fue insertada por Facturación o por Ventas las otras aplicaciones no deberían poder cambiarla.

Cualquier aplicación puede consultar una fila, pero solamente la aplicación que la insertó puede modificarla o borrarla.

APLICACIÓN3

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

Y para ver los nombres de las aplicaciones que insertaron las filas podríamos escribir algo como:

SELECT
   C.CLI_IDENTI,
   C.CLI_NOMBRE,
   C.CLI_IDEAPL,
   A.APL_NOMBRE AS CLI_NOMAPL
FROM
   CLIENTES C
JOIN
   APLICACIONES A
      ON C.CLI_IDEAPL = A.APL_IDENTI

que nos dará como resultado algo similar a:

APLICACIÓN4

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

Aquí como puedes ver las filas de MARCELA, DIANA y MIRTA fueron insertadas por CONTABILIDAD, en cambio la fila de SILVIA fue insertada por FACTURACIÓN. Eso implica que si debemos modificar o borrar las filas de MARCELA, DIANA, o MIRTA deberemos hacerlo desde la CONTABILIDAD y si queremos modificar o borrar la fila de SILVIA deberemos hacerlo desde FACTURACIÓN.

Esto nos asegura que nunca una aplicación modifique o borre las filas que no le corresponden.

Las excepciones para mostrar los mensajes de error

CREATE EXCEPTION E_UPDATE_NO_ADMITIDO 'No puedes modificar el contenido de esta fila';

CREATE EXCEPTION E_DELETE_NO_ADMITIDO 'No tienes permiso para borrar filas de esta tabla';

El trigger para modificar o borrar filas de la tabla CLIENTES

CREATE TRIGGER BUD_CLIENTES1 FOR CLIENTES
   ACTIVE BEFORE UPDATE OR DELETE
      POSITION 0
AS
   DECLARE VARIABLE lnAplicacion SMALLINT;
BEGIN

   lnAplicacion = (SELECT CON_APLICA FROM CONEXION_ACTUAL);

   IF (UPDATING AND NEW.CLI_IDEAPL <> lnAplicacion) THEN
      EXCEPTION E_UPDATE_NO_ADMITIDO;

   IF (DELETING AND OLD.CLI_IDEAPL <> lnAplicacion) THEN
      EXCEPTION E_DELETE_NO_ADMITIDO;

END;

Terminando:

Y listo, eso es todo, ahora solamente la aplicación que insertó una fila podrá modificar o borrar esa fila. Los intentos realizados por las demás aplicaciones serán  rechazados, como tiene que ser.

Conclusión:

Siempre es muy importante delimitar exactamente que puede hacer cada aplicación que se conecta a nuestra Base de Datos, no podemos permitir que una aplicación modifique o borre filas que fueron insertadas por otra aplicación porque eso solamente nos traerá problemas y ningún beneficio.

Artículos relacionados:

Los triggers de la Base de Datos

El índice del blog Firebird21

Creando y actualizando tablas agregadas de uso general

1 comentario

En este artículo ya habíamos visto una forma de crear tablas agregadas y de insertarles datos:

https://firebird21.wordpress.com/2013/12/28/creando-tablas-agregadas-e-insertandoles-datos/

sin embargo, tenía un problema: para cada tabla agregada que decidieras crear tendrías que escribir dos stored procedures:

  • Uno para crear la tabla y su Primary Key
  • Otro para insertarle datos a la tabla

Si solamente vas a necesitar una o dos tablas agregadas eso no sería un problema pero si necesitarás 30 ó 40 tablas agregadas entonces ya es otro tema porque tendrías que escribir 60 u 80 stored procedures. Muy aburrido.

Por ese motivo escribí dos stored procedures de uso general. Siempre se necesitan al menos dos porque dentro de un stored procedure no se puede escribir un COMMIT y si no se le hizo el COMMIT a una tabla entonces no se le pueden insertar datos.

También escribí otro stored procedure llamado PARSER cuya misión es extraer un subtexto dentro de un texto.

El stored procedure PARSER

CREATE PROCEDURE PARSER(
      tcTexto     VARCHAR(18192),
      tcSeparador VARCHAR(12))
   RETURNS(
      ftcNombre    VARCHAR(1024))
AS
   DECLARE VARIABLE lnPosicion SMALLINT;
BEGIN

   lnPosicion = Position(tcSeparador IN tcTexto);

   ftcNombre = Left(tcTexto, lnPosicion - 1) ;

END;

Este stored procedure extrae de un texto un subtexto, para saber cual es el subtexto que debe extraer se usa un separador o delimitador. Algunos ejemplos:

PARSER de ‘hoy,es,un,día,soleado’ usando como delimitador la coma nos dará como resultado ‘hoy’

PARSER de ‘mañana*será*otro*día’ usando como delimitador el asterisco nos dará como resultado ‘mañana’

El delimitador puede tener más de un carácter si lo deseas. Está definido VARCHAR(12) así que podrías tener delimitadores de hasta 12 caracteres. Nunca necesitarás tanto, pero bueno, ahí está.

Las vista V_MOVIMIENTOS

Los datos que se insertarán en la tabla agregada deben tener un origen, o sea que deben salir de algún lado. Ese origen puede ser una tabla o una vista, en estos ejemplos usé una vista llamada V_MOVIMIENTOS.

CREATE VIEW V_MOVIMIENTOS(
   MOV_IDECAB,
   MOV_FECHAX,
   MOV_TIPDOC,
   MOV_NRODOC,
   MOV_IDEPRD,
   MOV_NOMPRD,
   MOV_CANTID)
AS
   SELECT
      MOV_IDECAB,
      MVC_FECHAX AS MOV_FECHAX,
      MVC_TIPDOC AS MOV_TIPDOC,
      MVC_NRODOC AS MOV_NRODOC,
      MOV_IDEPRD,
      PRD_NOMBRE AS MOV_NOMPRD,
      MOV_CANTID
   FROM
      MOVIMDET
   JOIN
      MOVIMCAB
         ON MOV_IDECAB = MVC_IDENTI
   JOIN
      PRODUCTOS
         ON MOV_IDEPRD = PRD_IDENTI;

En la tabla PRODUCTOS tenemos los datos de los productos, en la tabla MOVIMDET (detalles de movimientos) los detalles de las ventas y en la tabla MOVIMCAB (cabecera de movimientos) los datos de cabecera.

PIVOT1

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

El stored procedure CREAR_TABLA_PIVOT

CREATE PROCEDURE CREAR_TABLA_PIVOT(
   tcNombreTabla            VARCHAR(28),
   tcVista                  VARCHAR(28),
   tcPrimeraColumnaCabecera VARCHAR(64),
   tcOtrasColumnasCabecera  VARCHAR(4096),
   tcColumnaDatos           VARCHAR(28),
   tcTipoDatos              VARCHAR(64))
AS
   DECLARE VARIABLE lcCreate VARCHAR(4096);
   DECLARE VARIABLE lcColumna VARCHAR(28);
   DECLARE VARIABLE lcMensajeError VARCHAR(1024);
BEGIN

   -- Primero, creamos la tabla

   lcCreate = 'CREATE TABLE ' || tcNombreTabla || ' (';

   lcCreate = lcCreate || tcPrimeraColumnaCabecera || ' NOT NULL, ';

   IF (Char_Length(tcOtrasColumnasCabecera) > 0) THEN BEGIN
      tcOtrasColumnasCabecera = tcOtrasColumnasCabecera || ',';
      WHILE (Char_Length(tcOtrasColumnasCabecera) > 0) DO BEGIN
         EXECUTE PROCEDURE Parser(tcOtrasColumnasCabecera, ',') RETURNING_VALUES :lcColumna;
         lcCreate = lcCreate || Trim(lcColumna) || ' , ' ;
         tcOtrasColumnasCabecera = Replace(tcOtrasColumnasCabecera, lcColumna || ',', '');
      END
   END

   FOR EXECUTE STATEMENT
      'SELECT DISTINCT ' || tcColumnaDatos || ' FROM ' || tcVista INTO :lcColumna
   DO BEGIN
      lcColumna = Left(lcColumna, 28);
      lcColumna = Replace(lcColumna, ' ', '_');
      lcColumna = Replace(lcColumna, '.', '_');
      lcColumna = Replace(lcColumna, '/', '_');
      lcColumna = Replace(lcColumna, '%', '_');
      lcCreate = lcCreate || lcColumna || ' ' || tcTipoDatos || ', ' ;
   END

   lcCreate = Left(lcCreate, Char_Length(lcCreate) - 2);

   lcCreate = lcCreate || ');';

   EXECUTE STATEMENT lcCreate;

   -- Segundo, le agregamos una Primary Key

   EXECUTE STATEMENT
      'ALTER TABLE ' || tcNombreTabla ||
      ' ADD CONSTRAINT PK_' || tcNombreTabla ||
      ' PRIMARY KEY (' || Left(:tcPrimeraColumnaCabecera, Position(' ', :tcPrimeraColumnaCabecera)) || ')';

END;

Este stored procedure te permite crear una tabla y es totalmente general, esa tabla puede tener la cantidad de columnas fijas que desees. Puedes crear una tabla con 1 columna fija, otra tabla con 2 columnas fijas, otra tabla con 3 columnas fijas, etc. Las columnas variables son ilimitadas pero debes mantener un número que sea manejable. En estos ejemplos si tienes 4 productos entonces tendrás 4 columnas, si tienes 20 productos entonces tendrás 20 columnas. Pero si tienes 300 productos nadie se molestará en mirar todas las columnas de la tabla, así que sería una pérdida de tiempo crearla.

Mi regla cuando uso tablas agregadas es nunca crearlas con más de 12 columnas variables porque simplemente nadie las utiliza cuando tienen más columnas, así que ni vale la pena crearlas.

El stored procedure ACTUALIZAR_TABLA_PIVOT

CREATE PROCEDURE ACTUALIZAR_TABLA_PIVOT(
   tcTablaPivot             VARCHAR(  28),
   tcVista                  VARCHAR(  28),
   tcPrimeraColumnaCabecera VARCHAR(  64),
   tcOtrasColumnasCabecera  VARCHAR(4096),
   tcColumnaDatos           VARCHAR(  28),
   tcValoresDatos           VARCHAR(1024))
AS
   DECLARE VARIABLE lcOtrasColumnasCabecera VARCHAR(4096);
   DECLARE VARIABLE lcSelect                VARCHAR(4096);
   DECLARE VARIABLE lcColumna               VARCHAR(  28);
   DECLARE VARIABLE lcGrupo                 VARCHAR(4096);
   DECLARE VARIABLE lcInto1                 VARCHAR(1024);
   DECLARE VARIABLE lcInto2                 VARCHAR(1024);
   DECLARE VARIABLE lcInto3                 VARCHAR(1024);
   DECLARE VARIABLE lcActualizar            VARCHAR(1024);
BEGIN

   lcOtrasColumnasCabecera = tcOtrasColumnasCabecera;

   lcSelect = 'SELECT ' ||
              Left(tcPrimeraColumnaCabecera, Position(' ', tcPrimeraColumnaCabecera) - 1) || ', ' ;

   lcGrupo = Left(tcPrimeraColumnaCabecera, Position(' ', tcPrimeraColumnaCabecera) - 1) || ', ' ;

   IF (Char_Length(lcOtrasColumnasCabecera) > 0) THEN BEGIN
      lcOtrasColumnasCabecera = lcOtrasColumnasCabecera || ',' ;
      WHILE (Char_Length(lcOtrasColumnasCabecera) > 0) DO BEGIN
         EXECUTE PROCEDURE Parser(lcOtrasColumnasCabecera, ',') RETURNING_VALUES :lcColumna;
         lcGrupo = lcGrupo || Left(lcColumna, Position(' ', lcColumna) - 1) || ',' ;
         lcOtrasColumnasCabecera = Replace(lcOtrasColumnasCabecera, lcColumna || ',', '') ;
      END
   END

   lcSelect = lcSelect ||
              tcColumnaDatos || ', ' ||
              tcValoresDatos ||
              ' FROM ' || tcVista ||
              ' GROUP BY ' || lcGrupo ||
              tcColumnaDatos ;

   FOR EXECUTE STATEMENT
      lcSelect
   INTO
      :lcInto1,
      :lcInto2,
      :lcInto3
   DO BEGIN
      -- Aquí se insertan o actualizan la primera columna y los datos agrupados
      lcInto2 = Left(lcInto2, 28);
      lcInto2 = Replace(lcInto2, ' ', '_');
      lcInto2 = Replace(lcInto2, '.', '_');
      lcInto2 = Replace(lcInto2, '/', '_');
      lcInto2 = Replace(lcInto2, '%', '_');
      lcActualizar = 'UPDATE OR INSERT INTO ' ||
                     tcTablaPivot || ' (' ||
                     Left(tcPrimeraColumnaCabecera, Position(' ', tcPrimeraColumnaCabecera) - 1) || ',' ||
                     lcInto2 ||
                     ') VALUES(' ||
                     lcInto1 || ',' ||
                     lcInto3 || ')' ;
      EXECUTE STATEMENT lcActualizar;
      IF (Char_Length(Trim(tcOtrasColumnasCabecera)) > 0) THEN BEGIN
         lcOtrasColumnasCabecera = tcOtrasColumnasCabecera || ',' ;
         lcActualizar = 'UPDATE ' || tcTablaPivot || ' SET ' ;
         WHILE (Char_Length(lcOtrasColumnasCabecera) > 0) DO BEGIN
            -- Aquí se actualizan todas las demás columnas de la cabecera
            EXECUTE PROCEDURE Parser(lcOtrasColumnasCabecera, ',') RETURNING_VALUES :lcColumna;
            lcActualizar = lcActualizar ||
                           Left(lcColumna, Position(' ', lcColumna) - 1) ||
                           ' = (SELECT ' || Left(lcColumna, Position(' ', lcColumna) - 1) ||
                           ' FROM ' || tcVista ||
                           ' WHERE ' ||
                           Left(tcPrimeraColumnaCabecera, Position(' ', tcPrimeraColumnaCabecera) - 1) ||
                           '=' || lcInto1 ||
                           ' ROWS 1),' ;
            lcOtrasColumnasCabecera = Replace(lcOtrasColumnasCabecera, lcColumna || ',', '') ;
         END
         lcActualizar = Left(lcActualizar, Char_Length(lcActualizar) - 1) ||
                       ' WHERE ' ||
                       Left(tcPrimeraColumnaCabecera, Position(' ', tcPrimeraColumnaCabecera) - 1) || '=' ||
         lcInto1;
         EXECUTE STATEMENT lcActualizar;
      END
   END

END;

Este stored procedure es el encargado de insertarle datos a la tabla agregada. No importa cuantas columnas tenga la tabla, él se encargará de la inserción y actualización de los datos.

Ejemplo:

EXECUTE PROCEDURE
   CREAR_TABLA_PIVOT(
      'PRUEBA1',
      'V_MOVIMIENTOS',
      'MOV_IDECAB INTEGER',
      'MOV_FECHAX DATE,MOV_TIPDOC SMALLINT,MOV_NRODOC VARCHAR(15)',
      'MOV_NOMPRD',
      'NUMERIC(17, 4)')

PIVOT3

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

Como puedes ver, la tabla PRUEBA1 se creó exactamente como se le indicó.

EXECUTE PROCEDURE ACTUALIZAR_TABLA_PIVOT(
   'PRUEBA1',
   'V_MOVIMIENTOS',
   'MOV_IDECAB INTEGER',
   'MOV_FECHAX DATE,MOV_TIPDOC SMALLINT,MOV_NRODOC VARCHAR(15)',
   'MOV_NOMPRD',
   'SUM(MOV_CANTID)')

Y este es el stored procedure encargado de insertarle y de actualizarle las filas a la tabla PRUEBA1. Al ejecutarlo esto es lo que obtenemos:

PIVOT2

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

Donde tenemos 4 columnas fijas (MOV_IDECAB, MOV_FECHAX, MOV_TIPDOC, MOV_NRODOC) y 4 columnas variables (todas las demás).

Los parámetros de entrada de los stored procedures

tcNombreTabla. Es el nombre que queremos darle a la tabla que vamos a crear, o el nombre que tiene la tabla a la cual le insertaremos datos. Para saber si la tabla ya existe y no intentar crearla en ese caso podrías escribir:

lcTablaExiste = IIF((SELECT RDB$RELATION_NAME FROM RDB$RELATIONS WHERE RDB$RELATION_NAME = 'PRUEBA1') = 'PRUEBA1', 'S', 'N')

Si el valor de la variable lcTablaExiste es igual a ‘S’ entonces ya existe una tabla con el nombre ‘PRUEBA1’.

tcVista. Es el nombre de la vista o de la tabla que contiene los datos que serán agrupados.

tcPrimeraColumnaCabecera. Es el nombre que veremos en la primera columna. Este dato siempre debe especificarse porque no se puede crear ni usar la tabla si no se lo especifica. Las filas de la vista tcvista son agrupadas según esta columna. Fíjate que debes escribir el nombre de una columna de tcvista y también su tipo de datos (SMALLINT, INTEGER, etc.).

tcOtrasColumnasCabecera. Esto es opcional, no es obligatorio tener más de una columna fija. Si las usarás entonces deberás escribir el nombre de la columna, su tipo de datos (INTEGER, DATE, NUMERIC, etc.) y luego una coma para separar a una columna de la siguiente.

tcColumnaDatos. Es la columna que tiene los datos variables. En nuestro ejemplo, es la columna donde se tienen registrados los nombres de los productos. En otros casos podría tener nombres de alumnos, de profesores, de países, de libros, etc.

tcTipoDatos. Es el tipo de los datos variables. Debe ser uno numérico (SMALLINT, INTEGER, DECIMAL, NUMERIC, etc.) y se lo necesita cuando se crea la tabla.

tcValoresDatos. En general se trata de una función agregada. El resultado de ejecutar la función agregada es el que se guardará en las columnas variables. Ejemplos: SUM(MOV_CANTID), SUM(MOV_CANTID * MOV_PRECIO), MAX(MOV_PRECIO), etc.

Manejo de errores

Para que los stored procedures no sean muy largos no les escribí código que se encargue de atrapar los errores, pero es algo que tú deberías hacer para evitar errores como los siguientes:

  • No existe la tabla
  • No existe el nombre de la columna
  • No existe el tipo de datos
  • El tipo de datos es incorrecto
  • etc.

Conclusión:

Si usamos tablas agregadas nuestras aplicaciones se potenciarán muchísimo porque ellas les permiten a los gerentes analizar lo que está ocurriendo y por lo tanto son de una gran ayuda para la toma de decisiones.

Los stored procedures que has visto en este artículo son de uso general, o sea que te servirán con cualquier tabla y con cualquier vista. Además, puedes tener todas las columnas fijas y todas las columnas variables que desees. Sin embargo, debes recordar que la mayoría de las personas no mirarán tablas que tengan más de 12 columnas variables y eso implica que en tu vista debes poner condiciones que limiten la cantidad de filas con nombres de productos (o de lo que sea) distintos.

Artículos relacionados:

Creando tablas agregadas e insertándoles datos

El índice del blog Firebird21

Entendiendo subconsultas y tablas derivadas

6 comentarios

El lenguaje SQL nos permite escribir un SELECT en muchos lugares, veamos algunos ejemplos:

Ejemplo 1. Un SELECT simple

SELECT
   MiColumna1,
   MiColumna2
FROM
   MiTabla

En este caso se escribió solamente un SELECT, es útil para consultas sencillas pero cuando las consultas se complican ya no será suficiente.

Ejemplo 2. Un SELECT con una subconsulta

SELECT
   MiColumna1,
   (SELECT MiColumna2 FROM MiTabla2 WHERE MiCondición)
FROM
   MiTabla

En este ejemplo hemos reemplazado a MiColumna2 por una subconsulta, o sea por un SELECT dentro de otro SELECT. El SELECT que está entre paréntesis es la subconsulta. Fíjate que además del nombre de la tabla se escribió la cláusula WHERE ¿por qué eso? porque la subconsulta debe devolver una sola columna y una sola fila.

Si la subconsulta devuelve más de una columna entonces verás el error: “Count of column list and variable list do not match”. O sea que se esperaba una sola columna pero la subconsulta está devolviendo más de una columna.

Si devuelve una sola columna pero más de una fila entonces el mensaje de error que verás será: “Multiple rows in singleton select”. ¿qué significa esa frase? que la subconsulta debería devolver una sola fila pero está devolviendo más de una fila.

La cláusula WHERE sirve justamente para poner una condición que estás seguro que se cumple para una sola fila. Además de la cláusula WHERE también podrías usar, dependiendo del caso, las cláusulas FIRST 1, ó ROWS 1 ó DISTINCT o las funciones agregadas MAX(), MIN(), etc. Lo que debes recordar es que la subconsulta debe devolver una fila y solamente una fila. Puede devolver cero filas, y en ese caso el valor de la columna será NULL (en SQL un valor de NULL significa “valor desconocido”) pero jamás más de una fila.

Ejemplo 3. Un SELECT a continuación del FROM

SELECT
   MiColumna1,
   MiColumna2
FROM
   (SELECT MiColumna1, MiColumna2 FROM MiTabla)

Aquí el SELECT interno se escribió después de la cláusula FROM. En estos casos no se le llama subconsulta sino que se le llama tabla derivada.

Para que funcione, la cantidad de columnas de la tabla derivada siempre debe ser igual o mayor a la cantidad de columnas del SELECT principal, nunca puede ser menor (y a la tabla principal si lo deseas puedes agregarle columnas que tengan valores constantes, como 17, 21, ‘Asunción’, etc.). Y los nombres de las columnas deben coincidir, no puedes usar una columna en el SELECT principal que no hayas usado en la tabla derivada.

Fíjate que la subconsulta debe devolver una sola columna pero la tabla derivada puede devolver varias columnas.

Ejemplo 4. Una subconsulta en la cláusula GROUP BY

SELECT
   MiColumna1,
   (SELECT MiColumna2 FROM MiTabla2 WHERE MiCondición)
FROM
   MiTabla
GROUP BY
   MiColumna1,
   (SELECT MiColumna2 FROM MiTabla2 WHERE MiCondición)

En este caso, la subconsulta se usó también luego de la cláusula GROUP BY, eso es necesario porque todas las columnas que se incluyen en el SELECT principal y que no son funciones agregadas también deben encontrarse a continuación de la cláusula GROUP BY.

Ejemplo 5. Una subconsulta en la cláusula ORDER BY

SELECT
   MiColumna1,
   MiColumna2
FROM
   MiTabla
ORDER BY
   (SELECT MiColumna3 FROM MiTabla2 WHERE MiCondición)

Aquí como puedes ver la subconsulta se escribió a continuación de la cláusula ORDER BY y por lo tanto el SELECT principal será ordenado por el contenido de la subconsulta.

Ejemplo 6. Un subconsulta en el JOIN

SELECT
   MiColumna1,
   MiColumna2
FROM
   MiTabla
JOIN
   MiTabla2
      ON MiColumna = (SELECT MiOtraColumna FROM MiOtraTabla)

También puede usarse una subconsulta en el JOIN, sea éste del tipo que sea (INNER, LEFT, RIGHT, FULL, CROSS, NATURAL)

Ejemplo 7. Insertando, modificando o borrando filas desde una subconsulta

Si necesitas insertarle datos a una tabla y los datos que necesitas se encuentran en otra u otras tablas la forma más rápida es hacerlo con una subconsulta:

INSERT INTO
MiTabla
   (MiColumna1, MiColumna2, MiColumna3)
   SELECT MiValor1, MiValor2, MiValor3 FROM MiOtraTabla

También puedes usar una subconsulta para establecer la condición que necesitas para modificar o borrar filas. En general se usan subconsultas cuando las filas que se quiere insertar, modificar o borrar son muchas.

Conclusión:

Como seguramente habrás deducido luego de ver los ejemplos anteriores, las subconsultas pueden usarse en muchos lugares, prácticamente puedes usarlas en cualquier lugar donde necesites el valor de una columna.

Hay muchos ejemplos más que podría escribir pero con los anteriores supongo que ya tienes bien clara la idea.

¿Es conveniente usar subconsultas?

A veces sí, a veces no. En ocasiones es lo mejor que puedes hacer y en ocasiones hay mejores alternativas así que debes tratarlas como una herramienta más, algo que te puede ayudar a conseguir los resultados que estás buscando. Pero debes recordar que en general usar subconsultas es más lento que no usarlas y por lo tanto debes siempre verificar que no exista una mejor opción.

Artículos relacionados:

Tablas derivadas

El índice del blog Firebird21

Older Entries