SlideShare una empresa de Scribd logo
1 de 150
Descargar para leer sin conexión
Breve introducción a SQL 451
balanceada. Estas instrucciones pueden ejecutarse periódicamente, para garantizar
índices con tiempo de acceso óptimo:
alter index NombreEmpleado inactive;
alter index NombreEmpleado active;
Otra instrucción que puede mejorar el rendimiento del sistema y que está relacionada
con los índices es set statistics. Este comando calcula las estadísticas de uso de las
claves dentro de un índice. El valor obtenido, conocido como selectividad del índice, es
utilizado por InterBase para elaborar el plan de implementación de consultas. Nor-
malmente no hay que invocar a esta función explícitamente, pero si las estadísticas de
uso del índice han variado mucho es quizás apropiado utilizar la instrucción:
set statistics index NombreEmpleado;
Por último, las instrucciones drop nos permiten borrar objetos definidos en la base
de datos, tanto tablas como índices:
drop table Tabla;
drop index Indice;
Creación de vistas
Uno de los recursos más potentes de SQL, y de las bases de datos relacionales en
general, es la posibilidad de definir tablas “virtuales” a partir de los datos almacena-
dos en tablas “físicas”. Para definir una de estas tablas virtuales hay que definir qué
operaciones relacionales se aplican a qué tablas bases. Este tipo de tabla recibe el
nombre de vista.
Como todavía no conocemos el lenguaje de consultas, que nos permite especificar
las operaciones sobre tablas, postergaremos el estudio de las vistas para más adelante.
Creación de usuarios
InterBase soporta el concepto de usuarios a nivel del servidor, no de las bases de
datos. Inicialmente, todos los servidores definen un único usuario especial: SYSDBA.
Este usuario tiene los derechos necesarios para crear otros usuarios y asignarles con-
traseñas. Toda esa información se almacena en la base de datos isc4.gdb, que se instala
automáticamente con InterBase. La gestión de los nombres de usuarios y sus contra-
señas se realiza mediante la utilidad Server Manager.
452 La Cara Oculta de Delphi
Dentro de esta aplicación, hay que ejecutar el comando de menú Tasks|User security,
para llegar al diálogo con el que podemos añadir, modificar o eliminar usuarios. La
siguiente imagen muestra el diálogo de creación de nuevos usuarios:
El nombre del usuario SYSDBA no puede cambiarse, pero es casi una obligación
cambiar su contraseña en cuanto termina la instalación de InterBase. Sin embargo,
podemos eliminar al administrador de la lista de usuarios del sistema. Si esto sucede,
ya no será posible añadir o modificar nuevos usuarios en ese servidor. Así que tenga
cuidado con lo que hace.
El sistema de seguridad explicado tiene un par de aparentes "fallos". En primer
lugar, cualquier usuario con acceso al disco duro puede sustituir el fichero isc4.gdb
con uno suyo. Más grave aún: si copiamos el fichero gdb de la base de datos en un
servidor en el cual conozcamos la contraseña del administrador, tendremos ac-
ceso total a los datos, aunque este acceso nos hubiera estado vedado en el servi-
dor original.
En realidad, el fallo consiste en permitir que cualquier mequetrefe pueda acceder
a nuestras apreciadas bases de datos. Así que, antes de planear la protección del
sistema de gestión de base de datos (ya sea InterBase o cualquier otro), ocúpese
de controlar el acceso al servidor de la gente indeseable.
Breve introducción a SQL 453
Asignación de privilegios
Una vez creados los objetos de la base de datos, es necesario asignar derechos sobre
los mismos a los demás usuarios. Inicialmente, el dueño de una tabla es el usuario
que la crea, y tiene todos los derechos de acceso sobre la tabla. Los derechos de ac-
ceso indican qué operaciones pueden realizarse con la tabla. Naturalmente, los nom-
bres de estos derechos o privilegios coinciden con los nombres de las operaciones
correspondientes:
Privilegio Operación
select Lectura de datos
update Modificación de datos existentes
insert Creación de nuevos registros
delete Eliminación de registros
all Los cuatro privilegios anteriores
execute Ejecución (para procedimientos almacenados)
La instrucción que otorga derechos sobre una tabla es la siguiente:
grant Privilegios on Tabla to Usuarios [with grant option]
Por ejemplo:
/* Derecho de sólo-lectura al público en general */
grant select on Articulos to public;
/* Todos los derechos a un par de usuarios */
grant all privileges on Clientes to Spade, Marlowe;
/* Monsieur Poirot sólo puede modificar salarios (¡qué peligro!) */
grant update(Salario) on Empleados to Poirot;
/* Privilegio de inserción y borrado, con opción de concesión */
grant insert, delete on Empleados to Vance with grant option;
He mostrado unas cuantas posibilidades de la instrucción. En primer lugar, podemos
utilizar la palabra clave public cuando queremos conceder ciertos derechos a todos
los usuarios. En caso contrario, podemos especificar uno o más usuarios como desti-
natarios del privilegio. Luego, podemos ver que el privilegio update puede llevar
entre paréntesis la lista de columnas que pueden ser modificadas. Por último, vemos
que a Mr. Philo Vance no solamente le permiten contratar y despedir empleados,
sino que también, gracias a la cláusula with grant option, puede conceder estos
derechos a otros usuarios, aún no siendo el creador de la tabla. Esta opción debe
utilizarse con cuidado, pues puede provocar una propagación descontrolada de pri-
vilegios entre usuarios indeseables.
¿Y qué pasa si otorgamos privilegios y luego nos arrepentimos? No hay problema,
pues para esto tenemos la instrucción revoke:
454 La Cara Oculta de Delphi
revoke [grant option for] Privilegios on Tabla from Usuarios
Hay que tener cuidado con los privilegios asignados al público. La siguiente instruc-
ción no afecta a los privilegios de Sam Spade sobre la tabla de artículos, porque antes
se le ha concedido al público en general el derecho de lectura sobre la misma:
/* Spade se ríe de este ridículo intento */
revoke all on Articulos from Spade;
Existen variantes de las instrucciones grant y revoke pensadas para asignar y retirar
privilegios sobre tablas a procedimientos almacenados, y para asignar y retirar dere-
chos de ejecución de procedimientos a usuarios. Estas instrucciones se estudiarán en
el momento adecuado.
Roles
Los roles son una especificación del SQL-3 que InterBase 5 ha implementado. Si los
usuarios se almacenan y administran a nivel de servidor, los roles, en cambio, se defi-
nen a nivel de cada base de datos. De este modo, podemos trasladar con más facili-
dad una base de datos desarrollada en determinado servidor, con sus usuarios parti-
culares, a otro servidor, en el cual existe históricamente otro conjunto de usuarios.
Primero necesitamos crear los roles adecuados en la base de datos:
create role Domador;
create role Payaso;
create role Mago;
Ahora debemos asignar los permisos sobre tablas y otros objetos a los roles, en vez
de a los usuarios directamente, como hacíamos antes. Observe que InterBase sigue
permitiendo ambos tipos de permisos:
grant all privileges on Animales to Domador, Mago;
grant select on Animales to Payaso;
Hasta aquí no hemos mencionado a los usuarios, por lo que los resultados de estas
instrucciones son válidos de servidor a servidor. Finalmente, debemos asignar los
usuarios en sus respectivos roles, y esta operación sí depende del conjunto de usua-
rios de un servidor:
grant Payaso to Bill, Steve, RonaldMcDonald;
grant Domador to Ian with admin option;
Breve introducción a SQL 455
La opción with admin option me permite asignar el rol de domador a otros usua-
rios. De este modo, siempre habrá quien se ocupe de los animales cuando me au-
sente del circo por vacaciones.
Un ejemplo completo de script SQL
Incluyo a continuación un ejemplo completo de script SQL con la definición de tablas
e índices para una sencilla aplicación de entrada de pedidos. En un capítulo posterior,
ampliaremos este script para incluir triggers, generadores y procedimientos almacena-
dos que ayuden a expresar las reglas de empresa de la base de datos.
create database "C:PedidosPedidos.GDB"
user "SYSDBA" password "masterkey"
page_size 2048;
/* Creación de las tablas */
create table Clientes (
Codigo int not null,
Nombre varchar(30) not null,
Direccion1 varchar(30),
Direccion2 varchar(30),
Telefono varchar(15),
UltimoPedido date default "Now",
primary key (Codigo)
);
create table Empleados (
Codigo int not null,
Apellidos varchar(20) not null,
Nombre varchar(15) not null,
FechaContrato date default "Now",
Salario int,
NombreCompleto computed by (Nombre || " " || Apellidos),
primary key (Codigo)
);
create table Articulos (
Codigo int not null,
Descripcion varchar(30) not null,
Existencias int default 0,
Pedidos int default 0,
Costo int,
PVP int,
primary key (Codigo)
);
create table Pedidos (
Numero int not null,
RefCliente int not null,
456 La Cara Oculta de Delphi
RefEmpleado int,
FechaVenta date default "Now",
Total int default 0,
primary key (Numero),
foreign key (RefCliente) references Clientes (Codigo)
on delete no action on update cascade
);
create table Detalles (
RefPedido int not null,
NumLinea int not null,
RefArticulo int not null,
Cantidad int default 1 not null,
Descuento int default 0 not null
check (Descuento between 0 and 100),
primary key (RefPedido, NumLinea),
foreign key (RefPedido) references Pedidos (Numero),
on delete cascade on update cascade
foreign key (RefArticulo) references Articulos (Codigo)
on delete no action on update cascade
);
/* Indices secundarios */
create index NombreCliente on Clientes(Nombre);
create index NombreEmpleado on Empleados(Apellidos, Nombre);
create index Descripcion on Articulos(Descripcion);
/***** FIN DEL SCRIPT *****/
Capítulo
22
Consultas y modificaciones enConsultas y modificaciones en
SQLSQL
ESDE SU MISMO ORIGEN, la definición del modelo relacional de Codd in-
cluía la necesidad de un lenguaje para realizar consultas ad-hoc. Debido a la
forma particular de representación de datos utilizada por este modelo, el
tener relaciones o tablas y no contar con un lenguaje de alto nivel para reintegrar los
datos almacenados es más bien una maldición que una bendición. Es asombroso, por
lo tanto, cuánto tiempo vivió el mundo de la programación sobre PCs sin poder
contar con SQL o algún mecanismo similar. Aún hoy, cuando un programador de
Clipper o de COBOL comienza a trabajar en Delphi, se sorprende de las posibilida-
des que le abre el uso de un lenguaje de consultas integrado dentro de sus aplica-
ciones.
La instrucción select, del Lenguaje de Manipulación de Datos de SQL nos permite
consultar la información almacenada en una base de datos relacional. La sintaxis y
posibilidades de esta sola instrucción son tan amplias y complicadas como para me-
recer un capítulo para ella solamente. En este mismo capítulo estudiaremos las posi-
bilidades de las instrucciones update, insert y delete, que permiten la modificación
del contenido de las tablas de una base de datos.
Para los ejemplos de este capítulo utilizaré la base de datos mastsql.gdb que viene con
los ejemplos de Delphi, en el subdirectorio demosdata, a partir del directorio de ins-
talación de Delphi. Estas tablas también se encuentran en formato Paradox, en el
mismo subdirectorio. Puede utilizar el programa Database Desktop para probar el uso
de SQL sobre tablas Paradox y dBase. Sin embargo, trataré de no tocar las peculiari-
dades del Motor de SQL Local ahora, dejando esto para el capítulo 26, que explica
cómo utilizar SQL desde Delphi.
La instrucción select: el lenguaje de consultas
A grandes rasgos, la estructura de la instrucción select es la siguiente:
D
458 La Cara Oculta de Delphi
select [distinct] lista-de-expresiones
from lista-de-tablas
[where condición-de-selección]
[group by lista-de-columnas]
[having condición-de-selección-de-grupos]
[order by lista-de-columnas]
[union instrucción-de-selección]
¿Qué se supone que “hace” una instrucción select? Esta es la pregunta del millón:
una instrucción select, en principio, no “hace” sino que “define”. La instrucción
define un conjunto virtual de filas y columnas, o más claramente, define una tabla
virtual. Qué se hace con esta “tabla virtual” es ya otra cosa, y depende de la aplica-
ción que le estemos dando. Si estamos en un intérprete que funciona en modo texto,
puede ser que la ejecución de un select se materialice mostrando en pantalla los re-
sultados, página a página, o quizás en salvar el resultado en un fichero de texto. En
Delphi, las instrucciones select se utilizan para “alimentar” un componente denomi-
nado TQuery, al cual se le puede dar casi el mismo uso que a una tabla “real”, almace-
nada físicamente.
A pesar de la multitud de secciones de una selección completa, el formato básico de
la misma es muy sencillo, y se reduce a las tres primeras secciones:
select lista-de-expresiones
from lista-de-tablas
[where condición-de-selección]
La cláusula from indica de dónde se extrae la información de la consulta, en la cláu-
sula where opcional se dice qué filas deseamos en el resultado, y con select especifi-
camos los campos o expresiones de estas filas que queremos dejar. Muchas veces se
dice que la cláusula where limita la tabla “a lo largo”, pues elimina filas de la misma,
mientras que la cláusula select es una selección “horizontal”.
Consultas y modificaciones en SQL 459
La condición de selección
La forma más simple de instrucción select es la que extrae el conjunto de filas de
una sola tabla que satisfacen cierta condición. Por ejemplo:
select *
from Customer
where State = "HI"
Esta consulta simple debe devolver todos los datos de los clientes ubicados en Ha-
wai. El asterisco que sigue a la cláusula select es una alternativa a listar todos los
nombres de columna de la tabla que se encuentra en la cláusula from.
En este caso hemos utilizado una simple igualdad. La condición de búsqueda de la
cláusula where admite los seis operadores de comparación (=, <>, <, >, <=, >=) y
la creación de condiciones compuestas mediante el uso de los operadores lógicos
and, or y not. La prioridad entre estos tres es la misma que en Pascal. Sin embargo,
no hace falta encerrar las comparaciones entre paréntesis, porque incluso not se
evalúa después de cualquier comparación:
select *
from Customer
where State = "HI"
and LastInvoiceDate > "1/1/1993"
Observe cómo la constante de fecha puede escribirse como si fuera una cadena de
caracteres.
Operadores de cadenas
Además de las comparaciones usuales, necesitamos operaciones más sofisticadas para
trabajar con las cadenas de caracteres. Uno de los operadores admitidos por SQL
estándar es el operador like, que nos permite averiguar si una cadena satisface o no
cierto patrón de caracteres. El segundo operando de este operador es el patrón, una
cadena de caracteres, dentro de la cual podemos incluir los siguientes comodines:
Carácter Significado
% Cero o más caracteres arbitrarios.
_ (subrayado) Un carácter cualquiera.
No vaya a pensar que el comodín % funciona como el asterisco en los nombres de
ficheros de MS-DOS; SQL es malo, pero no tanto. Después de colocar un asterisco
en un nombre de fichero, MS-DOS ignora cualquier otro carácter que escribamos a
continuación, mientras que like sí los tiene en cuenta. También es diferente el com-
460 La Cara Oculta de Delphi
portamiento del subrayado con respecto al signo de interrogación de DOS: en el
intérprete de comandos de este sistema operativo significa cero o un caracteres,
mientras que en SQL significa exactamente un carácter.
Expresión Cadena aceptada Cadena no aceptada
Customer like '% Ocean' 'Pacific Ocean' 'Ocean Paradise'
Fruta like 'Manzana_' 'Manzanas' 'Manzana'
También es posible aplicar funciones para extraer o modificar información de una
cadena de caracteres; el repertorio de funciones disponibles depende del sistema de
bases de datos con el que se trabaje. Por ejemplo, el intérprete SQL para tablas loca-
les de Delphi acepta las funciones upper, lower, trim y substring de SQL estándar.
Esta última función tiene una sintaxis curiosa. Por ejemplo, para extraer las tres pri-
meras letras de una cadena se utiliza la siguiente expresión:
select substring(Nombre from 1 for 3)
from Empleados
Si estamos trabajando con InterBase, podemos aumentar el repertorio de funciones
utilizando funciones definidas por el usuario. En el capítulo 36 mostraremos cómo.
El valor nulo: enfrentándonos a lo desconocido
La edad de una persona es un valor no negativo, casi siempre menor de 969 años,
que es la edad a la que dicen que llegó Matusalén. Puede ser un entero igual a 1, 20,
40 ... o no conocerse. Se puede “resolver” este problema utilizando algún valor espe-
cial para indicar el valor desconocido, digamos -1. Claro, el valor especial escogido no
debe formar parte del dominio posible de valores. Por ejemplo, en el archivo de Ur-
gencias de un hospital americano, John Doe es un posible valor para los pacientes no
identificados.
¿Y qué pasa si no podemos prescindir de valor alguno dentro del rango? Porque John
Doe es un nombre raro, pero posible. ¿Y qué pasaría si se intentan operaciones con
valores desconocidos? Por ejemplo, para representar un envío cuyo peso se desco-
noce se utiliza el valor -1, un peso claramente imposible excepto para entes como
Kate Moss. Luego alguien pregunta a la base de datos cuál es el peso total de los
envíos de un período dado. Si en ese período se realizaron dos envíos, uno de 25
kilogramos y otro de peso desconocido, la respuesta errónea será un peso total de 24
kilogramos. Es evidente que la respuesta debería ser, simplemente, “peso total des-
conocido”.
La solución de SQL es introducir un nuevo valor, null, que pertenece a cualquier
dominio de datos, para representar la información desconocida. La regla principal
Consultas y modificaciones en SQL 461
que hay que conocer cuando se trata con valores nulos es que cualquier expresión,
aparte de las expresiones lógicas, en la que uno de sus operandos tenga el valor nulo
se evalúa automáticamente a nulo. Esto es: nulo más veinticinco vale nulo, ¿de
acuerdo?
Cuando se trata de evaluar expresiones lógicas en las cuales uno de los operandos
puede ser nulo las cosas se complican un poco, pues hay que utilizar una lógica de
tres valores. De todos modos, las reglas son intuitivas. Una proposición falsa en con-
junción con cualquier otra da lugar a una proposición falsa; una proposición verda-
dera en disyunción con cualquier otra da lugar a una proposición verdadera. La si-
guiente tabla resume las reglas del uso del valor nulo en expresiones lógicas:
AND false null true OR false null true
false false false false false false null true
null false null null null null null true
true false null true true true true true
Por último, si lo que desea es saber si el valor de un campo es nulo o no, debe utilizar
el operador is null:
select *
from Events
where Event_Description is null
La negación de este operador es el operador is not null, con la negación en medio.
Esta sintaxis no es la usual en lenguajes de programación, pero se suponía que SQL
debía parecerse lo más posible al idioma inglés.
Eliminación de duplicados
Normalmente, no solemos guardar filas duplicadas en una tabla, por razones obvias.
Pero es bastante frecuente que el resultado de una consulta contenga filas duplicadas.
El operador distinct se puede utilizar, en la cláusula select, para corregir esta situa-
ción. Por ejemplo, si queremos conocer en qué ciudades residen nuestros clientes
podemos preguntar lo siguiente:
select City
from Customer
Pero en este caso obtenemos 55 ciudades, algunas de ellas duplicadas. Para obtener
las 47 diferentes ciudades de la base de datos tecleamos:
select distinct City
from Customer
462 La Cara Oculta de Delphi
Productos cartesianos y encuentros
Como para casi todas las cosas, la gran virtud del modelo relacional es, a la vez, su
mayor debilidad. Me refiero a que cualquier modelo del “mundo real” puede repre-
sentarse atomizándolo en relaciones: objetos matemáticos simples y predecibles, de
fácil implementación en un ordenador (¡aquellos ficheros dbfs…!). Para reconstruir el
modelo original, en cambio, necesitamos una operación conocida como “encuentro
natural” (natural join).
Comencemos con algo más sencillo: con los productos cartesianos. Un producto
cartesiano es una operación matemática entre conjuntos, la cual produce todas las
parejas posibles de elementos, perteneciendo el primer elemento de la pareja al pri-
mer conjunto, y el segundo elemento de la pareja al segundo conjunto. Esta es la
operación habitual que efectuamos mentalmente cuando nos ofrecen el menú en un
restaurante. Los dos conjuntos son el de los primeros platos y el de los segundos
platos. Desde la ventana de la habitación donde escribo puedo ver el menú del me-
són de la esquina:
Primer plato Segundo plato
Macarrones a la boloñesa Escalope a la milanesa
Judías verdes con jamón Pollo a la parrilla
Crema de champiñones Chuletas de cordero
Si PrimerPlato y SegundoPlato fuesen tablas de una base de datos, la instrucción
select *
from PrimerPlato, SegundoPlato
devolvería el siguiente conjunto de filas:
Primer plato Segundo plato
Macarrones a la boloñesa Escalope a la milanesa
Macarrones a la boloñesa Pollo a la parrilla
Macarrones a la boloñesa Chuletas de cordero
Judías verdes con jamón Escalope a la milanesa
Judías verdes con jamón Pollo a la parrilla
Judías verdes con jamón Chuletas de cordero
Crema de champiñones Escalope a la milanesa
Crema de champiñones Pollo a la parrilla
Crema de champiñones Chuletas de cordero
Es fácil ver que, incluso con tablas pequeñas, el tamaño del resultado de un producto
cartesiano es enorme. Si a este ejemplo “real” le añadimos el hecho también “real”
Consultas y modificaciones en SQL 463
de que el mismo mesón ofrece al menos tres tipos diferentes de postres, elegir nues-
tro menú significa seleccionar entre 27 posibilidades distintas. Por eso siempre pido
un café solo al terminar con el segundo plato.
Claro está, no todas las combinaciones de platos hacen una buena comida. Pero para
eso tenemos la cláusula where: para eliminar aquellas combinaciones que no satisfa-
cen ciertos criterios. ¿Volvemos al mundo de las facturas y órdenes de compra? En la
base de datos dbdemos, la información sobre pedidos está en la tabla orders, mientras
que la información sobre clientes se encuentra en customer. Queremos obtener la lista
de clientes y sus totales por pedidos. Estupendo, pero los totales de pedidos están en
la tabla orders, en el campo ItemsTotal, y en esta tabla sólo tenemos el código del
cliente, en el campo CustNo. Los nombres de clientes se encuentran en el campo
Company de la tabla customer, donde además volvemos a encontrar el código de cliente,
CustNo. Así que partimos de un producto cartesiano entre las dos tablas, en el cual
mostramos los nombres de clientes y los totales de pedidos:
select Company, ItemsTotal
from Customer, Orders
Como tenemos unos 55 clientes y 205 pedidos, esta inocente consulta genera unas
11275 filas. La última vez que hice algo así fue siendo estudiante, en el centro de
cálculos de mi universidad, para demostrarle a una profesora de Filosofía lo ocupado
que estaba en ese momento.
En realidad, de esas 11275 filas nos sobran unas 11070, pues solamente son válidas
las combinaciones en las que coinciden los códigos de cliente. La instrucción que
necesitamos es:
select Company, ItemsTotal
from Customer, Orders
where Customer.CustNo = Orders.CustNo
Esto es un encuentro natural, un producto cartesiano restringido mediante la igualdad
de los valores de dos columnas de las tablas básicas.
El ejemplo anterior ilustra también un punto importante: cuando queremos utilizar
en la instrucción el nombre de los campos ItemsTotal y Company los escribimos tal y
como son. Sin embargo, cuando utilizamos CustNo hay que aclarar a qué tabla origi-
nal nos estamos refiriendo. Esta técnica se conoce como calificación de campos.
¿Un ejemplo más complejo? Suponga que desea añadir el nombre del empleado que
recibió el pedido. La tabla orders tiene un campo EmpNo para el código del empleado,
mientras que la información sobre empleados se encuentra en la tabla employee. La
instrucción necesaria es una simple ampliación de la anterior:
464 La Cara Oculta de Delphi
select Company, ItemsTotal, FirstName || " " || LastName
from Customer, Orders, Employee
where Customer.CustNo = Orders.CustNo
and Orders.EmpNo = Employee.EmpNo
Con 42 empleados en la base de datos de ejemplo y sin las restricciones de la cláusula
where, hubiéramos obtenido un resultado de 473550 filas.
Ordenando los resultados
Una de las garantías de SQL es que podemos contar con que el compilador SQL
genere automáticamente, o casi, el mejor código posible para evaluar las instruccio-
nes. Esto también significa que, en el caso general, no podemos predecir con com-
pleta seguridad cuál será la estrategia utilizada para esta evaluación. Por ejemplo, en la
instrucción anterior no sabemos si el compilador va a recorrer cada fila de la tabla de
clientes para encontrar las filas correspondientes de pedidos o empleados, o si resul-
tará más ventajoso recorrer las filas de pedidos para recuperar los nombres de clien-
tes y empleados. Esto quiere decir, en particular, que no sabemos en qué orden se
nos van a presentar las filas. En mi ordenador, utilizando Database Desktop sobre las
tablas originales en formato Paradox, parece ser que se recorren primeramente las
filas de la tabla de empleados.
¿Qué hacemos si el resultado debe ordenarse por el nombre de compañía? Para esto
contamos con la cláusula order by, que se sitúa siempre al final de la consulta. En
este caso, ordenamos por nombre de compañía el resultado con la instrucción:
select Company, ItemsTotal, FirstName || " " || LastName
from Customer, Orders, Employee
where Customer.CustNo = Orders.CustNo
and Orders.EmpNo = Employee.EmpNo
order by Company
No se puede ordenar por una fila que no existe en el resultado de la instrucción. Si
quisiéramos que los pedidos de cada compañía se ordenaran, a su vez, por la fecha de
venta, habría que añadir el campo SalesDate al resultado y modificar la cláusula de
ordenación del siguiente modo:
select Company, ItemsTotal, SalesDate, FirstName || " " || LastName
from Customer, Orders, Employee
where Customer.CustNo = Orders.CustNo
and Orders.EmpNo = Employee.EmpNo
order by Company, SalesDate desc
Con la opción desc obtenemos los registros por orden descendente de fechas: pri-
mero los más recientes. Existe una opción asc, para cuando queremos enfatizar el
Consultas y modificaciones en SQL 465
sentido ascendente de una ordenación. Generalmente no se usa, pues es lo que
asume el compilador.
Otra posibilidad de la cláusula order by es utilizar números en vez de nombres de
columnas. Esto es necesario si se utilizan expresiones en la cláusula select y se quiere
ordenar por dicha expresión. Por ejemplo:
select OrderNo, SalesDate, ItemsTotal - AmountPaid
from Orders
order by 3 desc
No se debe abusar de los números de columnas, pues esta técnica puede desaparecer
en SQL-3 y hace menos legible la consulta. Una forma alternativa de ordenar por
columnas calculadas es utilizar sinónimos para las columnas:
select OrderNo, SalesDate, ItemsTotal – AmountPaid as Diferencia
from Orders
order by Diferencia desc
El uso de grupos
Ahora queremos sumar todos los totales de los pedidos para cada compañía, y orde-
nar el resultado por este total de forma descendente, para obtener una especie de
ranking de las compañías según su volumen de compras. Esto es, hay que agrupar
todas las filas de cada compañía y mostrar la suma de cierta columna dentro de cada
uno de esos grupos.
Para producir grupos de filas en SQL se utiliza la cláusula group by. Cuando esta
cláusula está presente en una consulta, va situada inmediatamente después de la cláu-
sula where, o de la cláusula from si no se han efectuado restricciones. En nuestro
caso, la instrucción con la cláusula de agrupamiento debe ser la siguiente:
select Company, sum(ItemsTotal)
from Customer, Orders
where Customer.CustNo = Orders.OrderNo
group by Company
order by 2 desc
Observe la forma en la cual se le ha aplicado la función sum a la columna ItemsTotal.
Aunque pueda parecer engorroso el diseño de una consulta con grupos, hay una
regla muy fácil que simplifica los razonamientos: en la cláusula select solamente
pueden aparecer columnas especificadas en la cláusula group by, o funciones esta-
dísticas aplicadas a cualquier otra expresión. Company, en este ejemplo, puede apare-
cer directamente porque es la columna por la cual se está agrupando. Si quisiéramos
obtener además el nombre de la persona de contacto en la empresa, el campo Contact
de la tabla customer, habría que agregar esta columna a la cláusula de agrupación:
466 La Cara Oculta de Delphi
select Company, Contact, sum(ItemsTotal)
from Customer, Orders
where Customer.CustNo = Orders.OrderNo
group by Company, Contact
order by 2 desc
En realidad, la adición de Contact es redundante, pues Company es única dentro de la
tabla customer, pero eso lo sabemos nosotros, no el compilador de SQL.
Funciones de conjuntos
Existen cinco funciones de conjuntos en SQL, conocidas en inglés como aggregate
functions. Estas funciones son:
Función Significado
count Cantidad de valores no nulos en el grupo
min El valor mínimo de la columna dentro del grupo
max El valor máximo de la columna dentro del grupo
sum La suma de los valores de la columna dentro del grupo
avg El promedio de la columna dentro del grupo
Por supuesto, no toda función es aplicable a cualquier tipo de columna. Las sumas,
por ejemplo, solamente valen para columnas de tipo numérico. Hay otros detalles
curiosos relacionados con estas funciones, como que los valores nulos son ignorados
por todas, o que se puede utilizar un asterisco como parámetro de la función count.
En este último caso, se calcula el número de filas del grupo. Así que no apueste a que
la siguiente consulta dé siempre dos valores idénticos, si es posible que la columna
involucrada contenga valores nulos:
select avg(Columna), sum(Columna)/count(*)
from Tabla
En el ejemplo se muestra la posibilidad de utilizar funciones de conjuntos sin utilizar
grupos. En esta situación se considera que toda la tabla constituye un único grupo.
Es también posible utilizar el operador distinct como prefijo del argumento de una
de estas funciones:
select Company, count(distinct EmpNo)
from Customer, Orders
where Customer.CustNo = Orders.CustNo
La consulta anterior muestra el número de empleados que han atendido los pedidos
de cada compañía.
Consultas y modificaciones en SQL 467
La cláusula having
Según lo que hemos explicado hasta el momento, en una instrucción select se evalúa
primeramente la cláusula from, que indica qué tablas participan en la consulta, luego
se eliminan las filas que no satisfacen la cláusula where y, si hay un group by por
medio, se agrupan las filas resultantes. Hay una posibilidad adicional: después de
agrupar se pueden descartar filas consolidadas de acuerdo a otra condición, esta vez
expresada en una cláusula having. En la parte having de la consulta solamente pue-
den aparecer columnas agrupadas o funciones estadísticas aplicadas al resto de las
columnas. Por ejemplo:
select Company
from Customer, Orders
where Customer.CustNo = Orders.CustNo
group by Company
having count(*) > 1
La consulta anterior muestra las compañías que han realizado más de un pedido. Es
importante darse cuenta de que no podemos modificar esta consulta para que nos
muestre las compañías que no han realizado todavía pedidos.
Una regla importante de optimización: si en la cláusula having existen condicio-
nes que implican solamente a las columnas mencionadas en la cláusula group by,
estas condiciones deben moverse a la cláusula where. Por ejemplo, si queremos
eliminar de la consulta utilizada como ejemplo a las compañías cuyo nombre
termina con las siglas 'S.L.' debemos hacerlo en where, no en group by. ¿Para
qué esperar a agrupar para eliminar filas que podían haberse descartado antes?
Aunque muchos compiladores realizan esta optimización automáticamente, es
mejor no fiarse.
El uso de sinónimos para tablas
Es posible utilizar dos o más veces una misma tabla en una misma consulta. Si ha-
cemos esto tendremos que utilizar sinónimos para distinguir entre los distintos usos de
la tabla en cuestión. Esto será necesario al calificar los campos que utilicemos. Un
sinónimo es simplemente un nombre que colocamos a continuación del nombre de
una tabla en la cláusula from, y que en adelante se usa como sustituto del nombre de
la tabla.
Por ejemplo, si quisiéramos averiguar si hemos introducido por error dos veces a la
misma compañía en la tabla de clientes, pudiéramos utilizar la instrucción:
468 La Cara Oculta de Delphi
select distinct C1.Company
from Customer C1, Customer C2
where C1.CustNo < C2.CustNo
and C1.Company = C2.Company
En esta consulta C1 y C2 se utilizan como sinónimos de la primera y segunda apari-
ción, respectivamente, de la tabla customer. La lógica de la consulta es sencilla. Busca-
mos todos los pares que comparten el mismo nombre de compañía y eliminamos
aquellos que tienen el mismo código de compañía. Pero en vez de utilizar una desi-
gualdad en la comparación de códigos, utilizamos el operador “menor que”, para
eliminar la aparición de pares dobles en el resultado previo a la aplicación del opera-
dor distinct. Estamos aprovechando, por supuesto, la unicidad del campo CustNo.
La siguiente consulta muestra otro caso en que una tabla aparece dos veces en una
cláusula from. En esta ocasión, la base de datos es iblocal, el ejemplo InterBase que
viene con Delphi. Queremos mostrar los empleados junto con los jefes de sus de-
partamentos:
select e2.full_name, e1.full_name
from employee e1, department d, employee e2
where d.dept_no = e1.dept_no
and d.mngr_no = e2.emp_no
and e1.emp_no <> e2.emp_no
order by 1, 2
Aquellos lectores que hayan trabajado en algún momento con lenguajes xBase reco-
nocerán en los sinónimos SQL un mecanismo similar al de los “alias” de xBase. Del-
phi utiliza, además, los sinónimos de tablas en el intérprete de SQL local cuando el
nombre de la tabla contiene espacios en blanco o el nombre de un directorio.
Subconsultas: selección única
Si nos piden el total vendido a una compañía determinada, digamos a Ocean Para-
dise, podemos resolverlo ejecutando dos instrucciones diferentes. En la primera
obtenemos el código de Ocean Paradise:
select Customer.CustNo
from Customer
where Customer.Company = "Ocean Paradise"
El código buscado es, supongamos, 1510. Con este valor en la mano, ejecutamos la
siguiente instrucción:
select sum(Orders.ItemsTotal)
from Orders
where Orders.CustNo = 1510
Consultas y modificaciones en SQL 469
Incómodo, ¿no es cierto? La alternativa es utilizar la primera instrucción como una
expresión dentro de la segunda, del siguiente modo:
select sum(Orders.ItemsTotal)
from Orders
where Orders.CustNo = (
select Customer.CustNo
from Customer
where Customer.Company = "Ocean Paradise")
Para que la subconsulta anterior pueda funcionar correctamente, estamos asumiendo
que el conjunto de datos retornado por la subconsulta produce una sola fila. Esto es,
realmente, una apuesta arriesgada. Puede fallar por dos motivos diferentes: puede
que la subconsulta no devuelva ningún valor o puede que devuelva más de uno. Si no
se devuelve ningún valor, se considera que la subconsulta devuelve el valor null. Si
devuelve dos o más valores, el intérprete produce un error.
A este tipo de subconsulta que debe retornar un solo valor se le denomina selección
única, en inglés, singleton select. Las selecciones únicas también pueden utilizarse con
otros operadores de comparación, además de la igualdad. Así por ejemplo, la si-
guiente consulta retorna información sobre los empleados contratados después de
Pedro Pérez:
select *
from Employee E1
where E1.HireDate > (
select E2.HireDate
from Employee E2
where E2.FirstName = "Pedro"
and E2.LastName = "Pérez")
Si está preguntándose acerca de la posibilidad de cambiar el orden de los operandos,
ni lo sueñe. La sintaxis de SQL es muy rígida, y no permite este tipo de virtuosismos.
Subconsultas: los operadores in y exists
En el ejemplo anterior garantizábamos la singularidad de la subconsulta gracias a la
cláusula where, que especificaba una búsqueda sobre una clave única. Sin embargo,
también se pueden aprovechar las situaciones en que una subconsulta devuelve un
conjunto de valores. En este caso, el operador a utilizar cambia. Por ejemplo, si que-
remos los pedidos correspondientes a las compañías en cuyo nombre figura la pala-
bra Ocean, podemos utilizar la instrucción:
select *
from Orders
where Orders.CustNo in (
470 La Cara Oculta de Delphi
select Customer.CustNo
from Customer
where upper(Customer.Company) like "%OCEAN%")
El nuevo operador es el operador in, y la expresión es verdadera si el operando iz-
quierdo se encuentra en la lista de valores retornada por la subconsulta. Esta consulta
puede descomponerse en dos fases. Durante la primera fase se evalúa el segundo
select:
select Customer.CustNo
from Customer
where upper(Customer.Company) like "%OCEAN%"
El resultado de esta consulta consiste en una serie de códigos: aquellos que corres-
ponden a las compañías con Ocean en su nombre. Supongamos que estos códigos
sean 1510 (Ocean Paradise) y 5515 (Ocean Adventures). Entonces puede ejecutarse
la segunda fase de la consulta, con la siguiente instrucción, equivalente a la original:
select *
from Orders
where Orders.OrderNo in (1510, 5515)
Este otro ejemplo utiliza la negación del operador in. Si queremos las compañías que
no nos han comprado nada, hay que utilizar la siguiente consulta:
select *
from Customer
where Customer.CustNo not in (
select Orders.CustNo
from Orders)
Otra forma de plantearse las consultas anteriores es utilizando el operador exists.
Este operador se aplica a una subconsulta y devuelve verdadero en cuanto localiza
una fila que satisface las condiciones de la instrucción select. El primer ejemplo de
este epígrafe puede escribirse de este modo:
select *
from Orders
where exists (
select *
from Customer
where upper(Customer.Company) like "%OCEAN%"
and Orders.CustNo = Customer.CustNo)
Observe el asterisco en la cláusula select de la subconsulta. Como lo que nos inte-
resa es saber si existen filas que satisfacen la expresión, nos da lo mismo qué valor se
está retornando. El segundo ejemplo del operador in se convierte en lo siguiente al
utilizar exists:
Consultas y modificaciones en SQL 471
select *
from Customer
where not exists (
select *
from Orders
where Orders.CustNo = Customer.CustNo)
Subconsultas correlacionadas
Preste atención al siguiente detalle: la última subconsulta del epígrafe anterior tiene
una referencia a una columna perteneciente a la tabla definida en la cláusula from
más externa. Esto quiere decir que no podemos explicar el funcionamiento de la
instrucción dividiéndola en dos fases, como con las selecciones únicas: la ejecución
de la subconsulta y la simplificación de la instrucción externa. En este caso, para cada
fila retornada por la cláusula from externa, la tabla customer, hay que volver a evaluar
la subconsulta teniendo en cuenta los valores actuales: los de la columna CustNo de la
tabla de clientes. A este tipo de subconsultas se les denomina, en el mundo de la
programación SQL, subconsultas correlacionadas.
Si hay que mostrar los clientes que han pagado algún pedido contra reembolso (en
inglés, COD, o cash on delivery), podemos realizar la siguiente consulta con una subse-
lección correlacionada:
select *
from Customer
where 'COD' in (
select distinct PaymentMethod
from Orders
where Orders.CustNo = Customer.CustNo)
En esta instrucción, para cada cliente se evalúan los pedidos realizados por el mismo,
y se muestra el cliente solamente si dentro del conjunto de métodos de pago está la
cadena 'COD'. El operador distinct de la subconsulta es redundante, pero nos ayuda
a entenderla mejor.
Otra subconsulta correlacionada: queremos los clientes que no han comprado nada
aún. Ya vimos como hacerlo utilizando el operador not in ó el operador not exists.
Una alternativa es la siguiente:
select *
from Customer
where 0 = (
select count(*)
from Orders
where Orders.CustNo = Customer.CustNo)
Sin embargo, utilizando SQL Local esta consulta es más lenta que las otras dos solu-
ciones. La mayor importancia del concepto de subconsulta correlacionada tiene que
472 La Cara Oculta de Delphi
ver con el hecho de que algunos sistemas de bases de datos limitan las actualizacio-
nes a vistas definidas con instrucciones que contienen subconsultas de este tipo.
Equivalencias de subconsultas
En realidad, las subconsultas son un método para aumentar la expresividad del len-
guaje SQL, pero no son imprescindibles. Con esto quiero decir que muchas consultas
se formulan de modo más natural si se utilizan subconsultas, pero que existen otras
consultas equivalentes que no hacen uso de este recurso. La importancia de esta equi-
valencia reside en que el intérprete de SQL Local de Delphi 1 no permitía subconsul-
tas. Los desarrolladores que estén programando aplicaciones con Delphi 1 para
Windows 3.1 y Paradox o dBase deben tener en cuenta esta restricción.
Un problema relacionado es que, aunque un buen compilador de SQL debe poder
identificar las equivalencias y evaluar la consulta de la forma más eficiente, en la
práctica el utilizar ciertas construcciones sintácticas puede dar mejor resultado que
utilizar otras equivalentes, de acuerdo al compilador que empleemos.
Veamos algunas equivalencias. Teníamos una consulta, en el epígrafe sobre selec-
ciones únicas, que mostraba el total de compras de Ocean Paradise:
select sum(Orders.ItemsTotal)
from Orders
where Orders.CustNo = (
select Customer.CustNo
from Customer
where Customer.Company = "Ocean Paradise")
Esta consulta es equivalente a la siguiente:
select sum(ItemsTotal)
from Customer, Orders
where Customer.Company = "Ocean Paradise"
and Customer.CustNo = Orders.OrderNo
Aquella otra consulta que mostraba los pedidos de las compañías en cuyo nombre
figuraba la palabra “Ocean”:
select *
from Orders
where Orders.CustNo in (
select Customer.CustNo
from Customer
where upper(Customer.Company) like "%OCEAN%")
es equivalente a esta otra:
Consultas y modificaciones en SQL 473
select *
from Customer, Orders
where upper(Customer.Company) like "%OCEAN%"
and Customer.CustNo = Orders.CustNo
Para esta consulta en particular, ya habíamos visto una consulta equivalente que hacía
uso del operador exists; en este caso, es realmente más difícil de entender la consulta
con exists que su equivalente sin subconsultas.
La consulta correlacionada que buscaba los clientes que en algún pedido habían pa-
gado contra reembolso:
select *
from Customer
where 'COD' in (
select distinct PaymentMethod
from Orders
where Orders.CustNo = Customer.CustNo)
puede escribirse, en primer lugar, mediante una subconsulta no correlacionada:
select *
from Customer
where Customer.CustNo in (
select Orders.CustNo
from Orders
where PaymentMethod = 'COD')
pero también se puede expresar en forma “plana”:
select distinct Customer.CustNo, Customer.Company
from Customer, Orders
where Customer.CustNo = Orders.CustNo
and Orders.PaymentMethod = 'COD'
Por el contrario, las consultas que utilizan el operador not in y, por lo tanto sus equi-
valentes con not exists, no tienen equivalente plano, con lo que sabemos hasta el
momento. Para poder aplanarlas hay que utilizar encuentros externos.
Encuentros externos
El problema de los encuentros naturales es que cuando relacionamos dos tablas,
digamos customer y orders, solamente mostramos las filas que tienen una columna en
común. No hay forma de mostrar los clientes que no tienen un pedido con su código
... y solamente esos. En realidad, se puede utilizar la operación de diferencia entre
conjuntos para lograr este objetivo, como veremos en breve. Se pueden evaluar todos
los clientes, y a ese conjunto restarle el de los clientes que sí tienen pedidos. Pero esta
474 La Cara Oculta de Delphi
operación, por lo general, se implementa de forma menos eficiente que la alternativa
que mostraremos a continuación.
¿Cómo funciona un encuentro natural? Una posible implementación consistiría en
recorrer mediante un bucle la primera tabla, supongamos que sea customer. Para cada
fila de esa tabla tomaríamos su columna CustNo y buscaríamos, posiblemente con un
índice, las filas correspondientes de orders que contengan ese mismo valor en la co-
lumna del mismo nombre. ¿Qué pasa si no hay ninguna fila en orders que satisfaga
esta condición? Si se trata de un encuentro natural, común y corriente, no se mues-
tran los datos de ese cliente. Pero si se trata de la extensión de esta operación, cono-
cida como encuentro externo (outer join), se muestra aunque sea una vez la fila corres-
pondiente al cliente. Un encuentro muestra, sin embargo, pares de filas, ¿qué valores
podemos esperar en la fila de pedidos? En ese caso, se considera que todas las co-
lumnas de la tabla de pedidos tienen valores nulos. Si tuviéramos estas dos tablas:
Customers Orders
CustNo Company OrderNo CustNo
1510 Ocean Paradise 1025 1510
1666 Marteens’ Diving Academy 1139 1510
el resultado de un encuentro externo como el que hemos descrito, de acuerdo a la
columna CustNo, sería el siguiente:
Customer.CustNo Company OrderNo Orders.CustNo
1510 Ocean Paradise 1025 1510
1510 Ocean Paradise 1139 1510
1666 Marteens’ Diving Academy null null
Con este resultado en la mano, es fácil descubrir quién es el tacaño que no nos ha
pedido nada todavía, dejando solamente las filas que tengan valores nulos para al-
guna de las columnas de la segunda tabla.
Este encuentro externo que hemos explicado es, en realidad, un encuentro externo
por la izquierda, pues la primera tabla tendrá todas sus filas en el resultado final, aun-
que no exista fila correspondiente en la segunda. Naturalmente, también existe un
encuentro externo por la derecha y un encuentro externo simétrico.
El problema de este tipo de operaciones es que su inclusión en SQL fue bastante
tardía. Esto trajo como consecuencia que distintos fabricantes utilizaran sintaxis
propias para la operación. En el estándar ANSI para SQL del año 87 no hay referen-
cias a esta instrucción, pero sí la hay en el estándar del 92. Utilizando esta sintaxis,
que es la permitida por el SQL local de Delphi, la consulta que queremos se escribe
del siguiente modo:
Consultas y modificaciones en SQL 475
select *
from Customer left outer join Orders
on Customer.CustNo = Orders.CustNo
where Orders.OrderNo is null
Observe que se ha extendido la sintaxis de la cláusula from.
El encuentro externo por la izquierda puede escribirse en Oracle de esta forma
alternativa:
select *
from Customer, Orders
where Customer.CustNo (+) = Orders.CustNo
and Orders.OrderNo is null
Las instrucciones de actualización
Son tres las instrucciones de actualización de datos reconocidas en SQL: delete,
update e insert. Estas instrucciones tienen una sintaxis relativamente simple y están
limitadas, en el sentido de que solamente cambian datos en una tabla a la vez. La más
sencilla de las tres es delete, la instrucción de borrado:
delete from Tabla
where Condición
La instrucción elimina de la tabla indicada todas las filas que se ajustan a cierta con-
dición. En dependencia del sistema de bases de datos de que se trate, la condición de
la cláusula where debe cumplir ciertas restricciones. Por ejemplo, aunque InterBase
admite la presencia de subconsultas en la condición de selección, otros sistemas no lo
permiten.
La segunda instrucción, update, nos sirve para modificar las filas de una tabla que
satisfacen cierta condición:
update Tabla
set Columna = Valor [, Columna = Valor ...]
where Condición
Al igual que sucede con la instrucción delete, las posibilidades de esta instrucción
dependen del sistema que la implementa. InterBase, en particular, permite actualizar
columnas con valores extraídos de otras tablas; para esto utilizamos subconsultas en
la cláusula set:
update Customer
set LastInvoiceDate =
476 La Cara Oculta de Delphi
(select max(SaleDate) from Orders
where Orders.CustNo = Customer.CustNo)
Por último, tenemos la instrucción insert, de la cual tenemos dos variantes. La pri-
mera permite insertar un solo registro, con valores constantes:
insert into Tabla [ (Columnas) ]
values (Valores)
La lista de columnas es opcional; si se omite, se asume que la instrucción utiliza todas
las columnas en orden de definición. En cualquier caso, el número de columnas em-
pleado debe coincidir con el número de valores. El objetivo de todo esto es que si no
se indica un valor para alguna columna, el valor de la columna en el nuevo registro se
inicializa con el valor definido por omisión; recuerde que si en la definición de la
tabla no se ha indicado nada, el valor por omisión es null:
insert into Employee(EmpNo, LastName, FirstName)
values (666, "Bonaparte", "Napoleón")
/* El resto de las columnas, incluida la del salario, son nulas */
La segunda variante de insert permite utilizar como fuente de valores una expresión
select:
insert into Tabla [ (Columnas) ]
InstrucciónSelect
Esta segunda variante no estuvo disponible en el intérprete de SQL local hasta la
versión 4 del BDE. Se utiliza con frecuencia para copiar datos de una tabla a otra:
insert into Resumen(Empresa, TotalVentas)
select Company, sum(ItemsTotal)
from Customer, Orders
where Customer.CustNo = Orders.CustNo
group by Company
Vistas
Se puede aprovechar una instrucción select de forma tal que el conjunto de datos
que define se pueda utilizar “casi” como una tabla real. Para esto, debemos definir
una vista. La instrucción necesaria tiene la siguiente sintaxis:
create view NombreVista[Columnas]
as InstrucciónSelect
[with check option]
Por ejemplo, podemos crear una vista para trabajar con los clientes que viven en
Hawai:
Consultas y modificaciones en SQL 477
create view Hawaianos as
select *
from Customer
where State = "HI"
A partir del momento en que se ejecuta esta instrucción, el usuario de la base de
datos se encuentra con una nueva tabla, Hawaianos, con la cual puede realizar las
mismas operaciones que realizaba con las tablas “físicas”. Puede utilizar la nueva
tabla en una instrucción select:
select *
from Hawaianos
where LastInvoiceDate >=
(select avg(LastInvoiceDate) from Customer)
En esta vista en particular, puede también eliminar insertar o actualizar registros:
delete from Hawaianos
where LastInvoiceDate is null;
insert into Hawaianos(CustNo, Company, State)
values (8888, "Ian Marteens' Diving Academy", "HI")
No todas las vistas permiten operaciones de actualización. Las condiciones que de-
ben cumplir para ser actualizables, además, dependen del sistema de bases de datos
en que se definan. Los sistemas más restrictivos exigen que la instrucción select
tenga una sola tabla en la cláusula from, que no contenga consultas anidadas y que
no haga uso de operadores tales como group by, distinct, etc.
Cuando una vista permite actualizaciones se nos plantea el problema de qué hacer si
se inserta un registro que no pertenece lógicamente a la vista. Por ejemplo, ¿pudié-
ramos insertar dentro de la vista Hawaianos una empresa con sede social en la ciudad
costera de Cantalapiedra20? Si permitiésemos esto, después de la inserción el registro
recién insertado “desaparecería” inmediatamente de la vista (aunque no de la tabla
base, Customer). El mismo conflicto se produciría al actualizar la columna State de un
hawaiano.
Para controlar este comportamiento, SQL define la cláusula with check option. Si
se especifica esta opción, no se permiten inserciones ni modificaciones que violen la
condición de selección impuesta a la vista; si intentamos una operación tal, se pro-
duce un error de ejecución. Por el contrario, si no se incluye la opción en la defini-
ción de la vista, estas operaciones se permiten, pero nos encontraremos con situa-
20 Cuando escribí la primera versión de este libro, no sabía que había un Cantalapiedra en
España; pensé que nombre tan improbable era invento mío. Mis disculpas a los cantala-
pedrenses, pues me parece que viven en ciudad sin costas.
478 La Cara Oculta de Delphi
ciones como las descritas, en que un registro recién insertado o modificado desapa-
rece misteriosamente por no pertenecer a la vista.
Capítulo
23
Procedimientos almacenados yProcedimientos almacenados y
triggerstriggers
ON ESTE CAPÍTULO COMPLETAMOS la presentación de los sublenguajes de
SQL, mostrando el lenguaje de definición de procedimientos de InterBase.
Desgraciadamente, los lenguajes de procedimientos de los distintos sistemas
de bases de datos son bastante diferentes entre sí, al no existir todavía un estándar al
respecto. De los dialectos existentes, he elegido nuevamente InterBase por dos razo-
nes. La primera, y fundamental, es que es el sistema SQL que usted tiene a mano
(asumiendo que tiene Delphi). La segunda es que el dialecto de InterBase para pro-
cedimientos es el que más se asemeja al propuesto en el borrador del estándar SQL-
3. De cualquier manera, las diferencias entre dialectos no son demasiadas, y no le
costará mucho trabajo entender el lenguaje de procedimientos de cualquier otro sis-
tema de bases de datos.
¿Para qué usar procedimientos almacenados?
Un procedimiento almacenado (stored procedure) es, sencillamente, un algoritmo cuya defi-
nición reside en la base de datos, y que es ejecutado por el servidor del sistema. Aun-
que SQL-3 define formalmente un lenguaje de programación para procedimientos
almacenados, cada uno de los sistemas de bases de datos importantes a nivel comer-
cial implementa su propio lenguaje para estos recursos. InterBase implementa un
dialecto parecido a la propuesta de SQL-3; Oracle tiene un lenguaje llamado PL-
SQL; Microsoft SQL Server ofrece el denominado Transact-SQL. No obstante, las
diferencias entre estos lenguajes son mínimas, principalmente en sintaxis, siendo casi
idénticas las capacidades expresivas.
El uso de procedimientos almacenados ofrece las siguientes ventajas:
• Los procedimientos almacenados ayudan a mantener la consistencia de la base de
datos.
C
480 La Cara Oculta de Delphi
Las instrucciones básicas de actualización, update, insert y delete, pueden
combinarse arbitrariamente si dejamos que el usuario tenga acceso ilimitado
a las mismas. No toda combinación de actualizaciones cumplirá con las re-
glas de consistencia de la base de datos. Hemos visto que algunas de estas
reglas se pueden expresar declarativamente durante la definición del esquema
relacional. El mejor ejemplo son las restricciones de integridad referencial.
Pero, ¿cómo expresar declarativamente que para cada artículo presente en un
pedido, debe existir un registro correspondiente en la tabla de movimientos
de un almacén? Una posible solución es prohibir el uso directo de las ins-
trucciones de actualización, revocando permisos de acceso al público, y
permitir la modificación de datos solamente a partir de procedimientos al-
macenados.
• Los procedimientos almacenados permiten superar las limitaciones del lenguaje
de consultas.
SQL no es un lenguaje completo. Un problema típico en que falla es en la
definición de clausuras relacionales. Tomemos como ejemplo una tabla con dos
columnas: Objeto y Parte. Esta tabla contiene pares como los siguientes:
Objeto Parte
Cuerpo humano Cabeza
Cuerpo humano Tronco
Cabeza Ojos
Cabeza Boca
Boca Dientes
¿Puede el lector indicar una consulta que liste todas las partes incluidas en la
cabeza? Lo que falla es la posibilidad de expresar algoritmos recursivos. Para
resolver esta situación, los procedimientos almacenados pueden implemen-
tarse de forma tal que devuelvan conjuntos de datos, en vez de valores esca-
lares. En el cuerpo de estos procedimientos se pueden realizar, entonces, lla-
madas recursivas.
• Los procedimientos almacenados pueden reducir el tráfico en la red.
Un procedimiento almacenado se ejecuta en el servidor, que es precisamente
donde se encuentran los datos. Por lo tanto, no tenemos que explorar una
tabla de arriba a abajo desde un ordenador cliente para extraer el promedio
de ventas por empleado durante el mes pasado. Además, por regla general el
servidor es una máquina más potente que las estaciones de trabajo, por lo
que puede que ahorremos tiempo de ejecución para una petición de infor-
Triggers y procedimientos almacenados 481
mación. No conviene, sin embargo, abusar de esta última posibilidad, porque
una de las ventajas de una red consiste en distribuir el tiempo de procesador.
• Con los procedimientos almacenados se puede ahorrar tiempo de desarrollo.
Siempre que existe una información, a alguien se le puede ocurrir un nuevo
modo de aprovecharla. En un entorno cliente/servidor es típico que varias
aplicaciones diferentes trabajen con las mismas bases de datos. Si centraliza-
mos en la propia base de datos la imposición de las reglas de consistencia, no
tendremos que volverlas a programar de una aplicación a otra. Además, evi-
tamos los riesgos de una mala codificación de estas reglas, con la consi-
guiente pérdida de consistencia.
Como todas las cosas de esta vida, los procedimientos almacenados también tienen
sus inconvenientes. Ya he mencionado uno de ellos: si se centraliza todo el trata-
miento de las reglas de consistencia en el servidor, corremos el riesgo de saturar los
procesadores del mismo. El otro inconveniente es la poca portabilidad de las defini-
ciones de procedimientos almacenados entre distintos sistemas de bases de datos. Si
hemos desarrollado procedimientos almacenados en InterBase y queremos migrar
nuestra base de datos a Oracle (o viceversa), estaremos obligados a partir “casi” de
cero; algo se puede aprovechar, de todos modos.
Cómo se utiliza un procedimiento almacenado
Un procedimiento almacenado puede utilizarse desde una aplicación cliente, desarro-
llada en cualquier lenguaje de programación que pueda acceder a la interfaz de pro-
gramación de la base de datos, o desde las propias utilidades interactivas del sistema.
En Delphi tenemos el componente TStoredProc, diseñado para la ejecución de estos
procedimientos. En un capítulo posterior veremos cómo suministrar parámetros,
ejecutar procedimientos y recibir información utilizando este componente.
En el caso de InterBase, también es posible ejecutar un procedimiento almacenado
directamente desde la aplicación Windows ISQL, mediante la siguiente instrucción:
execute procedure NombreProcedimiento [ListaParámetros];
La misma instrucción puede utilizarse en el lenguaje de definición de procedimientos
y triggers para llamar a un procedimiento dentro de la definición de otro. Es posible
también definir procedimientos recursivos. InterBase permite hasta un máximo de
1000 llamadas recursivas por procedimiento.
482 La Cara Oculta de Delphi
El carácter de terminación
Los procedimientos almacenados de InterBase deben necesariamente escribirse en
un fichero script de SQL. Más tarde, este fichero debe ser ejecutado desde la utilidad
Windows ISQL para que los procedimientos sean incorporados a la base de datos.
Hemos visto las reglas generales del uso de scripts en InterBase en el capítulo de in-
troducción a SQL. Ahora tenemos que estudiar una característica de estos scripts que
anteriormente hemos tratado superficialmente: el carácter de terminación.
Por regla general, cada instrucción presente en un script es leída y ejecutada de forma
individual y secuencial. Esto quiere decir que el intérprete de scripts lee del fichero
hasta que detecta el fin de instrucción, ejecuta la instrucción recuperada, y sigue así
hasta llegar al final del mismo. El problema es que este proceso de extracción de
instrucciones independientes se basa en la detección de un carácter especial de ter-
minación. Por omisión, este carácter es el punto y coma; el lector habrá observado
que todos los ejemplos de instrucciones SQL que deben colocarse en scripts han sido,
hasta el momento, terminados con este carácter.
Ahora bien, al tratar con el lenguaje de procedimientos y triggers encontraremos ins-
trucciones y cláusulas que deben terminar con puntos y comas. Si el intérprete de
scripts tropieza con uno de estos puntos y comas pensará que se encuentra frente al
fin de la instrucción, e intentará ejecutar lo que ha leído hasta el momento; casi
siempre, una instrucción incompleta. Por lo tanto, debemos cambiar el carácter de
terminación de Windows ISQL cuando estamos definiendo triggers o procedimientos
almacenados. La instrucción que nos ayuda para esto es la siguiente:
set term Terminador
Como carácter de terminación podemos escoger cualquier carácter o combinación
de los mismos lo suficientemente rara como para que no aparezca dentro de una
instrucción del lenguaje de procedimientos. Por ejemplo, podemos utilizar el acento
circunflejo:
set term ^;
Observe cómo la instrucción que cambia el carácter de terminación debe terminar
ella misma con el carácter antiguo. Al finalizar la creación de todos los procedimien-
tos que necesitamos, debemos restaurar el antiguo carácter de terminación:
set term ;^
En lo sucesivo asumiremos que el carácter de terminación ha sido cambiado al
acento circunflejo.
Triggers y procedimientos almacenados 483
Procedimientos almacenados en InterBase
La sintaxis para la creación de un procedimiento almacenado en InterBase es la si-
guiente:
create procedure Nombre
[ ( ParámetrosDeEntrada ) ]
[ returns ( ParámetrosDeSalida ) ]
as CuerpoDeProcedimiento
Las cláusulas ParámetrosDeEntrada y ParámetrosDeSalida representan listas de decla-
raciones de parámetros. Los parámetros de salida pueden ser más de uno; esto signi-
fica que el procedimiento almacenado que retorna valores no se utiliza como si fuese
una función de un lenguaje de programación tradicional. El siguiente es un ejemplo
de cabecera de procedimiento:
create procedure TotalPiezas(PiezaPrincipal char(15))
returns (Total integer)
as
/* ... Aquí va el cuerpo ... */
El cuerpo del procedimiento, a su vez, se divide en dos secciones, siendo opcional la
primera de ellas: la sección de declaración de variables locales, y una instrucción
compuesta, begin...end, que agrupa las instrucciones del procedimiento. Las varia-
bles se declaran en este verboso estilo, á la 1970:
declare variable V1 integer;
declare variable V2 char(50);
Estas son las instrucciones permitidas por los procedimientos almacenados de In-
terBase:
• Asignaciones:
Variable = Expresión
Las variables pueden ser las declaradas en el propio procedimiento, paráme-
tros de entrada o parámetros de salida.
• Llamadas a procedimientos:
execute procedure NombreProc [ParsEntrada]
[returning_values ParsSalida]
No se admiten expresiones en los parámetros de entrada; mucho menos en
los de salida.
484 La Cara Oculta de Delphi
• Condicionales:
if (Condición) then Instrucción [else Instrucción]
• Bucles controlados por condiciones:
while (Condición) do Instrucción
• Instrucciones SQL:
Cualquier instrucción de manipulación, insert, update ó delete, puede in-
cluirse en un procedimiento almacenado. Estas instrucciones pueden utilizar
variables locales y parámetros, siempre que estas variables estén precedidas
de dos puntos, para distinguirlas de los nombres de columnas. Por ejemplo,
si Minimo y Aumento son variables o parámetros, puede ejecutarse la siguiente
instrucción:
update Empleados
set Salario = Salario * :Aumento
where Salario < :Minimo;
Se permite el uso directo de instrucciones select si devuelven una sola fila;
para consultas más generales se utiliza la instrucción for que veremos dentro
de poco. Estas selecciones únicas van acompañadas por una cláusula into
para transferir valores a variables o parámetros:
select Empresa
from Clientes
where Codigo = 1984
into :NombreEmpresa;
• Iteración sobre consultas:
for InstrucciónSelect into Variables do Instrucción
Esta instrucción recorre el conjunto de filas definido por la instrucción se-
lect. Para cada fila, transfiere los valores a las variables de la cláusula into, de
forma similar a lo que sucede con las selecciones únicas, y ejecuta entonces
la instrucción de la sección do.
• Lanzamiento de excepciones:
exception NombreDeExcepción
Similar a la instrucción raise de Delphi.
Triggers y procedimientos almacenados 485
• Captura de excepciones:
when ListaDeErrores do Instrucción
Similar a la cláusula except de la instrucción try...except de Delphi. Los
errores capturados pueden ser excepciones propiamente dichas o errores re-
portados con la variable SQLCODE. Estos últimos errores se producen al
ejecutarse instrucciones SQL. Las instrucciones when deben colocarse al fi-
nal de los procedimientos.
• Instrucciones de control:
exit;
suspend;
La instrucción exit termina la ejecución del procedimiento actual, y es simi-
lar al procedimiento Exit de Delphi. Por su parte, suspend se utiliza en pro-
cedimientos que devuelven un conjunto de filas para retornar valores a la
rutina que llama a este procedimiento. Con esta última instrucción, se inte-
rrumpe temporalmente el procedimiento, hasta que la rutina que lo llama
haya procesado los valores retornados.
• Instrucciones compuestas:
begin ListaDeInstrucciones end
La sintaxis de los procedimientos de InterBase es similar a la de Pascal. A di-
ferencia de este último lenguaje, la palabra end no puede tener un punto y
coma a continuación.
Mostraré ahora un par de procedimientos sencillos, que ejemplifiquen el uso de estas
instrucciones. El siguiente procedimiento, basado en las tablas definidas en el capí-
tulo sobre DDL, sirve para recalcular la suma total de un pedido, si se suministra el
número de pedido correspondiente:
create procedure RecalcularTotal(NumPed int) as
declare variable Total integer;
begin
select sum(Cantidad * PVP * (100 - Descuento) / 100)
from Detalles, Articulos
where Detalles.RefArticulo = Articulos.Codigo
and Detalles.RefPedido = :NumPed
into :Total;
if (Total is null) then
Total = 0;
486 La Cara Oculta de Delphi
update Pedidos
set Total = :Total
where Numero = :NumPed;
end ^
El procedimiento consiste básicamente en una instrucción select que calcula la suma
de los totales de todas las líneas de detalles asociadas al pedido; esta instrucción ne-
cesita mezclar datos provenientes de las líneas de detalles y de la tabla de artículos. Si
el valor total es nulo, se cambia a cero. Esto puede suceder si el pedido no tiene lí-
neas de detalles; en este caso, la instrucción select retorna el valor nulo. Finalmente,
se localiza el pedido indicado y se le actualiza el valor a la columna Total, utilizando el
valor depositado en la variable local del mismo nombre.
El procedimiento que definimos a continuación se basa en el anterior, y permite
recalcular los totales de todas las filas almacenadas en la tabla de pedidos; de este
modo ilustramos el uso de las instrucciones for select … do y execute procedure:
create procedure RecalcularPedidos as
declare variable Pedido integer;
begin
for select Numero from Pedidos into :Pedido do
execute procedure RecalcularTotal :Pedido;
end ^
Procedimientos que devuelven un conjunto de datos
Antes he mencionado la posibilidad de superar las restricciones de las expresiones
select del modelo relacional mediante el uso de procedimientos almacenados. Un
procedimiento puede diseñarse de modo que devuelva un conjunto de filas; para esto
tiene que utilizar la instrucción suspend, que transfiere el control temporalmente a la
rutina que llama al procedimiento, para que ésta pueda hacer algo con los valores
asignados a los parámetros de salida. Esta técnica es poco habitual en los lenguajes
de programación más extendidos; si quiere encontrar algo parecido, puede desente-
rrar los iteradores del lenguaje CLU, diseñado por Barbara Liskov a mediados de los
setenta.
Supongamos que necesitamos obtener la lista de los primeros veinte, treinta o mil
cuatrocientos números primos. Comencemos por algo fácil, con la función que ana-
liza un número y dice si es primo o no:
create procedure EsPrimo(Numero integer)
returns (Respuesta integer) as
declare variable I integer;
begin
I = 2;
while (I < Numero) do
begin
if (cast((Numero / I) as integer) * I = Numero) then
Triggers y procedimientos almacenados 487
begin
Respuesta = 0;
exit;
end
I = I + 1;
end
Respuesta = 1;
end ^
Ya sé que hay implementaciones más eficientes, pero no quería complicar mucho el
ejemplo. Observe, de paso, la pirueta que he tenido que realizar para ver si el número
es divisible por el candidato a divisor. He utilizado el criterio del lenguaje C para las
expresiones lógicas: devuelvo 1 si el número es primo, y 0 si no lo es. Recuerde que
InterBase no tiene un tipo Boolean.
Ahora, en base al procedimiento anterior, implementamos el nuevo procedimiento
Primos:
create procedure Primos(Total integer)
returns (Primo Integer) as
declare variable I integer;
declare variable Respuesta integer;
begin
I = 0;
Primo = 2;
while (I < Total) do
begin
execute procedure EsPrimo Primo
returning_values Respuesta;
if (Respuesta = 1) then
begin
I = I + 1;
suspend; /* ¡¡¡ Nuevo !!! */
end
Primo = Primo + 1;
end
end ^
Este procedimiento puede ejecutarse en dos contextos diferentes: como un pro-
cedimiento normal, o como procedimiento de selección. Como procedimiento nor-
mal, utilizamos la instrucción execute procedure, como hasta ahora:
execute procedure Primos(100);
No obstante, no van a resultar tan sencillas las cosas. Esta llamada, si se realiza desde
Windows ISQL, solamente devuelve el primer número primo (era el 2, ¿o no?). El
problema es que, en este contexto, la primera llamada a suspend termina completa-
mente el algoritmo.
488 La Cara Oculta de Delphi
La segunda posibilidad es utilizar el procedimiento como si fuera una tabla o vista. Desde
Windows ISQL podemos lanzar la siguiente instrucción, que nos mostrará los prime-
ros cien números primos:
select * from Primos(100);
Por supuesto, el ejemplo anterior se refiere a una secuencia aritmética. En la práctica,
un procedimiento de selección se implementa casi siempre llamando a suspend
dentro de una instrucción for...do, que recorre las filas de una consulta.
Recorriendo un conjunto de datos
En esta sección mostraré un par de ejemplos más complicados de procedimientos
que utilizan la instrucción for...select de InterBase. El primero tiene que ver con un
sistema de entrada de pedidos. Supongamos que queremos actualizar las existencias
en el inventario después de haber grabado un pedido. Tenemos dos posibilidades, en
realidad: realizar esta actualización mediante un trigger que se dispare cada vez que se
guarda una línea de detalles, o ejecutar un procedimiento almacenado al finalizar la
grabación de todas las líneas del pedido.
La primera técnica será explicada en breve, pero adelanto en estos momentos que
tiene un defecto. Pongamos como ejemplo que dos usuarios diferentes están pasando
por el cajero, simultáneamente. El primero saca un pack de Coca-Colas de la cesta de
la compra, mientras el segundo pone Pepsis sobre el mostrador. Si, como es de espe-
rar, la grabación del pedido tiene lugar mediante una transacción, al dispararse el
trigger se han modificado las filas de estas dos marcas de bebidas, y se han bloqueado
hasta el final de la transacción. Ahora, inesperadamente, el primer usuario saca Pepsis
mientras el segundo nos sorprende con Coca-Colas; son unos fanáticos de las bebi-
das americanas estos individuos. El problema es que el primero tiene que esperar a
que el segundo termine para poder modificar la fila de las Pepsis, mientras que el
segundo se halla en una situación similar.
Esta situación se denomina abrazo mortal (deadlock) y realmente no es problema al-
guno para InterBase, en el cual los procesos fallan inmediatamente cuando se les
niega un bloqueo21. Pero puede ser un peligro en otros sistemas con distinta estrate-
gia de espera. La solución más común consiste en que cuando un proceso necesita
bloquear ciertos recursos, lo haga siempre en el mismo orden. Si nuestros dos con-
sumidores de líquidos oscuros con burbujas hubieran facturado sus compras en or-
den alfabético, no se hubiera producido este conflicto. Por supuesto, esto descarta el
uso de un trigger para actualizar el inventario, pues hay que esperar a que estén todos
21 Realmente, es el BDE quien configura a InterBase de este modo.
Triggers y procedimientos almacenados 489
los productos antes de ordenar y realizar entonces la actualización. El siguiente pro-
cedimiento se encarga de implementar el algoritmo explicado:
create procedure ActualizarInventario(Pedido integer) as
declare variable CodArt integer;
declare variable Cant integer;
begin
for select RefArticulo, Cantidad
from Detalles
where RefPedido = :Pedido
order by RefArticulo
into :CodArt, :Cant do
update Articulos
set Pedidos = Pedidos + :Cant
where Codigo = :CodArt;
end ^
Otro ejemplo: necesitamos conocer los diez mejores clientes de nuestra tienda. Pero
sólo los diez primeros, y no vale mirar hacia otro lado cuando aparezca el undécimo.
Algunos sistemas SQL tienen un operador first para este propósito, pero no Inter-
Base. Este procedimiento, que devuelve un conjunto de datos, nos servirá de ayuda:
create procedure MejoresClientes(Rango integer)
returns (Codigo int, Nombre varchar(30), Total int) as
begin
for select Codigo, Nombre, sum(Total)
from Clientes, Pedidos
where Clientes.Codigo = Pedidos.Cliente
order by 3 desc
into :Codigo, :Nombre, :Total do
begin
suspend;
Rango = Rango - 1;
if (Rango = 0) then
exit;
end
end ^
Entonces podremos realizar consultas como la siguiente:
select *
from MejoresClientes(10)
Triggers, o disparadores
Una de las posibilidades más interesantes de los sistemas de bases de datos relacio-
nales son los triggers, o disparadores; en adelante, utilizaré preferentemente la palabra
inglesa original. Se trata de un tipo de procedimiento almacenado que se activa au-
tomáticamente al efectuar operaciones de modificación sobre ciertas tablas de la base
de datos.
490 La Cara Oculta de Delphi
La sintaxis de la declaración de un trigger es la siguiente:
create trigger NombreTrigger for Tabla [active | inactive]
{before | after} {delete | insert | update}
[position Posición]
as CuerpoDeProcedimiento
El cuerpo de procedimiento tiene la misma sintaxis que los cuerpos de los procedi-
mientos almacenados. Las restantes cláusulas del encabezamiento de esta instrucción
tienen el siguiente significado:
Cláusula Significado
NombreTrigger El nombre que se le va a asignar al trigger
Tabla El nombre de la tabla a la cual está asociado
active | inactive Puede crearse inactivo, y activarse después
before | after Se activa antes o después de la operación
delete | insert | update Qué operación provoca el disparo del trigger
position Orden de disparo para la misma operación
A diferencia de otros sistemas de bases de datos, los triggers de InterBase se definen
para una sola operación sobre una sola tabla. Si queremos compartir código para
eventos de actualización de una o varias tablas, podemos situar este código en un
procedimiento almacenado y llamar a este algoritmo desde los diferentes triggers defi-
nidos.
Un parámetro interesante es el especificado por position. Para una operación sobre
una tabla pueden definirse varios triggers. El número indicado en position determina
el orden en que se disparan los diferentes sucesos; mientras más bajo sea el número,
mayor será la prioridad. Si dos triggers han sido definidos con la misma prioridad, el
orden de disparo entre ellos será aleatorio.
Hay una instrucción similar que permite modificar algunos parámetros de la defini-
ción de un trigger, como su orden de disparo, si está activo o no, o incluso su propio
cuerpo:
alter trigger NombreTrigger [active | inactive]
[{before | after} {delete | insert | update}]
[position Posición]
[as CuerpoProcedimiento]
Podemos eliminar completamente la definición de un trigger de la base de datos me-
diante la instrucción:
drop trigger NombreTrigger
Triggers y procedimientos almacenados 491
Las variables new y old
Dentro del cuerpo de un trigger pueden utilizarse las variables predefinidas new y old.
Estas variables hacen referencia a los valores nuevos y anteriores de las filas involu-
cradas en la operación que dispara el trigger. Por ejemplo, en una operación de modi-
ficación update, old se refiere a los valores de la fila antes de la modificación y new a
los valores después de modificados. Para una inserción, solamente tiene sentido la
variable new, mientras que para un borrado, solamente tiene sentido old.
El siguiente trigger hace uso de la variable new, para acceder a los valores del nuevo
registro después de una inserción:
create trigger UltimaFactura for Pedidos
active after insert position 0 as
declare variable UltimaFecha date;
begin
select UltimoPedido
from Clientes
where Codigo = new.RefCliente
into :UltimaFecha;
if (UltimaFecha < new.FechaVenta) then
update Clientes
set UltimoPedido = new.FechaVenta
where Codigo = new.RefCliente;
end ^
Este trigger sirve de contraejemplo a un error muy frecuente en la programación
SQL. La primera instrucción busca una fila particular de la tabla de clientes y, una vez
encontrada, extrae el valor de la columna UltimoPedido para asignarlo a la variable
local UltimaFecha. El error consiste en pensar que esta instrucción, a la vez, deja a la
fila encontrada como “fila activa”. El lenguaje de triggers y procedimientos almacena-
dos de InterBase, y la mayor parte de los restantes sistemas, no utiliza “filas activas”.
Es por eso que en la instrucción update hay que incluir una cláusula where para
volver a localizar el registro del cliente. De no incluirse esta cláusula, cambiaríamos la
fecha para todos los clientes.
Es posible cambiar el valor de una columna correspondiente a la variable new, pero
solamente si el trigger se define “antes” de la operación de modificación. En cualquier
caso, el nuevo valor de la columna se hace efectivo después de que la operación tenga
lugar.
Más ejemplos de triggers
Para mostrar el uso de triggers, las variables new y old y los procedimientos almace-
nados, mostraré cómo se puede actualizar automáticamente el inventario de artículos
492 La Cara Oculta de Delphi
y el total almacenado en la tabla de pedidos en la medida en que se realizan actualiza-
ciones en la tabla que contiene las líneas de detalles.
Necesitaremos un par de procedimientos auxiliares para lograr una implementación
más modular. Uno de estos procedimientos, RecalcularTotal, debe actualizar el total de
venta de un pedido determinado, y ya lo hemos implementado antes. Repito aquí su
código, para mayor comodidad:
create procedure RecalcularTotal(NumPed int) as
declare variable Total integer;
begin
select sum(Cantidad * PVP * (100 - Descuento) / 100)
from Detalles, Articulos
where Detalles.RefArticulo = Articulos.Codigo
and Detalles.RefPedido = :NumPed
into :Total;
if (Total is null) then
Total = 0;
update Pedidos
set Total = :Total
where Numero = :NumPed;
end ^
El otro procedimiento debe modificar el inventario de artículos. Su implementación
es muy simple:
create procedure ActInventario(CodArt integer, Cant Integer) as
begin
update Articulos
set Pedidos = Pedidos + :Cant
where Codigo = :CodArt;
end ^
Ahora le toca el turno a los triggers. Los más sencillos son los relacionados con la
inserción y borrado; en el primero utilizaremos la variable new, y en el segundo, old:
create trigger NuevoDetalle for Detalles
active after insert position 1 as
begin
execute procedure RecalcularTotal new.RefPedido;
execute procedure ActInventario
new.RefArticulo, new.Cantidad;
end ^
create trigger EliminarDetalle for Detalles
active after delete position 1 as
declare variable Decremento integer;
begin
Decremento = - old.Cantidad;
execute procedure RecalcularTotal old.RefPedido;
execute procedure ActInventario
old.RefArticulo, :Decremento;
end ^
Triggers y procedimientos almacenados 493
Es curiosa la forma en que se pasan los parámetros a los procedimientos almacena-
dos. Tome nota, en particular, de que hemos utilizado una variable local, Decremento,
en el trigger de eliminación. Esto es así porque no se puede pasar expresiones como
parámetros a los procedimientos almacenados, ni siquiera para los parámetros de
entrada.
Finalmente, nos queda el trigger de modificación:
create trigger ModificarDetalle for Detalles
active after update position 1 as
declare variable Decremento integer;
begin
execute procedure RecalcularTotal new.RefPedido;
if (new.RefArticulo <> old.RefArticulo) then
begin
Decremento = -old.Cantidad;
execute procedure ActInventario
old.RefArticulo, :Decremento;
execute procedure ActInventario
new.RefArticulo, new.Cantidad;
end
else
begin
Decremento = new.Cantidad - old.Cantidad;
execute procedure ActInventario
new.RefArticulo, :Decremento;
end
end ^
Observe cómo comparamos el valor del código del artículo antes y después de la
operación. Si solamente se ha producido un cambio en la cantidad vendida, tenemos
que actualizar un solo registro de inventario; en caso contrario, tenemos que actuali-
zar dos registros. No hemos tenido en cuenta la posibilidad de modificar el pedido al
cual pertenece la línea de detalles. Suponemos que esta operación no va a permitirse,
por carecer de sentido, en las aplicaciones clientes.
Generadores
Los generadores (generators) son un recurso de InterBase para poder disponer de valores
secuenciales, que pueden utilizarse, entre otras cosas, para garantizar la unicidad de
las claves artificiales. Un generador se crea, del mismo modo que los procedimientos
almacenados y triggers, en un fichero script de SQL. El siguiente ejemplo muestra
cómo crear un generador:
create generator CodigoEmpleado;
494 La Cara Oculta de Delphi
Un generador define una variable interna persistente, cuyo tipo es un entero de 32
bits. Aunque esta variable se inicializa automáticamente a 0, tenemos una instrucción
para cambiar el valor de un generador:
set generator CodigoEmpleado to 1000;
Por el contrario, no existe una instrucción específica que nos permita eliminar un
generador. Esta operación debemos realizarla directamente en la tabla del sistema
que contiene las definiciones y valores de todos los generadores:
delete from rdb$generators
where rdb$generator_name = 'CODIGOEMPLEADO'
Para utilizar un generador necesitamos la función gen_id. Esta función utiliza dos
parámetros. El primero es el nombre del generador, y el segundo debe ser la cantidad
en la que se incrementa o decrementa la memoria del generador. La función retorna
entonces el valor ya actualizado. Utilizaremos el generador anterior para suministrar
automáticamente un código de empleado si la instrucción insert no lo hace:
create trigger NuevoEmpleado for Empleados
active before insert
as
begin
if (new.Codigo is null) then
new.Codigo = gen_id(CodigoEmpleado, 1);
end ^
Al preguntar primeramente si el código del nuevo empleado es nulo, estamos permi-
tiendo la posibilidad de asignar manualmente un código de empleado durante la in-
serción.
Los programas escritos en Delphi tienen problemas cuando se genera una clave pri-
maria para una fila mediante un generador, pues el registro recién insertado “desapa-
rece” según el punto de vista de la tabla. Para no tener que abandonar los generado-
res, la solución consiste en crear un procedimiento almacenado que obtenga el pró-
ximo valor del generador, y utilizar este valor para asignarlo a la clave primaria en el
evento BeforePost de la tabla. En el lado del servidor se programaría algo parecido a lo
siguiente:
create procedure ProximoCodigo returns (Cod integer) as
begin
Cod = gen_id(CodigoEmpleado);
end ^
En la aplicación crearíamos un componente spProximoCodigo, de la clase TStoredProc, y
lo aprovecharíamos de esta forma en uno de los eventos BeforePost o OnNewRecord de
la tabla de clientes:
Triggers y procedimientos almacenados 495
procedure TmodDatos.tbClientesBeforePost(DataSet: TDataSet);
begin
spProximoCodigo.ExecProc;
tbClientesCodigo.Value :=
spProximoCodigo.ParaByName('COD').AsInteger;
end;
NOTA IMPORTANTE
En cualquier caso, si necesita valores únicos y consecutivos en alguna columna de
una tabla, no utilice generadores (ni secuencias de Oracle, o identidades de MS
SQL Server). El motivo es que los generadores no se bloquean durante las tran-
sacciones. Usted pide un valor dentro de una transacción, y le es concedido; to-
davía no ha terminado su transacción. A continuación, otro usuario pide el si-
guiente valor, y sus deseos se cumplen. Pero entonces usted aborta la transac-
ción, por el motivo que sea. La consecuencia: se pierde el valor que recibió, y se
produce un "hueco" en la secuencia.
Simulando la integridad referencial
Como hemos explicado, mediante los triggers y los procedimientos almacenados po-
demos expresar reglas de consistencia en forma imperativa, en contraste con las reglas
declarativas que se enuncian al crear tablas: claves primarias, alternativas y externas,
condiciones de verificación, etc. En general, es preferible utilizar una regla declarativa
antes que su equivalente imperativo. Pero sucede que las posibilidades de las reglas
declarativas son más limitadas que las posibilidades de las reglas imperativas.
En InterBase 4, por ejemplo, las restricciones de integridad referencial no admiten
modificaciones ni borrados en las tablas maestras de una clave externa. Sin embargo,
a veces es deseable permitir estas operaciones y propagar los cambios en cascada a
las tablas dependientes.
Ilustraré la forma de lograr restricciones de integridad referencial con propagación
de cambios mediante el ejemplo de la tabla de pedidos y líneas de detalles. Recorde-
mos la definición de la tabla de pedidos, en el capítulo sobre el Lenguaje de Defini-
ción de Datos:
create table Pedidos (
Numero int not null,
RefCliente int not null,
RefEmpleado int,
FechaVenta date default "Now",
Total int default 0,
496 La Cara Oculta de Delphi
primary key (Numero),
foreign key (RefCliente) references Clientes (Codigo)
);
La definición de la tabla de detalles cambia ahora, sustituyéndose la cláusula foreign
key que hacía referencia a la tabla de pedidos:
create table Detalles (
RefPedido int not null,
NumLinea int not null,
RefArticulo int not null,
Cantidad int default 1 not null,
Descuento int default 0 not null
check (Descuento between 0 and 100),
primary key (RefPedido, NumLinea),
foreign key (RefArticulo) references Articulos (Codigo),
/*
Antes: foreign key(RefPedido) references Pedidos(Numero)
*/
check (RefPedido in (select Numero from Pedidos))
);
La nueva cláusula check verifica en cada inserción y modificación que no se intro-
duzca un número de pedido inexistente. El borrado en cascada se puede lograr de la
siguiente manera:
create trigger BorrarDetallesEnCascada for Pedidos
active after delete
position 0
as
begin
delete from Detalles
where RefPedido = old.Numero;
end ^
Un poco más larga es la implementación de actualizaciones en cascada.
create trigger ModificarDetallesEnCascada for Pedidos
active after update
position 0
as
begin
if (old.Numero <> new.Numero) then
update Detalles
set RefPedido = new.Numero
where RefPedido = old.Numero;
end ^
Por supuesto, los triggers hubieran sido mucho más complicados si hubiéramos man-
tenido la restricción foreign key en la declaración de la tabla de detalles, en particu-
lar, la propagación de modificaciones.
Triggers y procedimientos almacenados 497
Excepciones
Sin embargo, todavía no contamos con medios para detener una operación SQL; esta
operación sería necesaria para simular imperativamente las restricciones a la propaga-
ción de cambios en cascada, en la integridad referencial. Lo que nos falta es el poder
lanzar excepciones desde un trigger o procedimiento almacenado. Las excepciones de
InterBase se crean asociando una cadena de mensaje a un identificador:
create exception CLIENTE_CON_PEDIDOS
"No se puede modificar este cliente"
Es necesario confirmar la transacción actual para poder utilizar una excepción recién
creada. Existen también instrucciones para modificar el mensaje asociado a una ex-
cepción (alter exception), y para eliminar la definición de una excepción de la base
de datos (drop exception).
Una excepción se lanza desde un procedimiento almacenado o trigger mediante la
instrucción exception:
create trigger CheckDetails for Clientes
active before delete
position 0
as
declare variable Numero int;
begin
select count(*)
from Pedidos
where RefCliente = old.Codigo
into :Numero;
if (:Numero > 0) then
exception CLIENTE_CON_PEDIDOS;
end ^
Las excepciones de InterBase determinan que cualquier cambio realizado dentro del
cuerpo del trigger o procedimiento almacenado, sea directa o indirectamente, se anule
automáticamente. De esta forma puede programarse algo parecido a las transaccio-
nes anidadas de otros sistemas de bases de datos.
Si la instrucción exception es similar a la instrucción raise de Delphi, el equivalente
más cercano a try...except es la instrucción when de InterBase. Esta instrucción
tiene tres formas diferentes. La primera intercepta las excepciones lanzadas con ex-
ception:
when exception NombreExcepción do
BloqueInstrucciones;
Con la segunda variante, se detectan los errores producidos por las instrucciones
SQL:
498 La Cara Oculta de Delphi
when sqlcode Numero do
BloqueInstrucciones;
Los números de error de SQL aparecen documentados en la ayuda en línea y en el
manual Language Reference de InterBase. A grandes rasgos, la ejecución correcta de una
instrucción devuelve un código igual a 0, cualquier valor negativo es un error pro-
piamente dicho (-803, por ejemplo, es un intento de violación de una clave primaria),
y los valores positivos son advertencias. En particular, 100 es el valor que se devuelve
cuando una selección única no encuentra el registro buscado. Este convenio es parte
del estándar de SQL, aunque los códigos de error concreto varíen de un sistema a
otro.
La tercera forma de la instrucción when es la siguiente:
when gdscode Numero do
BloqueInstrucciones;
En este caso, se están interceptando los mismos errores que con sqlcode, pero se
utilizan los códigos internos de InterBase, que ofrecen más detalles sobre la causa.
Por ejemplo, los valores 335544349 y 35544665 corresponden a –803, la violación de
unicidad, pero el primero se produce cuando se inserta un valor duplicado en cual-
quier índice único, mientras que el segundo se reserva para las violaciones específicas
de clave primaria o alternativa.
En cualquier caso, las instrucciones when deben ser las últimas del bloque en que se
incluyen, y pueden colocarse varias simultáneamente, para atender varios casos:
begin
/* Instrucciones */
/* … */
when sqlcode –803 do
Resultado = "Violación de unicidad";
when exception CLIENTE_CON_PEDIDOS do
Resultado = "Elimine primero los pedidos realizados";
end
La Tercera Regla de Marteens sigue siendo aplicable a estas instrucciones: no detenga
la propagación de una excepción, a no ser que tenga una solución a su causa.
Alertadores de eventos
Los alertadores de eventos (event alerters) son un recurso único, por el momento, de
InterBase. Los procedimientos almacenados y triggers de InterBase pueden utilizar la
instrucción siguiente:
post_event NombreDeEvento
Triggers y procedimientos almacenados 499
El nombre de evento puede ser una constante de cadena o una variable del mismo
tipo. Cuando se produce un evento, InterBase avisa a todos los clientes interesados
de la ocurrencia del mismo.
Los alertadores de eventos son un recurso muy potente. Sitúese en un entorno
cliente/servidor donde se producen con frecuencia cambios en una base de datos.
Las estaciones de trabajo normalmente no reciben aviso de estos cambios, y los
usuarios deben actualizar periódica y frecuentemente sus pantallas para reflejar los
cambios realizados por otros usuarios, pues en caso contrario puede suceder que
alguien tome una decisión equivocada en base a lo que está viendo en pantalla. Sin
embargo, refrescar la pantalla toma tiempo, pues hay que traer cierta cantidad de
información desde el servidor de bases de datos, y las estaciones de trabajo realizan
esta operación periódicamente, colapsando la red. El personal de la empresa se abu-
rre en los tiempos de espera, la moral se resquebraja y la empresa se sitúa al borde
del caos...
Entonces aparece Usted, un experto programador de Delphi e InterBase, y añade
triggers a discreción a la base de datos, en este estilo:
create trigger AlertarCambioBolsa for Cotizaciones
active after update position 10
as
begin
post_event "CambioCotizacion";
end ^
Observe que se ha definido una prioridad baja para el orden de disparo del trigger.
Hay que aplicar la misma técnica para cada una de las operaciones de actualización
de la tabla de cotizaciones.
Luego, en el módulo de datos de la aplicación que se ejecuta en las estaciones de
trabajo, hay que añadir el componente TIBEventAlerter, que se encuentra en la página
Samples de la Paleta de Componentes. Este componente tiene las siguientes propie-
dades, métodos y eventos:
Nombre Tipo Propósito
Events Propiedad Los nombres de eventos que nos interesan.
Registered Propiedad Debe ser True para notificar, en tiempo de diseño,
nuestro interés en los eventos almacenados en la
propiedad anterior.
Database Propiedad La base de datos a la cual nos conectaremos.
500 La Cara Oculta de Delphi
Nombre Tipo Propósito
RegisterEvents Método Notifica a la base de datos nuestro interés por los
eventos de la propiedad Events.
UnRegisterEvents Método El método inverso del método anterior.
OnEventAlert Evento Se dispara cada vez que se produce el evento.
En nuestro caso, podemos editar la propiedad Events y teclear la cadena CambioCotiza-
cion, que es el nombre del evento que necesitamos. Conectamos la propiedad Database
del componente a nuestro componente de bases de datos y activamos la propiedad
Registered. Luego creamos un manejador para el evento OnEventAlert similar a éste:
procedure TForm1.IBEventAlerter1EventAlert(Sender: TObject;
EventName: string; EventCount: Longint;
var CancelAlerts: Boolean);
begin
tbCotizaciones.Refresh;
end;
Cada vez que se modifique el contenido de la tabla Cotizaciones, el servidor de Inter-
Base lanzará el evento identificado por la cadena CambioCotizacion, y este evento será
recibido por todas las aplicaciones interesadas. Cada aplicación realizará consecuen-
temente la actualización visual de la tabla en cuestión.
Esta historia termina previsiblemente. La legión de usuarios del sistema lo aclama
con fervor, su jefe le duplica el salario, usted se casa ... o se compra un perro ... o ...
Bueno, se me ha complicado un poco el guión; póngale usted su final preferido.
Capítulo
24
Microsoft SQL ServerMicrosoft SQL Server
N ESTE CAPÍTULO ANALIZAREMOS LAS CARACTERÍSTICAS generales de la
implementación de SQL por parte de Microsoft SQL Server. Este sistema es
uno de los más extendidos en el mundo de las redes basadas en Windows
NT. No está, sin embargo, dentro de la lista de mis sistemas favoritos, por dos razo-
nes. La primera: una arquitectura bastante pobre, con tamaño fijo de página, blo-
queos a nivel de página que disminuyen la capacidad de modificación concurrente,
ficheros de datos y de transacciones de tamaño fijo... La segunda razón: MS SQL
Server tiene uno de los dialectos de SQL más retorcidos y horribles del mundo de los
servidores de datos relacionales, dudoso privilegio que comparte con Sybase.
Es muy probable que la primera razón deje de ser tal con la próxima aparición de la
versión 7 de este sistema de bases de datos, que mejora bastante la arquitectura física
utilizada. Este capítulo está basado fundamentalmente en la versión actual, la 6.5. En
el momento de la escritura de este libro, la versión 7 estaba en fase beta. Trataré en lo
posible de adelantar algunas características de la misma, pero advierto al lector de los
peligros de deducir características de la versión final a partir de una beta.
Herramientas de desarrollo en el cliente
La herramienta adecuada para diseñar y administrar bases de datos de MS SQL Ser-
ver se llama SQL Enterprise Manager. Normalmente, esta aplicación se instala en el
servidor de datos, pero podemos también instalarla en el cliente. Con el Enterprise
Manager podemos crear dispositivos (más adelante veremos qué son), bases de datos,
crear usuarios y administrar sus contraseñas, e incluso gestionar otros servidores de
forma remota.
Si lo que necesitamos es ejecutar consultas individuales o todo un script con instruc-
ciones, el arma adecuada es el SQL Query Tool, que puede ejecutarse como aplicación
independiente, o desde un comando de menú de SQL Enterprise Manager, como se
muestra en la siguiente imagen. Dentro del menú File de esta aplicación encontrare-
mos un comando para que ejecutemos nuestros scripts SQL.
E
502 La Cara Oculta de Delphi
Hay que reconocer que las herramientas de administración de MS SQL Server clasi-
fican entre las amigables con el usuario. Esto se acentúa en la versión 7, en la cual
pegas una patada al servidor y saltan cinco o seis asistentes (wizards) para adivinar qué
es lo que quieres realmente hacer.
Creación de bases de datos con MS SQL Server
Uno de los cambios que introduce la versión 7 de SQL Server es precisamente en la
forma de crear bases de datos. En esta sección, sin embargo, describiré fundamen-
talmente la técnica empleada hasta la versión 6.5, pues pueden aparecer novedades en
la versión final. Además, las bases de datos de Sybase se crean con mecanismos si-
milares.
Para trabajar con SQL Server hay que comprender qué es un dispositivo (device). Muy
fácil: es un fichero del sistema operativo. ¿Entonces por qué inventar un nombre
diferente? La razón está en la prehistoria de este producto, a cargo de Sybase. A dife-
rencia de SQL Server, el servidor de Sybase puede ejecutarse en distintos sistemas
operativos, y puede que, en determinado sistema, un fichero físico no sea el soporte
más adecuado para el almacenamiento de una base de datos.
Dispositivo Dispositivo
Servidor
Dispositivo Dispositivo Dispositivo
Servidor
Dispositivo Dispositivo
Servidor
SQL Server
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602
INFOSAN Delphi  453-602

