Página principal
Artículos y trucos
Catálogo de productos
Ejemplos y descargas
Mis libros
Cursos de formación
Investigación y desarrollo
Libros recomendados
Mis páginas favoritas
Acerca del autor
 
En colaboración con Amazon
 
Intuitive Sight

¿Consulta o procedimiento?

Bien sabido es que InterBase ofrece generadores como técnica favorita para obtener valores incrementales. A diferencia de lo que sucede en SQL Server con las columnas marcadas con el atributo identity, es el programador quien debe recuperar el valor actual del generador e incrementarlo, para luego asignarlo explícitamente a las filas nuevas antes de grabarlas. Si utilizamos DataSnap, lo más sencillo y eficiente es que interceptemos el evento BeforeUpdateRecord de un TDataSetProvider, para desde ahí recuperar un valor desde el generador adecuado y asignarlo como valor en el campo correspondiente.


La ventaja que ofrece un generador frente a una técnica alternativa, como utilizar una tabla de contadores, consiste en que el generador no se queda bloqueado hasta el final de la transacción activa, como sucede con las filas de una tabla de contadores.

Sea cual sea nuestra metodología de programación, es casi seguro que en algún momento necesitaremos pedir un valor a un generador desde nuestra aplicación. Algunos programadores, para ahorrarse trabajo, utilizan un componente de consulta para ejecutar la siguiente sentencia SQL (suponiendo que GenClaves es un generador):

select gen_id(GenClaves, 1)
from   RDB$DATABASE

La tabla RDB$DATABASE es una tabla de sistema de InterBase, que siempre contiene una sola línea, por lo que la consulta anterior siempre devuelve una sola fila. En los heroicos tiempos anteriores a InterBase 6.5, no se podía denegar a los usuarios invitados el acceso a las tablas del sistema, por lo que siempre podíamos contar con la "existencia" de RDB$DATABASE. Esta situación ha cambiado a partir de InterBase 6.5, pero también es cierto que para la consulta anterior podíamos haber utilizado cualquier otra tabla a la que tuviera acceso el usuario y que contuviese un único registro.

Para ejecutar la sentencia anterior, el programador necesita un componente de consulta. Si suponemos que está utilizando DB Express, ese componente pertenecería a la clase TSQLQuery, y el código en Delphi que recuperaría el siguiente valor de un generador podría escribirse de este modo:

function TDatos.Generar: Integer;
begin
   SQLQuery1.Open;
   try
      Result := SQLQuery1.Fields[0].AsInteger;
   finally
      SQLQuery1.Close;
   end;
end;

¿Hay alguna alternativa? ¡Por supuesto que la hay! A no ser que nos invada la pereza, podríamos también crear un procedimiento almacenado en el servidor como el siguiente:

create procedure ProcClaves returns (id integer) as
begin
    id = gen_id(GenClaves, 1);
end

Suponiendo que el componente SQLStoredProc1 hace referencia a ProcClaves, el código alternativo para Generar quedaría así:

function TDatos.Generar: Integer;
begin
   SQLStoredProc1.ExecProc;
   Result := SQLStoredProc1.Params[0].AsInteger;
end;

Y ahora, la pregunta del millón de euros: ¿cuál de las dos técnicas es la más eficiente? Siempre digo que la Informática es una ciencia experimental (esto es, cuando no estoy borracho, porque entonces digo que es un Arte). Antes de embarcarnos en razonamientos teóricos, deberíamos hacer una prueba...

...como la que ya he realizado por usted.Utilizando Delphi 7, InterBase 7 y dos ordenadores en red, ejecuté las dos variantes un número suficientemente elevado de veces. El resultado:

Gana el procedimiento almacenado, porque tarda sólo el 60% del tiempo de la consulta.

Era predecible. Para ejecutar la consulta hace falta al menos dos "viajes de ida y vuelta" (o round trips) a través de la red. En el primero, se abre la consulta y probablemente se recupera el primer y único registro del resultado. El segundo avisa al servidor que debe cerrar el cursor asociado a la consulta. En contraste, ejecutar el procedimiento almacenado sólo requiere un viaje de ida y vuelta. Este resultado no se ve alterado por la configuración del acceso a datos (el parámetro CommitRetain, por ejemplo), ni por la forma en que lancemos las transacciones (una transacción implícita, una transacción para cada petición, etcétera). Tampoco influye el valor que podamos asignar a la propiedad Prepared de la consulta. Probablemente, esto último se deba a que DB Express parece preparar siempre sus consultas de manera automática. Y por supuesto, las propiedades GetMetadata y NoMetadata del componente TSQLQuery se configuraron durante la prueba para que no perdiesen tiempo recuperando información sobre metadatos.

Puede parecer ridículo intentar ahorrar una fracción de tiempo aparentemente insignificante, pero:

  • Estas "minucias" se vuelven sumamente importantes cuando el número de usuarios concurrentes comienza a crecer.
  • Si utilizamos un procedimiento almacenado, nuestras intenciones quedan más claramente documentadas en el código fuente de la base de datos y en el de la aplicación.
  • Con la técnica de la consulta, corremos el riesgo de que el administrador de la base de datos restrinja el acceso a las tablas del sistema, y nos deje con tres palmos de narices. Es cierto que siempre podremos crear una tabla con una fila para que sustituya a RDB$DATABASE... pero ya puestos a añadir objetos ad hoc en la base de datos, ¿no sería mejor dejarnos de rodeos y crear directamente el procedimiento almacenado? Además, no las tengo todas conmigo respecto a la creación de versiones innecesarias de registros, si utilizamos una tabla modificable en vez de RDB$DATABASE.

Por último, se trata de un problema de disciplina del programador. Me explico: si ahora estuviese desarrollando una aplicación con un plazo ajustado de tiempo, no perdería mi tiempo haciendo este tipo de experimentos, ni recomendaría a nadie que lo hiciese. Pero una vez hecha la prueba (¡me sobraba algo de tiempo, para variar!) y conociendo el resultado, ¿qué sentido tendría seguir a sabiendas la peor de las alternativas?