Más contenido relacionado

La actualidad más candente

Tema 15 aplicaciones de dos capas por gio
Tema 15   aplicaciones de dos capas por gioTema 15   aplicaciones de dos capas por gio
Tema 15 aplicaciones de dos capas por gioRobert Wolf
 
Lenguaje transact sql
Lenguaje transact sqlLenguaje transact sql
Lenguaje transact sqlSuarezJhon
 
Seguridad Oracle 11g R2
Seguridad Oracle 11g R2Seguridad Oracle 11g R2
Seguridad Oracle 11g R2Carmen Soler
 
MySQL. Tutorial Básico
MySQL. Tutorial BásicoMySQL. Tutorial Básico
MySQL. Tutorial BásicoJosu Orbe
 
Practicas tablespaces tema5 oracle tablespace
Practicas tablespaces tema5 oracle tablespacePracticas tablespaces tema5 oracle tablespace
Practicas tablespaces tema5 oracle tablespaceCarlos Ocola Ugarte
 
Administrando Usuarios de Oracle Database. Z052 08
Administrando Usuarios de Oracle Database. Z052 08Administrando Usuarios de Oracle Database. Z052 08
Administrando Usuarios de Oracle Database. Z052 08Alexander Calderón
 
58517228 postgre sql-desarrolladoresbasico
58517228 postgre sql-desarrolladoresbasico58517228 postgre sql-desarrolladoresbasico
58517228 postgre sql-desarrolladoresbasicoLucia Zambrano Franco
 
F:\basesdatos teo2 sistemas_db
F:\basesdatos teo2 sistemas_dbF:\basesdatos teo2 sistemas_db
F:\basesdatos teo2 sistemas_dbsuperinfopoderosas
 
Android DB por Cesar Cespedes
Android DB por Cesar CespedesAndroid DB por Cesar Cespedes
Android DB por Cesar CespedesLima GTUG
 
Gestión de la instancia de Oracle 11g R2
Gestión de la instancia de Oracle 11g R2Gestión de la instancia de Oracle 11g R2
Gestión de la instancia de Oracle 11g R2Carmen Soler
 
Yupa cesar bdii_t7
Yupa cesar bdii_t7Yupa cesar bdii_t7
Yupa cesar bdii_t7Cesar Yupa
 
Programación de Base de Datos - Unidad 4 Representacion de la info
Programación de Base de Datos - Unidad 4 Representacion de la infoProgramación de Base de Datos - Unidad 4 Representacion de la info
Programación de Base de Datos - Unidad 4 Representacion de la infoJosé Antonio Sandoval Acosta
 
Oracle3
Oracle3Oracle3
Oracle3Jualme
 
Guía 01. Ejercicios. Creación de Base de Datos en MySQL - José J Sánchez H
Guía 01. Ejercicios. Creación de Base de Datos en MySQL - José J Sánchez HGuía 01. Ejercicios. Creación de Base de Datos en MySQL - José J Sánchez H
Guía 01. Ejercicios. Creación de Base de Datos en MySQL - José J Sánchez HJosé Ricardo Tillero Giménez
 
Sesión13 - Archivos de Control (Oracle)
Sesión13 - Archivos de Control (Oracle)Sesión13 - Archivos de Control (Oracle)
Sesión13 - Archivos de Control (Oracle)José Toro
 

La actualidad más candente (19)

Tema 15 aplicaciones de dos capas por gio
Tema 15   aplicaciones de dos capas por gioTema 15   aplicaciones de dos capas por gio
Tema 15 aplicaciones de dos capas por gio
 
Lenguaje transact sql
Lenguaje transact sqlLenguaje transact sql
Lenguaje transact sql
 
Ejemplo Base de Datos SQLite (Android)
Ejemplo Base de Datos SQLite (Android)Ejemplo Base de Datos SQLite (Android)
Ejemplo Base de Datos SQLite (Android)
 
Seguridad Oracle 11g R2
Seguridad Oracle 11g R2Seguridad Oracle 11g R2
Seguridad Oracle 11g R2
 
MySQL. Tutorial Básico
MySQL. Tutorial BásicoMySQL. Tutorial Básico
MySQL. Tutorial Básico
 
Practicas tablespaces tema5 oracle tablespace
Practicas tablespaces tema5 oracle tablespacePracticas tablespaces tema5 oracle tablespace
Practicas tablespaces tema5 oracle tablespace
 
Administrando Usuarios de Oracle Database. Z052 08
Administrando Usuarios de Oracle Database. Z052 08Administrando Usuarios de Oracle Database. Z052 08
Administrando Usuarios de Oracle Database. Z052 08
 
58517228 postgre sql-desarrolladoresbasico
58517228 postgre sql-desarrolladoresbasico58517228 postgre sql-desarrolladoresbasico
58517228 postgre sql-desarrolladoresbasico
 
Sq lite
Sq lite Sq lite
Sq lite
 
Abd2
Abd2Abd2
Abd2
 
F:\basesdatos teo2 sistemas_db
F:\basesdatos teo2 sistemas_dbF:\basesdatos teo2 sistemas_db
F:\basesdatos teo2 sistemas_db
 
Android DB por Cesar Cespedes
Android DB por Cesar CespedesAndroid DB por Cesar Cespedes
Android DB por Cesar Cespedes
 
Taller de Base de Datos - Unidad 3 lenguage DML
Taller de Base de Datos - Unidad 3 lenguage DMLTaller de Base de Datos - Unidad 3 lenguage DML
Taller de Base de Datos - Unidad 3 lenguage DML
 
Gestión de la instancia de Oracle 11g R2
Gestión de la instancia de Oracle 11g R2Gestión de la instancia de Oracle 11g R2
Gestión de la instancia de Oracle 11g R2
 
Yupa cesar bdii_t7
Yupa cesar bdii_t7Yupa cesar bdii_t7
Yupa cesar bdii_t7
 
Programación de Base de Datos - Unidad 4 Representacion de la info
Programación de Base de Datos - Unidad 4 Representacion de la infoProgramación de Base de Datos - Unidad 4 Representacion de la info
Programación de Base de Datos - Unidad 4 Representacion de la info
 
Oracle3
Oracle3Oracle3
Oracle3
 
Guía 01. Ejercicios. Creación de Base de Datos en MySQL - José J Sánchez H
Guía 01. Ejercicios. Creación de Base de Datos en MySQL - José J Sánchez HGuía 01. Ejercicios. Creación de Base de Datos en MySQL - José J Sánchez H
Guía 01. Ejercicios. Creación de Base de Datos en MySQL - José J Sánchez H
 
Sesión13 - Archivos de Control (Oracle)
Sesión13 - Archivos de Control (Oracle)Sesión13 - Archivos de Control (Oracle)
Sesión13 - Archivos de Control (Oracle)
 

Similar a INFOSAN Delphi 453-602

Seguridad 2° exp_ooo
Seguridad 2° exp_oooSeguridad 2° exp_ooo
Seguridad 2° exp_oooYuzel Sederap
 
Gestion de datos[1].pdf
Gestion de datos[1].pdfGestion de datos[1].pdf
Gestion de datos[1].pdfMARIAQUIROS19
 
Seguridad 2° exp_ooo
Seguridad 2° exp_oooSeguridad 2° exp_ooo
Seguridad 2° exp_oooYuzel Sederap
 
Conexión remota a base de datos con Oracle y MySQL
Conexión remota a base de datos con Oracle y MySQLConexión remota a base de datos con Oracle y MySQL
Conexión remota a base de datos con Oracle y MySQLIvan Luis Jimenez
 
Administracion de base de datos postgresql
Administracion de base de datos postgresqlAdministracion de base de datos postgresql
Administracion de base de datos postgresqlAlvaro Paz
 
Administracion de base de datos postgresql
Administracion de base de datos postgresqlAdministracion de base de datos postgresql
Administracion de base de datos postgresqlAlvaro Paz
 
Obvios herramientas de un SGDB
Obvios herramientas de un SGDBObvios herramientas de un SGDB
Obvios herramientas de un SGDBliras loca
 
sentencia Grant y Revoke
sentencia Grant y Revokesentencia Grant y Revoke
sentencia Grant y Revokesuperusuario2
 
Como utilizar sql en visual basic 6
Como utilizar sql en visual basic 6Como utilizar sql en visual basic 6
Como utilizar sql en visual basic 6Narcisa Fuentes
 
Como utilizar sql en visual basic 6
Como utilizar sql en visual basic 6Como utilizar sql en visual basic 6
Como utilizar sql en visual basic 6Narcisa Fuentes
 
curso-servidores-apache-2
curso-servidores-apache-2curso-servidores-apache-2
curso-servidores-apache-2greenpeans
 
Rosero inés bdii_t7 (1)
Rosero inés bdii_t7 (1)Rosero inés bdii_t7 (1)
Rosero inés bdii_t7 (1)Inés Rosero
 

Similar a INFOSAN Delphi 453-602 (20)

Ejercicio privilegios
Ejercicio privilegiosEjercicio privilegios
Ejercicio privilegios
 
Seguridad 2° exp_ooo
Seguridad 2° exp_oooSeguridad 2° exp_ooo
Seguridad 2° exp_ooo
 
Gestion de datos[1].pdf
Gestion de datos[1].pdfGestion de datos[1].pdf
Gestion de datos[1].pdf
 
Mysql
MysqlMysql
Mysql
 
Seguridad 2° exp_ooo
Seguridad 2° exp_oooSeguridad 2° exp_ooo
Seguridad 2° exp_ooo
 
Conexión remota a base de datos con Oracle y MySQL
Conexión remota a base de datos con Oracle y MySQLConexión remota a base de datos con Oracle y MySQL
Conexión remota a base de datos con Oracle y MySQL
 
U3-ACT02-TBD-ISC-MSG.pdf
U3-ACT02-TBD-ISC-MSG.pdfU3-ACT02-TBD-ISC-MSG.pdf
U3-ACT02-TBD-ISC-MSG.pdf
 
Admon oracle
Admon oracleAdmon oracle
Admon oracle
 
Ejemplo bbdd sqlite (android)
Ejemplo bbdd sqlite (android)Ejemplo bbdd sqlite (android)
Ejemplo bbdd sqlite (android)
 
Capitulo 6
Capitulo 6Capitulo 6
Capitulo 6
 
Tema9
Tema9Tema9
Tema9
 
Administracion de base de datos postgresql
Administracion de base de datos postgresqlAdministracion de base de datos postgresql
Administracion de base de datos postgresql
 
Administracion de base de datos postgresql
Administracion de base de datos postgresqlAdministracion de base de datos postgresql
Administracion de base de datos postgresql
 
Obvios herramientas de un SGDB
Obvios herramientas de un SGDBObvios herramientas de un SGDB
Obvios herramientas de un SGDB
 
sentencia Grant y Revoke
sentencia Grant y Revokesentencia Grant y Revoke
sentencia Grant y Revoke
 
Como utilizar sql en visual basic 6
Como utilizar sql en visual basic 6Como utilizar sql en visual basic 6
Como utilizar sql en visual basic 6
 
Como utilizar sql en visual basic 6
Como utilizar sql en visual basic 6Como utilizar sql en visual basic 6
Como utilizar sql en visual basic 6
 
curso-servidores-apache-2
curso-servidores-apache-2curso-servidores-apache-2
curso-servidores-apache-2
 
PostgreSQL
PostgreSQLPostgreSQL
PostgreSQL
 
Rosero inés bdii_t7 (1)
Rosero inés bdii_t7 (1)Rosero inés bdii_t7 (1)
Rosero inés bdii_t7 (1)
 

Más de FRANCIACOCO

INFOSAN Tutorial python3 (1)
INFOSAN Tutorial python3 (1)INFOSAN Tutorial python3 (1)
INFOSAN Tutorial python3 (1)FRANCIACOCO
 
INFOSAN APA 5 hojas
INFOSAN  APA  5 hojasINFOSAN  APA  5 hojas
INFOSAN APA 5 hojasFRANCIACOCO
 
INFOSAN APA 5 hojas
INFOSAN APA  5 hojasINFOSAN APA  5 hojas
INFOSAN APA 5 hojasFRANCIACOCO
 
INFOSAN Delphi 753-914
INFOSAN Delphi  753-914INFOSAN Delphi  753-914
INFOSAN Delphi 753-914FRANCIACOCO
 
INFOSAN Delphi 603-752
INFOSAN Delphi  603-752INFOSAN Delphi  603-752
INFOSAN Delphi 603-752FRANCIACOCO
 
INFOSAN Delphi 303 - 452
INFOSAN  Delphi 303 - 452INFOSAN  Delphi 303 - 452
INFOSAN Delphi 303 - 452FRANCIACOCO
 
INFOSAN Delphi 151- 302 PAGINAS
INFOSAN Delphi  151-  302 PAGINASINFOSAN Delphi  151-  302 PAGINAS
INFOSAN Delphi 151- 302 PAGINASFRANCIACOCO
 
INFOSAN Delphi 1 -150 paginas
INFOSAN Delphi  1 -150 paginasINFOSAN Delphi  1 -150 paginas
INFOSAN Delphi 1 -150 paginasFRANCIACOCO
 
INFOSAN Planeación didáctica
INFOSAN Planeación didácticaINFOSAN Planeación didáctica
INFOSAN Planeación didácticaFRANCIACOCO
 
INFOSAN La educación basada en competencias
INFOSAN La educación basada en competenciasINFOSAN La educación basada en competencias
INFOSAN La educación basada en competenciasFRANCIACOCO
 
INFOSAN Guia delphi
INFOSAN Guia delphiINFOSAN Guia delphi
INFOSAN Guia delphiFRANCIACOCO
 
INFOSAN Curso de delphi básico
INFOSAN Curso de delphi básicoINFOSAN Curso de delphi básico
INFOSAN Curso de delphi básicoFRANCIACOCO
 
INFOSAN Cuestionario 1 de delphi
INFOSAN Cuestionario 1 de delphiINFOSAN Cuestionario 1 de delphi
INFOSAN Cuestionario 1 de delphiFRANCIACOCO
 
INFOSAN Cuestionario de borlandc++
INFOSAN Cuestionario de borlandc++INFOSAN Cuestionario de borlandc++
INFOSAN Cuestionario de borlandc++FRANCIACOCO
 
INFOSAN Cuestionario 3 de borland c
INFOSAN Cuestionario 3 de borland cINFOSAN Cuestionario 3 de borland c
INFOSAN Cuestionario 3 de borland cFRANCIACOCO
 
INFOSAN Reparación de discos duro (datos)
INFOSAN  Reparación de discos duro (datos)INFOSAN  Reparación de discos duro (datos)
INFOSAN Reparación de discos duro (datos)FRANCIACOCO
 
INFOSAN Motherboard
INFOSAN MotherboardINFOSAN Motherboard
INFOSAN MotherboardFRANCIACOCO
 
INFOSAN Mantenimiento 1
INFOSAN Mantenimiento 1INFOSAN Mantenimiento 1
INFOSAN Mantenimiento 1FRANCIACOCO
 

Más de FRANCIACOCO (20)

INFOSAN Ventas
INFOSAN  VentasINFOSAN  Ventas
INFOSAN Ventas
 
INFOSAN Costos
INFOSAN CostosINFOSAN Costos
INFOSAN Costos
 
INFOSAN Tutorial python3 (1)
INFOSAN Tutorial python3 (1)INFOSAN Tutorial python3 (1)
INFOSAN Tutorial python3 (1)
 
INFOSAN APA 5 hojas
INFOSAN  APA  5 hojasINFOSAN  APA  5 hojas
INFOSAN APA 5 hojas
 
INFOSAN APA 5 hojas
INFOSAN APA  5 hojasINFOSAN APA  5 hojas
INFOSAN APA 5 hojas
 
INFOSAN Delphi 753-914
INFOSAN Delphi  753-914INFOSAN Delphi  753-914
INFOSAN Delphi 753-914
 
INFOSAN Delphi 603-752
INFOSAN Delphi  603-752INFOSAN Delphi  603-752
INFOSAN Delphi 603-752
 
INFOSAN Delphi 303 - 452
INFOSAN  Delphi 303 - 452INFOSAN  Delphi 303 - 452
INFOSAN Delphi 303 - 452
 
INFOSAN Delphi 151- 302 PAGINAS
INFOSAN Delphi  151-  302 PAGINASINFOSAN Delphi  151-  302 PAGINAS
INFOSAN Delphi 151- 302 PAGINAS
 
INFOSAN Delphi 1 -150 paginas
INFOSAN Delphi  1 -150 paginasINFOSAN Delphi  1 -150 paginas
INFOSAN Delphi 1 -150 paginas
 
INFOSAN Planeación didáctica
INFOSAN Planeación didácticaINFOSAN Planeación didáctica
INFOSAN Planeación didáctica
 
INFOSAN La educación basada en competencias
INFOSAN La educación basada en competenciasINFOSAN La educación basada en competencias
INFOSAN La educación basada en competencias
 
INFOSAN Guia delphi
INFOSAN Guia delphiINFOSAN Guia delphi
INFOSAN Guia delphi
 
INFOSAN Curso de delphi básico
INFOSAN Curso de delphi básicoINFOSAN Curso de delphi básico
INFOSAN Curso de delphi básico
 
INFOSAN Cuestionario 1 de delphi
INFOSAN Cuestionario 1 de delphiINFOSAN Cuestionario 1 de delphi
INFOSAN Cuestionario 1 de delphi
 
INFOSAN Cuestionario de borlandc++
INFOSAN Cuestionario de borlandc++INFOSAN Cuestionario de borlandc++
INFOSAN Cuestionario de borlandc++
 
INFOSAN Cuestionario 3 de borland c
INFOSAN Cuestionario 3 de borland cINFOSAN Cuestionario 3 de borland c
INFOSAN Cuestionario 3 de borland c
 
INFOSAN Reparación de discos duro (datos)
INFOSAN  Reparación de discos duro (datos)INFOSAN  Reparación de discos duro (datos)
INFOSAN Reparación de discos duro (datos)
 
INFOSAN Motherboard
INFOSAN MotherboardINFOSAN Motherboard
INFOSAN Motherboard
 
INFOSAN Mantenimiento 1
INFOSAN Mantenimiento 1INFOSAN Mantenimiento 1
INFOSAN Mantenimiento 1
 

Último

Buscadores, SEM SEO: el desafío de ser visto en la web
Buscadores, SEM SEO: el desafío de ser visto en la webBuscadores, SEM SEO: el desafío de ser visto en la web
Buscadores, SEM SEO: el desafío de ser visto en la webDecaunlz
 
Producto académico 03 - Habilidades Comunicativas.pptx
Producto académico 03 - Habilidades Comunicativas.pptxProducto académico 03 - Habilidades Comunicativas.pptx
Producto académico 03 - Habilidades Comunicativas.pptx46828205
 
locomotas v siclo.ppt de ingenieria de minas
locomotas v siclo.ppt de ingenieria de minaslocomotas v siclo.ppt de ingenieria de minas
locomotas v siclo.ppt de ingenieria de minasMirkaCBauer
 
institucion educativa la esperanza sede magdalena
institucion educativa la esperanza sede magdalenainstitucion educativa la esperanza sede magdalena
institucion educativa la esperanza sede magdalenajuniorcuellargomez
 
rodriguez_DelAngel_MariaGPE_M1S3AL6.pptx
rodriguez_DelAngel_MariaGPE_M1S3AL6.pptxrodriguez_DelAngel_MariaGPE_M1S3AL6.pptx
rodriguez_DelAngel_MariaGPE_M1S3AL6.pptxssuser61dda7
 
COMPETENCIAS CIUDADANASadadadadadadada .pdf
COMPETENCIAS CIUDADANASadadadadadadada .pdfCOMPETENCIAS CIUDADANASadadadadadadada .pdf
COMPETENCIAS CIUDADANASadadadadadadada .pdfOscarBlas6
 
CamposGarcia_MariaMagdalena_M1S3AI6.pptx
CamposGarcia_MariaMagdalena_M1S3AI6.pptxCamposGarcia_MariaMagdalena_M1S3AI6.pptx
CamposGarcia_MariaMagdalena_M1S3AI6.pptx241518192
 
2º SOY LECTOR PART 2- MD EDUCATIVO (6).pdf
2º SOY LECTOR PART 2- MD  EDUCATIVO (6).pdf2º SOY LECTOR PART 2- MD  EDUCATIVO (6).pdf
2º SOY LECTOR PART 2- MD EDUCATIVO (6).pdfFernandaHernandez312615
 
FLUIDEZ-Teatro-Leido-4to-Grado-El-leon-y-el-raton- (1).pdf
FLUIDEZ-Teatro-Leido-4to-Grado-El-leon-y-el-raton- (1).pdfFLUIDEZ-Teatro-Leido-4to-Grado-El-leon-y-el-raton- (1).pdf
FLUIDEZ-Teatro-Leido-4to-Grado-El-leon-y-el-raton- (1).pdfYuriFuentesMartinez2
 
El uso de las tic en la vida continúa , ambiente positivo y negativo.
El uso de las tic  en la vida continúa , ambiente positivo y negativo.El uso de las tic  en la vida continúa , ambiente positivo y negativo.
El uso de las tic en la vida continúa , ambiente positivo y negativo.ayalayenifer617
 
libro de Ciencias Sociales_6to grado.pdf
libro de Ciencias Sociales_6to grado.pdflibro de Ciencias Sociales_6to grado.pdf
libro de Ciencias Sociales_6to grado.pdfFAUSTODANILOCRUZCAST
 
Historia de la Medicina y bases para desarrollo de ella
Historia de la Medicina y bases para desarrollo de ellaHistoria de la Medicina y bases para desarrollo de ella
Historia de la Medicina y bases para desarrollo de ellajuancamilo3111391
 
MODELO CARACTERIZACION DE PROCESOS SENA.
MODELO CARACTERIZACION DE PROCESOS SENA.MODELO CARACTERIZACION DE PROCESOS SENA.
MODELO CARACTERIZACION DE PROCESOS SENA.imejia2411
 
Institucion educativa la esperanza sede la magdalena
Institucion educativa la esperanza sede la magdalenaInstitucion educativa la esperanza sede la magdalena
Institucion educativa la esperanza sede la magdalenadanielaerazok
 
3Mayo2023 Taller construcción de Prototipos.pptx
3Mayo2023 Taller construcción de Prototipos.pptx3Mayo2023 Taller construcción de Prototipos.pptx
3Mayo2023 Taller construcción de Prototipos.pptxadso2024sena
 
INSTITUCION EDUCATIVA LA ESPERANZA SEDE MAGDALENA
INSTITUCION EDUCATIVA LA ESPERANZA SEDE MAGDALENAINSTITUCION EDUCATIVA LA ESPERANZA SEDE MAGDALENA
INSTITUCION EDUCATIVA LA ESPERANZA SEDE MAGDALENAdanielaerazok
 
actividad.06_crea_un_recurso_multimedia_M01_S03_M01.ppsx
actividad.06_crea_un_recurso_multimedia_M01_S03_M01.ppsxactividad.06_crea_un_recurso_multimedia_M01_S03_M01.ppsx
actividad.06_crea_un_recurso_multimedia_M01_S03_M01.ppsx241532171
 
PRIMARIA 1. RESUELVE PROBLEMAS DE FORMA MOVIMIENTO Y LOCALIZACIÓN 2 (2).pptx
PRIMARIA 1. RESUELVE PROBLEMAS DE FORMA MOVIMIENTO Y LOCALIZACIÓN 2 (2).pptxPRIMARIA 1. RESUELVE PROBLEMAS DE FORMA MOVIMIENTO Y LOCALIZACIÓN 2 (2).pptx
PRIMARIA 1. RESUELVE PROBLEMAS DE FORMA MOVIMIENTO Y LOCALIZACIÓN 2 (2).pptxRodriguezLucero
 

Último (18)

Buscadores, SEM SEO: el desafío de ser visto en la web
Buscadores, SEM SEO: el desafío de ser visto en la webBuscadores, SEM SEO: el desafío de ser visto en la web
Buscadores, SEM SEO: el desafío de ser visto en la web
 
Producto académico 03 - Habilidades Comunicativas.pptx
Producto académico 03 - Habilidades Comunicativas.pptxProducto académico 03 - Habilidades Comunicativas.pptx
Producto académico 03 - Habilidades Comunicativas.pptx
 
locomotas v siclo.ppt de ingenieria de minas
locomotas v siclo.ppt de ingenieria de minaslocomotas v siclo.ppt de ingenieria de minas
locomotas v siclo.ppt de ingenieria de minas
 
institucion educativa la esperanza sede magdalena
institucion educativa la esperanza sede magdalenainstitucion educativa la esperanza sede magdalena
institucion educativa la esperanza sede magdalena
 
rodriguez_DelAngel_MariaGPE_M1S3AL6.pptx
rodriguez_DelAngel_MariaGPE_M1S3AL6.pptxrodriguez_DelAngel_MariaGPE_M1S3AL6.pptx
rodriguez_DelAngel_MariaGPE_M1S3AL6.pptx
 
COMPETENCIAS CIUDADANASadadadadadadada .pdf
COMPETENCIAS CIUDADANASadadadadadadada .pdfCOMPETENCIAS CIUDADANASadadadadadadada .pdf
COMPETENCIAS CIUDADANASadadadadadadada .pdf
 
CamposGarcia_MariaMagdalena_M1S3AI6.pptx
CamposGarcia_MariaMagdalena_M1S3AI6.pptxCamposGarcia_MariaMagdalena_M1S3AI6.pptx
CamposGarcia_MariaMagdalena_M1S3AI6.pptx
 
2º SOY LECTOR PART 2- MD EDUCATIVO (6).pdf
2º SOY LECTOR PART 2- MD  EDUCATIVO (6).pdf2º SOY LECTOR PART 2- MD  EDUCATIVO (6).pdf
2º SOY LECTOR PART 2- MD EDUCATIVO (6).pdf
 
FLUIDEZ-Teatro-Leido-4to-Grado-El-leon-y-el-raton- (1).pdf
FLUIDEZ-Teatro-Leido-4to-Grado-El-leon-y-el-raton- (1).pdfFLUIDEZ-Teatro-Leido-4to-Grado-El-leon-y-el-raton- (1).pdf
FLUIDEZ-Teatro-Leido-4to-Grado-El-leon-y-el-raton- (1).pdf
 
El uso de las tic en la vida continúa , ambiente positivo y negativo.
El uso de las tic  en la vida continúa , ambiente positivo y negativo.El uso de las tic  en la vida continúa , ambiente positivo y negativo.
El uso de las tic en la vida continúa , ambiente positivo y negativo.
 
libro de Ciencias Sociales_6to grado.pdf
libro de Ciencias Sociales_6to grado.pdflibro de Ciencias Sociales_6to grado.pdf
libro de Ciencias Sociales_6to grado.pdf
 
Historia de la Medicina y bases para desarrollo de ella
Historia de la Medicina y bases para desarrollo de ellaHistoria de la Medicina y bases para desarrollo de ella
Historia de la Medicina y bases para desarrollo de ella
 
MODELO CARACTERIZACION DE PROCESOS SENA.
MODELO CARACTERIZACION DE PROCESOS SENA.MODELO CARACTERIZACION DE PROCESOS SENA.
MODELO CARACTERIZACION DE PROCESOS SENA.
 
Institucion educativa la esperanza sede la magdalena
Institucion educativa la esperanza sede la magdalenaInstitucion educativa la esperanza sede la magdalena
Institucion educativa la esperanza sede la magdalena
 
3Mayo2023 Taller construcción de Prototipos.pptx
3Mayo2023 Taller construcción de Prototipos.pptx3Mayo2023 Taller construcción de Prototipos.pptx
3Mayo2023 Taller construcción de Prototipos.pptx
 
INSTITUCION EDUCATIVA LA ESPERANZA SEDE MAGDALENA
INSTITUCION EDUCATIVA LA ESPERANZA SEDE MAGDALENAINSTITUCION EDUCATIVA LA ESPERANZA SEDE MAGDALENA
INSTITUCION EDUCATIVA LA ESPERANZA SEDE MAGDALENA
 
actividad.06_crea_un_recurso_multimedia_M01_S03_M01.ppsx
actividad.06_crea_un_recurso_multimedia_M01_S03_M01.ppsxactividad.06_crea_un_recurso_multimedia_M01_S03_M01.ppsx
actividad.06_crea_un_recurso_multimedia_M01_S03_M01.ppsx
 
PRIMARIA 1. RESUELVE PROBLEMAS DE FORMA MOVIMIENTO Y LOCALIZACIÓN 2 (2).pptx
PRIMARIA 1. RESUELVE PROBLEMAS DE FORMA MOVIMIENTO Y LOCALIZACIÓN 2 (2).pptxPRIMARIA 1. RESUELVE PROBLEMAS DE FORMA MOVIMIENTO Y LOCALIZACIÓN 2 (2).pptx
PRIMARIA 1. RESUELVE PROBLEMAS DE FORMA MOVIMIENTO Y LOCALIZACIÓN 2 (2).pptx
 

INFOSAN Delphi 453-602

  • 1. Breve introducción a SQL 451 balanceada. Estas instrucciones pueden ejecutarse periódicamente, para garantizar índices con tiempo de acceso óptimo: alter index NombreEmpleado inactive; alter index NombreEmpleado active; Otra instrucción que puede mejorar el rendimiento del sistema y que está relacionada con los índices es set statistics. Este comando calcula las estadísticas de uso de las claves dentro de un índice. El valor obtenido, conocido como selectividad del índice, es utilizado por InterBase para elaborar el plan de implementación de consultas. Nor- malmente no hay que invocar a esta función explícitamente, pero si las estadísticas de uso del índice han variado mucho es quizás apropiado utilizar la instrucción: set statistics index NombreEmpleado; Por último, las instrucciones drop nos permiten borrar objetos definidos en la base de datos, tanto tablas como índices: drop table Tabla; drop index Indice; Creación de vistas Uno de los recursos más potentes de SQL, y de las bases de datos relacionales en general, es la posibilidad de definir tablas “virtuales” a partir de los datos almacena- dos en tablas “físicas”. Para definir una de estas tablas virtuales hay que definir qué operaciones relacionales se aplican a qué tablas bases. Este tipo de tabla recibe el nombre de vista. Como todavía no conocemos el lenguaje de consultas, que nos permite especificar las operaciones sobre tablas, postergaremos el estudio de las vistas para más adelante. Creación de usuarios InterBase soporta el concepto de usuarios a nivel del servidor, no de las bases de datos. Inicialmente, todos los servidores definen un único usuario especial: SYSDBA. Este usuario tiene los derechos necesarios para crear otros usuarios y asignarles con- traseñas. Toda esa información se almacena en la base de datos isc4.gdb, que se instala automáticamente con InterBase. La gestión de los nombres de usuarios y sus contra- señas se realiza mediante la utilidad Server Manager.
  • 2. 452 La Cara Oculta de Delphi Dentro de esta aplicación, hay que ejecutar el comando de menú Tasks|User security, para llegar al diálogo con el que podemos añadir, modificar o eliminar usuarios. La siguiente imagen muestra el diálogo de creación de nuevos usuarios: El nombre del usuario SYSDBA no puede cambiarse, pero es casi una obligación cambiar su contraseña en cuanto termina la instalación de InterBase. Sin embargo, podemos eliminar al administrador de la lista de usuarios del sistema. Si esto sucede, ya no será posible añadir o modificar nuevos usuarios en ese servidor. Así que tenga cuidado con lo que hace. El sistema de seguridad explicado tiene un par de aparentes "fallos". En primer lugar, cualquier usuario con acceso al disco duro puede sustituir el fichero isc4.gdb con uno suyo. Más grave aún: si copiamos el fichero gdb de la base de datos en un servidor en el cual conozcamos la contraseña del administrador, tendremos ac- ceso total a los datos, aunque este acceso nos hubiera estado vedado en el servi- dor original. En realidad, el fallo consiste en permitir que cualquier mequetrefe pueda acceder a nuestras apreciadas bases de datos. Así que, antes de planear la protección del sistema de gestión de base de datos (ya sea InterBase o cualquier otro), ocúpese de controlar el acceso al servidor de la gente indeseable.
  • 3. Breve introducción a SQL 453 Asignación de privilegios Una vez creados los objetos de la base de datos, es necesario asignar derechos sobre los mismos a los demás usuarios. Inicialmente, el dueño de una tabla es el usuario que la crea, y tiene todos los derechos de acceso sobre la tabla. Los derechos de ac- ceso indican qué operaciones pueden realizarse con la tabla. Naturalmente, los nom- bres de estos derechos o privilegios coinciden con los nombres de las operaciones correspondientes: Privilegio Operación select Lectura de datos update Modificación de datos existentes insert Creación de nuevos registros delete Eliminación de registros all Los cuatro privilegios anteriores execute Ejecución (para procedimientos almacenados) La instrucción que otorga derechos sobre una tabla es la siguiente: grant Privilegios on Tabla to Usuarios [with grant option] Por ejemplo: /* Derecho de sólo-lectura al público en general */ grant select on Articulos to public; /* Todos los derechos a un par de usuarios */ grant all privileges on Clientes to Spade, Marlowe; /* Monsieur Poirot sólo puede modificar salarios (¡qué peligro!) */ grant update(Salario) on Empleados to Poirot; /* Privilegio de inserción y borrado, con opción de concesión */ grant insert, delete on Empleados to Vance with grant option; He mostrado unas cuantas posibilidades de la instrucción. En primer lugar, podemos utilizar la palabra clave public cuando queremos conceder ciertos derechos a todos los usuarios. En caso contrario, podemos especificar uno o más usuarios como desti- natarios del privilegio. Luego, podemos ver que el privilegio update puede llevar entre paréntesis la lista de columnas que pueden ser modificadas. Por último, vemos que a Mr. Philo Vance no solamente le permiten contratar y despedir empleados, sino que también, gracias a la cláusula with grant option, puede conceder estos derechos a otros usuarios, aún no siendo el creador de la tabla. Esta opción debe utilizarse con cuidado, pues puede provocar una propagación descontrolada de pri- vilegios entre usuarios indeseables. ¿Y qué pasa si otorgamos privilegios y luego nos arrepentimos? No hay problema, pues para esto tenemos la instrucción revoke:
  • 4. 454 La Cara Oculta de Delphi revoke [grant option for] Privilegios on Tabla from Usuarios Hay que tener cuidado con los privilegios asignados al público. La siguiente instruc- ción no afecta a los privilegios de Sam Spade sobre la tabla de artículos, porque antes se le ha concedido al público en general el derecho de lectura sobre la misma: /* Spade se ríe de este ridículo intento */ revoke all on Articulos from Spade; Existen variantes de las instrucciones grant y revoke pensadas para asignar y retirar privilegios sobre tablas a procedimientos almacenados, y para asignar y retirar dere- chos de ejecución de procedimientos a usuarios. Estas instrucciones se estudiarán en el momento adecuado. Roles Los roles son una especificación del SQL-3 que InterBase 5 ha implementado. Si los usuarios se almacenan y administran a nivel de servidor, los roles, en cambio, se defi- nen a nivel de cada base de datos. De este modo, podemos trasladar con más facili- dad una base de datos desarrollada en determinado servidor, con sus usuarios parti- culares, a otro servidor, en el cual existe históricamente otro conjunto de usuarios. Primero necesitamos crear los roles adecuados en la base de datos: create role Domador; create role Payaso; create role Mago; Ahora debemos asignar los permisos sobre tablas y otros objetos a los roles, en vez de a los usuarios directamente, como hacíamos antes. Observe que InterBase sigue permitiendo ambos tipos de permisos: grant all privileges on Animales to Domador, Mago; grant select on Animales to Payaso; Hasta aquí no hemos mencionado a los usuarios, por lo que los resultados de estas instrucciones son válidos de servidor a servidor. Finalmente, debemos asignar los usuarios en sus respectivos roles, y esta operación sí depende del conjunto de usua- rios de un servidor: grant Payaso to Bill, Steve, RonaldMcDonald; grant Domador to Ian with admin option;
  • 5. Breve introducción a SQL 455 La opción with admin option me permite asignar el rol de domador a otros usua- rios. De este modo, siempre habrá quien se ocupe de los animales cuando me au- sente del circo por vacaciones. Un ejemplo completo de script SQL Incluyo a continuación un ejemplo completo de script SQL con la definición de tablas e índices para una sencilla aplicación de entrada de pedidos. En un capítulo posterior, ampliaremos este script para incluir triggers, generadores y procedimientos almacena- dos que ayuden a expresar las reglas de empresa de la base de datos. create database "C:PedidosPedidos.GDB" user "SYSDBA" password "masterkey" page_size 2048; /* Creación de las tablas */ create table Clientes ( Codigo int not null, Nombre varchar(30) not null, Direccion1 varchar(30), Direccion2 varchar(30), Telefono varchar(15), UltimoPedido date default "Now", primary key (Codigo) ); create table Empleados ( Codigo int not null, Apellidos varchar(20) not null, Nombre varchar(15) not null, FechaContrato date default "Now", Salario int, NombreCompleto computed by (Nombre || " " || Apellidos), primary key (Codigo) ); create table Articulos ( Codigo int not null, Descripcion varchar(30) not null, Existencias int default 0, Pedidos int default 0, Costo int, PVP int, primary key (Codigo) ); create table Pedidos ( Numero int not null, RefCliente int not null,
  • 6. 456 La Cara Oculta de Delphi RefEmpleado int, FechaVenta date default "Now", Total int default 0, primary key (Numero), foreign key (RefCliente) references Clientes (Codigo) on delete no action on update cascade ); create table Detalles ( RefPedido int not null, NumLinea int not null, RefArticulo int not null, Cantidad int default 1 not null, Descuento int default 0 not null check (Descuento between 0 and 100), primary key (RefPedido, NumLinea), foreign key (RefPedido) references Pedidos (Numero), on delete cascade on update cascade foreign key (RefArticulo) references Articulos (Codigo) on delete no action on update cascade ); /* Indices secundarios */ create index NombreCliente on Clientes(Nombre); create index NombreEmpleado on Empleados(Apellidos, Nombre); create index Descripcion on Articulos(Descripcion); /***** FIN DEL SCRIPT *****/
  • 7. Capítulo 22 Consultas y modificaciones enConsultas y modificaciones en SQLSQL ESDE SU MISMO ORIGEN, la definición del modelo relacional de Codd in- cluía la necesidad de un lenguaje para realizar consultas ad-hoc. Debido a la forma particular de representación de datos utilizada por este modelo, el tener relaciones o tablas y no contar con un lenguaje de alto nivel para reintegrar los datos almacenados es más bien una maldición que una bendición. Es asombroso, por lo tanto, cuánto tiempo vivió el mundo de la programación sobre PCs sin poder contar con SQL o algún mecanismo similar. Aún hoy, cuando un programador de Clipper o de COBOL comienza a trabajar en Delphi, se sorprende de las posibilida- des que le abre el uso de un lenguaje de consultas integrado dentro de sus aplica- ciones. La instrucción select, del Lenguaje de Manipulación de Datos de SQL nos permite consultar la información almacenada en una base de datos relacional. La sintaxis y posibilidades de esta sola instrucción son tan amplias y complicadas como para me- recer un capítulo para ella solamente. En este mismo capítulo estudiaremos las posi- bilidades de las instrucciones update, insert y delete, que permiten la modificación del contenido de las tablas de una base de datos. Para los ejemplos de este capítulo utilizaré la base de datos mastsql.gdb que viene con los ejemplos de Delphi, en el subdirectorio demosdata, a partir del directorio de ins- talación de Delphi. Estas tablas también se encuentran en formato Paradox, en el mismo subdirectorio. Puede utilizar el programa Database Desktop para probar el uso de SQL sobre tablas Paradox y dBase. Sin embargo, trataré de no tocar las peculiari- dades del Motor de SQL Local ahora, dejando esto para el capítulo 26, que explica cómo utilizar SQL desde Delphi. La instrucción select: el lenguaje de consultas A grandes rasgos, la estructura de la instrucción select es la siguiente: D
  • 8. 458 La Cara Oculta de Delphi select [distinct] lista-de-expresiones from lista-de-tablas [where condición-de-selección] [group by lista-de-columnas] [having condición-de-selección-de-grupos] [order by lista-de-columnas] [union instrucción-de-selección] ¿Qué se supone que “hace” una instrucción select? Esta es la pregunta del millón: una instrucción select, en principio, no “hace” sino que “define”. La instrucción define un conjunto virtual de filas y columnas, o más claramente, define una tabla virtual. Qué se hace con esta “tabla virtual” es ya otra cosa, y depende de la aplica- ción que le estemos dando. Si estamos en un intérprete que funciona en modo texto, puede ser que la ejecución de un select se materialice mostrando en pantalla los re- sultados, página a página, o quizás en salvar el resultado en un fichero de texto. En Delphi, las instrucciones select se utilizan para “alimentar” un componente denomi- nado TQuery, al cual se le puede dar casi el mismo uso que a una tabla “real”, almace- nada físicamente. A pesar de la multitud de secciones de una selección completa, el formato básico de la misma es muy sencillo, y se reduce a las tres primeras secciones: select lista-de-expresiones from lista-de-tablas [where condición-de-selección] La cláusula from indica de dónde se extrae la información de la consulta, en la cláu- sula where opcional se dice qué filas deseamos en el resultado, y con select especifi- camos los campos o expresiones de estas filas que queremos dejar. Muchas veces se dice que la cláusula where limita la tabla “a lo largo”, pues elimina filas de la misma, mientras que la cláusula select es una selección “horizontal”.
  • 9. Consultas y modificaciones en SQL 459 La condición de selección La forma más simple de instrucción select es la que extrae el conjunto de filas de una sola tabla que satisfacen cierta condición. Por ejemplo: select * from Customer where State = "HI" Esta consulta simple debe devolver todos los datos de los clientes ubicados en Ha- wai. El asterisco que sigue a la cláusula select es una alternativa a listar todos los nombres de columna de la tabla que se encuentra en la cláusula from. En este caso hemos utilizado una simple igualdad. La condición de búsqueda de la cláusula where admite los seis operadores de comparación (=, <>, <, >, <=, >=) y la creación de condiciones compuestas mediante el uso de los operadores lógicos and, or y not. La prioridad entre estos tres es la misma que en Pascal. Sin embargo, no hace falta encerrar las comparaciones entre paréntesis, porque incluso not se evalúa después de cualquier comparación: select * from Customer where State = "HI" and LastInvoiceDate > "1/1/1993" Observe cómo la constante de fecha puede escribirse como si fuera una cadena de caracteres. Operadores de cadenas Además de las comparaciones usuales, necesitamos operaciones más sofisticadas para trabajar con las cadenas de caracteres. Uno de los operadores admitidos por SQL estándar es el operador like, que nos permite averiguar si una cadena satisface o no cierto patrón de caracteres. El segundo operando de este operador es el patrón, una cadena de caracteres, dentro de la cual podemos incluir los siguientes comodines: Carácter Significado % Cero o más caracteres arbitrarios. _ (subrayado) Un carácter cualquiera. No vaya a pensar que el comodín % funciona como el asterisco en los nombres de ficheros de MS-DOS; SQL es malo, pero no tanto. Después de colocar un asterisco en un nombre de fichero, MS-DOS ignora cualquier otro carácter que escribamos a continuación, mientras que like sí los tiene en cuenta. También es diferente el com-
  • 10. 460 La Cara Oculta de Delphi portamiento del subrayado con respecto al signo de interrogación de DOS: en el intérprete de comandos de este sistema operativo significa cero o un caracteres, mientras que en SQL significa exactamente un carácter. Expresión Cadena aceptada Cadena no aceptada Customer like '% Ocean' 'Pacific Ocean' 'Ocean Paradise' Fruta like 'Manzana_' 'Manzanas' 'Manzana' También es posible aplicar funciones para extraer o modificar información de una cadena de caracteres; el repertorio de funciones disponibles depende del sistema de bases de datos con el que se trabaje. Por ejemplo, el intérprete SQL para tablas loca- les de Delphi acepta las funciones upper, lower, trim y substring de SQL estándar. Esta última función tiene una sintaxis curiosa. Por ejemplo, para extraer las tres pri- meras letras de una cadena se utiliza la siguiente expresión: select substring(Nombre from 1 for 3) from Empleados Si estamos trabajando con InterBase, podemos aumentar el repertorio de funciones utilizando funciones definidas por el usuario. En el capítulo 36 mostraremos cómo. El valor nulo: enfrentándonos a lo desconocido La edad de una persona es un valor no negativo, casi siempre menor de 969 años, que es la edad a la que dicen que llegó Matusalén. Puede ser un entero igual a 1, 20, 40 ... o no conocerse. Se puede “resolver” este problema utilizando algún valor espe- cial para indicar el valor desconocido, digamos -1. Claro, el valor especial escogido no debe formar parte del dominio posible de valores. Por ejemplo, en el archivo de Ur- gencias de un hospital americano, John Doe es un posible valor para los pacientes no identificados. ¿Y qué pasa si no podemos prescindir de valor alguno dentro del rango? Porque John Doe es un nombre raro, pero posible. ¿Y qué pasaría si se intentan operaciones con valores desconocidos? Por ejemplo, para representar un envío cuyo peso se desco- noce se utiliza el valor -1, un peso claramente imposible excepto para entes como Kate Moss. Luego alguien pregunta a la base de datos cuál es el peso total de los envíos de un período dado. Si en ese período se realizaron dos envíos, uno de 25 kilogramos y otro de peso desconocido, la respuesta errónea será un peso total de 24 kilogramos. Es evidente que la respuesta debería ser, simplemente, “peso total des- conocido”. La solución de SQL es introducir un nuevo valor, null, que pertenece a cualquier dominio de datos, para representar la información desconocida. La regla principal
  • 11. Consultas y modificaciones en SQL 461 que hay que conocer cuando se trata con valores nulos es que cualquier expresión, aparte de las expresiones lógicas, en la que uno de sus operandos tenga el valor nulo se evalúa automáticamente a nulo. Esto es: nulo más veinticinco vale nulo, ¿de acuerdo? Cuando se trata de evaluar expresiones lógicas en las cuales uno de los operandos puede ser nulo las cosas se complican un poco, pues hay que utilizar una lógica de tres valores. De todos modos, las reglas son intuitivas. Una proposición falsa en con- junción con cualquier otra da lugar a una proposición falsa; una proposición verda- dera en disyunción con cualquier otra da lugar a una proposición verdadera. La si- guiente tabla resume las reglas del uso del valor nulo en expresiones lógicas: AND false null true OR false null true false false false false false false null true null false null null null null null true true false null true true true true true Por último, si lo que desea es saber si el valor de un campo es nulo o no, debe utilizar el operador is null: select * from Events where Event_Description is null La negación de este operador es el operador is not null, con la negación en medio. Esta sintaxis no es la usual en lenguajes de programación, pero se suponía que SQL debía parecerse lo más posible al idioma inglés. Eliminación de duplicados Normalmente, no solemos guardar filas duplicadas en una tabla, por razones obvias. Pero es bastante frecuente que el resultado de una consulta contenga filas duplicadas. El operador distinct se puede utilizar, en la cláusula select, para corregir esta situa- ción. Por ejemplo, si queremos conocer en qué ciudades residen nuestros clientes podemos preguntar lo siguiente: select City from Customer Pero en este caso obtenemos 55 ciudades, algunas de ellas duplicadas. Para obtener las 47 diferentes ciudades de la base de datos tecleamos: select distinct City from Customer
  • 12. 462 La Cara Oculta de Delphi Productos cartesianos y encuentros Como para casi todas las cosas, la gran virtud del modelo relacional es, a la vez, su mayor debilidad. Me refiero a que cualquier modelo del “mundo real” puede repre- sentarse atomizándolo en relaciones: objetos matemáticos simples y predecibles, de fácil implementación en un ordenador (¡aquellos ficheros dbfs…!). Para reconstruir el modelo original, en cambio, necesitamos una operación conocida como “encuentro natural” (natural join). Comencemos con algo más sencillo: con los productos cartesianos. Un producto cartesiano es una operación matemática entre conjuntos, la cual produce todas las parejas posibles de elementos, perteneciendo el primer elemento de la pareja al pri- mer conjunto, y el segundo elemento de la pareja al segundo conjunto. Esta es la operación habitual que efectuamos mentalmente cuando nos ofrecen el menú en un restaurante. Los dos conjuntos son el de los primeros platos y el de los segundos platos. Desde la ventana de la habitación donde escribo puedo ver el menú del me- són de la esquina: Primer plato Segundo plato Macarrones a la boloñesa Escalope a la milanesa Judías verdes con jamón Pollo a la parrilla Crema de champiñones Chuletas de cordero Si PrimerPlato y SegundoPlato fuesen tablas de una base de datos, la instrucción select * from PrimerPlato, SegundoPlato devolvería el siguiente conjunto de filas: Primer plato Segundo plato Macarrones a la boloñesa Escalope a la milanesa Macarrones a la boloñesa Pollo a la parrilla Macarrones a la boloñesa Chuletas de cordero Judías verdes con jamón Escalope a la milanesa Judías verdes con jamón Pollo a la parrilla Judías verdes con jamón Chuletas de cordero Crema de champiñones Escalope a la milanesa Crema de champiñones Pollo a la parrilla Crema de champiñones Chuletas de cordero Es fácil ver que, incluso con tablas pequeñas, el tamaño del resultado de un producto cartesiano es enorme. Si a este ejemplo “real” le añadimos el hecho también “real”
  • 13. Consultas y modificaciones en SQL 463 de que el mismo mesón ofrece al menos tres tipos diferentes de postres, elegir nues- tro menú significa seleccionar entre 27 posibilidades distintas. Por eso siempre pido un café solo al terminar con el segundo plato. Claro está, no todas las combinaciones de platos hacen una buena comida. Pero para eso tenemos la cláusula where: para eliminar aquellas combinaciones que no satisfa- cen ciertos criterios. ¿Volvemos al mundo de las facturas y órdenes de compra? En la base de datos dbdemos, la información sobre pedidos está en la tabla orders, mientras que la información sobre clientes se encuentra en customer. Queremos obtener la lista de clientes y sus totales por pedidos. Estupendo, pero los totales de pedidos están en la tabla orders, en el campo ItemsTotal, y en esta tabla sólo tenemos el código del cliente, en el campo CustNo. Los nombres de clientes se encuentran en el campo Company de la tabla customer, donde además volvemos a encontrar el código de cliente, CustNo. Así que partimos de un producto cartesiano entre las dos tablas, en el cual mostramos los nombres de clientes y los totales de pedidos: select Company, ItemsTotal from Customer, Orders Como tenemos unos 55 clientes y 205 pedidos, esta inocente consulta genera unas 11275 filas. La última vez que hice algo así fue siendo estudiante, en el centro de cálculos de mi universidad, para demostrarle a una profesora de Filosofía lo ocupado que estaba en ese momento. En realidad, de esas 11275 filas nos sobran unas 11070, pues solamente son válidas las combinaciones en las que coinciden los códigos de cliente. La instrucción que necesitamos es: select Company, ItemsTotal from Customer, Orders where Customer.CustNo = Orders.CustNo Esto es un encuentro natural, un producto cartesiano restringido mediante la igualdad de los valores de dos columnas de las tablas básicas. El ejemplo anterior ilustra también un punto importante: cuando queremos utilizar en la instrucción el nombre de los campos ItemsTotal y Company los escribimos tal y como son. Sin embargo, cuando utilizamos CustNo hay que aclarar a qué tabla origi- nal nos estamos refiriendo. Esta técnica se conoce como calificación de campos. ¿Un ejemplo más complejo? Suponga que desea añadir el nombre del empleado que recibió el pedido. La tabla orders tiene un campo EmpNo para el código del empleado, mientras que la información sobre empleados se encuentra en la tabla employee. La instrucción necesaria es una simple ampliación de la anterior:
  • 14. 464 La Cara Oculta de Delphi select Company, ItemsTotal, FirstName || " " || LastName from Customer, Orders, Employee where Customer.CustNo = Orders.CustNo and Orders.EmpNo = Employee.EmpNo Con 42 empleados en la base de datos de ejemplo y sin las restricciones de la cláusula where, hubiéramos obtenido un resultado de 473550 filas. Ordenando los resultados Una de las garantías de SQL es que podemos contar con que el compilador SQL genere automáticamente, o casi, el mejor código posible para evaluar las instruccio- nes. Esto también significa que, en el caso general, no podemos predecir con com- pleta seguridad cuál será la estrategia utilizada para esta evaluación. Por ejemplo, en la instrucción anterior no sabemos si el compilador va a recorrer cada fila de la tabla de clientes para encontrar las filas correspondientes de pedidos o empleados, o si resul- tará más ventajoso recorrer las filas de pedidos para recuperar los nombres de clien- tes y empleados. Esto quiere decir, en particular, que no sabemos en qué orden se nos van a presentar las filas. En mi ordenador, utilizando Database Desktop sobre las tablas originales en formato Paradox, parece ser que se recorren primeramente las filas de la tabla de empleados. ¿Qué hacemos si el resultado debe ordenarse por el nombre de compañía? Para esto contamos con la cláusula order by, que se sitúa siempre al final de la consulta. En este caso, ordenamos por nombre de compañía el resultado con la instrucción: select Company, ItemsTotal, FirstName || " " || LastName from Customer, Orders, Employee where Customer.CustNo = Orders.CustNo and Orders.EmpNo = Employee.EmpNo order by Company No se puede ordenar por una fila que no existe en el resultado de la instrucción. Si quisiéramos que los pedidos de cada compañía se ordenaran, a su vez, por la fecha de venta, habría que añadir el campo SalesDate al resultado y modificar la cláusula de ordenación del siguiente modo: select Company, ItemsTotal, SalesDate, FirstName || " " || LastName from Customer, Orders, Employee where Customer.CustNo = Orders.CustNo and Orders.EmpNo = Employee.EmpNo order by Company, SalesDate desc Con la opción desc obtenemos los registros por orden descendente de fechas: pri- mero los más recientes. Existe una opción asc, para cuando queremos enfatizar el
  • 15. Consultas y modificaciones en SQL 465 sentido ascendente de una ordenación. Generalmente no se usa, pues es lo que asume el compilador. Otra posibilidad de la cláusula order by es utilizar números en vez de nombres de columnas. Esto es necesario si se utilizan expresiones en la cláusula select y se quiere ordenar por dicha expresión. Por ejemplo: select OrderNo, SalesDate, ItemsTotal - AmountPaid from Orders order by 3 desc No se debe abusar de los números de columnas, pues esta técnica puede desaparecer en SQL-3 y hace menos legible la consulta. Una forma alternativa de ordenar por columnas calculadas es utilizar sinónimos para las columnas: select OrderNo, SalesDate, ItemsTotal – AmountPaid as Diferencia from Orders order by Diferencia desc El uso de grupos Ahora queremos sumar todos los totales de los pedidos para cada compañía, y orde- nar el resultado por este total de forma descendente, para obtener una especie de ranking de las compañías según su volumen de compras. Esto es, hay que agrupar todas las filas de cada compañía y mostrar la suma de cierta columna dentro de cada uno de esos grupos. Para producir grupos de filas en SQL se utiliza la cláusula group by. Cuando esta cláusula está presente en una consulta, va situada inmediatamente después de la cláu- sula where, o de la cláusula from si no se han efectuado restricciones. En nuestro caso, la instrucción con la cláusula de agrupamiento debe ser la siguiente: select Company, sum(ItemsTotal) from Customer, Orders where Customer.CustNo = Orders.OrderNo group by Company order by 2 desc Observe la forma en la cual se le ha aplicado la función sum a la columna ItemsTotal. Aunque pueda parecer engorroso el diseño de una consulta con grupos, hay una regla muy fácil que simplifica los razonamientos: en la cláusula select solamente pueden aparecer columnas especificadas en la cláusula group by, o funciones esta- dísticas aplicadas a cualquier otra expresión. Company, en este ejemplo, puede apare- cer directamente porque es la columna por la cual se está agrupando. Si quisiéramos obtener además el nombre de la persona de contacto en la empresa, el campo Contact de la tabla customer, habría que agregar esta columna a la cláusula de agrupación:
  • 16. 466 La Cara Oculta de Delphi select Company, Contact, sum(ItemsTotal) from Customer, Orders where Customer.CustNo = Orders.OrderNo group by Company, Contact order by 2 desc En realidad, la adición de Contact es redundante, pues Company es única dentro de la tabla customer, pero eso lo sabemos nosotros, no el compilador de SQL. Funciones de conjuntos Existen cinco funciones de conjuntos en SQL, conocidas en inglés como aggregate functions. Estas funciones son: Función Significado count Cantidad de valores no nulos en el grupo min El valor mínimo de la columna dentro del grupo max El valor máximo de la columna dentro del grupo sum La suma de los valores de la columna dentro del grupo avg El promedio de la columna dentro del grupo Por supuesto, no toda función es aplicable a cualquier tipo de columna. Las sumas, por ejemplo, solamente valen para columnas de tipo numérico. Hay otros detalles curiosos relacionados con estas funciones, como que los valores nulos son ignorados por todas, o que se puede utilizar un asterisco como parámetro de la función count. En este último caso, se calcula el número de filas del grupo. Así que no apueste a que la siguiente consulta dé siempre dos valores idénticos, si es posible que la columna involucrada contenga valores nulos: select avg(Columna), sum(Columna)/count(*) from Tabla En el ejemplo se muestra la posibilidad de utilizar funciones de conjuntos sin utilizar grupos. En esta situación se considera que toda la tabla constituye un único grupo. Es también posible utilizar el operador distinct como prefijo del argumento de una de estas funciones: select Company, count(distinct EmpNo) from Customer, Orders where Customer.CustNo = Orders.CustNo La consulta anterior muestra el número de empleados que han atendido los pedidos de cada compañía.
  • 17. Consultas y modificaciones en SQL 467 La cláusula having Según lo que hemos explicado hasta el momento, en una instrucción select se evalúa primeramente la cláusula from, que indica qué tablas participan en la consulta, luego se eliminan las filas que no satisfacen la cláusula where y, si hay un group by por medio, se agrupan las filas resultantes. Hay una posibilidad adicional: después de agrupar se pueden descartar filas consolidadas de acuerdo a otra condición, esta vez expresada en una cláusula having. En la parte having de la consulta solamente pue- den aparecer columnas agrupadas o funciones estadísticas aplicadas al resto de las columnas. Por ejemplo: select Company from Customer, Orders where Customer.CustNo = Orders.CustNo group by Company having count(*) > 1 La consulta anterior muestra las compañías que han realizado más de un pedido. Es importante darse cuenta de que no podemos modificar esta consulta para que nos muestre las compañías que no han realizado todavía pedidos. Una regla importante de optimización: si en la cláusula having existen condicio- nes que implican solamente a las columnas mencionadas en la cláusula group by, estas condiciones deben moverse a la cláusula where. Por ejemplo, si queremos eliminar de la consulta utilizada como ejemplo a las compañías cuyo nombre termina con las siglas 'S.L.' debemos hacerlo en where, no en group by. ¿Para qué esperar a agrupar para eliminar filas que podían haberse descartado antes? Aunque muchos compiladores realizan esta optimización automáticamente, es mejor no fiarse. El uso de sinónimos para tablas Es posible utilizar dos o más veces una misma tabla en una misma consulta. Si ha- cemos esto tendremos que utilizar sinónimos para distinguir entre los distintos usos de la tabla en cuestión. Esto será necesario al calificar los campos que utilicemos. Un sinónimo es simplemente un nombre que colocamos a continuación del nombre de una tabla en la cláusula from, y que en adelante se usa como sustituto del nombre de la tabla. Por ejemplo, si quisiéramos averiguar si hemos introducido por error dos veces a la misma compañía en la tabla de clientes, pudiéramos utilizar la instrucción:
  • 18. 468 La Cara Oculta de Delphi select distinct C1.Company from Customer C1, Customer C2 where C1.CustNo < C2.CustNo and C1.Company = C2.Company En esta consulta C1 y C2 se utilizan como sinónimos de la primera y segunda apari- ción, respectivamente, de la tabla customer. La lógica de la consulta es sencilla. Busca- mos todos los pares que comparten el mismo nombre de compañía y eliminamos aquellos que tienen el mismo código de compañía. Pero en vez de utilizar una desi- gualdad en la comparación de códigos, utilizamos el operador “menor que”, para eliminar la aparición de pares dobles en el resultado previo a la aplicación del opera- dor distinct. Estamos aprovechando, por supuesto, la unicidad del campo CustNo. La siguiente consulta muestra otro caso en que una tabla aparece dos veces en una cláusula from. En esta ocasión, la base de datos es iblocal, el ejemplo InterBase que viene con Delphi. Queremos mostrar los empleados junto con los jefes de sus de- partamentos: select e2.full_name, e1.full_name from employee e1, department d, employee e2 where d.dept_no = e1.dept_no and d.mngr_no = e2.emp_no and e1.emp_no <> e2.emp_no order by 1, 2 Aquellos lectores que hayan trabajado en algún momento con lenguajes xBase reco- nocerán en los sinónimos SQL un mecanismo similar al de los “alias” de xBase. Del- phi utiliza, además, los sinónimos de tablas en el intérprete de SQL local cuando el nombre de la tabla contiene espacios en blanco o el nombre de un directorio. Subconsultas: selección única Si nos piden el total vendido a una compañía determinada, digamos a Ocean Para- dise, podemos resolverlo ejecutando dos instrucciones diferentes. En la primera obtenemos el código de Ocean Paradise: select Customer.CustNo from Customer where Customer.Company = "Ocean Paradise" El código buscado es, supongamos, 1510. Con este valor en la mano, ejecutamos la siguiente instrucción: select sum(Orders.ItemsTotal) from Orders where Orders.CustNo = 1510
  • 19. Consultas y modificaciones en SQL 469 Incómodo, ¿no es cierto? La alternativa es utilizar la primera instrucción como una expresión dentro de la segunda, del siguiente modo: select sum(Orders.ItemsTotal) from Orders where Orders.CustNo = ( select Customer.CustNo from Customer where Customer.Company = "Ocean Paradise") Para que la subconsulta anterior pueda funcionar correctamente, estamos asumiendo que el conjunto de datos retornado por la subconsulta produce una sola fila. Esto es, realmente, una apuesta arriesgada. Puede fallar por dos motivos diferentes: puede que la subconsulta no devuelva ningún valor o puede que devuelva más de uno. Si no se devuelve ningún valor, se considera que la subconsulta devuelve el valor null. Si devuelve dos o más valores, el intérprete produce un error. A este tipo de subconsulta que debe retornar un solo valor se le denomina selección única, en inglés, singleton select. Las selecciones únicas también pueden utilizarse con otros operadores de comparación, además de la igualdad. Así por ejemplo, la si- guiente consulta retorna información sobre los empleados contratados después de Pedro Pérez: select * from Employee E1 where E1.HireDate > ( select E2.HireDate from Employee E2 where E2.FirstName = "Pedro" and E2.LastName = "Pérez") Si está preguntándose acerca de la posibilidad de cambiar el orden de los operandos, ni lo sueñe. La sintaxis de SQL es muy rígida, y no permite este tipo de virtuosismos. Subconsultas: los operadores in y exists En el ejemplo anterior garantizábamos la singularidad de la subconsulta gracias a la cláusula where, que especificaba una búsqueda sobre una clave única. Sin embargo, también se pueden aprovechar las situaciones en que una subconsulta devuelve un conjunto de valores. En este caso, el operador a utilizar cambia. Por ejemplo, si que- remos los pedidos correspondientes a las compañías en cuyo nombre figura la pala- bra Ocean, podemos utilizar la instrucción: select * from Orders where Orders.CustNo in (
  • 20. 470 La Cara Oculta de Delphi select Customer.CustNo from Customer where upper(Customer.Company) like "%OCEAN%") El nuevo operador es el operador in, y la expresión es verdadera si el operando iz- quierdo se encuentra en la lista de valores retornada por la subconsulta. Esta consulta puede descomponerse en dos fases. Durante la primera fase se evalúa el segundo select: select Customer.CustNo from Customer where upper(Customer.Company) like "%OCEAN%" El resultado de esta consulta consiste en una serie de códigos: aquellos que corres- ponden a las compañías con Ocean en su nombre. Supongamos que estos códigos sean 1510 (Ocean Paradise) y 5515 (Ocean Adventures). Entonces puede ejecutarse la segunda fase de la consulta, con la siguiente instrucción, equivalente a la original: select * from Orders where Orders.OrderNo in (1510, 5515) Este otro ejemplo utiliza la negación del operador in. Si queremos las compañías que no nos han comprado nada, hay que utilizar la siguiente consulta: select * from Customer where Customer.CustNo not in ( select Orders.CustNo from Orders) Otra forma de plantearse las consultas anteriores es utilizando el operador exists. Este operador se aplica a una subconsulta y devuelve verdadero en cuanto localiza una fila que satisface las condiciones de la instrucción select. El primer ejemplo de este epígrafe puede escribirse de este modo: select * from Orders where exists ( select * from Customer where upper(Customer.Company) like "%OCEAN%" and Orders.CustNo = Customer.CustNo) Observe el asterisco en la cláusula select de la subconsulta. Como lo que nos inte- resa es saber si existen filas que satisfacen la expresión, nos da lo mismo qué valor se está retornando. El segundo ejemplo del operador in se convierte en lo siguiente al utilizar exists:
  • 21. Consultas y modificaciones en SQL 471 select * from Customer where not exists ( select * from Orders where Orders.CustNo = Customer.CustNo) Subconsultas correlacionadas Preste atención al siguiente detalle: la última subconsulta del epígrafe anterior tiene una referencia a una columna perteneciente a la tabla definida en la cláusula from más externa. Esto quiere decir que no podemos explicar el funcionamiento de la instrucción dividiéndola en dos fases, como con las selecciones únicas: la ejecución de la subconsulta y la simplificación de la instrucción externa. En este caso, para cada fila retornada por la cláusula from externa, la tabla customer, hay que volver a evaluar la subconsulta teniendo en cuenta los valores actuales: los de la columna CustNo de la tabla de clientes. A este tipo de subconsultas se les denomina, en el mundo de la programación SQL, subconsultas correlacionadas. Si hay que mostrar los clientes que han pagado algún pedido contra reembolso (en inglés, COD, o cash on delivery), podemos realizar la siguiente consulta con una subse- lección correlacionada: select * from Customer where 'COD' in ( select distinct PaymentMethod from Orders where Orders.CustNo = Customer.CustNo) En esta instrucción, para cada cliente se evalúan los pedidos realizados por el mismo, y se muestra el cliente solamente si dentro del conjunto de métodos de pago está la cadena 'COD'. El operador distinct de la subconsulta es redundante, pero nos ayuda a entenderla mejor. Otra subconsulta correlacionada: queremos los clientes que no han comprado nada aún. Ya vimos como hacerlo utilizando el operador not in ó el operador not exists. Una alternativa es la siguiente: select * from Customer where 0 = ( select count(*) from Orders where Orders.CustNo = Customer.CustNo) Sin embargo, utilizando SQL Local esta consulta es más lenta que las otras dos solu- ciones. La mayor importancia del concepto de subconsulta correlacionada tiene que
  • 22. 472 La Cara Oculta de Delphi ver con el hecho de que algunos sistemas de bases de datos limitan las actualizacio- nes a vistas definidas con instrucciones que contienen subconsultas de este tipo. Equivalencias de subconsultas En realidad, las subconsultas son un método para aumentar la expresividad del len- guaje SQL, pero no son imprescindibles. Con esto quiero decir que muchas consultas se formulan de modo más natural si se utilizan subconsultas, pero que existen otras consultas equivalentes que no hacen uso de este recurso. La importancia de esta equi- valencia reside en que el intérprete de SQL Local de Delphi 1 no permitía subconsul- tas. Los desarrolladores que estén programando aplicaciones con Delphi 1 para Windows 3.1 y Paradox o dBase deben tener en cuenta esta restricción. Un problema relacionado es que, aunque un buen compilador de SQL debe poder identificar las equivalencias y evaluar la consulta de la forma más eficiente, en la práctica el utilizar ciertas construcciones sintácticas puede dar mejor resultado que utilizar otras equivalentes, de acuerdo al compilador que empleemos. Veamos algunas equivalencias. Teníamos una consulta, en el epígrafe sobre selec- ciones únicas, que mostraba el total de compras de Ocean Paradise: select sum(Orders.ItemsTotal) from Orders where Orders.CustNo = ( select Customer.CustNo from Customer where Customer.Company = "Ocean Paradise") Esta consulta es equivalente a la siguiente: select sum(ItemsTotal) from Customer, Orders where Customer.Company = "Ocean Paradise" and Customer.CustNo = Orders.OrderNo Aquella otra consulta que mostraba los pedidos de las compañías en cuyo nombre figuraba la palabra “Ocean”: select * from Orders where Orders.CustNo in ( select Customer.CustNo from Customer where upper(Customer.Company) like "%OCEAN%") es equivalente a esta otra:
  • 23. Consultas y modificaciones en SQL 473 select * from Customer, Orders where upper(Customer.Company) like "%OCEAN%" and Customer.CustNo = Orders.CustNo Para esta consulta en particular, ya habíamos visto una consulta equivalente que hacía uso del operador exists; en este caso, es realmente más difícil de entender la consulta con exists que su equivalente sin subconsultas. La consulta correlacionada que buscaba los clientes que en algún pedido habían pa- gado contra reembolso: select * from Customer where 'COD' in ( select distinct PaymentMethod from Orders where Orders.CustNo = Customer.CustNo) puede escribirse, en primer lugar, mediante una subconsulta no correlacionada: select * from Customer where Customer.CustNo in ( select Orders.CustNo from Orders where PaymentMethod = 'COD') pero también se puede expresar en forma “plana”: select distinct Customer.CustNo, Customer.Company from Customer, Orders where Customer.CustNo = Orders.CustNo and Orders.PaymentMethod = 'COD' Por el contrario, las consultas que utilizan el operador not in y, por lo tanto sus equi- valentes con not exists, no tienen equivalente plano, con lo que sabemos hasta el momento. Para poder aplanarlas hay que utilizar encuentros externos. Encuentros externos El problema de los encuentros naturales es que cuando relacionamos dos tablas, digamos customer y orders, solamente mostramos las filas que tienen una columna en común. No hay forma de mostrar los clientes que no tienen un pedido con su código ... y solamente esos. En realidad, se puede utilizar la operación de diferencia entre conjuntos para lograr este objetivo, como veremos en breve. Se pueden evaluar todos los clientes, y a ese conjunto restarle el de los clientes que sí tienen pedidos. Pero esta
  • 24. 474 La Cara Oculta de Delphi operación, por lo general, se implementa de forma menos eficiente que la alternativa que mostraremos a continuación. ¿Cómo funciona un encuentro natural? Una posible implementación consistiría en recorrer mediante un bucle la primera tabla, supongamos que sea customer. Para cada fila de esa tabla tomaríamos su columna CustNo y buscaríamos, posiblemente con un índice, las filas correspondientes de orders que contengan ese mismo valor en la co- lumna del mismo nombre. ¿Qué pasa si no hay ninguna fila en orders que satisfaga esta condición? Si se trata de un encuentro natural, común y corriente, no se mues- tran los datos de ese cliente. Pero si se trata de la extensión de esta operación, cono- cida como encuentro externo (outer join), se muestra aunque sea una vez la fila corres- pondiente al cliente. Un encuentro muestra, sin embargo, pares de filas, ¿qué valores podemos esperar en la fila de pedidos? En ese caso, se considera que todas las co- lumnas de la tabla de pedidos tienen valores nulos. Si tuviéramos estas dos tablas: Customers Orders CustNo Company OrderNo CustNo 1510 Ocean Paradise 1025 1510 1666 Marteens’ Diving Academy 1139 1510 el resultado de un encuentro externo como el que hemos descrito, de acuerdo a la columna CustNo, sería el siguiente: Customer.CustNo Company OrderNo Orders.CustNo 1510 Ocean Paradise 1025 1510 1510 Ocean Paradise 1139 1510 1666 Marteens’ Diving Academy null null Con este resultado en la mano, es fácil descubrir quién es el tacaño que no nos ha pedido nada todavía, dejando solamente las filas que tengan valores nulos para al- guna de las columnas de la segunda tabla. Este encuentro externo que hemos explicado es, en realidad, un encuentro externo por la izquierda, pues la primera tabla tendrá todas sus filas en el resultado final, aun- que no exista fila correspondiente en la segunda. Naturalmente, también existe un encuentro externo por la derecha y un encuentro externo simétrico. El problema de este tipo de operaciones es que su inclusión en SQL fue bastante tardía. Esto trajo como consecuencia que distintos fabricantes utilizaran sintaxis propias para la operación. En el estándar ANSI para SQL del año 87 no hay referen- cias a esta instrucción, pero sí la hay en el estándar del 92. Utilizando esta sintaxis, que es la permitida por el SQL local de Delphi, la consulta que queremos se escribe del siguiente modo:
  • 25. Consultas y modificaciones en SQL 475 select * from Customer left outer join Orders on Customer.CustNo = Orders.CustNo where Orders.OrderNo is null Observe que se ha extendido la sintaxis de la cláusula from. El encuentro externo por la izquierda puede escribirse en Oracle de esta forma alternativa: select * from Customer, Orders where Customer.CustNo (+) = Orders.CustNo and Orders.OrderNo is null Las instrucciones de actualización Son tres las instrucciones de actualización de datos reconocidas en SQL: delete, update e insert. Estas instrucciones tienen una sintaxis relativamente simple y están limitadas, en el sentido de que solamente cambian datos en una tabla a la vez. La más sencilla de las tres es delete, la instrucción de borrado: delete from Tabla where Condición La instrucción elimina de la tabla indicada todas las filas que se ajustan a cierta con- dición. En dependencia del sistema de bases de datos de que se trate, la condición de la cláusula where debe cumplir ciertas restricciones. Por ejemplo, aunque InterBase admite la presencia de subconsultas en la condición de selección, otros sistemas no lo permiten. La segunda instrucción, update, nos sirve para modificar las filas de una tabla que satisfacen cierta condición: update Tabla set Columna = Valor [, Columna = Valor ...] where Condición Al igual que sucede con la instrucción delete, las posibilidades de esta instrucción dependen del sistema que la implementa. InterBase, en particular, permite actualizar columnas con valores extraídos de otras tablas; para esto utilizamos subconsultas en la cláusula set: update Customer set LastInvoiceDate =
  • 26. 476 La Cara Oculta de Delphi (select max(SaleDate) from Orders where Orders.CustNo = Customer.CustNo) Por último, tenemos la instrucción insert, de la cual tenemos dos variantes. La pri- mera permite insertar un solo registro, con valores constantes: insert into Tabla [ (Columnas) ] values (Valores) La lista de columnas es opcional; si se omite, se asume que la instrucción utiliza todas las columnas en orden de definición. En cualquier caso, el número de columnas em- pleado debe coincidir con el número de valores. El objetivo de todo esto es que si no se indica un valor para alguna columna, el valor de la columna en el nuevo registro se inicializa con el valor definido por omisión; recuerde que si en la definición de la tabla no se ha indicado nada, el valor por omisión es null: insert into Employee(EmpNo, LastName, FirstName) values (666, "Bonaparte", "Napoleón") /* El resto de las columnas, incluida la del salario, son nulas */ La segunda variante de insert permite utilizar como fuente de valores una expresión select: insert into Tabla [ (Columnas) ] InstrucciónSelect Esta segunda variante no estuvo disponible en el intérprete de SQL local hasta la versión 4 del BDE. Se utiliza con frecuencia para copiar datos de una tabla a otra: insert into Resumen(Empresa, TotalVentas) select Company, sum(ItemsTotal) from Customer, Orders where Customer.CustNo = Orders.CustNo group by Company Vistas Se puede aprovechar una instrucción select de forma tal que el conjunto de datos que define se pueda utilizar “casi” como una tabla real. Para esto, debemos definir una vista. La instrucción necesaria tiene la siguiente sintaxis: create view NombreVista[Columnas] as InstrucciónSelect [with check option] Por ejemplo, podemos crear una vista para trabajar con los clientes que viven en Hawai:
  • 27. Consultas y modificaciones en SQL 477 create view Hawaianos as select * from Customer where State = "HI" A partir del momento en que se ejecuta esta instrucción, el usuario de la base de datos se encuentra con una nueva tabla, Hawaianos, con la cual puede realizar las mismas operaciones que realizaba con las tablas “físicas”. Puede utilizar la nueva tabla en una instrucción select: select * from Hawaianos where LastInvoiceDate >= (select avg(LastInvoiceDate) from Customer) En esta vista en particular, puede también eliminar insertar o actualizar registros: delete from Hawaianos where LastInvoiceDate is null; insert into Hawaianos(CustNo, Company, State) values (8888, "Ian Marteens' Diving Academy", "HI") No todas las vistas permiten operaciones de actualización. Las condiciones que de- ben cumplir para ser actualizables, además, dependen del sistema de bases de datos en que se definan. Los sistemas más restrictivos exigen que la instrucción select tenga una sola tabla en la cláusula from, que no contenga consultas anidadas y que no haga uso de operadores tales como group by, distinct, etc. Cuando una vista permite actualizaciones se nos plantea el problema de qué hacer si se inserta un registro que no pertenece lógicamente a la vista. Por ejemplo, ¿pudié- ramos insertar dentro de la vista Hawaianos una empresa con sede social en la ciudad costera de Cantalapiedra20? Si permitiésemos esto, después de la inserción el registro recién insertado “desaparecería” inmediatamente de la vista (aunque no de la tabla base, Customer). El mismo conflicto se produciría al actualizar la columna State de un hawaiano. Para controlar este comportamiento, SQL define la cláusula with check option. Si se especifica esta opción, no se permiten inserciones ni modificaciones que violen la condición de selección impuesta a la vista; si intentamos una operación tal, se pro- duce un error de ejecución. Por el contrario, si no se incluye la opción en la defini- ción de la vista, estas operaciones se permiten, pero nos encontraremos con situa- 20 Cuando escribí la primera versión de este libro, no sabía que había un Cantalapiedra en España; pensé que nombre tan improbable era invento mío. Mis disculpas a los cantala- pedrenses, pues me parece que viven en ciudad sin costas.
  • 28. 478 La Cara Oculta de Delphi ciones como las descritas, en que un registro recién insertado o modificado desapa- rece misteriosamente por no pertenecer a la vista.
  • 29. Capítulo 23 Procedimientos almacenados yProcedimientos almacenados y triggerstriggers ON ESTE CAPÍTULO COMPLETAMOS la presentación de los sublenguajes de SQL, mostrando el lenguaje de definición de procedimientos de InterBase. Desgraciadamente, los lenguajes de procedimientos de los distintos sistemas de bases de datos son bastante diferentes entre sí, al no existir todavía un estándar al respecto. De los dialectos existentes, he elegido nuevamente InterBase por dos razo- nes. La primera, y fundamental, es que es el sistema SQL que usted tiene a mano (asumiendo que tiene Delphi). La segunda es que el dialecto de InterBase para pro- cedimientos es el que más se asemeja al propuesto en el borrador del estándar SQL- 3. De cualquier manera, las diferencias entre dialectos no son demasiadas, y no le costará mucho trabajo entender el lenguaje de procedimientos de cualquier otro sis- tema de bases de datos. ¿Para qué usar procedimientos almacenados? Un procedimiento almacenado (stored procedure) es, sencillamente, un algoritmo cuya defi- nición reside en la base de datos, y que es ejecutado por el servidor del sistema. Aun- que SQL-3 define formalmente un lenguaje de programación para procedimientos almacenados, cada uno de los sistemas de bases de datos importantes a nivel comer- cial implementa su propio lenguaje para estos recursos. InterBase implementa un dialecto parecido a la propuesta de SQL-3; Oracle tiene un lenguaje llamado PL- SQL; Microsoft SQL Server ofrece el denominado Transact-SQL. No obstante, las diferencias entre estos lenguajes son mínimas, principalmente en sintaxis, siendo casi idénticas las capacidades expresivas. El uso de procedimientos almacenados ofrece las siguientes ventajas: • Los procedimientos almacenados ayudan a mantener la consistencia de la base de datos. C
  • 30. 480 La Cara Oculta de Delphi Las instrucciones básicas de actualización, update, insert y delete, pueden combinarse arbitrariamente si dejamos que el usuario tenga acceso ilimitado a las mismas. No toda combinación de actualizaciones cumplirá con las re- glas de consistencia de la base de datos. Hemos visto que algunas de estas reglas se pueden expresar declarativamente durante la definición del esquema relacional. El mejor ejemplo son las restricciones de integridad referencial. Pero, ¿cómo expresar declarativamente que para cada artículo presente en un pedido, debe existir un registro correspondiente en la tabla de movimientos de un almacén? Una posible solución es prohibir el uso directo de las ins- trucciones de actualización, revocando permisos de acceso al público, y permitir la modificación de datos solamente a partir de procedimientos al- macenados. • Los procedimientos almacenados permiten superar las limitaciones del lenguaje de consultas. SQL no es un lenguaje completo. Un problema típico en que falla es en la definición de clausuras relacionales. Tomemos como ejemplo una tabla con dos columnas: Objeto y Parte. Esta tabla contiene pares como los siguientes: Objeto Parte Cuerpo humano Cabeza Cuerpo humano Tronco Cabeza Ojos Cabeza Boca Boca Dientes ¿Puede el lector indicar una consulta que liste todas las partes incluidas en la cabeza? Lo que falla es la posibilidad de expresar algoritmos recursivos. Para resolver esta situación, los procedimientos almacenados pueden implemen- tarse de forma tal que devuelvan conjuntos de datos, en vez de valores esca- lares. En el cuerpo de estos procedimientos se pueden realizar, entonces, lla- madas recursivas. • Los procedimientos almacenados pueden reducir el tráfico en la red. Un procedimiento almacenado se ejecuta en el servidor, que es precisamente donde se encuentran los datos. Por lo tanto, no tenemos que explorar una tabla de arriba a abajo desde un ordenador cliente para extraer el promedio de ventas por empleado durante el mes pasado. Además, por regla general el servidor es una máquina más potente que las estaciones de trabajo, por lo que puede que ahorremos tiempo de ejecución para una petición de infor-
  • 31. Triggers y procedimientos almacenados 481 mación. No conviene, sin embargo, abusar de esta última posibilidad, porque una de las ventajas de una red consiste en distribuir el tiempo de procesador. • Con los procedimientos almacenados se puede ahorrar tiempo de desarrollo. Siempre que existe una información, a alguien se le puede ocurrir un nuevo modo de aprovecharla. En un entorno cliente/servidor es típico que varias aplicaciones diferentes trabajen con las mismas bases de datos. Si centraliza- mos en la propia base de datos la imposición de las reglas de consistencia, no tendremos que volverlas a programar de una aplicación a otra. Además, evi- tamos los riesgos de una mala codificación de estas reglas, con la consi- guiente pérdida de consistencia. Como todas las cosas de esta vida, los procedimientos almacenados también tienen sus inconvenientes. Ya he mencionado uno de ellos: si se centraliza todo el trata- miento de las reglas de consistencia en el servidor, corremos el riesgo de saturar los procesadores del mismo. El otro inconveniente es la poca portabilidad de las defini- ciones de procedimientos almacenados entre distintos sistemas de bases de datos. Si hemos desarrollado procedimientos almacenados en InterBase y queremos migrar nuestra base de datos a Oracle (o viceversa), estaremos obligados a partir “casi” de cero; algo se puede aprovechar, de todos modos. Cómo se utiliza un procedimiento almacenado Un procedimiento almacenado puede utilizarse desde una aplicación cliente, desarro- llada en cualquier lenguaje de programación que pueda acceder a la interfaz de pro- gramación de la base de datos, o desde las propias utilidades interactivas del sistema. En Delphi tenemos el componente TStoredProc, diseñado para la ejecución de estos procedimientos. En un capítulo posterior veremos cómo suministrar parámetros, ejecutar procedimientos y recibir información utilizando este componente. En el caso de InterBase, también es posible ejecutar un procedimiento almacenado directamente desde la aplicación Windows ISQL, mediante la siguiente instrucción: execute procedure NombreProcedimiento [ListaParámetros]; La misma instrucción puede utilizarse en el lenguaje de definición de procedimientos y triggers para llamar a un procedimiento dentro de la definición de otro. Es posible también definir procedimientos recursivos. InterBase permite hasta un máximo de 1000 llamadas recursivas por procedimiento.
  • 32. 482 La Cara Oculta de Delphi El carácter de terminación Los procedimientos almacenados de InterBase deben necesariamente escribirse en un fichero script de SQL. Más tarde, este fichero debe ser ejecutado desde la utilidad Windows ISQL para que los procedimientos sean incorporados a la base de datos. Hemos visto las reglas generales del uso de scripts en InterBase en el capítulo de in- troducción a SQL. Ahora tenemos que estudiar una característica de estos scripts que anteriormente hemos tratado superficialmente: el carácter de terminación. Por regla general, cada instrucción presente en un script es leída y ejecutada de forma individual y secuencial. Esto quiere decir que el intérprete de scripts lee del fichero hasta que detecta el fin de instrucción, ejecuta la instrucción recuperada, y sigue así hasta llegar al final del mismo. El problema es que este proceso de extracción de instrucciones independientes se basa en la detección de un carácter especial de ter- minación. Por omisión, este carácter es el punto y coma; el lector habrá observado que todos los ejemplos de instrucciones SQL que deben colocarse en scripts han sido, hasta el momento, terminados con este carácter. Ahora bien, al tratar con el lenguaje de procedimientos y triggers encontraremos ins- trucciones y cláusulas que deben terminar con puntos y comas. Si el intérprete de scripts tropieza con uno de estos puntos y comas pensará que se encuentra frente al fin de la instrucción, e intentará ejecutar lo que ha leído hasta el momento; casi siempre, una instrucción incompleta. Por lo tanto, debemos cambiar el carácter de terminación de Windows ISQL cuando estamos definiendo triggers o procedimientos almacenados. La instrucción que nos ayuda para esto es la siguiente: set term Terminador Como carácter de terminación podemos escoger cualquier carácter o combinación de los mismos lo suficientemente rara como para que no aparezca dentro de una instrucción del lenguaje de procedimientos. Por ejemplo, podemos utilizar el acento circunflejo: set term ^; Observe cómo la instrucción que cambia el carácter de terminación debe terminar ella misma con el carácter antiguo. Al finalizar la creación de todos los procedimien- tos que necesitamos, debemos restaurar el antiguo carácter de terminación: set term ;^ En lo sucesivo asumiremos que el carácter de terminación ha sido cambiado al acento circunflejo.
  • 33. Triggers y procedimientos almacenados 483 Procedimientos almacenados en InterBase La sintaxis para la creación de un procedimiento almacenado en InterBase es la si- guiente: create procedure Nombre [ ( ParámetrosDeEntrada ) ] [ returns ( ParámetrosDeSalida ) ] as CuerpoDeProcedimiento Las cláusulas ParámetrosDeEntrada y ParámetrosDeSalida representan listas de decla- raciones de parámetros. Los parámetros de salida pueden ser más de uno; esto signi- fica que el procedimiento almacenado que retorna valores no se utiliza como si fuese una función de un lenguaje de programación tradicional. El siguiente es un ejemplo de cabecera de procedimiento: create procedure TotalPiezas(PiezaPrincipal char(15)) returns (Total integer) as /* ... Aquí va el cuerpo ... */ El cuerpo del procedimiento, a su vez, se divide en dos secciones, siendo opcional la primera de ellas: la sección de declaración de variables locales, y una instrucción compuesta, begin...end, que agrupa las instrucciones del procedimiento. Las varia- bles se declaran en este verboso estilo, á la 1970: declare variable V1 integer; declare variable V2 char(50); Estas son las instrucciones permitidas por los procedimientos almacenados de In- terBase: • Asignaciones: Variable = Expresión Las variables pueden ser las declaradas en el propio procedimiento, paráme- tros de entrada o parámetros de salida. • Llamadas a procedimientos: execute procedure NombreProc [ParsEntrada] [returning_values ParsSalida] No se admiten expresiones en los parámetros de entrada; mucho menos en los de salida.
  • 34. 484 La Cara Oculta de Delphi • Condicionales: if (Condición) then Instrucción [else Instrucción] • Bucles controlados por condiciones: while (Condición) do Instrucción • Instrucciones SQL: Cualquier instrucción de manipulación, insert, update ó delete, puede in- cluirse en un procedimiento almacenado. Estas instrucciones pueden utilizar variables locales y parámetros, siempre que estas variables estén precedidas de dos puntos, para distinguirlas de los nombres de columnas. Por ejemplo, si Minimo y Aumento son variables o parámetros, puede ejecutarse la siguiente instrucción: update Empleados set Salario = Salario * :Aumento where Salario < :Minimo; Se permite el uso directo de instrucciones select si devuelven una sola fila; para consultas más generales se utiliza la instrucción for que veremos dentro de poco. Estas selecciones únicas van acompañadas por una cláusula into para transferir valores a variables o parámetros: select Empresa from Clientes where Codigo = 1984 into :NombreEmpresa; • Iteración sobre consultas: for InstrucciónSelect into Variables do Instrucción Esta instrucción recorre el conjunto de filas definido por la instrucción se- lect. Para cada fila, transfiere los valores a las variables de la cláusula into, de forma similar a lo que sucede con las selecciones únicas, y ejecuta entonces la instrucción de la sección do. • Lanzamiento de excepciones: exception NombreDeExcepción Similar a la instrucción raise de Delphi.
  • 35. Triggers y procedimientos almacenados 485 • Captura de excepciones: when ListaDeErrores do Instrucción Similar a la cláusula except de la instrucción try...except de Delphi. Los errores capturados pueden ser excepciones propiamente dichas o errores re- portados con la variable SQLCODE. Estos últimos errores se producen al ejecutarse instrucciones SQL. Las instrucciones when deben colocarse al fi- nal de los procedimientos. • Instrucciones de control: exit; suspend; La instrucción exit termina la ejecución del procedimiento actual, y es simi- lar al procedimiento Exit de Delphi. Por su parte, suspend se utiliza en pro- cedimientos que devuelven un conjunto de filas para retornar valores a la rutina que llama a este procedimiento. Con esta última instrucción, se inte- rrumpe temporalmente el procedimiento, hasta que la rutina que lo llama haya procesado los valores retornados. • Instrucciones compuestas: begin ListaDeInstrucciones end La sintaxis de los procedimientos de InterBase es similar a la de Pascal. A di- ferencia de este último lenguaje, la palabra end no puede tener un punto y coma a continuación. Mostraré ahora un par de procedimientos sencillos, que ejemplifiquen el uso de estas instrucciones. El siguiente procedimiento, basado en las tablas definidas en el capí- tulo sobre DDL, sirve para recalcular la suma total de un pedido, si se suministra el número de pedido correspondiente: create procedure RecalcularTotal(NumPed int) as declare variable Total integer; begin select sum(Cantidad * PVP * (100 - Descuento) / 100) from Detalles, Articulos where Detalles.RefArticulo = Articulos.Codigo and Detalles.RefPedido = :NumPed into :Total; if (Total is null) then Total = 0;
  • 36. 486 La Cara Oculta de Delphi update Pedidos set Total = :Total where Numero = :NumPed; end ^ El procedimiento consiste básicamente en una instrucción select que calcula la suma de los totales de todas las líneas de detalles asociadas al pedido; esta instrucción ne- cesita mezclar datos provenientes de las líneas de detalles y de la tabla de artículos. Si el valor total es nulo, se cambia a cero. Esto puede suceder si el pedido no tiene lí- neas de detalles; en este caso, la instrucción select retorna el valor nulo. Finalmente, se localiza el pedido indicado y se le actualiza el valor a la columna Total, utilizando el valor depositado en la variable local del mismo nombre. El procedimiento que definimos a continuación se basa en el anterior, y permite recalcular los totales de todas las filas almacenadas en la tabla de pedidos; de este modo ilustramos el uso de las instrucciones for select … do y execute procedure: create procedure RecalcularPedidos as declare variable Pedido integer; begin for select Numero from Pedidos into :Pedido do execute procedure RecalcularTotal :Pedido; end ^ Procedimientos que devuelven un conjunto de datos Antes he mencionado la posibilidad de superar las restricciones de las expresiones select del modelo relacional mediante el uso de procedimientos almacenados. Un procedimiento puede diseñarse de modo que devuelva un conjunto de filas; para esto tiene que utilizar la instrucción suspend, que transfiere el control temporalmente a la rutina que llama al procedimiento, para que ésta pueda hacer algo con los valores asignados a los parámetros de salida. Esta técnica es poco habitual en los lenguajes de programación más extendidos; si quiere encontrar algo parecido, puede desente- rrar los iteradores del lenguaje CLU, diseñado por Barbara Liskov a mediados de los setenta. Supongamos que necesitamos obtener la lista de los primeros veinte, treinta o mil cuatrocientos números primos. Comencemos por algo fácil, con la función que ana- liza un número y dice si es primo o no: create procedure EsPrimo(Numero integer) returns (Respuesta integer) as declare variable I integer; begin I = 2; while (I < Numero) do begin if (cast((Numero / I) as integer) * I = Numero) then
  • 37. Triggers y procedimientos almacenados 487 begin Respuesta = 0; exit; end I = I + 1; end Respuesta = 1; end ^ Ya sé que hay implementaciones más eficientes, pero no quería complicar mucho el ejemplo. Observe, de paso, la pirueta que he tenido que realizar para ver si el número es divisible por el candidato a divisor. He utilizado el criterio del lenguaje C para las expresiones lógicas: devuelvo 1 si el número es primo, y 0 si no lo es. Recuerde que InterBase no tiene un tipo Boolean. Ahora, en base al procedimiento anterior, implementamos el nuevo procedimiento Primos: create procedure Primos(Total integer) returns (Primo Integer) as declare variable I integer; declare variable Respuesta integer; begin I = 0; Primo = 2; while (I < Total) do begin execute procedure EsPrimo Primo returning_values Respuesta; if (Respuesta = 1) then begin I = I + 1; suspend; /* ¡¡¡ Nuevo !!! */ end Primo = Primo + 1; end end ^ Este procedimiento puede ejecutarse en dos contextos diferentes: como un pro- cedimiento normal, o como procedimiento de selección. Como procedimiento nor- mal, utilizamos la instrucción execute procedure, como hasta ahora: execute procedure Primos(100); No obstante, no van a resultar tan sencillas las cosas. Esta llamada, si se realiza desde Windows ISQL, solamente devuelve el primer número primo (era el 2, ¿o no?). El problema es que, en este contexto, la primera llamada a suspend termina completa- mente el algoritmo.
  • 38. 488 La Cara Oculta de Delphi La segunda posibilidad es utilizar el procedimiento como si fuera una tabla o vista. Desde Windows ISQL podemos lanzar la siguiente instrucción, que nos mostrará los prime- ros cien números primos: select * from Primos(100); Por supuesto, el ejemplo anterior se refiere a una secuencia aritmética. En la práctica, un procedimiento de selección se implementa casi siempre llamando a suspend dentro de una instrucción for...do, que recorre las filas de una consulta. Recorriendo un conjunto de datos En esta sección mostraré un par de ejemplos más complicados de procedimientos que utilizan la instrucción for...select de InterBase. El primero tiene que ver con un sistema de entrada de pedidos. Supongamos que queremos actualizar las existencias en el inventario después de haber grabado un pedido. Tenemos dos posibilidades, en realidad: realizar esta actualización mediante un trigger que se dispare cada vez que se guarda una línea de detalles, o ejecutar un procedimiento almacenado al finalizar la grabación de todas las líneas del pedido. La primera técnica será explicada en breve, pero adelanto en estos momentos que tiene un defecto. Pongamos como ejemplo que dos usuarios diferentes están pasando por el cajero, simultáneamente. El primero saca un pack de Coca-Colas de la cesta de la compra, mientras el segundo pone Pepsis sobre el mostrador. Si, como es de espe- rar, la grabación del pedido tiene lugar mediante una transacción, al dispararse el trigger se han modificado las filas de estas dos marcas de bebidas, y se han bloqueado hasta el final de la transacción. Ahora, inesperadamente, el primer usuario saca Pepsis mientras el segundo nos sorprende con Coca-Colas; son unos fanáticos de las bebi- das americanas estos individuos. El problema es que el primero tiene que esperar a que el segundo termine para poder modificar la fila de las Pepsis, mientras que el segundo se halla en una situación similar. Esta situación se denomina abrazo mortal (deadlock) y realmente no es problema al- guno para InterBase, en el cual los procesos fallan inmediatamente cuando se les niega un bloqueo21. Pero puede ser un peligro en otros sistemas con distinta estrate- gia de espera. La solución más común consiste en que cuando un proceso necesita bloquear ciertos recursos, lo haga siempre en el mismo orden. Si nuestros dos con- sumidores de líquidos oscuros con burbujas hubieran facturado sus compras en or- den alfabético, no se hubiera producido este conflicto. Por supuesto, esto descarta el uso de un trigger para actualizar el inventario, pues hay que esperar a que estén todos 21 Realmente, es el BDE quien configura a InterBase de este modo.
  • 39. Triggers y procedimientos almacenados 489 los productos antes de ordenar y realizar entonces la actualización. El siguiente pro- cedimiento se encarga de implementar el algoritmo explicado: create procedure ActualizarInventario(Pedido integer) as declare variable CodArt integer; declare variable Cant integer; begin for select RefArticulo, Cantidad from Detalles where RefPedido = :Pedido order by RefArticulo into :CodArt, :Cant do update Articulos set Pedidos = Pedidos + :Cant where Codigo = :CodArt; end ^ Otro ejemplo: necesitamos conocer los diez mejores clientes de nuestra tienda. Pero sólo los diez primeros, y no vale mirar hacia otro lado cuando aparezca el undécimo. Algunos sistemas SQL tienen un operador first para este propósito, pero no Inter- Base. Este procedimiento, que devuelve un conjunto de datos, nos servirá de ayuda: create procedure MejoresClientes(Rango integer) returns (Codigo int, Nombre varchar(30), Total int) as begin for select Codigo, Nombre, sum(Total) from Clientes, Pedidos where Clientes.Codigo = Pedidos.Cliente order by 3 desc into :Codigo, :Nombre, :Total do begin suspend; Rango = Rango - 1; if (Rango = 0) then exit; end end ^ Entonces podremos realizar consultas como la siguiente: select * from MejoresClientes(10) Triggers, o disparadores Una de las posibilidades más interesantes de los sistemas de bases de datos relacio- nales son los triggers, o disparadores; en adelante, utilizaré preferentemente la palabra inglesa original. Se trata de un tipo de procedimiento almacenado que se activa au- tomáticamente al efectuar operaciones de modificación sobre ciertas tablas de la base de datos.
  • 40. 490 La Cara Oculta de Delphi La sintaxis de la declaración de un trigger es la siguiente: create trigger NombreTrigger for Tabla [active | inactive] {before | after} {delete | insert | update} [position Posición] as CuerpoDeProcedimiento El cuerpo de procedimiento tiene la misma sintaxis que los cuerpos de los procedi- mientos almacenados. Las restantes cláusulas del encabezamiento de esta instrucción tienen el siguiente significado: Cláusula Significado NombreTrigger El nombre que se le va a asignar al trigger Tabla El nombre de la tabla a la cual está asociado active | inactive Puede crearse inactivo, y activarse después before | after Se activa antes o después de la operación delete | insert | update Qué operación provoca el disparo del trigger position Orden de disparo para la misma operación A diferencia de otros sistemas de bases de datos, los triggers de InterBase se definen para una sola operación sobre una sola tabla. Si queremos compartir código para eventos de actualización de una o varias tablas, podemos situar este código en un procedimiento almacenado y llamar a este algoritmo desde los diferentes triggers defi- nidos. Un parámetro interesante es el especificado por position. Para una operación sobre una tabla pueden definirse varios triggers. El número indicado en position determina el orden en que se disparan los diferentes sucesos; mientras más bajo sea el número, mayor será la prioridad. Si dos triggers han sido definidos con la misma prioridad, el orden de disparo entre ellos será aleatorio. Hay una instrucción similar que permite modificar algunos parámetros de la defini- ción de un trigger, como su orden de disparo, si está activo o no, o incluso su propio cuerpo: alter trigger NombreTrigger [active | inactive] [{before | after} {delete | insert | update}] [position Posición] [as CuerpoProcedimiento] Podemos eliminar completamente la definición de un trigger de la base de datos me- diante la instrucción: drop trigger NombreTrigger
  • 41. Triggers y procedimientos almacenados 491 Las variables new y old Dentro del cuerpo de un trigger pueden utilizarse las variables predefinidas new y old. Estas variables hacen referencia a los valores nuevos y anteriores de las filas involu- cradas en la operación que dispara el trigger. Por ejemplo, en una operación de modi- ficación update, old se refiere a los valores de la fila antes de la modificación y new a los valores después de modificados. Para una inserción, solamente tiene sentido la variable new, mientras que para un borrado, solamente tiene sentido old. El siguiente trigger hace uso de la variable new, para acceder a los valores del nuevo registro después de una inserción: create trigger UltimaFactura for Pedidos active after insert position 0 as declare variable UltimaFecha date; begin select UltimoPedido from Clientes where Codigo = new.RefCliente into :UltimaFecha; if (UltimaFecha < new.FechaVenta) then update Clientes set UltimoPedido = new.FechaVenta where Codigo = new.RefCliente; end ^ Este trigger sirve de contraejemplo a un error muy frecuente en la programación SQL. La primera instrucción busca una fila particular de la tabla de clientes y, una vez encontrada, extrae el valor de la columna UltimoPedido para asignarlo a la variable local UltimaFecha. El error consiste en pensar que esta instrucción, a la vez, deja a la fila encontrada como “fila activa”. El lenguaje de triggers y procedimientos almacena- dos de InterBase, y la mayor parte de los restantes sistemas, no utiliza “filas activas”. Es por eso que en la instrucción update hay que incluir una cláusula where para volver a localizar el registro del cliente. De no incluirse esta cláusula, cambiaríamos la fecha para todos los clientes. Es posible cambiar el valor de una columna correspondiente a la variable new, pero solamente si el trigger se define “antes” de la operación de modificación. En cualquier caso, el nuevo valor de la columna se hace efectivo después de que la operación tenga lugar. Más ejemplos de triggers Para mostrar el uso de triggers, las variables new y old y los procedimientos almace- nados, mostraré cómo se puede actualizar automáticamente el inventario de artículos
  • 42. 492 La Cara Oculta de Delphi y el total almacenado en la tabla de pedidos en la medida en que se realizan actualiza- ciones en la tabla que contiene las líneas de detalles. Necesitaremos un par de procedimientos auxiliares para lograr una implementación más modular. Uno de estos procedimientos, RecalcularTotal, debe actualizar el total de venta de un pedido determinado, y ya lo hemos implementado antes. Repito aquí su código, para mayor comodidad: create procedure RecalcularTotal(NumPed int) as declare variable Total integer; begin select sum(Cantidad * PVP * (100 - Descuento) / 100) from Detalles, Articulos where Detalles.RefArticulo = Articulos.Codigo and Detalles.RefPedido = :NumPed into :Total; if (Total is null) then Total = 0; update Pedidos set Total = :Total where Numero = :NumPed; end ^ El otro procedimiento debe modificar el inventario de artículos. Su implementación es muy simple: create procedure ActInventario(CodArt integer, Cant Integer) as begin update Articulos set Pedidos = Pedidos + :Cant where Codigo = :CodArt; end ^ Ahora le toca el turno a los triggers. Los más sencillos son los relacionados con la inserción y borrado; en el primero utilizaremos la variable new, y en el segundo, old: create trigger NuevoDetalle for Detalles active after insert position 1 as begin execute procedure RecalcularTotal new.RefPedido; execute procedure ActInventario new.RefArticulo, new.Cantidad; end ^ create trigger EliminarDetalle for Detalles active after delete position 1 as declare variable Decremento integer; begin Decremento = - old.Cantidad; execute procedure RecalcularTotal old.RefPedido; execute procedure ActInventario old.RefArticulo, :Decremento; end ^
  • 43. Triggers y procedimientos almacenados 493 Es curiosa la forma en que se pasan los parámetros a los procedimientos almacena- dos. Tome nota, en particular, de que hemos utilizado una variable local, Decremento, en el trigger de eliminación. Esto es así porque no se puede pasar expresiones como parámetros a los procedimientos almacenados, ni siquiera para los parámetros de entrada. Finalmente, nos queda el trigger de modificación: create trigger ModificarDetalle for Detalles active after update position 1 as declare variable Decremento integer; begin execute procedure RecalcularTotal new.RefPedido; if (new.RefArticulo <> old.RefArticulo) then begin Decremento = -old.Cantidad; execute procedure ActInventario old.RefArticulo, :Decremento; execute procedure ActInventario new.RefArticulo, new.Cantidad; end else begin Decremento = new.Cantidad - old.Cantidad; execute procedure ActInventario new.RefArticulo, :Decremento; end end ^ Observe cómo comparamos el valor del código del artículo antes y después de la operación. Si solamente se ha producido un cambio en la cantidad vendida, tenemos que actualizar un solo registro de inventario; en caso contrario, tenemos que actuali- zar dos registros. No hemos tenido en cuenta la posibilidad de modificar el pedido al cual pertenece la línea de detalles. Suponemos que esta operación no va a permitirse, por carecer de sentido, en las aplicaciones clientes. Generadores Los generadores (generators) son un recurso de InterBase para poder disponer de valores secuenciales, que pueden utilizarse, entre otras cosas, para garantizar la unicidad de las claves artificiales. Un generador se crea, del mismo modo que los procedimientos almacenados y triggers, en un fichero script de SQL. El siguiente ejemplo muestra cómo crear un generador: create generator CodigoEmpleado;
  • 44. 494 La Cara Oculta de Delphi Un generador define una variable interna persistente, cuyo tipo es un entero de 32 bits. Aunque esta variable se inicializa automáticamente a 0, tenemos una instrucción para cambiar el valor de un generador: set generator CodigoEmpleado to 1000; Por el contrario, no existe una instrucción específica que nos permita eliminar un generador. Esta operación debemos realizarla directamente en la tabla del sistema que contiene las definiciones y valores de todos los generadores: delete from rdb$generators where rdb$generator_name = 'CODIGOEMPLEADO' Para utilizar un generador necesitamos la función gen_id. Esta función utiliza dos parámetros. El primero es el nombre del generador, y el segundo debe ser la cantidad en la que se incrementa o decrementa la memoria del generador. La función retorna entonces el valor ya actualizado. Utilizaremos el generador anterior para suministrar automáticamente un código de empleado si la instrucción insert no lo hace: create trigger NuevoEmpleado for Empleados active before insert as begin if (new.Codigo is null) then new.Codigo = gen_id(CodigoEmpleado, 1); end ^ Al preguntar primeramente si el código del nuevo empleado es nulo, estamos permi- tiendo la posibilidad de asignar manualmente un código de empleado durante la in- serción. Los programas escritos en Delphi tienen problemas cuando se genera una clave pri- maria para una fila mediante un generador, pues el registro recién insertado “desapa- rece” según el punto de vista de la tabla. Para no tener que abandonar los generado- res, la solución consiste en crear un procedimiento almacenado que obtenga el pró- ximo valor del generador, y utilizar este valor para asignarlo a la clave primaria en el evento BeforePost de la tabla. En el lado del servidor se programaría algo parecido a lo siguiente: create procedure ProximoCodigo returns (Cod integer) as begin Cod = gen_id(CodigoEmpleado); end ^ En la aplicación crearíamos un componente spProximoCodigo, de la clase TStoredProc, y lo aprovecharíamos de esta forma en uno de los eventos BeforePost o OnNewRecord de la tabla de clientes:
  • 45. Triggers y procedimientos almacenados 495 procedure TmodDatos.tbClientesBeforePost(DataSet: TDataSet); begin spProximoCodigo.ExecProc; tbClientesCodigo.Value := spProximoCodigo.ParaByName('COD').AsInteger; end; NOTA IMPORTANTE En cualquier caso, si necesita valores únicos y consecutivos en alguna columna de una tabla, no utilice generadores (ni secuencias de Oracle, o identidades de MS SQL Server). El motivo es que los generadores no se bloquean durante las tran- sacciones. Usted pide un valor dentro de una transacción, y le es concedido; to- davía no ha terminado su transacción. A continuación, otro usuario pide el si- guiente valor, y sus deseos se cumplen. Pero entonces usted aborta la transac- ción, por el motivo que sea. La consecuencia: se pierde el valor que recibió, y se produce un "hueco" en la secuencia. Simulando la integridad referencial Como hemos explicado, mediante los triggers y los procedimientos almacenados po- demos expresar reglas de consistencia en forma imperativa, en contraste con las reglas declarativas que se enuncian al crear tablas: claves primarias, alternativas y externas, condiciones de verificación, etc. En general, es preferible utilizar una regla declarativa antes que su equivalente imperativo. Pero sucede que las posibilidades de las reglas declarativas son más limitadas que las posibilidades de las reglas imperativas. En InterBase 4, por ejemplo, las restricciones de integridad referencial no admiten modificaciones ni borrados en las tablas maestras de una clave externa. Sin embargo, a veces es deseable permitir estas operaciones y propagar los cambios en cascada a las tablas dependientes. Ilustraré la forma de lograr restricciones de integridad referencial con propagación de cambios mediante el ejemplo de la tabla de pedidos y líneas de detalles. Recorde- mos la definición de la tabla de pedidos, en el capítulo sobre el Lenguaje de Defini- ción de Datos: create table Pedidos ( Numero int not null, RefCliente int not null, RefEmpleado int, FechaVenta date default "Now", Total int default 0,
  • 46. 496 La Cara Oculta de Delphi primary key (Numero), foreign key (RefCliente) references Clientes (Codigo) ); La definición de la tabla de detalles cambia ahora, sustituyéndose la cláusula foreign key que hacía referencia a la tabla de pedidos: create table Detalles ( RefPedido int not null, NumLinea int not null, RefArticulo int not null, Cantidad int default 1 not null, Descuento int default 0 not null check (Descuento between 0 and 100), primary key (RefPedido, NumLinea), foreign key (RefArticulo) references Articulos (Codigo), /* Antes: foreign key(RefPedido) references Pedidos(Numero) */ check (RefPedido in (select Numero from Pedidos)) ); La nueva cláusula check verifica en cada inserción y modificación que no se intro- duzca un número de pedido inexistente. El borrado en cascada se puede lograr de la siguiente manera: create trigger BorrarDetallesEnCascada for Pedidos active after delete position 0 as begin delete from Detalles where RefPedido = old.Numero; end ^ Un poco más larga es la implementación de actualizaciones en cascada. create trigger ModificarDetallesEnCascada for Pedidos active after update position 0 as begin if (old.Numero <> new.Numero) then update Detalles set RefPedido = new.Numero where RefPedido = old.Numero; end ^ Por supuesto, los triggers hubieran sido mucho más complicados si hubiéramos man- tenido la restricción foreign key en la declaración de la tabla de detalles, en particu- lar, la propagación de modificaciones.
  • 47. Triggers y procedimientos almacenados 497 Excepciones Sin embargo, todavía no contamos con medios para detener una operación SQL; esta operación sería necesaria para simular imperativamente las restricciones a la propaga- ción de cambios en cascada, en la integridad referencial. Lo que nos falta es el poder lanzar excepciones desde un trigger o procedimiento almacenado. Las excepciones de InterBase se crean asociando una cadena de mensaje a un identificador: create exception CLIENTE_CON_PEDIDOS "No se puede modificar este cliente" Es necesario confirmar la transacción actual para poder utilizar una excepción recién creada. Existen también instrucciones para modificar el mensaje asociado a una ex- cepción (alter exception), y para eliminar la definición de una excepción de la base de datos (drop exception). Una excepción se lanza desde un procedimiento almacenado o trigger mediante la instrucción exception: create trigger CheckDetails for Clientes active before delete position 0 as declare variable Numero int; begin select count(*) from Pedidos where RefCliente = old.Codigo into :Numero; if (:Numero > 0) then exception CLIENTE_CON_PEDIDOS; end ^ Las excepciones de InterBase determinan que cualquier cambio realizado dentro del cuerpo del trigger o procedimiento almacenado, sea directa o indirectamente, se anule automáticamente. De esta forma puede programarse algo parecido a las transaccio- nes anidadas de otros sistemas de bases de datos. Si la instrucción exception es similar a la instrucción raise de Delphi, el equivalente más cercano a try...except es la instrucción when de InterBase. Esta instrucción tiene tres formas diferentes. La primera intercepta las excepciones lanzadas con ex- ception: when exception NombreExcepción do BloqueInstrucciones; Con la segunda variante, se detectan los errores producidos por las instrucciones SQL:
  • 48. 498 La Cara Oculta de Delphi when sqlcode Numero do BloqueInstrucciones; Los números de error de SQL aparecen documentados en la ayuda en línea y en el manual Language Reference de InterBase. A grandes rasgos, la ejecución correcta de una instrucción devuelve un código igual a 0, cualquier valor negativo es un error pro- piamente dicho (-803, por ejemplo, es un intento de violación de una clave primaria), y los valores positivos son advertencias. En particular, 100 es el valor que se devuelve cuando una selección única no encuentra el registro buscado. Este convenio es parte del estándar de SQL, aunque los códigos de error concreto varíen de un sistema a otro. La tercera forma de la instrucción when es la siguiente: when gdscode Numero do BloqueInstrucciones; En este caso, se están interceptando los mismos errores que con sqlcode, pero se utilizan los códigos internos de InterBase, que ofrecen más detalles sobre la causa. Por ejemplo, los valores 335544349 y 35544665 corresponden a –803, la violación de unicidad, pero el primero se produce cuando se inserta un valor duplicado en cual- quier índice único, mientras que el segundo se reserva para las violaciones específicas de clave primaria o alternativa. En cualquier caso, las instrucciones when deben ser las últimas del bloque en que se incluyen, y pueden colocarse varias simultáneamente, para atender varios casos: begin /* Instrucciones */ /* … */ when sqlcode –803 do Resultado = "Violación de unicidad"; when exception CLIENTE_CON_PEDIDOS do Resultado = "Elimine primero los pedidos realizados"; end La Tercera Regla de Marteens sigue siendo aplicable a estas instrucciones: no detenga la propagación de una excepción, a no ser que tenga una solución a su causa. Alertadores de eventos Los alertadores de eventos (event alerters) son un recurso único, por el momento, de InterBase. Los procedimientos almacenados y triggers de InterBase pueden utilizar la instrucción siguiente: post_event NombreDeEvento
  • 49. Triggers y procedimientos almacenados 499 El nombre de evento puede ser una constante de cadena o una variable del mismo tipo. Cuando se produce un evento, InterBase avisa a todos los clientes interesados de la ocurrencia del mismo. Los alertadores de eventos son un recurso muy potente. Sitúese en un entorno cliente/servidor donde se producen con frecuencia cambios en una base de datos. Las estaciones de trabajo normalmente no reciben aviso de estos cambios, y los usuarios deben actualizar periódica y frecuentemente sus pantallas para reflejar los cambios realizados por otros usuarios, pues en caso contrario puede suceder que alguien tome una decisión equivocada en base a lo que está viendo en pantalla. Sin embargo, refrescar la pantalla toma tiempo, pues hay que traer cierta cantidad de información desde el servidor de bases de datos, y las estaciones de trabajo realizan esta operación periódicamente, colapsando la red. El personal de la empresa se abu- rre en los tiempos de espera, la moral se resquebraja y la empresa se sitúa al borde del caos... Entonces aparece Usted, un experto programador de Delphi e InterBase, y añade triggers a discreción a la base de datos, en este estilo: create trigger AlertarCambioBolsa for Cotizaciones active after update position 10 as begin post_event "CambioCotizacion"; end ^ Observe que se ha definido una prioridad baja para el orden de disparo del trigger. Hay que aplicar la misma técnica para cada una de las operaciones de actualización de la tabla de cotizaciones. Luego, en el módulo de datos de la aplicación que se ejecuta en las estaciones de trabajo, hay que añadir el componente TIBEventAlerter, que se encuentra en la página Samples de la Paleta de Componentes. Este componente tiene las siguientes propie- dades, métodos y eventos: Nombre Tipo Propósito Events Propiedad Los nombres de eventos que nos interesan. Registered Propiedad Debe ser True para notificar, en tiempo de diseño, nuestro interés en los eventos almacenados en la propiedad anterior. Database Propiedad La base de datos a la cual nos conectaremos.
  • 50. 500 La Cara Oculta de Delphi Nombre Tipo Propósito RegisterEvents Método Notifica a la base de datos nuestro interés por los eventos de la propiedad Events. UnRegisterEvents Método El método inverso del método anterior. OnEventAlert Evento Se dispara cada vez que se produce el evento. En nuestro caso, podemos editar la propiedad Events y teclear la cadena CambioCotiza- cion, que es el nombre del evento que necesitamos. Conectamos la propiedad Database del componente a nuestro componente de bases de datos y activamos la propiedad Registered. Luego creamos un manejador para el evento OnEventAlert similar a éste: procedure TForm1.IBEventAlerter1EventAlert(Sender: TObject; EventName: string; EventCount: Longint; var CancelAlerts: Boolean); begin tbCotizaciones.Refresh; end; Cada vez que se modifique el contenido de la tabla Cotizaciones, el servidor de Inter- Base lanzará el evento identificado por la cadena CambioCotizacion, y este evento será recibido por todas las aplicaciones interesadas. Cada aplicación realizará consecuen- temente la actualización visual de la tabla en cuestión. Esta historia termina previsiblemente. La legión de usuarios del sistema lo aclama con fervor, su jefe le duplica el salario, usted se casa ... o se compra un perro ... o ... Bueno, se me ha complicado un poco el guión; póngale usted su final preferido.
  • 51. Capítulo 24 Microsoft SQL ServerMicrosoft SQL Server N ESTE CAPÍTULO ANALIZAREMOS LAS CARACTERÍSTICAS generales de la implementación de SQL por parte de Microsoft SQL Server. Este sistema es uno de los más extendidos en el mundo de las redes basadas en Windows NT. No está, sin embargo, dentro de la lista de mis sistemas favoritos, por dos razo- nes. La primera: una arquitectura bastante pobre, con tamaño fijo de página, blo- queos a nivel de página que disminuyen la capacidad de modificación concurrente, ficheros de datos y de transacciones de tamaño fijo... La segunda razón: MS SQL Server tiene uno de los dialectos de SQL más retorcidos y horribles del mundo de los servidores de datos relacionales, dudoso privilegio que comparte con Sybase. Es muy probable que la primera razón deje de ser tal con la próxima aparición de la versión 7 de este sistema de bases de datos, que mejora bastante la arquitectura física utilizada. Este capítulo está basado fundamentalmente en la versión actual, la 6.5. En el momento de la escritura de este libro, la versión 7 estaba en fase beta. Trataré en lo posible de adelantar algunas características de la misma, pero advierto al lector de los peligros de deducir características de la versión final a partir de una beta. Herramientas de desarrollo en el cliente La herramienta adecuada para diseñar y administrar bases de datos de MS SQL Ser- ver se llama SQL Enterprise Manager. Normalmente, esta aplicación se instala en el servidor de datos, pero podemos también instalarla en el cliente. Con el Enterprise Manager podemos crear dispositivos (más adelante veremos qué son), bases de datos, crear usuarios y administrar sus contraseñas, e incluso gestionar otros servidores de forma remota. Si lo que necesitamos es ejecutar consultas individuales o todo un script con instruc- ciones, el arma adecuada es el SQL Query Tool, que puede ejecutarse como aplicación independiente, o desde un comando de menú de SQL Enterprise Manager, como se muestra en la siguiente imagen. Dentro del menú File de esta aplicación encontrare- mos un comando para que ejecutemos nuestros scripts SQL. E
  • 52. 502 La Cara Oculta de Delphi Hay que reconocer que las herramientas de administración de MS SQL Server clasi- fican entre las amigables con el usuario. Esto se acentúa en la versión 7, en la cual pegas una patada al servidor y saltan cinco o seis asistentes (wizards) para adivinar qué es lo que quieres realmente hacer. Creación de bases de datos con MS SQL Server Uno de los cambios que introduce la versión 7 de SQL Server es precisamente en la forma de crear bases de datos. En esta sección, sin embargo, describiré fundamen- talmente la técnica empleada hasta la versión 6.5, pues pueden aparecer novedades en la versión final. Además, las bases de datos de Sybase se crean con mecanismos si- milares. Para trabajar con SQL Server hay que comprender qué es un dispositivo (device). Muy fácil: es un fichero del sistema operativo. ¿Entonces por qué inventar un nombre diferente? La razón está en la prehistoria de este producto, a cargo de Sybase. A dife- rencia de SQL Server, el servidor de Sybase puede ejecutarse en distintos sistemas operativos, y puede que, en determinado sistema, un fichero físico no sea el soporte más adecuado para el almacenamiento de una base de datos. Dispositivo Dispositivo Servidor Dispositivo Dispositivo Dispositivo Servidor Dispositivo Dispositivo Servidor SQL Server