Está en la página 1de 103

Consultas multitabla

Introducción

Hasta ahora hemos visto consultas que obtienen los datos de una sola tabla, en
este tema veremos cómo obtener datos de diferentes tablas.

En esta parte ampliaremos la cláusula FROM y descubriremos nuevas palabras


reservadas (UNION, EXCEPT e INTERSECT) que corresponden a operaciones
relacionales.

Para obtener datos de varias tablas tenemos que combinar estas tablas mediante
alguna operación basada en el álgebra relacional.

El álgebra relacional define una serie de operaciones cuyos operandos son tablas
y cuyo resultado es también una tabla.

Las operaciones de álgebra relacional implementadas en Transact-Sql son:

 La unión UNION
 La diferencia EXCEPT
 La intersección INTERSECT
 El producto cartesiano CROSS JOIN
 La composición interna INNER JOIN
 La composición externa LEFT JOIN, RIGHT JOIN Y FULL JOIN

 En todo el tema cuando hablemos de tablas nos referiremos tanto a las tablas que
físicamente están almacenadas en la base de datos como a las tablas temporales
y a las resultantes de una consulta o vista.

La unión de tablas UNION

La unión de tablas consiste en coger dos tablas y obtener una tabla con las filas
de las dos tablas, en el resultado aparecerán las filas de una tabla y, a
continuación, las filas de la otra tabla.

Para poder realizar la operación, las dos tablas tienen que tener el mismo
esquema (mismo número de columnas y tipos compatibles) y la tabla resultante
hereda los encabezados de la primera tabla.

Puede ser cualquier especificación de consulta con la limitación de que no admite


la cláusula ORDER BY, los alias de campo se pueden definir pero sólo tienen
efecto cuando se indican en la primera consulta ya que el resultado toma los
nombres de columna de esta.
Ejemplo: Suponemos que tenemos una tabla Vallarta con las nuevas oficinas de
Vallarta y otra tabla Michoacán con las nuevas oficinas de Michoacán y queremos
obtener una tabla con las nuevas oficinas de las dos ciudades:

SELECT oficina as OFI, ciudad FROM Vallarta


UNION ALL
SELECT oficina, ciudad FROM Michoacan;

El resultado sería:

OFI ciudad
11 Valllarta
28 Valllarta
23 Michoacán

El resultado toma los nombres de columna de la primera consulta y aparecen


primero las filas de la primera consulta y después las de la segunda.

Si queremos que el resultado aparezca ordenado podemos incluir la cláusula


ORDER BY, pero después de la última especificación de consulta, y
expresion_columna será cualquier columna válida de la primera consulta.

SELECT oficina as OFI, ciudad FROM Vallarta


UNION
SELECT oficina, ciudad FROM Michoacan
ORDER BY ofi;

OFI ciudad
11 Vallarta
23 Michoacán
28 Vallarta

Ahora las filas aparecen ordenadas por el número de oficina y hemos utilizado el
nombre de columna de la primera consulta.

Cuando aparezcan en el resultado varias filas iguales, el sistema por defecto


elimina las repeticiones.

Si se especifica ALL, el sistema devuelve todas las filas resultante de la unión


incluidas las repetidas

El empleo de ALL también hace que la consulta se ejecute más rápidamente ya


que el sistema no tiene que eliminar las repeticiones.
Se pueden combinar varias tablas con el operador UNION. Por ejemplo
supongamos que tenemos otra tabla Pachuca con las oficinas nuevas de
Pachuca:

SELECT oficina, ciudad FROM Vallarta


UNION
SELECT oficina, ciudad FROM Michoacan
UNION
SELECT oficina, ciudad FROM Pachuca;

Combinamos las tres tablas.

Otro ejemplo:

Obtener todos los productos cuyo precio exceda de 20 € o que se haya vendido
más de 300 euros del producto en algún pedido.

BD GestionA

SELECT idfab, idproducto


FROM productos
WHERE precio > 20
UNION
SELECT fab, producto
FROM pedidos
WHERE importe > 300
La diferencia EXCEPT

Aparecen en la tabla resultante las filas de la primera consulta que no aparecen en


la segunda.

Las condiciones son las mismas que las de la unión.

Por ejemplo tenemos las tablas T1 y T2.

T1 T2
Cod
Codigo
1
2
2
3
4
4
5
5
6

SELECT cod FROM T1


EXCEPT
SELECT codigo FROM T2;

Devuelve:
Cod
1
6
Ejemplo:

Listar los productos que no aparezcan en ningún pedido.

SELECT idfab, idproducto


FROM productos
EXCEPT
SELECT DISTINCT fab, producto
FROM pedidos;

productos
idfab idproducto
aci 41001
aci 41002
aci 41003 pedidos resultado
aci 41004 fab producto idfab idproducto
aci 4100x aci 41002 aci 41001
aci 4100y aci 41003 bic 41089
aci 4100z aci 41004 bic 41672
bic 41003 aci 4100x imm 775c
bic 41089 aci 4100y imm 887h
bic 41672 aci 4100z imm 887p
fea 112 bic 41003 imm 887x
fea 114 fea 112 qsa xk48
imm 773c fea 114 qsa xk48a
imm 775c imm 773c aci 41001
imm 779c imm 779c bic 41089
imm 887h qsa xk47 bic 41672
imm 887p rei 2a44g imm 775c
imm 887x rei 2a44l imm 887h
qsa xk47 rei 2a44r imm 887p
qsa xk48 rei 2a45c imm 887x
qsa xk48a aci 41002 qsa xk48
rei 2a44g qsa xk48a
rei 2a44l
rei 2a44r
rei 2a45c

La intersección INTERSECT

Tiene una sintaxis parecida a las anteriores pero en el resultado de la intersección


aparecen las filas que están simultáneamente en las dos consultas.
Las condiciones son las mismas que las de la unión.

Retomando el ejemplo anterior:

SELECT cod FROM T1


INTERSECT
SELECT cod FROM T2;

Devuelve:

Cod
2
4
5

Ejemplo: Obtener todos los productos que valen más de 20 euros y que además
se haya vendido en un pedido más de 300 euros de ese producto.

SELECT idfab, idproducto


FROM productos
WHERE precio > 20
INTERSECT
SELECT fab, producto
FROM pedidos
WHERE importe > 300;

Resultado

Probar de Forma Parcial


La composición de tablas

Hasta ahora hemos operado con tablas que tenían el mismo esquema, pero
muchas veces lo que necesitamos es obtener una tabla que tenga en una misma
fila datos de varias tablas, por ejemplo, obtener las facturas y que en la misma fila
de factura aparezca el nombre y dirección del cliente. Pues en lo que queda del
tema estudiaremos este tipo de consultas basadas en la composición de tablas. La
composición de tablas consiste en obtener a partir de dos tablas cualesquiera una
nueva tabla fusionando las filas de una con las filas de la otra, concatenando los
esquemas de ambas tablas. Consiste en formar parejas de filas.

La sentencia SELECT permite realizar esta composición, incluyendo dos o más


tablas en la cláusula FROM.

Es hora de ampliar la cláusula FROM que vimos en el tema anterior.

Empezaremos por estudiar la operación a partir de la cual están definidas las


demás operaciones de composición de tabla, el producto cartesiano.

La composición interna INNER JOIN

Una composición interna es aquella en la que los valores de las columnas que se
están combinando se comparan mediante un operador de comparación.

Es otra forma, mejor, de expresar un producto cartesiano con una condición.

Es la operación que más emplearemos ya que lo más frecuente es querer juntar


los registros de una tabla relacionada con los registros correspondientes en la
tabla de referencia (añadir a cada factura los datos de su cliente, añadir a cada
línea de pedido los datos de su producto, etc..,).

SELECT *
FROM empleados INNER JOIN oficinas
ON empleados.oficina=oficinas.oficina;

Obtiene los empleados combinados con los datos de su oficina.

SELECT *
FROM pedidos INNER JOIN productos
ON producto = idproducto AND fab = idfab;

Obtiene los pedidos combinados con los productos correspondientes.

Normalmente la condición de combinación será una igualdad pero se puede


utilizar cualquier operador de comparación (<>, >…).
Es fácil ver la utilidad de esta instrucción y de hecho se utilizará muy a menudo,
pero hay algún caso que no resuelve. En las consultas anteriores, no aparecen las
filas que no tienen fila correspondiente en la otra tabla.

SELECT numemp,nombre,empleados.oficina, ciudad


FROM empleados INNER JOIN oficinas
ON empleados.oficina=oficinas.oficina;

numemp nombre oficina ciudad


101 Antonio Viguer 12 Aguas Calientes
102 Alvaro Jaumes 21 Baja California
103 Juan Rovira 12 Aguas Calientes
104 José González 12 Aguas Calientes
105 Vicente Pantalla 13 Colima
106 Luis Antonio 11 Vallarta
107 Jorge Gutiérrez 22 Alvarado
108 Ana Bustamante 21 Baja California

No aparecen los empleados que no tienen oficina,  ni las oficinas que no tienen
empleados, porque para que salga la fila, debe de existir una fila de la otra tabla
que cumpla la condición.

Para resolver este problema debemos utilizar otro tipo de composición, la


composición externa.
La Composición externa LEFT, RIGHT y FULL OUTER JOIN

La composición externa se escribe de manera similar al INNER JOIN indicando


una condición de combinación pero en el resultado se añaden filas que no
cumplen la condición de combinación.

La palabra OUTER es opcional y no añade ninguna función.

Las palabras LEFT, RIGHT y FULL indican la tabla de la cual se van a añadir las
filas sin correspondencia.

SELECT numemp,nombre,empleados.oficina, ciudad


FROM empleados LEFT JOIN oficinas
ON empleados.oficina=oficinas.oficina;

numemp nombre oficina ciudad


101 Antonio Viguer 12 Aguas Calientes
102 Alvaro Jaumes 21 Baja California
103 Juan Rovira 12 Aguas Calientes
104 José González 12 Aguas Calientes
105 Vicente Pantalla 13 Colima
106 Luis Antonio 11 Vallarta
107 Jorge Gutiérrez 22 Alvarado
108 Ana Bustamante 21 Baja California
109 María Sunta 11 Vallarta
110 Juan Victor NULL NULL
111 Juan Gris NULL NULL
112 Julián Martorell NULL NULL
113 Juan Gris NULL NULL

Ahora sí aparece el empleado 110 que no tiene oficina

Obtiene los empleados con su oficina y los empleados (tabla a la izquierda LEFT
del JOIN) que no tienen oficina aparecerán también en el resultado con los
campos de la tabla oficinas rellenados a NULL.
SELECT numemp,nombre,empleados.oficina, ciudad,
oficinas.oficina
FROM empleados RIGHT JOIN oficinas
ON empleados.oficina=oficinas.oficina;

numemp nombre oficina ciudad oficina


106 Luis Antonio 11 Vallarta 11
101 Antonio Viguer 12 Aguas Calientes 12
103 Juan Rovira 12 Aguas Calientes 12
104 José González 12 Aguas Calientes 12
105 Vicente Pantalla 13 Colima 13
102 Alvaro Jaumes 21 Baja California 21
108 Ana Bustamante 21 Baja California 21
107 Jorge Gutiérrez 22 Alvarado 22
NULL NULL NULL Michoacán 23
NULL NULL NULL Acapulco 24
NULL NULL NULL Vallarta 25
NULL NULL NULL Pachuca 26
NULL NULL NULL Matamoros 27
NULL NULL NULL Vallarta 28
NULL NULL NULL Vallarta 29
NULL NULL NULL Pachuca 30
NULL NULL NULL Ecatepec 31

Las oficinas de la 23 a la 31 no tienen empleados.

Obtiene los empleados con su oficina y las oficinas (tabla a la derecha RIGHT del
JOIN) que no tienen empleados aparecerán también en el resultado con los
campos de la tabla empleados rellenados a NULL.

SELECT numemp,nombre,empleados.oficina, ciudad,


oficinas.oficina
FROM empleados FULL JOIN oficinas
ON empleados.oficina=oficinas.oficina;

numemp Nombre oficina ciudad oficina


101 Antonio Viguer 12 Aguas Calientes 12
102 Alvaro Jaumes 21 Baja California 21
103 Juan Rovira 12 Aguas Calientes 12
104 José González 12 Aguas Calientes 12
105 Vicente Pantalla 13 Colima 13
106 Luis Antonio 11 Vallarta 11
107 Jorge Gutiérrez 22 Alvarado 22
108 Ana Bustamante 21 Baja California 21
109 María Sunta NULL NULL NULL
110 Juan Victor NULL NULL NULL
111 Juan Gris NULL NULL NULL
112 Julián Martorell NULL NULL NULL
113 Juan Gris NULL NULL NULL
NULL NULL NULL Michoacán 23
NULL NULL NULL Acapulco 24
NULL NULL NULL Vallarta 25
NULL NULL NULL Pachuca 26
NULL NULL NULL Matamoros 27
NULL NULL NULL Vallarta 28
NULL NULL NULL Vallarta 29
NULL NULL NULL Pachuca 30
NULL NULL NULL Ecatepec 31

Aparecen tanto los empleados sin oficina como las oficinas sin empleados.

NOTA: Cuando necesitamos obtener filas con datos de dos tablas con una
condición de combinación utilizaremos un JOIN, aconsejo empezar por escribir el
JOIN con la condición que sea necesaria para combinar las filas, y luego
plantearos si la composición debe de ser interna o externa. Para este segundo
paso ésta sería la norma a seguir:

Empezamos con INNER JOIN.

 Si pueden haber filas de la primera tabla que no estén  relacionadas con


filas de la segunda tabla y nos interesa que salgan en el resultado,
entonces cambiamos a LEFT JOIN.
 Si pueden haber filas de la segunda tabla que no estén  relacionadas con
filas de la primera tabla y nos interesa que salgan en el resultado, entonces
cambiamos a RIGHT JOIN.
 Si necesitamos LEFT y RIGHT entonces utilizamos FULL JOIN.

Siguiendo el ejemplo anterior nos preguntaríamos:

¿Pueden haber empleados que no tengan oficina y nos interesan?, si es que sí,
necesitamos un LEFT JOIN.

Seguiríamos preguntando:

¿Pueden haber oficinas que no tengan empleados y nos interesan?, si es que sí,
necesitamos un RIGHT JOIN.

Si al final necesitamos LEFT y también RIGHT entonces utilizamos FULL JOIN.


4.9. Combinar varias operaciones

En las operaciones anteriores tabla_origen puede ser a su vez una composición


de tablas, en este caso aunque sólo sea obligatorio cuando queramos cambiar el
orden de ejecución de las composiciones, es recomendable utilizar paréntesis
para delimitar las composiciones.

Por ejemplo:

SELECT numemp, nombre, empleados.oficina, ciudad,


oficinas.oficina, pedidos.*
FROM (oficinas RIGHT JOIN empleados
       ON empleados.oficina = oficinas.oficina)
        INNER JOIN pedidos on rep=numemp;

O bien:

SELECT numemp, nombre, empleados.oficina, ciudad,


oficinas.oficina, pedidos.*
FROM oficinas RIGHT JOIN (empleados INNER JOIN pedidos on rep
= numemp)
       ON empleados.oficina = oficinas.oficina);
5. Consultas de resumen (I)
5.1. Introducción

Una de las funcionalidades de la sentencia SELECT es el permitir obtener


resúmenes de los datos contenidos en las columnas de las tablas.

Para poder llevarlo a cabo la sentencia SELECT consta de una serie de cláusulas
específicas (GROUP BY, HAVING), y Transact-SQL tiene definidas unas
funciones para poder realizar estos cálculos, las funciones de agregado (también
llamadas funciones de columna).

La diferencia entre una consulta de resumen y una consulta de las que hemos
visto hasta ahora es que en las consultas normales las filas del resultado se
obtienen directamente de las filas del origen de datos y cada dato que aparece en
el resultado tiene su dato correspondiente en el origen de la consulta mientras que
las filas generadas por las consultas de resumen no representan datos del origen
sino un total calculado sobre estos datos. Esta diferencia hará que las consultas
de resumen tengan algunas limitaciones que veremos a lo largo del tema.

Un ejemplo sería:

A la izquierda tenemos una consulta simple que nos saca las oficinas con sus
ventas ordenadas por región, y a la derecha una consulta de resumen que obtiene
la suma de las ventas de las oficinas de cada región

5.2. Las funciones de agregado


Una función de agregado SQL acepta un grupo de datos (normalmente una
columna de datos) como argumento, y produce un único dato que resume el
grupo. Por ejemplo la función AVG() acepta una columna de datos numéricos y
devuelve la media aritmética (average) de los valores contenidos en la  columna.

El mero hecho de utilizar una función de agregado en una consulta, convierte ésta
en una consulta de resumen.

Todas las funciones de agregado tienen una estructura muy parecida:

Función ([ALL|DISTINCT] expression) 

El grupo de valores sobre el que actúa la función lo determina el resultado de la


expresión que será un nombre de columna o una expresión basada en una
columna o varias del origen de datos. En la expresión nunca puede aparecer una
función de agregado ni una subconsulta.

La palabra ALL indica que se tiene que tomar en cuenta todos los valores de la
columna. Es el valor por defecto.

La palabra DISTINCT hace que se consideren todas las repeticiones del mismo
valor como uno sólo (considera valores distintos).

Todas las funciones de agregado se aplican a las filas del origen de datos una vez
ejecutada la cláusula WHERE (si la hubiera).

Si exceptuamos la función COUNT, todas las funciones de agregado ignoran los


valores NULL.

Una función de agregado puede aparecer en la lista de selección en cualquier


lugar en el que puede aparecer un nombre de columna. Puede, por ejemplo,
formar parte de una expresión pero no se pueden anidar funciones de agregado.

Tampoco se pueden mezclar funciones de columna con nombres de columna


ordinarios. Hay excepciones a esta regla pero cuando definimos agrupaciones y
subconsultas que veremos más adelante.
.3. La función COUNT
COUNT ({[ALL|DISTINCT] expresion | * } )

Expresion puede ser de cualquier tipo excepto text, image o ntext. No se permite
utilizar funciones de agregado ni subconsultas. El tipo de dato devuelto es int.

Si el número de valores devueltos por expresion es superior a 231-1, COUNT


genera un error, en ese caso hay que utilizar la función COUNT_BIG.

La función cuenta los valores distintos de NULL que hay en la columna. La palabra
ALL indica que se tienen que tomar todos los valores de la columna, mientras que
DISTINCT hace que se consideren todas las repeticiones del mismo valor como
uno solo. Estos parámetros son opcionales, por defecto se considera ALL.

Por ejemplo:

SELECT COUNT(region) FROM oficinas;

Devuelve 9 porque tenemos nueve valores no nulos en la columna region. A la


hora de interpretar un COUNT es conveniente no olvidar que cuenta valores no
nulos, por ejemplo si interpretáramos la sentencia tal cual se lee, “cuántas
regiones tenemos en oficinas”  sería erróneo, realmente estamos obteniendo
cuántas oficinas tienen una región asignada.

SELECT COUNT(DISTINCT region) FROM oficinas;

Devuelve 4 porque tenemos cuatro valores distintos, no nulos, en la columna


región, los valores repetidos los considera sólo una vez. Ahora sí nos devuelve
cuántas regiones tenemos en oficinas.

Si utilizamos * en vez de expresión, devuelve el número de filas del origen que nos
quedan después de ejecutar la cláusula WHERE.

COUNT(*) no acepta parámetros y no se puede utilizar con DISTINCT. COUNT(*)


no requiere un parámetro expression porque, por definición, no utiliza información
sobre ninguna columna específica. En el recuento se incluyen las filas que
contienen valores NULL.

SELECT COUNT(*) FROM empleados WHERE oficina=12;

Obtiene el número de empleados asignados a la oficina 12.


Si tenemos un COUNT(columna) y columna no contiene valores nulos, se obtiene
el mismo resultado que COUNT(*)  pero el COUNT(*) es más rápido por lo que en
este caso hay que utilizarlo en vez de COUNT(columna).

Por ejemplo:

SELECT COUNT(*) FROM empleados WHERE oficina IS NOT NULL;

Es mejor que:

SELECT COUNT(oficina) FROM empleados WHERE oficina IS NOT NULL;

Las dos nos devuelven el número de empleados que tienen una oficina asignada
pero la primera es mejor porque se calcula más rápidamente.

5.4. La función MAX


MAX ([ALL|DISTINCT] expression) 

Devuelve el valor máximo de la expresión sin considerar los nulos.

MAX se puede usar con columnas numéricas, de caracteres y de datetime, pero


no con columnas de bit. No se permiten funciones de agregado ni subconsultas.

Utilizar DISTINCT no tiene ningún sentido con MAX (el valor máximo será el
mismo si consideramos las repeticiones o no) y sólo se incluye para la
compatibilidad con SQL-92.

Por ejemplo:

SELECT SUM(ventas) AS VentasTotales, MAX(objetivo) AS MayorObjetivo


FROM oficinas;

Devuelve 9 porque tenemos nueve valores no nulos en la columna region. A la


hora de interpretar un COUNT es conveniente no olvidar que cuenta valores no
nulos, por ejemplo si interpretáramos la sentencia tal cual se lee, “cuántas
regiones tenemos en oficinas”  sería erróneo, realmente estamos obteniendo
cuántas oficinas tienen una región asignada.

5.5. La función MIN


MIN ([ALL|DISTINCT] expression)

Devuelve el valor mínimo de la expresión sin considerar los nulos.

MIN se puede usar con columnas numéricas, de caracteres y de datetime, pero


no con columnas de bit. No se permiten funciones de agregado ni subconsultas.
Utilizar DISTINCT no tiene ningún sentido con MIN (el valor mínimo será el mismo
si consideramos las repeticiones o no) y sólo se incluye para la compatibilidad con
SQL-92.
5.6. La función SUM
SUM ([ALL|DISTINCT] expresion )

Devuelve la suma de los valores devueltos por la expresión.

Sólo puede utilizarse con columnas numéricas.

El resultado será del mismo tipo aunque puede tener una precisión mayor.

SELECT SUM(importe) FROM pedidos;

Obtiene el importe total vendido en todos los pedidos.

SELECT SUM(ventas) AS VentasTotales, MAX(objetivo) AS MayorObjetivo


FROM oficinas;

Devuelve la suma de las ventas de todas las oficinas y de los objetivos de todas
las oficinas, el de mayor importe.

5.7. La función AVG


AVG ([ALL|DISTINCT] expresion )

Devuelve el promedio de los valores de un grupo, para calcular el promedio se


omiten los valores nulos.

El grupo de valores lo determina el resultado de la expresión que será un nombre


de columna o una expresión basada en una columna o varias del origen de datos.

La función se aplica también a campos numéricos, y en este caso el tipo de dato


del resultado puede cambiar según las necesidades del sistema para representar
el valor del resultado.

5.8. La función VAR


VAR ([ALL|DISTINCT] expresion )

Devuelve la varianza estadística de todos los valores de la expresión especificada.

VAR sólo se puede utilizar con columnas numéricas. Los valores NULL se pasan
por alto.

5.9. La función VARP


VARP ([ALL|DISTINCT] expresion )
Devuelve la varianza estadística de la población para todos los valores de la
expresión especificada.

Sólo se puede utilizar con columnas numéricas. Los valores NULL se pasan por
alto.

5.10.  La función STDEV


STDEV ([ALL|DISTINCT] expresion )

Devuelve la desviación típica estadística de todos los valores de la expresión


especificada.

Sólo se puede utilizar con columnas numéricas. Los valores NULL se pasan por
alto.

5.11. La función STDEVP


STDEVP ([ALL|DISTINCT] expresion )

Devuelve la desviación estadística estándar para la población de todos los valores


de la expresión especificada.

Sólo se puede utilizar con columnas numéricas. Los valores NULL se pasan por
alto.

5.12. La función GROUPING


GROUPING (nb_columna)

Es una función de agregado que genera como salida una columna adicional con el
valor 1 si la fila se agrega mediante el operador CUBE o ROLLUP, o el valor 0
cuando la fila no es el resultado de CUBE o ROLLUP.

Nb_columna tiene que ser una de las columnas de agrupación y la cláusula


GROUP BY debe contener  el operador CUBE o ROLLUP.

En el siguiente punto, cuando veamos las cláusulas CUBE y ROLLUP quedará


más claro.
5.13. Agrupamiento de filas (cláusula GROUP BY).

Hasta ahora las consultas sumarias que hemos visto obtienen totales de todas las
filas del origen y producen una única fila de resultado.

Muchas veces cuando calculamos resúmenes nos interesan totales parciales, por
ejemplo saber de cada empleado cuánto ha vendido, y cuál ha sido su pedido
máximo, de cada cliente cuándo fue la última vez que nos compró, etc.

En todos estos casos en vez de obtener una fila única de resultados necesitamos
una fila por cada empleado, cliente, etc.

 Podemos obtener estos subtotales con la cláusula GROUP BY.

GROUP BY [ ALL ] expresion_agrupacion [ ,...n ]


[ WITH { CUBE | ROLLUP } ]

Una consulta con una cláusula GROUP BY agrupa los datos de la tabla origen y
produce una única fila resultado por cada grupo formado. Las columnas indicadas
en el GROUP BY se llaman columnas de agrupación o agrupamiento .

Cuando queremos realizar una agrupación múltiple, por varias columnas, éstas se
indican en la cláusula GROUP BY en el orden de mayor a menor agrupación igual
que con la cláusula ORDER BY.

expresion_agrupacion puede ser una columna o una expresión no agregada que


haga referencia a una columna devuelta por la cláusula FROM. Un alias de
columna que esté definido en la lista de selección no puede utilizarse para
especificar una columna de agrupamiento.

No se pueden utilizar columnas de tipo text, ntext e image en


expresion_agrupacion.

En las cláusulas GROUP BY que no contengan CUBE o ROLLUP, el número de


columnas de agrupación está limitado por los tamaños de columna de GROUP
BY, las columnas de agregado y los valores de agregado que participan en la
consulta. Este límite procede del límite de 8.060 bytes de la tabla de trabajo
intermedia que se necesita para contener los resultados intermedios de la
consulta. Se permite un máximo de 10 expresiones de agrupamiento cuando se
especifica CUBE o ROLLUP.

Si en la columna de agrupación existen valores nulos, se generará una fila de


resumen para este “valor”, en este caso se considera el valor nulo como otro valor
cualquiera.
Ejemplo:

SELECT oficina, count(numemp) AS [Número de empleados]


FROM empleados
GROUP BY oficina;

Resultado:

oficina Número de empleados


NULL 2
11 2
12 3
13 1
21 2
22 1

Hay empleados sin oficinas (con oficina a nulo), estos forman un grupo con el
valor NULL en oficina, en este caso hay dos empleados así.

Podemos indicar varias columnas de agrupación.

Ejemplo:

SELECT rep, clie, count(numpedido) AS [Número de pedidos], MAX(importe)


AS [Importe máximo]
FROM pedidos
WHERE YEAR(fechapedido) = 1997
GROUP BY  rep, clie
ORDER BY rep, clie;

Resultado:

Número Importe
rep clie
de pedidos máximo
101 2113 1 225,00
102 2106 2 21,30
102 2120 1 37,50
103 2111 2 21,00
105 2103 4 275,00
105 2111 1 37,45
106 2101 1 14,58
107 2109 1 313,50
107 2124 2 24,30
108 2112 1 29,25
108 2114 1 71,00
108 2118 3 14,20
De cada representante obtenemos el número de pedidos y el importe máximo
vendido a cada cliente, de las ventas de 1997. La cláusula ORDER BY se ha
incluido para que las filas aparezcan ordenadas y quede más claro.

Hemos dicho que los resúmenes se calculan sobre todas las filas del origen
después de haber ejecutado el WHERE, pues ALL permite obtener un resumen de
las filas que no cumplen el WHERE.

ALL Incluye todos los grupos y conjuntos de resultados, incluso aquellos en los
que no hay filas que cumplan la condición de búsqueda especificada en la
cláusula WHERE. Cuando se especifica ALL, se devuelven valores NULL para las
columnas de resumen de los grupos que no cumplen la condición de búsqueda.
No se puede especificar ALL con los operadores CUBE y ROLLUP.

GROUP BY ALL no se admite en consultas que tienen acceso a tablas remotas si


también hay una cláusula WHERE en la consulta.

Por ejemplo, vamos a modificar la consulta anterior:

SELECT rep, clie, count(numpedido) AS [Número de pedidos], MAX(importe)


AS [Importe máximo]
FROM pedidos
WHERE YEAR(fechapedido) = 1997
GROUP BY  ALL rep, clie
ORDER BY rep, clie;

Resultado:

Número Importe
rep clie
de pedidos máximo
101 2102 0 NULL
101 2108 0 NULL
101 2113 1 225,00
102 2106 2 21,30
102 2120 1 37,50
103 2111 2 21,00
105 2103 4 275,00
105 2111 1 37,45
106 2101 1 14,58
106 2117 0 NULL
107 2109 1 313,50
107 2124 2 24,30
108 2112 1 29,25
108 2114 1 71,00
108 2118 3 14,20
Cuál ha sido el efecto de añadir ALL? Se han añadido filas para las filas del origen
que no cumplen la condición del WHERE pero sin que intervengan en el cálculo de
las funciones de agregado.

Por ejemplo el representante 101 tiene pedidos con el cliente 2102 pero estos
pedidos no son del año 1997, por eso aparece la primera fila (no estaba en el
resultado de la otra consulta) pero con 0 y NULL como resultados de las funciones
de agregado.

ROLLUP especifica que, además de las filas que normalmente proporciona


GROUP BY, se incluyen filas de resumen en el conjunto de resultados. Los grupos
se resumen en un orden jerárquico, desde el nivel inferior del grupo hasta el
superior. La jerarquía del grupo se determina por el orden en que se especifican
las columnas de agrupamiento. Cambiar el orden de las columnas de
agrupamiento puede afectar al número de filas generadas en el conjunto de
resultados.

Por ejemplo:

SELECT rep, clie, count(numpedido) AS [Número de pedidos], MAX(importe)


AS [Importe máximo]
FROM pedidos
WHERE YEAR(fechapedido) = 1997
GROUP BY  rep, clie WITH ROLLUP;

  Resultado:

Número Importe
rep clie
de pedidos máximo
101 2113 1 225,00
101 NULL 1 225,00
102 2106 1 21,30
102 2120 1 37,50
102 NULL 3 37,50
103 2111 2 21,00
103 NULL 2 21,00
105 2103 4 275,00
105 2111 1 37,45
105 NULL 5 275,00
106 2101 1 14,28
106 NULL 1 14,28
107 2109 1 313,50
107 2124 2 24,30
107 NULL 3 313,50
108 2112 1 29,25
108 2114 1 71,00
108 2118 3 14,20
108 NULL 5 71,00
... ... ... ...
NULL NULL 23 450,00

Efecto: Se han añadido automáticamente subtotales por cada nivel de


agrupamiento y una línea de totales generales al final. En este caso no hemos
incluido ORDER BY porque las filas salen ya ordenadas.
5. Consultas de resumen (VI)
CUBE especifica que, además de las filas que normalmente proporciona GROUP
BY, deben incluirse filas de resumen en el conjunto de resultados. Se devuelve
una fila de resumen GROUP BY por cada posible combinación de grupo y
subgrupo del conjunto de resultados. En el resultado se muestra una fila de
resumen GROUP BY como NULL, pero se utiliza para indicar todos los valores.

Por ejemplo:

SELECT rep, clie, count(numpedido) AS [Número de pedidos], MAX(importe)


AS [Importe máximo]
FROM pedidos
WHERE YEAR(fechapedido) = 1997
GROUP BY  rep, clie WITH CUBE;

Resultado:

Número Importe
rep clie
de pedidos máximo
101 2113 1 225,00
101 NULL 1 225,00
102 2106 1 21,30
102 2120 1 37,50
102 NULL 3 37,50
103 2111 2 21,00
103 NULL 2 21,00
105 2103 4 275,00
105 2111 1 37,45
105 NULL 5 275,00
106 2101 1 14,28
106 NULL 1 14,28
107 2109 1 313,50
107 2124 2 24,30
107 NULL 3 313,50
108 2112 1 29,25
108 2114 1 71,00
108 2118 3 14,20
108 NULL 5 71,00
... ... ... ...
NULL NULL 23 450,00
NULL 2101 1 14,58
NULL 2103 4 275,00
NULL 2106 2 21,30
NULL 2107 1 6,32
NULL 2108 1 56,25
NULL 2109 1 313,50
NULL 2111 3 37,45
NULL 2112 2 450,00
NULL 2113 1 225,00
NULL 2114 1 71,00
NULL 2118 3 14,20
NULL 2120 1 37,50
NULL 2124 2 24,30

Efecto: Obtenemos además de los resultados obtenidos con ROLLUP (los totales
por cada representante), los totales por el otro criterio (los totales por cada
cliente).

El número de filas de resumen del conjunto de resultados se determina mediante


el número de columnas que contiene la cláusula GROUP BY. Cada operando
(columna) de la cláusula GROUP BY se enlaza según el agrupamiento NULL y se
aplica el agrupamiento al resto de los operandos (columnas). CUBE devuelve
todas las combinaciones posibles de grupo y subgrupo.

Tanto si utilizamos CUBE como ROLLUP, nos será útil la función de agregado
GROUPING.

Si cogemos por ejemplo la primera fila remarcada (101 NULL …) el valor NULL, no
sabemos si se refiere a una fila de subtotal o a que el representante 101 ha
realizado un pedido sin número de cliente. Para poder salvar este problema se
utiliza la función de agregado GROUPING.

SELECT rep, clie, count(numpedido) AS [Número de pedidos], MAX(importe)


AS [Importe máximo], GROUPING(clie) AS [Fila resumen]
FROM pedidos
WHERE YEAR(fechapedido) = 1997
GROUP BY  rep, clie WITH ROLLUP;
Número Importe Fila
rep clie
de pedidos máximo Resumen
101 2113 1 225,00 0
101 NULL 1 225,00 1
102 2106 2 21,30 0
102 2120 1 37,50 0
102 NULL 3 37,50 1
103 2111 2 21,00 0
Las filas que corresponden a subtotales aparecen con un 1 y las normales con un
cero.

Ahora que estamos más familiarizados con las columnas de agrupamiento


debemos comentar una regla a no olvidar:

EN LA LISTA DE SELECCIÓN DE UNA CONSULTA DE RESUMEN UN NOMBRE


DE COLUMNA NO PUEDE APARECER FUERA DE UNA FUNCIÓN DE
AGREGADO SI NO ES UNA COLUMNA DE AGRUPACIÓN.
Consultas de resumen (VII)
5.15. Selección sobre grupos de filas, la cláusula HAVING

Cuando queremos incluir una cláusula de selección sobre las filas del origen,
utilizamos la cláusula WHERE, pero cuando estamos definiendo una consulta de
resumen, no podemos utilizar esta cláusula para seleccionar filas del resultado ya
que cada una de éstas representa un grupo de filas de la tabla original. Para
seleccionar filas del resumen tenemos la cláusula HAVING.

HAVING condición de búsqueda

HAVING funciona igual que la cláusula WHERE pero en vez de actuar sobre las
filas del origen de datos, actúa sobre las filas del resultado, selecciona grupos de
filas por lo que la condición de búsqueda sufrirá alguna limitación, la misma que
para la lista de selección:

Ejemplo:

SELECT oficina, count(numemp) AS [Número de empleados]


FROM empleados
GROUP BY oficina
HAVING COUNT(numemp)<2;

Resultado:

oficina Número de empleados


13 1
22 1

Esta SELECT es la misma que la del primer ejemplo del apartado sobre la
cláusula GROUP BY, la diferencia es que le hemos añadido la cláusula HAVING,
que hace que del resultado sólo se visualicen los grupos que cumplan la
condición. Es decir sólo aparecen las oficinas que tienen menos de 2 empleados.

Siempre que en una condición de selección haya una función de columna, la


condición deberá incluirse en la cláusula HAVING, además, como HAVING filtra
filas del resultado, sólo puede contener expresiones (nombres de columnas,
expresiones, funciones…) que también pueden aparecer en la lista de selección,
por lo que también se aplica la misma regla a no olvidar:

EN LA CLÁUSULA HAVING UN NOMBRE DE COLUMNA NO PUEDE


APARECER FUERA DE UNA FUNCIÓN DE AGREGADO SI NO ES UNA
COLUMNA DE AGRUPACIÓN.
Las expresiones que pongamos en HAVING no tienen porqué aparecer en la lista
de selección, por ejemplo en la SELECT anterior se podía haber escrito:

HAVING SUM(ventas)=10000

Ejercicios Consultas de resumen


Para realizar los ejercicios, deberás utilizar la base de datos GestionSimples.

Ejercicio 1: Funciones de agregado

1. ¿Cuántas oficinas tenemos en Vallarta?

Resultado:

Vallartanas
4
2. Hallar cuántos pedidos hay de más de 250 euros.

Resultado:

Superiores a 250
4
3. ¿Cuántos títulos (cargos) de empleados se usan?

Resultado:

Cuántos títulos
3
4. ¿Entre qué cuotas se mueven los empleados?

Resultado:

Cuota mínima Cuota máxima


3000,00 35000,00

Ejercicio 2: Agrupamiento de filas: GROUP BY

1. De cada vendedor (todos) queremos saber su nombre y el importe total


vendido. En caso de que el importe sea NULL, cambiarlo por 0,00 con la
función ISNULL().

Resultado:

numemp nombre Importe vendido


101 Antonio Viguer 266,28
102 Alvaro Jaumes 77,76
103 Juan Rovira 21,00
104 José González 0,00
105 Vicente Pantalla 772,746
106 Luis Antonio 46,08
107 Jorge Gutiérrez 344,32
108 Ana Bustamante 286,33
109 María Sunta 71,05
110 Juan Victor 478,82
111 Juan Gris 0,00
112 Julián Martorell 0,00
113 Juan Gris 0,00
114 Pablo Moreno 0,00

2. De cada empleado, obtener el importe vendido a cada cliente.

Resultado:

rep Cliente Importe vendido


106 2101 14,58
101 2102 39,78
105 2103 735,296
102 2106 40,26
110 2107 28,82
101 2108 1,50
109 2108 71,05
107 2109 313,50
103 2111 21,00
105 2111 37,45
108 2112 29,25
110 2112 450,00
101 2113 225,00
108 2114 221,00
106 2117 31,50
108 2118 36,08
102 2120 37,50
107 2124 30,82
3. Repetir la consulta anterior pero ahora deben aparecer también los
empleados que no han vendido nada.

Resultado:

rep Cliente Importe vendido


101 2102 39,78
101 2108 1,50
101 2113 225,00
102 2106 40,26
102 2120 37,50
103 2111 21,00
104 NULL NULL
105 2103 735,296
105 2111 37,45
106 2101 14,58
106 2117 31,50
107 2109 313,50
107 2124 30,82
108 2112 29,25
108 2114 221,00
108 2118 36,08
109 2108 71,05
110 2107 28,82
110 2112 450,00
111 NULL NULL
112 NULL NULL
113 NULL NULL
114 NULL NULL
4. Repetir la consulta pero ahora debe aparecer también el total de cuánto ha
vendido cada empleado. (Recuerda una opción de la cláusula GROUP BY)

Resultado:

rep clie Importe vendido


NULL NULL 2364,386
101 NULL 266,28
101 2102 39,78
101 2108 1,50
101 2113 225,00
102 NULL 77,76
102 2106 40,26
102 2120 37,50
103 NULL 21,00
103 2111 21,00
104 NULL NULL
104 NULL NULL
105 NULL 772,746
105 2103 735,296
105 2111 37,45
106 NULL 46,08
106 2101 14,58
106 2117 31,50
107 NULL 344,32
107 2109 313,50
107 2124 30,82
108 NULL 286,33
108 2112 29,25
108 2114 221,00
108 2118 36,08
109 NULL 71,05
109 2108 71,05
110 NULL 478,82
110 2107 28,82
110 2112 450,00
111 NULL NULL
111 NULL NULL
112 NULL NULL
112 NULL NULL
113 NULL NULL
113 NULL NULL
114 NULL NULL
114 NULL NULL

5. En los resultados anteriores no se distinguen bien las líneas que corresponden a totales.
Modificar la consulta para obtener este resultado:

Agrupa Agrupa
rep clie Importe vendido
clie numemp
NULL NULL 2364,386 1 1
101 NULL 266,28 1 0
101 2102 39,78 0 0
101 2108 1,50 0 0
101 2113 225,00 0 0
102 NULL 77,76 1 0
102 2106 40,26 0 0
102 2120 37,50 0 0
103 NULL 21,00 1 0
103 2111 21,00 0 0
104 NULL NULL 0 0
104 NULL NULL 1 0
... .... ... (sigue) ... ...
6. Ahora modifica la consulta para que las filas de totales aparezcan más
claras. (Recuerda la función CASE)

Resultado:

Agrupa
rep clie Importe vendido Agrupa clie
numemp
NULL NULL 2364,386 Total empleado Total final
101 NULL 266,28 Total empleado  
101 2102 39,78    
101 2108 1,50    
101 2113 225,00    
102 NULL 77,76 Total empleado  
102 2106 40,26    
102 2120 37,50    
103 NULL 21,00 Total empleado  
103 2111 21,00    
104 NULL NULL    
104 NULL NULL Total empleado  
... .... ... (sigue) ... ...

7. Ahora coloca las columnas Agrupa delante de las demás columnas:

Resultado:

Agrupa
Agrupa clie rep clie Importe vendido
numemp
Total final Total empleado NULL NULL 2364,386
  Total empleado 101 NULL 266,28
    101 2102 39,78
    101 2108 1,50
    101 2113 225,00
  Total empleado 102 NULL 77,76
    102 2106 40,26
    102 2120 37,50
  Total empleado 103 NULL 21,00
    103 2111 21,00
    104 NULL NULL
  Total empleado 104 NULL NULL
... (sigue) ... ... .... ...

8. Ahora queremos que "Total empleado" aparezca en la columna clie. Piensa


primero en cuántas columnas quieres y luego en cada columna que tiene
que salir.

Resultado:

Agrupa
numemp clie Importe vendido
numemp
Total final NULL Total empleado 2364,386
  101 Total empleado 266,28
  101 2102 39,78
  101 2108 1,50
  101 2113 225,00
  102 Total empleado 77,76
  102 2106 40,26
  102 2120 37,50
  103 Total empleado 21,00
  103 2111 21,00
  104 NULL NULL
  104 Total empleado NULL
... (sigue) ... .... ...

9. El empleado 104 (y otros) no ha vendido a nadie y por eso sale en la


columna clie la palabra NULL, queremos que en estos casos no aparezca
nada (se deje en blanco), y el importe si es NULL que salga un cero.

Resultado:

Agrupa
numemp clie Importe vendido
numemp
Total final NULL Total empleado 2364,386
  101 2102 39,78
  101 2108 1,50
  101 2113 225,00
  101 Total empleado 266,28
  102 2106 40,26
  102 2120 37,50
  102 Total empleado 77,76
  103 2111 21,00
  103 Total empleado 21,00
  104   0,00
  104 Total empleado 0,00
... (sigue) ... .... ...

Nota: Recuerda la función  ISNULL() (para la columna importe vendido)  y


la función CASE con diferentes condiciones (para la columna clie).

10. Lo rematamos para que el resultado quede así:

  numemp clie Importe vendido


Total final ... ... 2364,386
  101 2102 39,78
  101 2108 1,50
  101 2113 225,00
  101 Total empleado 266,28
  102 2106 40,26
  102 2120 37,50
  102 Total empleado 77,76
  103 2111 21,00
  103 Total empleado 21,00
  104   0,00
  104 Total empleado 0,00
... (sigue) ... .... ...
Ejercicio 1: Funciones de agregado

1. ¿Cuántas oficinas tenemos en Vallarta?

SELECT COUNT(*) AS Vallartanas


FROM oficinas
WHERE ciudad = 'Vallarta';

2. Hallar cuántos pedidos hay de más de 250 euros.

SELECT COUNT(*) AS [Superiores a 250]


FROM pedidos
WHERE importe > 250;

3. ¿Cuántos títulos (cargos) de empleados se usan?

SELECT COUNT(DISTINCT titulo) AS [Cuántos títulos]


FROM empleados;

4. ¿Entre qué cuotas se mueven los empleados?

SELECT MIN(cuota) AS [Cuota mínima], MAX(cuota) AS [Cuota máxima]


FROM empleados;

Ejercicio 2: Agrupamiento de filas: GROUP BY

1. De cada vendedor (todos) queremos saber su nombre y el importe total


vendido. En caso de que el importe sea NULL, cámbialo por 0,00 con la función
ISNULL().

SELECT numemp, nombre, ISNULL(SUM(importe),0) AS [Importe vendido]


FROM empleados LEFT JOIN pedidos ON numemp = rep
GROUP BY numemp, nombre;

2. De cada empleado, obtener el importe vendido a cada cliente.

SELECT rep, clie AS Cliente, SUM(importe) AS [Importe vendido]


FROM pedidos
GROUP BY rep, clie;

3. Repetir la consulta anterior pero ahora deben aparecer también los


empleados que no han vendido nada.

SELECT numemp, clie, SUM(importe) AS [Importe vendido]


FROM empleados LEFT JOIN pedidos ON numemp = rep
GROUP BY numemp, clie
ORDER BY numemp,clie;
4. Repetir la consulta pero ahora debe aparecer también el total de cuánto ha
vendido cada empleado.

SELECT numemp, clie, SUM(importe) AS [Importe vendido]


FROM empleados LEFT JOIN pedidos ON numemp = rep
GROUP BY numemp, clie WITH ROLLUP
ORDER BY numemp,clie;

5. En los resultados anteriores no se distinguen bien las líneas que


corresponden a totales. Modificar la consulta para indicar con un 1 si es una fila de
totales y con un 0 si no lo es.

SELECT numemp, clie, SUM(importe) AS [Importe vendido], GROUPING(clie) AS


[Agrupa clie], GROUPING(numemp) AS [Agrupa numemp]
FROM empleados LEFT JOIN pedidos ON numemp = rep
GROUP BY numemp, clie WITH ROLLUP
ORDER BY numemp,clie;

6. Ahora modifica la consulta para que las filas de totales aparezcan más claras,
substituyendo el 1 de Agrupa clie por "Total empleado", el 1 de Agrupa numemp
por Total final y el valor 0 por espacio en blanco.

SELECT numemp, clie, SUM(importe) AS [Importe vendido],


CASE GROUPING(clie) WHEN 0 THEN ' ' WHEN 1 THEN 'Total empleado' END
AS [Agrupa clie],
CASE GROUPING(numemp) WHEN 0 THEN ' ' ELSE 'Total Final' END AS
[Agrupa numemp]
FROM empleados LEFT JOIN pedidos ON numemp = rep
GROUP BY numemp, clie WITH ROLLUP
ORDER BY numemp,clie;

7. Ahora coloca las columnas Agrupa delante de las demás columnas.

SELECT CASE GROUPING(numemp) WHEN 0 THEN ' ' ELSE 'Total Final' END AS
[Agrupa numemp],
CASE GROUPING(clie) WHEN 0 THEN ' ' WHEN 1 THEN 'Total empleado'
END AS [Agrupa clie],
numemp, clie, SUM(importe) AS [Importe vendido]
FROM empleados LEFT JOIN pedidos ON numemp = rep
GROUP BY numemp, clie WITH ROLLUP
ORDER BY numemp,clie;

8. Ahora queremos que "Total empleado" aparezca en la columna clie.

SELECT CASE GROUPING(numemp) WHEN 0 THEN ' ' ELSE 'Total Final' END AS
[Agrupa numemp],
numemp,
CASE GROUPING(clie) WHEN 0 THEN CONVERT(CHAR(4),clie) WHEN 1
THEN 'Total empleado' END AS [Clie],
SUM(importe) AS [Importe vendido]
FROM empleados LEFT JOIN pedidos ON numemp = rep
GROUP BY numemp, clie WITH ROLLUP
ORDER BY numemp,clie;
9. El empleado 104 (y otros) no ha vendido a nadie y por eso sale en la columna
clie la palabra NULL, queremos que en estos casos no aparezca nada (se deje en
blanco), y el importe si es NULL que salga un cero.

SELECT CASE GROUPING(numemp) WHEN 0 THEN ' ' ELSE 'Total Final' END AS
[Agrupa numemp],
numemp,
CASE WHEN GROUPING(clie) = 1 THEN 'Total empleado' WHEN clie IS
NULL THEN ' ' ELSE CONVERT(CHAR(4),clie) END AS [Clie],
ISNULL(SUM(importe),0) AS [Importe vendido]
FROM empleados LEFT JOIN pedidos ON numemp = rep
GROUP BY numemp, clie WITH ROLLUP
ORDER BY numemp,clie;

10. Lo rematamos para que la fila del Total final no muestre los valores "NULL"
ni "Total empleado". En cambio, los cambiaremos por tres puntos. Tampoco
mostraremos el encabezado "Agrupa numemp".

SELECT CASE GROUPING(numemp) WHEN 0 THEN ' ' ELSE 'Total Final' END AS
[ ],
ISNULL(CONVERT(CHAR(3),numemp),'... ') AS [numemp],
CASE WHEN GROUPING(clie) = 1 AND GROUPING(numemp) = 0
THEN 'Total empleado' WHEN GROUPING(clie) = 1
AND GROUPING(numemp) = 1 THEN '...'
WHEN clie IS NULL THEN ' ' ELSE CONVERT(CHAR(4),clie) END AS
[Clie],
ISNULL(SUM(importe),0) AS [Importe vendido]
FROM empleados LEFT JOIN pedidos ON numemp = rep
GROUP BY numemp, clie WITH ROLLUP
ORDER BY numemp,clie;
Subconsultas
Introducción

Una subconsulta es una consulta que aparece dentro de otra consulta o


subconsultas, en la lista de selección o en la cláusula WHERE o HAVING,
originalmente no se podían incluir en la lista de selección.

Una subconsulta se denomina también consulta o selección interna, mientras que


la instrucción que contiene la subconsulta es conocida como consulta o selección
externa.

Aparece siempre encerrada entre paréntesis y tiene la misma sintaxis que una
sentencia SELECT normal con alguna limitación:

No puede incluir una cláusula COMPUTE o FOR BROWSE y sólo puede incluir
una cláusula ORDER BY cuando se especifica también una cláusula TOP.

Una subconsulta puede anidarse en la cláusula WHERE o HAVING de una


instrucción externa SELECT, INSERT, UPDATE o DELETE, o bien en otra
subconsulta. Se puede disponer de hasta 32 niveles de anidamiento, aunque el
límite varía dependiendo de la memoria disponible y de la complejidad del resto de
las expresiones de la consulta. Hay que tener en cuenta que para cada fila de la
consulta externa, se calcula la subconsulta, si anidamos varias consultas, el
número de veces que se ejecutarán las subconsultas ¡puede dispararse!

Cuando la subconsulta aparece en la lista de selección de otra consulta, deberá


devolver un solo valor, de lo contrario provocará un error.

Ejemplo de subconsulta: Listar los empleados cuya cuota no supere el importe


vendido por el empleado.

SELECT nombre
FROM empleados
WHERE cuota <= (SELECT SUM(importe)
FROM pedidos
WHERE rep = numemp);

Por cada fila de la tabla de empleados (de la consulta externa) se calcula la


subconsulta y se evalúa la condición, por lo que utilizar una subconsulta puede en
algunos casos ‘ralentizar’ la consulta, en contrapartida se necesita menos
memoria que una composición de tablas.
Muchas de las instrucciones Transact-SQL que incluyen subconsultas se pueden
formular también utilizando composiciones de tablas. Otras preguntas se pueden
formular sólo con subconsultas.

En Transact-SQL, normalmente no hay una regla fija en cuanto a diferencias de


rendimiento entre una instrucción que incluya una subconsulta y una versión
semánticamente equivalente que no la incluya.

Podremos utilizar una subconsulta siempre y cuando no se quiera que aparezcan


en el resultado columnas de la subconsulta ya que si una tabla aparece en la
subconsulta y no en la consulta externa, las columnas de esa tabla no se pueden
incluir en la salida (la lista de selección de la consulta externa).

En el ejemplo, se toma el primer empleado (numemp= 101, por ejemplo) y se


calcula la subconsulta sustituyendo numemp por el valor 101, se calcula la suma
de los pedidos del rep = 101, y el resultado se compara con la cuota de ese
empleado, y así se repite el proceso con todas las filas de empleados.

El nombre de una columna dentro de la subconsulta se presupone del origen de


datos de la subconsulta y, sólo si no se encuentra en ese origen, la considera
como columna externa y la busca en el origen de la consulta externa.

Por ejemplo:

SELECT oficina, ciudad


FROM oficinas
WHERE objetivo > (SELECT SUM(ventas)
FROM empleados
WHERE oficina = oficina);

 La columna oficina se encuentra en los dos orígenes (oficinas y empleados) pero
esta consulta no dará error (no se nos pedirá cualificar los nombres como pasaría
en una composición de tablas), dentro de la subconsulta se considera oficina el
campo de la tabla empleados. Con lo que compararía la oficina del empleado con
la misma oficina del empleado y eso no es lo que queremos, queremos comparar
la oficina del empleado con la oficina de oficinas, lo escribiremos pues así para
forzar a que busque la columna en la tabla oficinas.

SELECT oficina, ciudad


FROM oficinas
WHERE objetivo > (SELECT SUM(ventas)
FROM empleados
WHERE oficina = oficinas.oficina);
Subconsultas de resultado único

Existen subconsultas que deben obligatoriamente devolver un único valor, son las
que aparecen en la lista de selección de la consulta externa o las que aparecen en
WHERE o HAVING combinadas con un operador de comparación sin modificar.

Los operadores de comparación sin modificar son los operadores de comparación


que vimos con la cláusula WHERE.

Sintaxis:

<expresion> {=|<>|!=|>|>=|!>|<|<=|!<} <subconsulta>

En este caso la segunda expresión será una subconsulta, con una sola columna
en la lista de selección y deberá devolver una única fila.

Ese valor único será el que se compare con el resultado de la primera expresión.

Si la subconsulta no devuelve ninguna fila, la comparación opera como si la


segunda expresión fuese nula.

Si la subconsulta devuelve más de una fila o más de una columna, da error.

Ejemplo:

SELECT nombre
FROM empleados
WHERE cuota <= (SELECT SUM(importe)
FROM pedidos
WHERE rep = numemp);

La subconsulta devuelve una sola columna y como maximo una fila ya que es una
consulta de resumen sin cláusula GROUP BY.

6.3. Subconsultas de lista de valores

Otro tipo de subconsultas son las que devuelven una lista de valores en forma de
una columna y cero, una o varias filas.

Estas consultas aparecen en las cláusulas WHERE o HAVING combinadas con el


operador IN o con comparaciones modificadas.

6.4. El operador IN con subconsulta


<expresion> IN subconsulta

IN examina si el valor de expresion es uno de los valores incluidos en la lista de


valores generados por la subconsulta.

La subconsulta tiene que generar valores de un tipo compatible con la expresión.

Ejemplo:

SELECT *
FROM empleados
WHERE oficina IN (SELECT oficina
FROM oficinas
WHERE region = 'Este');

Por cada empleado se calcula la lista de las oficinas del Este (nº de oficina) y se evalúa si la oficina
del empleado está en esta lista. Obtenemos pues los empleados de oficinas del Este.

numem eda oficin


nombre titulo contrato jefe cuota ventas
p d a
101 Antonio 45 12 representant 1986-10-20 104 30000.0 30500.0
Viguer e 00:00:00.00 0 0
0
103 Juan 29 12 representant 1987-03-01 104 27500.0 28600.0
Rovira e 00:00:00.00 0 0
0
104 José 33 12 dir ventas 1987-05-19 106 20000.0 14300.0
Gonzále 00:00:00.00 0 0
z 0
105 Vicente 37 13 representant 1988-02-12 104 35000.0 36800.0
Pantalla e 00:00:00.00 0 0
0
106 Luis 52 11 director 1988-06-14 NUL 27500.0 29900.0
Antonio general 00:00:00.00 L 0 0
0
114 Pablo 45 13 representant 2008-11-03 NUL 40000.0 37000.0
Moreno e 00:00:00.00 L 0 0
0

Si la subconsulta no devuelve ninguna fila:

SELECT *
FROM empleados
WHERE oficina IN (SELECT oficina
FROM oficinas
WHERE region = 'Otro');

La lista generada está vacía por lo que la condición IN devuelve FALSE y en este
caso no sale  ningún empleado.
6.5. La comparación modificada (ANY,
ALL)
Los operadores de comparación que presentan una subconsulta se pueden
modificar mediante las palabras clave ALL, ANY o SOME. SOME es un
equivalente del estándar de SQL-92 de ANY.

Se utiliza este tipo de comparación cuando queremos comparar el resultado de la


expresión con una lista de valores y actuar en función del modificador empleado.

El test ANY
<expresion> {=|<>|!=|>|>=|!>|<|<=|!<} {ANY|SOME} subconsulta

ANY significa que, para que una fila de la consulta externa satisfaga la condición
especificada, la comparación se debe cumplir para al menos un valor de los
devueltos por la subconsulta.

Por cada fila de la consulta externa se evalúa la comparación con cada uno de los
valores devueltos por la subconsulta y si la comparación es True para alguno de
los valores ANY es verdadero, si la comparación no se cumple con ninguno de los
valores de la consulta, ANY da False a no ser que todos los valores devueltos por
la subconsulta sean nulos en tal caso ANY dará NULL.

Si la subconsulta no devuelve filas ANY da False incluso si expresion es nula.

Ejemplo:

SELECT *
FROM empleados
WHERE cuota > ANY (SELECT cuota
FROM empleados empleados2
WHERE empleados.oficina = empleados2.oficina);

Obtenemos los empleados que tienen una cuota superior a la cuota de alguno de
sus compañeros de oficina, es decir los empleados que no tengan la menor cuota
de su oficina.

En este caso hemos tenido un alias de tabla en la subconsulta (empleados2) para


poder utilizar una referencia externa.

El test ALL
<expresion> {=|<>|!=|>|>=|!>|<|<=|!<} ALL subconsulta
Con el modificador ALL, para que se cumpla la condición, la comparación se debe
cumplir con cada uno de los valores devueltos por la subconsulta.

Si la subconsulta no devuelve ninguna fila ALL da True.

SELECT *
FROM empleados
WHERE cuota > ALL (SELECT cuota
FROM empleados empleados2
WHERE empleados.oficina = empleados2.oficina);

En el ejemplo anterior obtenemos los empleados que tengan una cuota superior a
todas las cuotas de la oficina del empleado. Podríamos pensar que obtenemos el
empleado de mayor cuota de su oficina pero no lo es, aquí tenemos un problema,
la cuota del empleado aparece en el resultado de subconsulta por lo tanto > no se
cumplirá para todos los valores y sólo saldrán los empleados que no tengan
oficina (para los que la subconsulta no devuelve filas).

Para salvar el problema tendríamos que quitar del resultado de la subconsulta la


cuota del empleado modificando el WHERE:

WHERE empleados.oficina = empleados2.oficina


AND empleados.numemp <> empleados2.numemp);

De esta forma saldrían los empleados que tienen una cuota mayor que cualquier
otro empleado de su misma oficina.

O bien

WHERE empleados.oficina = empleados2.oficina


AND empleados.cuota <> empleados2.cuota);

Para no considerar los empleados que tengan la misma cuota que el empleado.
En este caso saldrían los empleados con la mayor cuota de sus oficina, pero si
dos empleados tienen la misma cuota superior, saldrían, hecho que no sucedería
con la otra versión.

Cuando la comparación es una igualdad, = ANY es equivalente a IN y <> ALL es


equivalente a NOT IN (con los mismos problemas).
Ejercicios unidad 6: Las subconsultas
Para realizar los ejercicios, deberás utilizar la base de datos GestionSimples.

1. Listar los clientes (numclie, nombre) asignados a Juan que no han remitido un pedido superior a
300 euros.

numclie nombre
2107 Julian López
2107 Julian López
2121 Vicente Ríos
2125 Pepito Grillo

2. Listar los empleados (numemp, nombre) mayores de 40 años que dirigen a un vendedor con
superávit (ha vendido más que su cuota).

numemp nombre
106 Luis Antonio
108 Ana Bustamante
110 Juan Victor

3. Listar los empleados (código de empleado) cuyo importe de pedido medio para productos
fabricados por ACI es superior al importe medio global (de todos los pedidos de todos los
empleados).

rep
105

4. Listar los empleados (numemp, nombre, ventas) cuyas ventas son iguales o
superiores al objetivo de las oficinas de una determinada ciudad (de todas las
oficinas de esa ciudad). Las oficinas con objetivo nulo no se deben de tener en
cuenta (como si no existiesen). Y si no hay oficinas en la ciudad no queremos que
salga ningún empleado. Intentar primero resolver la consulta utilizando >=ALL.

Probar primero con Alvarado:

numemp nombre ventas


102 Alvaro Jaumes 47400,00
111 Juan Gris 60000,00
112 Julián Martorell 91000,00

Ahora con Pachuca. Pachuca tiene una oficina con objetivo nulo, en este caso no
queremos que esa oficina cuente.
numemp nombre ventas
101 Antonio Viguer 30500,00
102 Alvaro Jaumes 47400,00
103 Juan Rovira 28600,00
105 Vicente Pantalla 36800,00
106 Luis Antonio 29900,00
108 Ana Bustamante 36100,00
109 María Sunta 39200,00
111 Juan Gris 60000,00
112 Julián Martorell 91000,00
114 Pablo Moreno 37000,00

Para Barcelona. En este caso no tenemos oficinas en Barcelona por lo que no


tiene que salir ningún empleado.

Ahora para Michoacán. Como en Michoacán sólo hay una oficina y no tiene
objetivo no tiene que salir ningún empleado.

Intentar resolver la consulta sin utilizar ALL.

5. Listar las oficinas en donde todos los empleados tienen ventas que superan al 50% del objetivo
de la oficina.

oficina ciudad
11 Vallarta
13 Castellon
22 Alvarado
Ayuda ejercicios unidad 6: Las
subconsultas
1. Listar los clientes (numclie, nombre) asignados a Juan que no han remitido un
pedido superior a 300 euros.

SELECT numclie, nombre


FROM clientes
WHERE repclie IN (SELECT numemp FROM empleados
WHERE nombre LIKE 'Juan%')
AND NOT EXISTS (SELECT * FROM pedidos
WHERE numclie = clie AND
importe > 300);

2. Listar los empleados (numemp, nombre) mayores de 40 años que dirigen a un


vendedor con superávit (ha vendido más que su cuota).

SELECT numemp, nombre


FROM empleados
WHERE edad > 40
and numemp IN (SELECT jefe FROM empleados WHERE ventas >
cuota );

3. Listar los empleados (código de empleado) cuyo importe de pedido medio para
productos fabricados por ACI es superior al importe medio global (de todos los
pedidos de todos los empleados).

SELECT rep
FROM pedidos
WHERE fab = 'ACI'
GROUP BY rep
HAVING AVG(importe) > (SELECT AVG(importe) FROM pedidos);

4. Listar los empleados (numemp, nombre, ventas) cuyas ventas son iguales o
superiores al objetivo de las oficinas de una determinada ciudad (de todas las
oficinas de esa ciudad). Las oficinas con objetivo nulo no se deben de tener en
cuenta (como si no existiesen). Y si no hay oficinas en la ciudad no queremos que
salga ningún empleado. Intentar primero resolver la consulta utilizando >=ALL.

Probar primero con Alvarado:

SELECT numemp, nombre, ventas


FROM empleados
WHERE ventas >= ALL (SELECT objetivo
FROM oficinas
WHERE ciudad = 'Alvarado');
Ahora con Pachuca. Pachuca tiene una oficina con objetivo nulo, en este caso
no queremos que esa oficina cuente.

SELECT numemp, nombre, ventas


FROM empleados
WHERE ventas >= ALL (SELECT objetivo
FROM oficinas
WHERE ciudad = 'Pachuca' and objetivo is not
null);

Para Barcelona. En este caso no tenemos oficinas en Barcelona por lo que no


tiene que salir ningún empleado. Si no queremos que salgan tendremos que
añadir una condición:

SELECT numemp, nombre, ventas


FROM empleados
WHERE ventas >= ALL (SELECT objetivo
FROM oficinas
WHERE ciudad = 'Barcelona' and objetivo is not
null)
AND EXISTS (SELECT *
FROM oficinas
WHERE ciudad = 'Barcelona');

Ahora para Michoacán. Como en Michoacán sólo hay una oficina y no tiene
objetivo no tiene que salir ningún empleado.

SELECT numemp, nombre, ventas


FROM empleados
WHERE ventas >= ALL (SELECT objetivo
FROM oficinas
WHERE ciudad = 'Michoacán' and objetivo is not
null)
AND EXISTS (SELECT *
FROM oficinas
WHERE ciudad = 'Michoacán' and objetivo is not
null);

Esta sería la consulta definitiva que nos serviría para cualquier situación. Pero
como se ve el modificador ALL puede darnos problemas. Para solucionarlo,
realizamos la siguiente consulta.

Intentar resolver la consulta sin utilizar ALL.

SELECT numemp, nombre, ventas


FROM empleados
WHERE ventas >= (SELECT MAX(objetivo)
FROM oficinas
WHERE ciudad = 'Michoacán');

5. Listar las oficinas en donde todos los empleados tienen ventas que superan al
50% del objetivo de la oficina.
SELECT oficina, ciudad
FROM oficinas
WHERE (objetivo * .5) < = (SELECT MIN(ventas)
FROM empleados WHERE empleados.oficina =
oficinas.oficina);
7. Actualización de datos (I)
7.1. Introducción

Hasta ahora hemos trabajado con tablas que tenían datos introducidos y cuando
nos ha hecho falta hemos añadido nuevos datos en las mismas y hemos
modificado algún dato directamente desde el entorno de SSMS, en este tema
veremos cómo hacerlo con instrucciones de Transact-SQL.

Seguimos en el DML porque las instrucciones que veremos actúan sobre los datos
de la base de datos no sobre su definición y tenemos tres tipos de operaciones
posibles:

 Insertar nuevas filas en una tabla.


 Modificar datos ya almacenados.
 Eliminar filas de una tabla.

7.2. Insertar creando una nueva tabla

Una forma de insertar datos es crear una tabla que incluya los datos de otra. Esta
es la sentencia SELECT... INTO.

SELECT ...
INTO nb_NuevaTabla
FROM ...

nb_NuevaTabla  es el nombre de la tabla que se va a crear, si en la base de datos


ya hay una tabla con ese nombre, el sistema genera un error y no se ejecuta la
sentencia.

En la nueva tabla las columnas tendrán el mismo tipo y tamaño que las columnas
del resultado de la SELECT,  se llamarán con el nombre de alias de la columna o
en su defecto con el nombre de la columna, pero no se transfiere ninguna otra
propiedad del campo o de la tabla como por ejemplo las claves e índices.

Si retomamos el ejemplo del punto anterior:

CREATE TABLE trabajo (col1 INT, col2 VARCHAR(20), col3 MONEY);


INSERT INTO trabajo SELECT oficina, ciudad, ventas
FROM oficinas
WHERE region = 'Centro';

Se podría obtener el mismo resultado con una sola instrucción:

SELECT oficina AS col1, ciudad AS col2, ventas AS col3


INTO trabajo
FROM oficinas
WHERE region = 'Centro'

Si se tiene poca experiencia en esta instrucción, lo mejor es primero escribir la


SELECT que permite obtener las filas a insertar y una vez la tenemos añadir la
cláusula INTO destino delante de FROM.

7.3. Insertar en una tabla existente  INSERT INTO

La inserción de nuevos datos en una tabla, se realiza añadiendo filas enteras a la


tabla, la sentencia SQL que lo permite es la orden INSERT (o también
denominada INSERT INTO).

De la sentencia INSERT completa, nosotros estudiaremos la sintaxis más utilizada


y estándar:

INSERT [INTO] <destino>


{
[(lista_columnas)]
{VALUES ({DEFAULT|NULL|expresion}[ ,...n ])
|tabla_derivada
}
}
|DEFAULT VALUES
[;]

<destino> ::=
{
[nbBaseDatos.nbEsquema. | nbEsquema.]nbTablaVista
}

Con esta instrucción podemos insertar una fila de valores determinados o un


conjunto de filas derivadas de otra consulta.
7.4. Insertar una fila de valores

Para insertar una fila de valores utilizamos la sintaxis:

INSERT [INTO] <destino> [(lista_columnas)]


VALUES ({DEFAULT|NULL|expresion}[ ,...n ]) [;]

La palabra reservada INTO es opcional y no añade funcionalidad a la instrucción,


originalmente era obligatoria.

Después de indicar que queremos insertar, debemos indicar dónde, mediante


<destino>.

<destino> es el nombre de la tabla donde queremos insertar, puede ser un nombre


de tabla o un nombre de vista.

Si utilizamos una vista, y ésta tiene un origen basado en varias tablas, en su lista
de selección deberán aparecer columnas de una sola tabla (no podemos insertar
datos en varias tablas a la vez).

Con la cláusula VALUES indicamos entre paréntesis los valores a insertar,


separados por comas.

Cada valor se puede indicar mediante:

 una expresión que normalmente será una constante,


 mediante la palabra reservada DEFAULT que indica ‘valor por defecto’ en
este caso la columna se rellenará con el valor predeterminado de la
columna, si la columna no tiene  DEFAULT se sustituirá por el valor nulo
NULL. 
 Mediante la palabra reservada NULL valor nulo.

Delante de VALUES, de forma opcional podemos indicar una lista de columnas


entre paréntesis. Las columnas son columnas del destino.

Cuando indicamos nombres de columnas, esas columnas serán las que reciban
los valores a insertar, la asignación de valores se realiza por posición, la primera
columna recibe el primer valor, la segunda columna el segundo, y así
sucesivamente.

En la lista, las columnas pueden estar en cualquier orden y también se pueden


omitir algunas columnas.
Una columna que no aparezca en la lista de columnas se rellenará de acuerdo a
su definición:

 con su valor por defecto si está definida con la cláusula DEFAULT


 con el valor de identidad incremental siguiente  si tiene la propiedad
IDENTITY.
 con el valor calculado si es una columna calculada.
 con el valor NULL , en cualquier otro caso y si la columna lo admite.

Cuando no se indica una lista de columnas el sistema asume por defecto todas las
columnas de la tabla y en el mismo orden que aparecen en la definición de la
tabla, en este caso, los valores se tienen que especificar en el mismo orden que
las columnas en la definición de la tabla, y se tiene que especificar un valor por
cada columna ya que los valores se rellenan por posición, la primera columna
recibe el primer valor, la segunda columna el segundo, y así sucesivamente.

En cualquier caso, el número de valores debe coincidir con el número de


columnas y los tipos de dato de los valores deben ser compatibles con las
columnas.

Aunque pueda parecer más engorroso escribir la lista de columnas, es un hábito


recomendable, hace que la sentencia sea más fácil de leer y mantener (cuando
leemos la sentencia sabemos en qué columnas asignamos los valores sin
necesidad de consultar la definición de la tabla) y evita que se tenga que cambiar
la sentencia si se modifica el esquema de la tabla (si el orden de las columnas
dentro de la tabla cambia).

Los registros se agregan al final de la tabla.

Cuando se insertan nuevas filas en una tabla, el sistema comprobará que la nueva
fila no infrinja ninguna regla de integridad, por ejemplo no podremos asignar a una
columna PRIMARY KEY un valor nulo o que ya exista en la tabla, a una columna
UNIQUE un valor que ya exista en la tabla, a una columna NOT NULL un valor
NULL, a una clave ajena (FOREIGN KEY) un valor que no exista en la tabla de
referencia.

De producirse alguna de las situaciones anterior, la instrucción genera un mensaje


de error y la fila no se inserta.

Ejemplos.

INSERT INTO oficinas (oficina, ciudad) VALUES (26, 'Elx');

En este caso hemos indicado sólo dos columnas y dos valores, las demás
columnas se rellenan con el valor por defecto si lo tiene (DEFAULT) o con NULL.
Si alguna columna no nombrada no admite nulos ni tiene cláusula DEFAULT
definida, la instrucción dará error.
INSERT INTO oficinas
VALUES (27,'Móstoles','Centro',default ,null, default)

Aquí no hemos indicado una lista de columnas luego los valores se tienen que
indicar en el mismo orden que las columnas dentro de la tabla, si nos
equivocamos de orden, el valor se guardará en una columna errónea (si los tipos
son compatibles) o generará un mensaje de error y la fila no se insertará (si los
tipos no son compatibles).
7.5. Inserción de varias filas

Si los valores que queremos insertar los tenemos en otras tablas, podemos
insertar varias filas a la vez indicando una consulta que genere las filas de valores
a insertar. En este caso utilizamos la sintaxis:

INSERT [INTO] <destino>  [(lista_columnas)]


tabla_derivada [;]

<destino> y lista_columnas funcionan igual que en el punto anterior.

Tabla_derivada es cualquier instrucción SELECT válida que devuelva filas con los
datos que se van a cargar en el destino.

Cada fila devuelta por la SELECT es una lista de valores que se intentará insertar
como con la cláusula VALUES, por lo que las columnas devueltas por la SELECT
deberán cumplir las mismas reglas que los valores de la lista de valores
anteriores.

Ejemplo:

CREATE TABLE trabajo (col1 INT, col2 VARCHAR(20), col3 MONEY);

Creamos una tabla trabajo de 3 columnas

INSERT INTO trabajo SELECT oficina, ciudad, ventas


FROM oficinas
WHERE region = 'Centro';

Insertamos en trabajo el resultado de la SELECT (el número de oficina, ciudad y


ventas de las oficinas del Centro).

En este caso no hemos incluido una lista de columnas, por lo que en la SELECT
tenemos que generar los valores en el mismo orden que en trabajo.

Si hubiésemos escrito:

INSERT INTO trabajo SELECT ciudad, oficina, ventas


FROM oficinas
WHERE region = 'Centro';

Hubiese dado error porque la columna col1 es INT y el valor a asignar es texto (el
nombre de la ciudad de la oficina).

INSERT INTO trabajo (col2, col1)


SELECT ciudad, oficina
FROM oficinas
WHERE region = 'Este';
En este caso hemos incluido una lista de columnas, la SELECT debe generar los
valores correspondientes, y col3 que no se rellena explícitamente se rellenará con
NULL porque la columna col3 no está definida como columna calculada, ni con
DEFAULT, ni IDENTITY y además admite nulos.

7.6. Insertar una fila de valores por defecto

TRANSACT-SQL nos permite insertar una fila de valores por defecto utilizando la
sintaxis:

INSERT [INTO] <destino> DEFAULT VALUES


[;]

Hace que la nueva fila contenga los valores predeterminados definidos para cada
columna.

Hay que tener en cuenta una serie de aspectos al utilizar esta instrucción:

Puede generar filas duplicadas en la tabla si los valores que se generan son
siempre los mismos.

Si la tabla tiene una clave principal, esta tendrá que estar basada en una columna
con la propiedad IDENTITY para que se generen valores diferentes
automáticamente.

Si una columna está definida como NOT NULL tendrá que incluir un DEFAULT o
ser una columna calculada con una expresión compatible.
7.7. Modificar datos almacenados - UPDATE

La sentencia UPDATE modifica los valores de una o más columnas en  las filas
seleccionadas de una única tabla.

Para modificar los datos de una tabla es necesario disponer del privilegio UPDATE
sobre dicha tabla.

UPDATE
   [ TOP ( expression ) [ PERCENT ] ]
   <destino>
   SET { nbcolumna = { expresion | DEFAULT | NULL }
       } [ ,...n ]
   [ FROM{ <origen> }]
   [ WHERE <condicion> ]
 [;]
<destino> ::=
{   
  [nbBaseDatos.[nbEsquema.]| nbEsquema.]nbTablaVista
}

Con <destino> indicamos la tabla que se va a actualizar.

La cláusula SET especifica qué columnas van a modificarse y con qué valor, el
valor se puede expresar mediante una expresión, la palabra DEFAULT que
equivale al valor predeterminado de la columna, o el valor nulo NULL.

Las columnas de identidad no se pueden actualizar.

Expresión en cada asignación  debe generar un valor del tipo de dato apropiado
para la columna indicada. La expresión debe ser calculable basada en los valores
de la fila actualmente en actualización. Si para el cálculo se utiliza una columna
que también se modifica, el valor que se utilizará es el de antes de la modificación,
lo mismo para la condición del WHERE.

Expresión también puede ser una subconsulta siempre y cuanto devuelva un único
valor y cumpla las condiciones anteriormente expuestas.

Por ejemplo:

UPDATE oficinas SET ventas = 0;

Actualiza todas las filas de la tabla oficinas dejando el campo ventas con el valor
cero.

Si el campo ventas está definido con un valor predeterminado 0, la sentencia


anterior equivale a:
UPDATE oficinas SET ventas = DEFAULT;

Si lo que queremos es dejar el campo a nulo:

UPDATE oficinas SET ventas = NULL;

En una misma sentencia podemos actualizar varias columnas, sólo tenemos que
indicar las distintas asignaciones separadas por comas:

UPDATE oficinas SET ventas = 0, objetivo = 0;

Los nombres de columna pueden especificarse en cualquier orden.

Si no queremos actualizar todas las filas de la tabla sino unas cuantas,


utilizaremos la cláusula TOP, o unas determinadas, utilizaremos la cláusula
WHERE.

TOP ( expresion ) [ PERCENT ]

Especifica el número o porcentaje de filas aleatorias que se van a modificar.

expression debe generar un valor numérico e indica el número de filas a modificar


empezando por el principio. Como en la SELECT, si añadimos la palabra
PERCENT, el número representado por expresión se refiere al porcentaje de filas
a modificar sobre el total. La cláusula TOP funciona casi igual que en la SELECT
pero en este caso, las filas no se ordenan, y la expresión debe ir entre paréntesis.

Por ejemplo:

UPDATE TOP (10) PERCENT oficinas


SET ventas = 0;

Actualiza el 10% de filas de la tabla oficinas.

[ WHERE <condicion> ]

Utilizamos la cláusula WHERE para filtrar las filas a actualizar. Se actualizarán


todas las filas que cumplan la condición. Por ejemplo si queremos actualizar sólo
las oficinas del Este:

UPDATE oficinas
SET ventas = 0
WHERE region = 'Este';

Cuando para la condición de la cláusula WHERE necesitamos un dato de otra


tabla podemos utilizar una subconsulta:

UPDATE empleados SET ventas = 0


WHERE oficina IN (SELECT oficina
FROM oficinas
WHERE region = 'Este');

Cuando el campo de la otra tabla se utiliza para la cláusula SET, entonces


debemos utilizar la cláusula FROM.

La cláusula FROM permite definir un origen de datos basado en varias tablas, y


ese origen será el utilizado para realizar la actualización.

Por ejemplo queremos actualizar el importe de los pedidos con el precio de la


tabla productos.

UPDATE pedidos SET importe = cant * precio


FROM pedidos INNER JOIN productos
ON fab = idfab AND producto = idproducto;

Modificamos la tabla pedidos dejando en la columna importe el resultado de


multiplicar la cantidad del pedido por el precio del producto que se encuentra en la
tabla productos.

Si la actualización de una fila infringe una restricción o una regla, infringe la


configuración de valores NULL de la columna o si el nuevo valor es de un tipo de
datos incompatible con la columna, se cancela la instrucción, se devuelve un error
y no se actualiza ningún registro.

Cuando una instrucción UPDATE encuentra un error aritmético (error de


desbordamiento, división por cero o de dominio) durante la evaluación de la
expresión, la actualización no se lleva a cabo. El resto del lote no se ejecuta y se
devuelve un mensaje de error.

Además del permiso de UPDATE, se requieren permisos SELECT para la tabla


que se actualiza si la instrucción UPDATE contiene una cláusula WHERE o en el
caso de que el argumento expression de la cláusula SET utilice una columna de la
tabla, y permisos SELECT para la tabla del origen si utilizamos una cláusula
FROM o un WHERE con subconsulta.
7. Actualización de datos (V)
7.8. Eliminar filas - DELETE

La sentencia DELETE elimina filas de una tabla. Si se borran todas las filas, o se
borra la única fila de una tabla, la definición de la tabla no desaparece, sólo que la
tabla se queda vacía.

DELETE
    [ TOP ( expression ) [ PERCENT ] ] [ FROM ] <destino>
    [ FROM <origen>]
    [ WHERE < condicion>]
[; ]
<destino> ::=
{   
  [nbBaseDatos. nbEsquema. | nbEsquema.]nbTablaVista
}

Con esta instrucción podemos eliminar una o varias filas de una tabla.

La palabra FROM (la primera) es opcional (originalmente era obligatorio) y no


añade funcionalidad sólo sirve para introducir el destino.

<destino> es el nombre de la tabla de donde queremos eliminar las filas, puede


ser un nombre de tabla o un nombre de vista (de momento basada en una sólo
tabla).

La segunda cláusula FROM sirve para indicar un origen que permita una condición
de WHERE sobre una tabla diferente de destino.

La instrucción básica sería pues:

DELETE oficinas;

Equivalente a:

DELETE FROM oficinas;

Con esta instrucción eliminamos todas las filas de la tabla oficinas.

La cláusula WHERE permite eliminar determinadas filas, indica una condición que
deben cumplir las filas que se eliminan.

Por ejemplo:

DELETE oficinas
WHERE region = ’Este’;
Elimina las oficinas del Este.

TOP ( expresion ) [ PERCENT ]

Especifica el número o porcentaje de filas aleatorias que se van a eliminar.

expression debe generar un valor numérico e indica el número de filas a eliminar


empezando por el principio. Como en la SELECT, si añadimos la palabra
PERCENT, el número representado por expresión se refiere al porcentaje de filas
a eliminar sobre el total. La cláusula TOP funciona casi igual que en la SELECT
pero en este caso, las filas no se ordenan, y la expresión debe ir entre paréntesis.

Por ejemplo:

DELETE TOP (10) PERCENT


FROM oficinas;

Elimina el 10% de filas de la tabla oficinas.

Originalmente sólo se podía indicar una tabla en la cláusula FROM, pero ahora
podemos indicar un origen basado en varias tablas.

Si utilizamos un origen basado en varias tablas, se debe de utilizar una extensión


de TRANSACT-SQL que consiste en escribir dos cláusulas FROM, una indica la
tabla de donde eliminamos las filas y la otra el origen que utilizamos para eliminar.

Este caso se produce cuando las filas a eliminar dependen de un valor que está
en otra tabla. Por ejemplo queremos eliminar los empleados de las oficinas del
Este. Como la región de la oficina no está en empleados, habría que añadir al
origen la tabla oficinas para poder formular la condición del WHERE:

DELETE FROM empleados


FROM empleados INNER JOIN oficinas
ON empleados.oficina = oficinas.oficina
WHERE region = 'Este';

En el origen tenemos las dos tablas y en la primera FROM indicamos de qué tabla
queremos borrar.

Esto se podía haber resuelto, como toda la vida, mediante una subconsulta:

DELETE FROM empleados


WHERE oficina IN (SELECT oficina
FROM oficinas
WHERE region = 'Este');

Para finalizar no debemos olvidar que para poder ejecutar un DELETE se


requieren permisos DELETE en la tabla de donde vamos a eliminar, y también se
requieren los permisos para utilizar SELECT si la instrucción contiene una cláusula
WHERE.

Muy importante siempre que actualicemos datos en nuestras tablas, no debemos


olvidar tampoco las reglas de integridad referencial. Si la tabla afectada interviene
como tabla principal en una relación con otra tabla, no se podrán eliminar sus filas
que estén relacionadas con registros de la otra tabla (no se pueden eliminar
‘padres’ que tengan ‘hijos’ ). Si se van a eliminar  varias filas y al menos una no se
puede eliminar por infringir las reglas de integridad, entonces la operación abortará
y no se eliminará ninguna fila.

En el ejemplo anterior, si un empleado asignado a una oficina del Este tiene


pedidos, no se podrá eliminar y entonces no se eliminará ningún empleado.
7.9. Borrado masivo - TRUNCATE

Si queremos eliminar todas las filas de una tabla podemos utilizar también la
instrucción TRUNCATE TABLE.

TRUNCATE TABLE
[nbBaseDatos.[nbEsquema.]| nbEsquema.]nbTabla [; ]

Esta sentencia quita todas las filas de una tabla sin registrar las eliminaciones
individuales de filas. Desde un punto de vista funcional, TRUNCATE TABLE es
equivalente a la instrucción DELETE sin una cláusula WHERE; no obstante,
TRUNCATE TABLE es más rápida y utiliza menos recursos de registros de
transacciones y de sistema.

En comparación con la instrucción DELETE, TRUNCATE TABLE ofrece las


siguientes ventajas:

 Se utiliza menos espacio del registro de transacciones.


 La instrucción DELETE quita una a una las filas y graba una entrada en el
registro de transacciones por cada fila eliminada.
 TRUNCATE TABLE quita los datos al cancelar la asignación de las páginas
de datos utilizadas para almacenar los datos de la tabla y sólo graba en el
registro de transacciones las cancelaciones de asignación de páginas.

 Por regla general, se utilizan menos bloqueos.


 Si se ejecuta la instrucción DELETE con un bloqueo de fila, se bloquea
cada fila de la tabla para su eliminación. TRUNCATE TABLE siempre
bloquea la tabla y la página, pero no cada fila.

 Las páginas cero se conservan en la tabla sin excepciones.


 Después de ejecutar una instrucción DELETE, la tabla puede seguir
conteniendo páginas vacías. Por ejemplo, no se puede cancelar la
asignación de las páginas vacías de un montón sin un bloqueo de tabla
exclusivo como mínimo. Si en la operación de eliminación no se utiliza un
bloqueo de tabla, la tabla contiene muchas páginas vacías. En el caso de
los índices, la operación de eliminación puede dejar páginas vacías, aunque
la asignación de estas páginas se puede cancelar rápidamente mediante un
proceso de limpieza en segundo plano.

Si la tabla contiene una columna de identidad, el contador para dicha columna se


restablece al valor de inicialización definido para ella. Si no se define ningún valor
de inicialización, se utiliza el valor predeterminado 1. Para conservar el contador
de identidad, se utiliza DELETE.

Pero no todo son ventajas, no se puede utilizar TRUNCATE TABLE en las


siguientes tablas:
 Tablas a las que se hace referencia mediante una restricción FOREIGN
KEY (las tablas que entran como principales en una relación).
 Tablas que participan en una vista indizada.
Ejercicios Actualización de datos (I)
Para realizar los ejercicios, deberás utilizar la base de datos Gestion8.

1. Añadir a la oficina 40 otro empleado, Luis Valverde, con número de empleado


436, con los mismos datos que el anterior pero su jefe será el director de la oficina
40 (no sabemos qué número tiene).

2. Pasar los pedidos de octubre 1989 a diciembre 2008. (3 filas afectadas)

3. Queremos actualizar el importe de los pedidos del mes actual con el precio
almacenado en la tabla productos.

Ayuda: En un primer paso obtener los pedidos del mes actual obteniendo también el precio unitario
dentro del pedido y el precio del producto de la tabla de productos.

codigo numpedido fechapedido cant importe precio pedido precio


1 110036 2008-12-12 00:00:00.000 9 22,50 2,50 NULL
2 110037 2008-12-12 00:00:00.000 7 31,50 4,50 45,00
6 112979 2008-12-12 00:00:00.000 6 150,00 25,00 NULL
9 112989 2008-12-10 00:00:00.000 6 14,58 2,43 2,43
16 113013 2008-12-28 00:00:00.000 1 6,52 6,52 5,16

Actualizar después la tabla de pedidos cambiando los importes para que el precio unitario
corresponda con el precio del producto. Los pedidos de los productos que no tienen precio se
quedarán como estaban. (3 filas afectadas)

codigo numpedido fechapedido cant importe precio pedido precio


1 110036 2008-12-12 00:00:00.000 9 22,50 2,50 NULL
2 110037 2008-12-12 00:00:00.000 7 315,00 4,50 45,00
6 112979 2008-12-12 00:00:00.000 6 150,00 25,00 NULL
9 112989 2008-12-10 00:00:00.000 6 14,58 2,43 2,43
16 113013 2008-12-28 00:00:00.000 1 5,16 6,52 5,16

4. Se ven algunos productos que no tienen precio, ahora vamos a actualizar el


precio de estos productos con el precio medio utilizado en los pedidos donde
aparecen.

Ayuda: Primero sacamos los productos que queremos actualizar con los pedidos correspondientes:

precio
idfab idproducto precio codigo numpedido fechapedido importe
pedido
aci 41001 NULL NULL NULL NULL NULL NULL
aci 41002 NULL 10 112992 1990-04-15 7,60 0,76
20:00:00.000
2008-02-05
aci 41002 NULL 18 113027 450,00 8,3333
00:00:00.000
2008-05-05
aci 41003 NULL 15 113012 37,45 1,07
00:00:00.000
2008-05-10
aci 41004 NULL 3 112963 3,276 0,117
00:00:00.000
1990-01-11
aci 41004 NULL 4 112968 39,78 1,17
00:00:00.000
2008-05-10
aci 41004 NULL 7 112983 7,02 1,17
00:00:00.000
2009-04-01
aci 4100x NULL 25 113055 1,50 0,25
00:00:00.000
2008-11-01
aci 4100x NULL 26 113057 NULL NULL
00:00:00.000
2008-01-01
aci 4100y NULL 8 112987 275,00 25,00
00:00:00.000
2008-12-12
aci 4100z NULL 1 110036 22,50 2,50
00:00:00.000
2008-12-12
aci 4100z NULL 6 112979 150,00 25,00
00:00:00.000

Vemos que el producto ACI 41001 no se podrá actualizar porque no tiene pedidos. Pero los demás
se actualizarán con el precio medio de sus pedidos, deberán quedar así (7 filas afectadas):

idfab idproducto precio


aci 41001 NULL
aci 41002 4,55
aci 41003 1,07
aci 41004 0,82
aci 4100x 0,25 *
aci 4100y 25,00
aci 4100z 13,75

* aci 4100x tiene 2 pedidos pero uno sin precio por lo que no cuenta

Ayuda ejercicios unidad 7:


Actualización de datos (I)
1. Añadir a la oficina 40 otro empleado, Luis Valverde, con número de empleado
436, con los mismos datos que el anterior pero su jefe será el director de la oficina
40 (no sabemos qué número tiene).
INSERT INTO empleados (numemp, nombre, titulo, contrato, ventas, cuota,
oficina, jefe)
SELECT 436, 'Luis Valverde','Vendedor', GETDATE(), 0, 1200.45, 40, dir
FROM oficinas WHERE oficina = 40;

2. Pasar los pedidos de octubre 1989 a diciembre 2008. (3 filas afectadas)

UPDATE pedidos SET fechapedido=DATEADD(month,230,fechapedido)


WHERE year(fechapedido)=1989 and month(fechapedido)=10;

3. Queremos actualizar el importe de los pedidos del mes actual con el precio
almacenado en la tabla productos.

SELECT codigo, numpedido,fechapedido,cant, importe,importe/cant AS


[precio pedido],precio
FROM pedidos inner join productos ON fab=idfab and producto = idproducto
WHERE YEAR(fechapedido)=YEAR(GETDATE()) and
MONTH(fechapedido)=MONTH(GETDATE())

Actualizar después la tabla de pedidos cambiando los importes para que el precio
unitario corresponda con el precio del producto. Los pedidos de los productos que
no tienen precio se quedarán como estaban. (3 filas afectadas)

UPDATE pedidos SET importe=cant*precio


FROM pedidos inner join productos ON fab=idfab and producto = idproducto
WHERE year(fechapedido)=2008 and month(fechapedido)=12 AND precio IS NOT
NULL;

4. Se ven algunos productos que no tienen precio, ahora vamos a actualizar el


precio de estos productos con el precio medio utilizado en los pedidos donde
aparecen. La primera SELECT saca los productos que queremos actualizar con
los pedidos correspondientes.

SELECT idfab, idproducto, precio, codigo, numpedido,fechapedido,importe,


importe/cant
FROM productos left join pedidos on idfab=fab AND idproducto=producto
WHERE precio IS NULL;

UPDATE productos SET precio = (SELECT ROUND(AVG(importe/cant),2) FROM


pedidos WHERE fab=idfab AND producto=idproducto)
WHERE precio IS NULL
8.8. Crear una vista CREATE VIEW

Una vista es una tabla virtual que representa los datos de una o más tablas de una
forma alternativa. Para crear una nueva vista se emplea la sentencia CREATE
VIEW, debe ser la primera instrucción en un lote de consultas.

Una vista sólo se puede crear en la base de datos actual.

 Para ejecutar CREATE VIEW, se necesita, como mínimo, el permiso CREATE


VIEW en la base de datos y el permiso ALTER en el esquema en el que se está
creando la vista.

Sintaxis:

CREATE VIEW [nbEsquema.] nbVista


[ (columna [ ,...n ] ) ]
    AS ( sentencia_select ) [ ; ]

nbEsquema Es el nombre del esquema al que pertenece la nueva tabla.

nbVista Es el nombre de la nueva vista. Los nombres de vistas deben seguir las
reglas de los identificadores.

sentencia_select Es la instrucción SELECT que define la vista. Dicha instrucción


puede utilizar más de una tabla y otras vistas. Se necesitan permisos adecuados
para seleccionar los objetos a los que se hace referencia en la cláusula SELECT
de la vista que se ha creado.

Una vista no tiene por qué ser un simple subconjunto de filas y de columnas de
una tabla determinada. Es posible crear una vista que utilice más de una tabla u
otras vistas mediante una cláusula SELECT de cualquier complejidad.

También se pueden utilizar funciones y varias instrucciones SELECT separadas


por UNION o UNION ALL.

Una vista puede tener como máximo 1.024 columnas.


Ejemplos:

CREATE VIEW oficinas_este


AS SELECT * FROM oficinas WHERE region = ‘Este’;

Crea una vista con las oficinas del este.

CREATE VIEW oficinas_empleados


AS
SELECT oficinas.oficina AS ofi, ciudad, dir, region, objetivo,
oficinas.ventas AS ventas_ofi, empleados.*
FROM oficinas INNER JOIN empleados
        ON oficinas.oficina = empleados.oficina;

Crea una vista con los datos de todos los empleados y de sus oficinas.

En este caso hemos tenido que definir alias de campo porque en el origen de la
sentencia SELECT existe duplicidad de nombres.

CREATE VIEW oficinas_EO


AS
SELECT * FROM oficinas WHERE region = ‘Este’;
UNION ALL
SELECT * FROM oficinas WHERE region = ‘Oeste’;

Por defecto las columnas de la vista heredan los nombres de las columnas de la
sentencia SELECT asociada, pero podemos cambiar estos nombres indicando
una lista de columnas después del nombre de la vista.

CREATE VIEW oficinas_este (Eoficina, Eciudad, Eregion, Edir,


Eobjetivo,Eventas)
AS SELECT * FROM oficinas WHERE region = ‘Este’;

Utilizando una lista de columnas ya no tenemos que definir alias de columna en la


sentencia SELECT como pasaba en el caso de la vista oficinas_empleados.

Normalmente se utiliza la lista de columnas cuando una columna proviene de una


expresión aritmética, una función o una constante; cuando dos o más columnas
puedan tener el mismo nombre, normalmente debido a una combinación; o
cuando una columna de una vista recibe un nombre distinto al de la columna de la
que proviene.

En definitiva se puede optar por utilizar la lista de columnas o definir alias de


campo en la sentencia SELECT.

Cuando utilizamos una vista en una operación de actualización (INSERT,


UPDATE, DELETE), la vista debe ser actualizable, para ello debe seguir las
siguientes reglas:
Cualquier modificación, incluida en las instrucciones UPDATE, INSERT y
DELETE, debe hacer referencia a las columnas de una única tabla base.

Las columnas que se vayan a modificar en la vista deben hacer referencia directa
a los datos subyacentes de las columnas de la tabla, es decir que las columnas no
se pueden obtener de otra forma, como con una función de agregado: AVG,
COUNT, SUM, MIN, MAX, GROUPING, STDEV, STDEVP, VAR y VARP, o un
cálculo.

Las columnas formadas mediante los operadores de conjunto UNION, UNION


ALL, CROSSJOIN, EXCEPT e INTERSECT equivalen a un cálculo y tampoco son
actualizables.

Las columnas que se van a modificar no se ven afectadas por las cláusulas
GROUP BY, HAVING o DISTINCT.

Las restricciones anteriores se aplican a cualquier subconsulta de la cláusula


FROM de la vista, al igual que a la propia vista. Normalmente, el Database Engine
(Motor de base de datos) debe poder realizar un seguimiento sin ambigüedades
de las modificaciones de la definición de la vista a una tabla base.

8.9. Eliminar una vista DROP VIEW

Para eliminar una vista de una base de datos tenemos la sentencia DROP TABLE.

Sintaxis:

DROP VIEW [nbEsquema.]nbVista[ ,...n ] [ ; ]

Se eliminan las vista de la base de datos actual. Cuando eliminamos una vista
eliminamos su definición y los permisos asociados a ella.

Se pueden quitar varias vistas en una misma sentencia DROP VIEW escribiendo
los nombres de las vistas a eliminar separados por comas.

Para ejecutar DROP VIEW, como mínimo, se necesita el permiso ALTER en


SCHEMA o el permiso CONTROL en OBJECT.

Ejemplo:

DROP VIEW oficinas_este, oficinas_EO;

Elimina las vistas oficinas_este y oficinas_EO.


Si eliminamos una tabla mediante DROP TABLE, se deben quitar explícitamente,
con DROP VIEW, las vistas basadas en esta tabla ya que no se quitarán por sí
solas.
9. Programación en TRANSACT SQL
(I)
9.1. Introducción

Hasta ahora hemos estudiado sentencias SQL orientadas a realizar una


determinada tarea sobre la base de datos como definir tablas, obtener información
de las tablas, actualizarlas; estas sentencias las hemos ejecutado desde el editor
de consultas del MSSMS de una en una o a lo sumo una a continuación de la otra
dentro de la misma consulta formando un lote de instrucciones.

Las sentencias que ya conocemos son las que en principio forman parte de
cualquier lenguaje SQL. Ahora veremos que TRANSACT-SQL va más allá de un
lenguaje SQL cualquiera ya que aunque no permita:

 Crear interfaces de usuario.


 Crear aplicaciones ejecutables, sino elementos que en algún momento
llegarán al servidor de datos y serán ejecutados.

Incluye características propias de cualquier lenguaje de programación,


características que nos permiten definir la lógica necesaria para el tratamiento de
la información:

 Tipos de datos.
 Definición de variables.
 Estructuras de control de flujo.
 Gestión de excepciones.
 Funciones predefinidas.
 Elementos para la visualización, que permiten mostrar mensajes definidos
por el usuario gracias a la cláusula PRINT.

Estas características nos van a permitir crear bloques de código orientados a


realizar operaciones más complejas. Estos bloques no son programas sino
procedimientos o funciones que podrán ser llamados en cualquier momento.

En SQL Server 2005 podemos definir tres tipos de bloques de código, los
procedimientos almacenados, los desencadenadores (o triggers) y funciones
definidas por el usuario.

Nosotros estudiaremos los dos primeros.

Empezaremos por los procedimientos almacenados, veremos primero las


sentencias para crear y eliminar procedimientos almacenados, luego estudiaremos
las instrucciones Transact-SQL más propias de un lenguaje de programación
nombradas en el tema de Introducción al Transact-SQL  como son por ejemplo los
bucles y estructuras condicionales. Estas instrucciones las utilizaremos dentro de
procedimientos almacenados pero veremos que también nos servirán para definir
otros bloques de código.

Terminaremos esta unidad de programación estudiando los disparadores o


Triggers muy similares a los procedimientos almacenados que difieren
básicamente en la forma en que entran en funcionamiento.

9.2. Procedimientos almacenados STORE PROCEDURE

Un procedimiento almacenado (STORE PROCEDURE) está formado por un


conjunto de instrucciones Transact-SQL que definen un determinado proceso,
puede aceptar parámetros de entrada y devolver un valor o conjunto de
resultados. Este procedimiento se guarda en el servidor y puede ser ejecutado en
cualquier momento.

Los procedimientos almacenados se diferencian de las instrucciones SQL


ordinarias y de los lotes de instrucciones SQL en que están precompilados. La
primera vez que se ejecuta un procedimiento, el procesador de consultas de SQL
Server lo analiza y prepara un plan de ejecución que se almacena en una tabla del
sistema. Posteriormente, el procedimiento se ejecuta según el plan almacenado.
Puesto que ya se ha realizado la mayor parte del trabajo de procesamiento de
consultas, los procedimientos almacenados se ejecutan casi de forma instantánea
por lo que el uso de procedimientos almacenados mejora notablemente la
potencia y eficacia del SQL.

SQL Server incorpora procedimientos almacenados del sistema, se encuentran en


la base de datos master y se reconocen por su nombre, todos tienen un nombre
que empieza por sp_. Permiten recuperar información de las tablas del sistema y
pueden ejecutarse en cualquier base de datos del servidor.

También están los procedimientos de usuario, los crea cualquier usuario que
tenga los permisos oportunos.

Se pueden crear también procedimiento temporales locales y globales. Un


procedimiento temporal local se crea por un usuario en una conexión determinada
y sólo se puede utilizar en esa sesión, un procedimiento temporal global lo pueden
utilizar todos los usuarios, cualquier conexión puede ejecutar un procedimiento
almacenado temporal global. Éste existe hasta que se cierra la conexión que el
usuario utilizó para crearlo, y hasta que se completan todas las versiones del
procedimiento que se estuvieran ejecutando mediante otras conexiones. Una vez
cerrada la conexión que se utilizó para crear el procedimiento, éste ya no se
puede volver a ejecutar, sólo podrán finalizar las conexiones que hayan empezado
a ejecutar el procedimiento.
Tanto los procedimientos temporales como los no temporales se crean y ejecutan
de la  misma forma, el nombre que le pongamos indicará de qué tipo es el
procedimiento.

Los procedimientos almacenados se crean mediante la sentencia CREATE


PROCEDURE y se ejecutan con EXEC (o EXECUTE). Para ejecutarlo también se
puede utilizar el nombre del procedimiento almacenado sólo, siempre que sea la
primera palabra del lote.  Para eliminar un procedimiento almacenado utilizamos la
sentencia DROP PROCEDURE.

9.3. Eliminar procedimientos almacenados

Aunque no sabemos todavía crear un procedimiento comentaremos aquí la


instrucción para eliminar procedimientos y así podremos utilizarla en los demás
ejercicios.

DROP {PROC|PROCEDURE} [nombreEsquema.]nombreProcedimiento [,...n ].

Transact-SQL permite abreviar la palabra reservada PROCEDURE por PROC sin


que ello afecte a la funcionalidad de la instrucción.

Ejemplos:

DROP PROCEDURE Dice_Hola;

Elimina el procedimiento llamado Dice_Hola.

DROP PROC Dice_Hola;

Es equivalente, PROC y PROCEDURE indican lo mismo.

Para eliminar varios procedimientos de golpe, indicamos sus nombres separados


por comas:

DROP PROCEDURE Dice_Hola, Ventas_anuales;

Elimina los procedimientos Dice_Hola y Ventas_anuales.


9.4. Crear y ejecutar un procedimiento

CREATE PROCEDURE

Para crear un procedimiento almacenado como hemos dicho se emplea la


instrucción CREATE PROCEDURE:

CREATE {PROC|PROCEDURE} [NombreEsquema.]NombreProcedimiento


    [{@parametro tipo} [VARYING] [= valorPredet] [OUT|OUTPUT] ]
[,...n]
    AS { <bloque_instrucciones> [ ...n] }[;]
<bloque_instrucciones> ::=
{[BEGIN] instrucciones [END] }

Las instrucciones CREATE PROCEDURE no se pueden combinar con otras


instrucciones SQL en el mismo lote.

Después del verbo CREATE PROCEDURE indicamos el nombre del


procedimiento, opcionalmente podemos incluir el nombre del esquema donde
queremos que se cree el procedimiento, por defecto se creará en dbo. Ya que
Sqlserver utiliza el prefijo sp_ para nombrar los procedimientos del sistema se
recomienda no utilizar nombres que empiecen por sp_.

Como se puede deducir de la sintaxis (no podemos indicar un nombre de base de


datos asociado al nombre del procedimiento) sólo se puede crear el procedimiento
almacenado en la base de datos actual, no se puede crear en otra base de datos.

Si queremos definir un procedimiento temporal local el nombre deberá empezar


por una almohadilla (#) y si el procedimiento es temporal global el nombre debe de
empezar por ##.

El nombre completo de un procedimiento almacenado o un procedimiento


almacenado temporal global, incluidas ##, no puede superar los 128 caracteres. El
nombre completo de un procedimiento almacenado temporal local, incluidas #, no
puede superar los 116 caracteres.

Transact-SQL permite abreviar la palabra reservada PROCEDURE por PROC sin


que ello afecte a la funcionalidad de la instrucción.

CREATE PROC Calcula_comision AS…

Es equivalente a

CREATE PROCEDURE calcula_comision AS…

@parametro:  representa el nombre de un parámetro. Se pueden declarar uno o


más parámetros indicando para cada uno su nombre (debe de empezar  por
arroba) y su tipo de datos, y opcionalmente un valor por defecto (=valorPredet)
este valor será el asumido si en la llamada el usuario no pasa ningún valor para el
parámetro. Un procedimiento almacenado puede tener un máximo de 2.100
parámetros.

Los parámetros son locales para el procedimiento; los mismos nombres de


parámetro se pueden utilizar en otros procedimientos. De manera predeterminada,
los parámetros sólo pueden ocupar el lugar de expresiones constantes; no se
pueden utilizar en lugar de nombres de tabla, nombres de columna o nombres de
otros objetos de base de datos.

VARYING Sólo se aplica a los parámetros de tipo cursor por lo que se explicará
cuando se expliquen los cursores.

OUTPUT | OUT (son equivalentes)

Indica que se trata de un parámetro de salida. El valor de esta opción puede


devolverse a la instrucción EXECUTE que realiza la llamada.

El parámetro variable OUTPUT debe definirse al crear el procedimiento y también


se indicará en la llamada junto a la variable que recogerá el valor devuelto del
parámetro. El nombre del parámetro y de la variable no tienen por qué coincidir;
sin embargo, el tipo de datos y la posición de los parámetros deben coincidir a
menos que se indique el nombre del parámetro en la llamada de la forma
@parametro=valor.

Procedimiento básico
CREATE PROCEDURE  Dice_Hola
AS
PRINT ‘Hola’;
GO           –- Indicamos GO para cerrar el lote que crea el
procedimiento y empezar otro lote.
EXEC Dice_Hola;  -- De esta forma llamamos al procedimiento (se
ejecuta).

En este caso, como la llamada es la primera del lote (va detrás del GO) podíamos
haber obviado la palabra EXEC y haber escrito directamente:

Dice_Hola
Con un parámetro de entrada (la palabra que queremos que escriba)
CREATE PROCEDURE Dice_Palabra @palabra CHAR(30)
AS
PRINT @palabra;
GO
EXEC Dice_Palabra ‘Lo que quiera’;
EXEC Dice_Palabra ‘Otra cosa’;

Aquí hemos hecho dos llamadas, una con el valor ‘Lo que quiera’ y otra con el
valor ‘Otra cosa’.

Con varios parámetros

Si queremos indicar varios parámetros los separamos por comas.

USE Biblio;
--DROP PROC VerUsuariosPoblacion; --La comentamos la primera vez
GO
CREATE PROCEDURE VerUsuariosPoblacion @pob CHAR(30),@pro CHAR(30)
AS
SELECT * FROM usuarios WHERE poblacion=@pob AND provincia = @pro;
GO
EXEC VerUsuariosPoblacion Michoacán, Vallarta

En la llamada podemos indicar los valores de los parámetros de varias formas,

indicando sólo el valor de los  parámetros (en este caso los tenemos que indicar
en el mismo orden en que están definidos) como en el ejemplo anterior, o bien
indicando el nombre del parámetro:

EXEC VerUsuariosPoblacion @pob=Michoacán, @pro=Vallarta

Indicar el nombre del parámetro en la llamada también nos permite indicar los
valores en cualquier orden, la siguiente llamada es equivalente a la anterior,
hemos invertido el orden y se ejecuta igual:

EXEC VerUsuariosPoblacion @pro=Vallarta, @pob=Michoacán

En este procedimiento todos los parámetros son obligatorios, no deja llamar con
un solo parámetro.

EXEC VerUsuariosPoblacion Michoacán

Da error.
Con parámetros opcionales

Para definir un parámetro opcional tenemos que asignarle un valor por defecto   en
la definición del procedimiento.

DROP PROC VerUsuariosPoblacion2;


GO
CREATE PROCEDURE VerUsuariosPoblacion2 @pob CHAR(30),@pro
CHAR(30)='Michoacán'
AS
SELECT * FROM usuarios WHERE poblacion=@pob AND provincia = @pro;
GO
EXEC VerUsuariosPoblacion2 Michoacán

En este caso, en la llamada sólo hemos indicado un valor, para el primer


parámetro, el paramétro opcional no lo hemos indicado y se rellenará con el valor
por defecto que indicamos en la definición.

Lo podemos hacer así porque el parámetro opcional es el último de la lista de


parámetros. Cuando el parámetro opcional no es el último la llamada tiene que ser
diferente, tenemos que nombrar los parámetros en la llamada, al menos a partir
del parámetro opcional.

DROP PROC VerUsuariosPoblacion3;


GO
CREATE PROCEDURE VerUsuariosPoblacion3 @a CHAR(2),@pob
CHAR(30)='Michoacán',@pro CHAR(30)
AS
SELECT * FROM usuarios WHERE poblacion=@pob AND provincia = @pro;
GO
EXEC VerUsuariosPoblacion3  a,@pro='Michoacán'

En este caso el parámetro opcional es el segundo de la lista de parámetros,


cuando hacemos la llamada no podemos hacer como en otros lenguajes de dejar
un hueco:

EXEC VerUsuariosPoblacion3  a, ,'Michoacán'

Da error.

 Esta forma da error, debemos utilizar la palabra DEFAULT o indicar el nombre de


todos los parámetros que van detrás del opcional.

EXEC VerUsuariosPoblacion3  a,DEFAULT,'Michoacán'


Con parámetros de salida

Un procedimiento almacenado puede también devolver resultados, definiendo el


paramétro como OUTPUT o bien utilizando la instrucción RETURN que veremos
en el siguiente apartado.

Para poder recoger el valor devuelto por el procedimiento, en la llamada se tiene


que indicar una variable donde guardar ese valor.

Ejemplo:

Definimos el procedimiento ultimo_contrato que nos devuelte la fecha en que se


firmó el último contrato en una determinada oficina. El procedimiento tendrá pues
dos parámetros, uno de entrada para indicar el número de la oficina a considerar y
uno de salida que devolverá la fecha del contrato más reciente de entre los
empleados de esa oficina.

USE Gestion
GO
CREATE PROC ultimo_contrato @ofi INT, @fecha DATETIME OUTPUT
AS
SELECT @fecha=(SELECT MAX(contrato) FROM empleados WHERE
oficina=@ofi)
GO

Con @fecha DATETIME OUTPUT indicamos que el parámetro @fecha es de


salida, el proceso que realice la llamada podrá recoger su valor después de
ejecutar el procedimiento.

En la llamada, para los parámetros de salida, en vez de indicar un valor de entrada


se indica un nombre de variable, variable que recogerá el valor devuelto por el
procedimiento sin olvidar la palabra OUTPUT:

DECLARE @ultima AS DATETIME;


EXEC ultimo_contrato 12,@ultima OUTPUT;
PRINT @ultima;

RETURN

RETURN ordena salir incondicionalmente de una consulta o procedimiento, se


puede utilizar en cualquier punto para salir del procedimiento y las instrucciones
que siguen a RETURN no se ejecutan. Además los procedimientos almacenados
pueden devolver un valor entero mediante esta orden.

RETURN [expresion_entera]

Expresion_entera es el valor entero que se devuelve.


A menos que se especifique lo contrario, todos los procedimientos almacenados
del sistema devuelven el valor 0. Esto indica que son correctos y un valor distinto
de cero indica que se ha producido un error.

Cuando se utiliza con un procedimiento almacenado, RETURN no puede devolver


un valor NULL. Si un procedimiento intenta devolver un valor NULL (por ejemplo,
al utilizar RETURN @var si @var es NULL), se genera un mensaje de advertencia
y se devuelve el valor 0.

Si queremos recoger el valor de estado devuelto por el procedimiento la llamada


debe ser distinta, y seguir el siguiente modelo:

EXECUTE @variable_donde_recogemos_estado = nombre_procedimiento


@par, ...

Con el procedimiento del punto anterior no se puede utilizar esta forma de


devolver un resultado porque lo que se devuelve es una fecha, no es un valor
entero, pero si quisiéramos definir un procedimiento que nos devuelva el número
de empleados de una oficina podríamos hacerlo de dos formas:

De la forma normal con un parámetro de salida:

USE Gestion
GO
CREATE PROC trabajadores @ofi INT, @num INT OUTPUT
AS
SELECT @num=(SELECT COUNT(*) FROM empleados WHERE oficina=@ofi)
GO

Para obtener en la variable @var el resultado devuelto por el procedimiento la


llamada sería:

DECLARE @var INT;


EXEC trabajadores 12, @var OUTPUT

Utilizando return en vez de un parámetro de salida:

CREATE PROC trabajadores2 @ofi INT


AS
RETURN (SELECT COUNT(*) FROM empleados WHERE oficina=@ofi)
GO

Para obtener en la variable @var el resultado devuelto por el procedimiento la


llamada sería:

DECLARE @var INT;


EXEC @var= trabajadores2 12
PRINT @var
Nos visualiza el número de trabajadores de la oficina número 12.

9.5. Instrucciones de control de flujo

Disponemos de diferentes elementos para el control de flujo, como pueden ser


RETURN, IF... ELSE, WHILE, BREAK, CONTINUE, GO TO, EXECUTE, etc. En
los siguientes apartados aprenderemos cómo utilizarlos.

9.6. IF…   ELSE

Proporciona una ejecución condicional, permite ejecutar o no ciertas instrucciones


dependiendo de si se cumple o no una determinada condición.

IF condicion
     { sentencia_sql | bloque_sql }
[ ELSE
     { sentencia_sql | bloque_sql } ]

Si la condición se cumple (da como resultado TRUE) se ejecuta la instrucción SQL


o bloque de instrucciones que aparecen a continuación de la condición, si la
condición no se cumple se ejecutan las sentencias que aparecen después de la
palabra ELSE. El bloque ELSE es opcional.

Ejemplo: Si nos queremos guardar en una consulta todos los ejemplos para
probarlos en cualquier momento, es conveniente antes de los  CREATE
PROCEDURE colocar un DROP PROCEDURE para que la instrucción CREATE
no dé error si el procedimiento ya existe, pero la primera vez la instrucción DROP
PROC nos dará error porque el procedimiento todavía no existe, así que lo mejor
es ejecutar el DROP sólo si el procedimiento existe, utilizando la función
object_id(‘nombre_de_objeto’,’tipo de objeto’) que nos devuelve el id del objeto y
NULL si el objeto no existe.

IF object_id('trabajadores', 'P') IS NOT NULL


      DROP PROCEDURE trabajadores;
GO
CREATE PROC trabajadores….

Otro ejemplo. Ahora queremos el procedimiento y si no existe se mandará un


mensaje "el procedimiento no existe":

IF object_id('trabajadores', 'P') IS NOT NULL


BEGIN
       PRINT 'Borramos el procedimiento'
       DROP PROC trabajadores
END
ELSE PRINT 'El procedimiento ya existe'

Cuando queremos definir un bloque de instrucciones utilizamos los delimitadores


BEGIN..END
Se pueden anidar varias sentencias IF hasta el límite que permita la memoria.

9.7. WHILE – BREAK- CONTINUE

Esta instrucción permite definir un bucle que repite una sentencia o bloque de
sentencias mientras se cumpla una determinada condición.

WHILE condicion
     { sentencia_sql | bloque_sql }
     | BREAK 
     | CONTINUE

Podemos anidar bucles, colocar un bucle WHILE dentro de otro

BREAK Produce la salida del bucle WHILE más interno. La instrucción BREAK
interna sale al siguiente bucle más externo. Todas las instrucciones que se
encuentren después del final del bucle interno se ejecutan primero y después se
reinicia el siguiente bucle más externo.

CONTINUE Hace que se reinicie el bucle WHILE y omite las instrucciones que
haya después de la palabra clave CONTINUE.

Por ejemplo, tenemos los siguientes bucles anidados:

WHILE condicion1                -- Bucle 1


Instrucciones_1_1
WHILE condicion2              -- Bucle 2
       Intrucciones2_1
       If condicion3 BREAK
       Instrucciones2_2
       IF condicion4 CONTINUE
       Instrucciones2_3
Instrucciones1_2

Las instrucciones se ejecutarían en este orden:

Preguntamos por condicion1:

  Si condicion1 se cumple:

           Se ejecuta el bloque Instrucciones1_1

           Preguntamos por condicon2:

           Si se cumple condicion2:

                     Se ejecuta el bloque de instrucciones Instrucciones_2_1.

                     Si condición3 se cumple:


                              Salimos del bucle 2,

                              Se ejecuta el bloque Instrucciones1_2

                              Y se vuelve a preguntar por condicion1.

                     Si condición3 no se cumple:

                              Se ejecuta el bloque instrucciones2_2

                              Si se cumple condicion4:

                                       Saltamos el bloque Instrucciones2_3

                                       Y se vuelve a preguntar por condicion2

                              Si no se cumple condicion4:

                                       Se ejecuta el bloque Instrucciones2_3

                                       Y se vuelve a preguntar por condicion2

           Si condicion2 no se cumple:

                     Se ejecuta el bloque Instrucciones 1_2

                     Se vuelve a preguntar por condicion1

  Si condicion1 no se cumple:

           Hemos terminado

9.8. WAITFOR

Bloquea la ejecución de un lote, un procedimiento almacenado o una transacción


hasta alcanzar la hora o el intervalo de tiempo especificado, o hasta que una
instrucción especificada modifique o devuelva al menos una fila. Nosotros
estudiaremos los dos primeros casos.

WAITFOR {DELAY 'tiempo_a_transcurrir'


        |TIME 'fechaHora_de_ejecucion'}

DELAY permite indicar un período de tiempo especificado (hasta un máximo de 24


horas) que debe transcurrir antes de la ejecución de un lote, un procedimiento
almacenado o una transacción.
'tiempo_a_transcurrir' Es el período de tiempo que hay que esperar, se puede
especificar en uno de los formatos aceptados para el tipo de datos datetime o
como una variable local. No se pueden especificar fechas; por lo tanto, no se
permite la parte de fecha del valor datetime.

TIME permite indicar la hora especifica a la que se ejecuta el lote, el procedimiento


almacenado o la transacción.

'fechaHora_de_ejecucion' Es la hora a la que termina la instrucción WAITFOR por


tanto a la que empieza la ejecución de las instrucciones siguientes a WAITFOR.
Se puede especificar en uno de los formatos aceptados para el tipo de datos
datetime o como una variable local. No se pueden especificar fechas; por lo tanto,
no se permite la parte de fecha del valor datetime.

Cada instrucción WAITFOR tiene un subproceso asociado. Si se especifica un


gran número de instrucciones WAITFOR en el mismo servidor, se pueden
acumular muchos subprocesos a la espera de que se ejecuten estas
instrucciones. SQL Server supervisa el número de subprocesos asociados con las
instrucciones WAITFOR y selecciona aleatoriamente algunos de estos
subprocesos para salir si el servidor empieza a experimentar la falta de
subprocesos.

Ejemplo:

PRINT CONVERT(CHAR(8),Getdate(),108);
WAITFOR DELAY '00:00:03'
PRINT CONVERT(CHAR(8),Getdate(), 108);
WAITFOR DELAY '00:03'
PRINT CONVERT(CHAR(8),Getdate(), 108);

Visualiza la hora actual, espera 3 segundos y vuelve a visualizar la hora actual,


después espera 3 minutos y vuelve a visualizar la hora actual. Se ha utilizado la
función CONVERT con el estilo 108 para que aparezca sólo la hora y con
segundos.

9.9. GOTO

Altera el flujo de ejecución y lo dirige a una etiqueta.

Las etiquetas se indican mediante un nombre seguido del carácter:

GOTO NombreEtiqueta

Ejemplo:

IF Condicion GOTO etiq


----
Etiq:
----
----

Es una instrucción a evitar porque puede llevar a redactar programas no


estructurados.

9.10. TRY... CATCH

Definición

La estructura TRY…CATCH implementa un mecanismo de control de errores para


Transact-SQL, permite incluir un grupo de instrucciones Transact-SQL en un
bloque TRY. Si se produce un error en el bloque TRY, el control se transfiere a
otro grupo de instrucciones que está incluido en un bloque CATCH.

BEGIN TRY
     {sentencia_sql|bloque_sql}
END TRY
BEGIN CATCH
      [{sentencia_sql|bloque_sql}]
END CATCH [ ; ]

Una construcción TRY…CATCH no puede abarcar varios bloques de instrucciones


Transact-SQL (varios bloques BEGIN…END de instrucciones Transact-SQL) ni 
una construcción IF…ELSE.

Si no hay errores en el código incluido en un bloque TRY, cuando la última


instrucción de este bloque ha terminado de ejecutarse, el control se transfiere a la
instrucción inmediatamente posterior a la instrucción END CATCH asociada. Si
hay un error en el código incluido en el bloque TRY, el control se transfiere a la
primera instrucción del bloque CATCH asociado. Si la instrucción END CATCH es
la última instrucción de un procedimiento almacenado o desencadenador, el
control se devuelve a la instrucción que llamó al procedimiento almacenado o
activó el desencadenador.

Las construcciones TRY…CATCH se pueden anidar.

Las construcciones TRY…CATCH capturan los errores no controlados de los


procedimientos almacenados o desencadenadores ejecutados por el código del
bloque TRY. Alternativamente, los procedimientos almacenados o
desencadenadores pueden contener sus propias construcciones TRY…CATCH
para controlar los errores generados por su código. Por ejemplo, cuando un
bloque TRY ejecuta un procedimiento almacenado y se produce un error en éste,
el error se puede controlar de las formas siguientes:

 Si el procedimiento almacenado no contiene su propia construcción TRY…CATCH, el error


devuelve el control al bloque CATCH asociado al bloque TRY que contiene la instrucción
EXECUTE.
 Si el procedimiento almacenado contiene una construcción TRY…CATCH, el error
transfiere el control al bloque CATCH del procedimiento almacenado. Cuando finaliza el
código del bloque CATCH, el control se devuelve a la instrucción inmediatamente posterior
a la instrucción EXECUTE que llamó al procedimiento almacenado.

No se pueden utilizar instrucciones GOTO para entrar en un bloque TRY o CATCH


pero se puede utilizar para saltar a una etiqueta dentro del mismo bloque TRY o
CATCH, o bien para salir de un bloque TRY o CATCH.

TRY…CATCH no se puede utilizar en una función definida por el usuario.


Recuperar información sobre errores

En el ámbito de un bloque CATCH, se pueden utilizar las siguientes funciones del


sistema para obtener información acerca del error que provocó la ejecución del
bloque CATCH:

 ERROR_NUMBER() devuelve el número del error.


 ERROR_SEVERITY() devuelve la gravedad.
 ERROR_STATE() devuelve el número de estado del error.
 ERROR_PROCEDURE() devuelve el nombre del procedimiento almacenado o
desencadenador donde se produjo el error.
 ERROR_LINE() devuelve el número de línea de la rutina que provocó el error.
 ERROR_MESSAGE() devuelve el texto completo del mensaje de error. Este texto incluye
los valores suministrados para los parámetros reemplazables, como longitudes, nombres
de objetos u horas.

Estas funciones devuelven NULL si se las llama desde fuera del ámbito del bloque
CATCH. Con ellas se puede recuperar información sobre los errores desde
cualquier lugar dentro del ámbito del bloque CATCH. Por ejemplo, en la siguiente
secuencia de comandos se muestra un procedimiento almacenado que contiene
funciones de control de errores. Se llama al procedimiento almacenado en el
bloque CATCH de una construcción TRY…CATCH y se devuelve información
sobre el error.

Ejemplo, vamos a definir un procedimiento para calcular el precio unitario a partir


de un importe y una cantidad:

-- Verificamos que el procedimiento que vamos a crear no existe.


IF OBJECT_ID ('CalculaPrecio', 'P' ) IS NOT NULL
    DROP PROCEDURE CalculaPrecio;
GO
-- Creamos el procedimiento
CREATE PROCEDURE CalculaPrecio @importe MONEY,@cant INT,@precio
MONEY OUTPUT
AS
BEGIN TRY
    SELECT @precio=@importe/@cant;
END TRY
BEGIN CATCH
IF ERROR_NUMBER() = 8134 SELECT @precio=0
ELSE SELECT ERROR_NUMBER() as ErrorNumero,ERROR_MESSAGE() as
MensajeDeError;
END CATCH;
GO
-- Lo utilizamos
DECLARE @resultado MONEY
EXEC CalculaPrecio 1000, 0, @resultado OUTPUT
SELECT 'resul', @resultado
Si al llamar al procedimiento le pasamos una cantidad igual a cero, la división
provocará un error, se pasará el control al bloque CATCH, en este bloque se
evalúa si el error corresponde al error de división por cero (8134), si es así el
procedimiento devuelve un precio igual a cero, sino se envía un mensaje para
avisar del error.

Errores controlados por TRY…CATCH

TRY…CATCH  detecta todos los errores de ejecución que tienen una gravedad
mayor de 10 y que no cierran la conexión de la base de datos.

TRY…CATCH no detecta:

 Advertencias o mensajes informativos que tienen gravedad 10 o inferior.


 Errores que tienen gravedad 20 o superior que detienen el procesamiento de las tareas de
SQL Server Database Engine en la sesión. Si se produce un error con una gravedad 20 o
superior y no se interrumpe la conexión con la base de datos, TRY…CATCH controlará el
error.
 Atenciones, como solicitudes de interrupción de clientes o conexiones de cliente
interrumpidas.
 Cuando el administrador del sistema finaliza la sesión mediante la instrucción KILL.
 Un bloque CATCH no controla los siguientes tipos de errores cuando se producen en el
mismo nivel de ejecución que la construcción TRY…CATCH:
 Errores de compilación, como errores de sintaxis, que impiden la ejecución de un lote.
 Errores que se producen durante la recompilación de instrucciones, como errores de
resolución de nombres de objeto que se producen después de la compilación debido a una
resolución de nombres diferida.

Estos errores se devuelven al nivel de ejecución del lote, procedimiento


almacenado o desencadenador.

En el ejemplo siguiente se muestra cómo la construcción TRY…CATCH no


captura un error de resolución de nombre de objeto generado por una instrucción
SELECT, sino que es el bloque CATCH el que lo captura cuando la misma
instrucción SELECT se ejecuta dentro de un procedimiento almacenado (a un
nivel inferior).

USE Gestion;
GO
BEGIN TRY
    SELECT * FROM TablaQueNoExiste;
END TRY
BEGIN CATCH
    SELECT ERROR_NUMBER() as ErrorNumero,ERROR_MESSAGE() as
MensajeDeError;
END CATCH

Intentamos ejecutar una SELECT de una tabla que no existe en la base de datos.
El error no se captura y el control se transfiere fuera de la construcción TRY…
CATCH, al siguiente nivel superior, en este caso salta el mensaje de error del
sistema.

Al ejecutar la misma instrucción SELECT dentro de un procedimiento almacenado,


el error se producirá en un nivel inferior al bloque TRY y la construcción TRY…
CATCH controlará el error.

IF OBJECT_ID ('TablaInexistente','P') IS NOT NULL


    DROP PROCEDURE TablaInexistente;
GO
-- Creamos el procedimiento.
CREATE PROCEDURE TablaInexistente
AS
    SELECT * FROM TablaQueNoExiste;
GO
-- Utilizamos el procedimiento
BEGIN TRY
    EXECUTE TablaInexistente
END TRY
BEGIN CATCH
    SELECT ERROR_NUMBER() as ErrorNumero,ERROR_MESSAGE() as
MensajeDeError;
END CATCH;

En este caso como el error se produce dentro del procedimiento, el control del
error se devuelve al nivel superior (el que ha realizado el EXECUTE) por lo que es
capturado por el TRY y entra en el bloque CATCH, en vez de que salte el mensaje
de error del sistema saldrá el nuestro.
Ejercicio paso a paso: Procedimientos
Objetivo

Realizar procedimientos en que utilizaremos estructuras de control de flujo como


BEGIN... END, IF y GO. Además, se hará un repaso a lo que ya hemos aprendido
a lo largo del curso, puesto que los procedimientos, al fin y al cabo sirven para
ejecutar sentencias SQL. Repasaremos cómo crear tablas e insertar datos en
ellas.

Ejercicio paso a paso

Utilizar las sentencias desarrolladas en un ejercicio propuesto del tema anterior


para crear la base de datos Gestion2  a partir de Gestion8 y crear otra base datos
Gestion10 sobre la que realizarás todos los ejercicios de esta unidad.

Para ello vamos a definir tres procedimientos, CreaBase que permita crear una
base de datos con el nombre que le indiquemos en la llamada, otro, BorrarBase
que borre una determinada base de datos, y por último RellenaBase para rellenar
con las tablas de Gestion8 la base de datos que indiquemos en la llamada.

Los procedimientos se crean en Gestion8.

Como no se puede utilizar una variable en un CREATE DATABASE, utilizamos la


función EXEC('cadena SQL') que ejecuta cualquier cadena SQL.

PRINT 'Empieza el ejercicio de procedimientos' -- Para visualizar un


mensaje que indique lo que realiza esta sentencia.
USE Gestion8                 -- Para crear los proc. en Gestion8    
if object_id('BorraBase') IS NOT NULL DROP PROC BorraBase
GO                        -- Necesario igual que el siguiente GO para
delimitar el CREATE PROC
CREATE PROCEDURE BorraBase @base sysname
AS
BEGIN TRY     -- Para que si no existe la base de datos no mande ningún
mensaje
EXEC('DROP DATABASE ' + @base) -- Si en @base se pasa el valor
Gestion10,  se ejecutará: DROP DATABASE Gestion10
      END TRY
      BEGIN CATCH -- Si se produce un error no hará nada
      END CATCH   -- no mandará ningún mensaje de error y seguirá
GO
if object_id('CreaBase') IS NOT NULL DROP PROC CreaBase
GO
CREATE PROCEDURE CreaBase @base sysname
AS
      BEGIN TRY   -- Para que si ya existe la base de datos la no mande
ningún mensaje
            EXEC('CREATE DATABASE '+ @base) -- Creamos la bd con los
parámetros por defecto para simplificar
      END TRY
      BEGIN CATCH
      END CATCH   -- Si se produce un error no hará nada (por ej. si ya
existe la base no mandará ningún mensaje de error y seguirá
GO
if object_id('RellenaGestion') IS NOT NULL DROP PROC RellenaGestion
GO
CREATE PROC RellenaGestion @base sysname
AS
BEGIN
      EXEC('CREATE TABLE '+ @base+'.dbo.empleados(
            numemp      INT NOT NULL PRIMARY KEY CLUSTERED,
            nombre      CHAR(20) NULL,
            edad INT NULL,
            oficina INT NULL,
            titulo      CHAR(20) NULL,
            contrato datetime NULL,
            jefe INT NULL,
            cuota       MONEY NULL,
            ventas      MONEY NULL
      )
      CREATE TABLE '+@base+'.dbo.oficinas(
            oficina  INT NOT NULL,
            ciudad   CHAR(20) NULL,
            region   CHAR(20) NULL,
            dir INT  NULL,
            objetivo MONEY NULL,
            ventas   MONEY NULL CONSTRAINT d1  DEFAULT (0),
            CONSTRAINT PK_oficinas PRIMARY KEY (oficina),
      )
      CREATE TABLE '+@base+'.dbo.clientes(
            numclie INT NOT NULL,
            nombre      CHAR(20) NULL,
            repclie INT NULL,
            limitecredito INT NULL,
            CONSTRAINT PK_clientes PRIMARY KEY (numclie) 
      )
      CREATE TABLE '+@base+'.dbo.productos(
            idfab           CHAR(5) NOT NULL,
            idproducto  CHAR(10) NOT NULL,
            descripcion CHAR(20) NULL,
            precio          MONEY NULL,
            existencias INT NULL,
            CONSTRAINT PK_productos PRIMARY KEY (idfab,idproducto)
      )
      CREATE TABLE '+@base+'.dbo.pedidos(
            codigo          INT IDENTITY NOT NULL,
            numpedido   DEC(10,0) NOT NULL,
            fechapedido DATETIME NOT NULL,
            clie     INT NOT NULL,
            rep       INT NOT NULL,
            fab       CHAR(5) NOT NULL,
            producto    CHAR(10) NOT NULL,
            cant     SMALLINT NULL,
            importe     MONEY NULL,
            CONSTRAINT PK_pedidos PRIMARY KEY (codigo),
      )
      INSERT INTO '+@base+'.dbo.Productos SELECT * FROM
Gestion8.dbo.Productos;
      INSERT INTO '+@base+'.dbo.Empleados SELECT * FROM
Gestion8.dbo.Empleados;
      INSERT INTO '+@base+'.dbo.Clientes SELECT * FROM
Gestion8.dbo.Clientes;
      INSERT INTO '+@base+'.dbo.Oficinas SELECT * FROM
Gestion8.dbo.Oficinas;
      SET IDENTITY_INSERT '+@base+'.dbo.Pedidos ON;
      INSERT INTO '+@base+'.dbo.Pedidos
(codigo,numpedido,fechapedido,clie,rep,fab,producto,cant,importe)
            SELECT * FROM Gestion8.dbo.Pedidos;
      SET IDENTITY_INSERT '+@base+'.dbo.Pedidos OFF;
      ALTER TABLE '+@base+'.dbo.Empleados
            ADD CONSTRAINT FK_empleados_jefe FOREIGN KEY (jefe)
REFERENCES empleados,
                  CONSTRAINT FK_empleados_oficinas FOREIGN KEY (oficina)
REFERENCES oficinas;
      ALTER TABLE '+@base+'.dbo.Oficinas
            ADD CONSTRAINT FK_oficinas_dir FOREIGN KEY (dir) REFERENCES
empleados
      ALTER TABLE '+@base+'.dbo.Clientes
            ADD CONSTRAINT FK_clientes_repclie FOREIGN KEY (repclie)
REFERENCES empleados;
      ALTER TABLE '+@base+'.dbo.Pedidos
            ADD CONSTRAINT FK_pedidos_rep FOREIGN KEY (rep) REFERENCES
empleados,
                CONSTRAINT FK_pedidos_clie FOREIGN KEY (clie) REFERENCES
clientes,
                  CONSTRAINT FK_pedidos_productos FOREIGN KEY
(fab,producto) REFERENCES productos;')
END

GO
Empezar:
USE Gestion8
EXEC BorraBase 'Gestion10'
GO
EXEC CreaBase 'Gestion10'
GO
EXEC RellenaGestion 'Gestion10'
9.11. Desencadenadores o TRIGGERS

Un desencadenador (o Trigger) es una clase especial de procedimiento


almacenado que se ejecuta automáticamente cuando se produce un evento en el
servidor de bases de datos.

SQL Server permite crear varios desencadenadores para una instrucción


específica.

Según el tipo de evento que los desencadena se clasifican en:

  Desencadenadores DML

  Desencadenadores DDL

  Desencadenadores LOGON

Los desencadenadores DML se ejecutan cuando un usuario intenta modificar


datos mediante un evento de lenguaje de manipulación de datos (DML). Los
eventos DML son instrucciones INSERT, UPDATE o DELETE de una tabla o vista.

Los desencadenadores DDL se ejecutan en respuesta a una variedad de eventos


de lenguaje de definición de datos (DDL). Estos eventos corresponden
principalmente a instrucciones CREATE, ALTER y DROP de Transact-SQL, y a
determinados procedimientos almacenados del sistema que ejecutan operaciones
de tipo DDL.

Los desencadenadores logon se activan en respuesta al evento LOGON que se


genera cuando se establece la sesión de un usuario.

Nosotros limitaremos nuestro estudio a los desencadenadores DML.

9.12. CREATE TRIGGER

Esta instrucción nos permite definir un trigger:

CREATE TRIGGER [NombreEsquema.]NombreTrigger


ON {tabla | vista } [,...n] ]
{FOR|AFTER|INSTEAD OF} {[INSERT][,][UPDATE][,][DELETE]}
AS sentencia_sql  [;] [,...n ]

NombreEsquema es el nombre del esquema al que pertenece el desencadenador


DML. Los desencadenadores DML se limitan al esquema de la tabla o vista en la
que se crearon.
NombreTrigger es el nombre que le queremos dar al desencadenador. No puede
comenzar con los símbolos # o ##.

Tabla | vista es el nombre de la tabla o vista en la que se ejecuta el


desencadenador. Sólo se puede hacer referencia a una vista mediante un
desencadenador INSTEAD OF. No es posible definir desencadenadores DML en
tablas temporales locales o globales.

DELETE  INSERT UPDATE Especifica las instrucciones de modificación de datos


que activan el desencadenador cuando se intenta ejecutarlas en esta tabla o vista.
Se debe especificar al menos una opción. En la definición del desencadenador se
permite cualquier combinación de estas opciones, en cualquier orden.

AFTER indica que el desencadenador sólo se activa cuando todas las operaciones
especificadas en la instrucción SQL desencadenadora se han ejecutado
correctamente. Además, todas las acciones referenciales en cascada y las
comprobaciones de restricciones deben ser correctas para que este
desencadenador se ejecute.

AFTER es el valor predeterminado cuando sólo se especifica la palabra clave


FOR.

Los desencadenadores AFTER no se pueden definir en las vistas.

INSTEAD OF indica que se ejecuta el desencadenador en vez de la instrucción


SQL desencadenadora, por lo que se suplantan las acciones de las instrucciones
desencadenadoras.

Como máximo, se puede definir un desencadenador INSTEAD OF por cada


instrucción INSERT, UPDATE o DELETE en cada tabla o vista. No obstante, en
las vistas es posible definir otras vistas que tengan su propio desencadenador
INSTEAD OF.

Los desencadenadores INSTEAD OF no se pueden utilizar en vistas actualizables


que usan WITH CHECK OPTION.

Los desencadenadores INSTEAD OF DELETE/UPDATE  no se permiten en tablas


que tengan una clave ajena definida con ON DELETE/UPDATE CASCADE.

CREATE TRIGGER debe ser la primera instrucción en el proceso por lotes.

Un desencadenador se crea solamente en la base de datos actual; sin embargo,


puede hacer referencia a objetos que están fuera de la base de datos actual.

Si se especifica el nombre del esquema del desencadenador hay que calificar


también el nombre de la tabla o vista.
En un desencadenador se puede especificar cualquier instrucción SET. La opción
SET seleccionada permanece en efecto durante la ejecución del desencadenador
y, después, vuelve automáticamente a su configuración anterior.

Un desencadenador está diseñado para comprobar o cambiar los datos en base a


una instrucción de modificación o definición de datos; no debe devolver datos al
usuario por lo que se aconseja no incluir en un desencadenador instrucciones
SELECT que devuelven resultados ni las instrucciones que realizan una
asignación variable. Si es preciso que existan asignaciones de variable en un
desencadenador, tenemos que utilizar la instrucción SET NOCOUNT al principio
del mismo para impedir la devolución de cualquier conjunto de resultados.

Los desencadenadores DML usan las tablas lógicas  deleted e inserted. Son de
estructura similar a la tabla en que se define el desencadenador, es decir, la tabla
en que se intenta la acción del usuario. Las tablas deleted e inserted guardan los
valores antiguos o nuevos de las filas que la acción del usuario puede cambiar.

Si un desencadenador INSTEAD OF definido en una tabla ejecuta una instrucción


en la tabla que normalmente volvería a activarlo, al desencadenador no se lo llama
de forma recursiva. En su lugar, la instrucción se procesa como si la tabla no
tuviera un desencadenador INSTEAD OF e inicia la cadena de operaciones de
restricción y ejecuciones de desencadenadores AFTER. Por ejemplo, si para una
tabla se define un desencadenador como INSTEAD OF INSERT, y éste ejecuta
una instrucción INSERT en la misma tabla, la instrucción INSERT ejecutada por el
desencadenador INSTEAD OF no vuelve a llamar al desencadenador. La
instrucción INSERT ejecutada por el desencadenador inicia el proceso que realiza
las acciones de restricción y activa cualquier desencadenador AFTER INSERT
definido para la tabla.

Si un desencadenador INSTEAD OF definido en una vista ejecuta una instrucción


en la vista que normalmente volvería a activarlo, no se llamará el desencadenador
de forma recursiva. En su lugar, la instrucción se resuelve a modo de
modificaciones en las tablas base subyacentes de la vista. En este caso, la
definición de la vista debe cumplir todas las restricciones para una vista
actualizable.

Aunque una instrucción TRUNCATE TABLE es en realidad un desencadenador


DELETE, no puede activar un desencadenador porque la operación no registra las
eliminaciones de fila individuales.

Las siguientes instrucciones Transact-SQL no están permitidas en un


desencadenador DML:

ALTER DATABASE       CREATE DATABASE            DROP DATABASE


Además, las siguientes instrucciones Transact-SQL no se permiten en el cuerpo
de un desencadenador DML cuando éste se utiliza en la tabla o vista que es
objeto de la acción desencadenadora:

CREATE INDEX          ALTER INDEX          DROP INDEX           DROP TABLE

ALTER TABLE cuando se utiliza para hacer lo siguiente:

 Agregar, modificar o quitar columnas.


 Cambiar particiones.
 Agregar o quitar restricciones de tipo PRIMARY KEY o UNIQUE.

Ejemplo:

USE Gestion8
GO
CREATE TRIGGER ActualizaVentasEmpleados
ON pedidos FOR INSERT
AS
UPDATE empleados SET ventas=ventas+inserted.importe
FROM empleados, inserted
WHERE numemp=inserted.rep;
GO
Cuando se INSERTe un pedido, entrará en funcionamiento el trigger
ActualizaVentasEmpleado y se ejecutarán las instrucciones que aparecen
después de AS, en este caso actualizará (UPDATE)  la tabla empleados sumará a
las ventas del empleado (ventas) el importe del pedido insertado
(inserted.importe), y sólo actualizará el empleado cuyo numemp coincida con el
campo rep del pedido insertado (WHERE numemp=inserted.rep).

-- Ahora comprobamos que funciona


SELECT * FROM empleados WHERE numemp=108;
INSERT INTO pedidos
(numpedido,fechapedido,rep,clie,cant,importe,fab,producto)
             VALUES (123456789,getdate(),108,2103,10,100,'Aci',41001)
SELECT * FROM empleados WHERE numemp=108;

Vemos que al insertar un pedido de 100 € del empleado 108, sus ventas han
aumentado en 100€.

ALTER TRIGGER

Permite modificar la definición del desencadenador, no permite cambiar su


nombre, para cambiar el nombre de un desencadenador hay que eliminarlo
(DROP TRIGGER) y volver a crearlo (CREATE TRIGGER).

ALTER TRIGGER [NombreEsquema.]NombreTrigger


ON {tabla|vista}
{FOR|AFTER|INSTEAD OF} {[INSERT][,][UPDATE][,][DELETE]} [WITH
APPEND]
AS sentencia_sql  [;] [,...n ]

La sintaxis es similar a la instrucción CREATE TRIGGER.

Ejemplo:

ALTER TRIGGER ActualizaVentasEmpleados


ON pedidos FOR INSERT
AS
UPDATE empleados SET ventas=ventas+inserted.importe
FROM empleados, inserted
WHERE numemp=inserted.rep AND inserted.importe IS NOT NULL;

Hemos modificado el desencadenador para que si el importe del pedido es nulo,


no haga nada, no actualice con un valor nulo.

Realiza el siguiente Ejercicio Triggers para practicar la creación de


desencadenadores.

9.13. DISABLE TRIGGER


En ocasiones puede ser útil inhabilitar temporalmente un desencadenador sin que
por ello suponga eliminarlo, para estos casos podemos utilizar la sentencia
DISABLE TRIGGER.

DISABLE TRIGGER {[NombreEsquema.]NombreTrigger [,...n] | ALL }


ON {NombreTablaVista | DATABASE | ALL SERVER} [;]

Ejemplo:

DISABLE TRIGGER ActualizaVentasEmpleado ON pedidos;

Deshabilita el desencadenador que hemos creado anteriormente, si después de


ejecutar esta sentencia se introduce un nuevo pedido, el empleado
correspondiente no se actualizará.

Lo podemos comprobar con:

SELECT * FROM empleados WHERE numemp=108;


INSERT INTO pedidos
(numpedido,fechapedido,rep,clie,cant,importe,fab,producto)
                   VALUES
(123456791,getdate(),108,2103,10,300,'Aci',41001)
SELECT * FROM empleados WHERE numemp=108;
DISABLE TRIGGER ALL ON pedidos;

Deshabilita todos los desencadenadores asociados a la tabla pedidos.

DISABLE TRIGGER ALL ON DATABASE;

Deshabilita todos los desencadenadores definidos en la base de datos actual.

DISABLE TRIGGER ALL ON ALL SERVER;

Deshabilita todos los desencadenadores definidos en el servidor.


9.14. ENABLE TRIGGER

Con esta instrucción volvemos a habilitar los desencadenadores desactivados por


la instrucción anterior.

ENABLE TRIGGER {[NombreEsquema.]NombreTrigger [,...n] | ALL }


ON {NombreTablaVista | DATABASE | ALL SERVER} [ ; ]

Funciona de la misma manera que DISABLE TRIGGER pero habilita en vez de


deshabilitar.

Con este ejemplo lo podemos comprobar:

ENABLE TRIGGER ActualizaVentasEmpleados ON pedidos;


SELECT * FROM empleados WHERE numemp=108;
INSERT INTO pedidos
(numpedido,fechapedido,rep,clie,cant,importe,fab,producto)
     VALUES (123456792,getdate(),108,2103,10,400,'Aci',41001)
SELECT * FROM empleados WHERE numemp=108;

9.15. DROP TRIGGER

Para eliminar un desencadenador tenemos la instrucción DROP TRIGGER elimina


la definición del desencadenador.

DROP TRIGGER NombreEsquema.NombreTrigger [,...n] [;]

Ejemplo:

DROP TRIGGER ActualizaVentasEmpleados

Elimina el desencadenador ActualizaVentasEmpleados.


Nota: Para realizar los ejercicios, utiliza Gestion10 que has creado en los
ejercicios paso a paso de este tema.

1. Añadir a la tabla de productos un campo StockMinimo, y rellenarlo de tal forma


que el stock mínimo valorado de cada artículo sea de 100€. El stock mínimo
valorado es el resultado de multiplicar el stock mínimo por el precio del producto.
No hace falta incluir estas sentencias en un procedimiento ya que sólo nos sirven
para preparar la tabla productos pero redactarlas en Transact-SQL para repasar
temas anteriores.

Seguidamente, escribir un procedimiento que se pueda ejecutar en cualquier


momento y que actualice una lista de productos bajo stock. Esta lista se guarda en
una tabla ProductosAPedir y contiene el código completo del producto, su
descripción, existencias y cantidad a pedir (para que el producto supere en 5
unidades su stock mínimo). Si un producto bajo mínimo (cuyas existencias no
llegan al stock mínimo) no está en esa tabla, insertarlo, si está actualizar el campo
CantidadAPedir, y si  ya no está bajo mínimo y está en la tabla, eliminar el registro
de ProductosAPedir.

2. Obtener un listado de las oficinas de una determinada región con ventas


superiores a un determinado importe.

3. Crea un nuevo procedimiento parecido al anterior pero que nos devuelva


también el número de oficinas recuperadas. Utilizar un parámetro de salida.

4. Crea un nuevo procedimiento parecido al anterior pero que nos devuelva


también el número de oficinas recuperadas. Sin utilizar parámetros de salida.

5. Crea un  procedimiento que muestre los n productos más caros, n es un valor
que se indicará en la llamada.

6. Crea otro procedimiento que muestre los n productos más caros con empates y
nos devuelva cuántos hay.

7. No dejar eliminar productos que tengan existencias.

8. Hacer que no se pueda insertar un pedido si no hay suficiente stock.

9. Crea un procedimiento que impida eliminar varias oficinas en una sóla


operación DELETE.
Ayuda ejercicios unidad 9:
Programación en TRANSACT SQL (I)
Consejo: Es recomedable copiar el código en la consulta del SSMS y tratar de
entenderlo desde ahí, porque son consultas extensas y se ven más claras con las
palabras coloreadas.

1. Añadir a la tabla de productos un campo StockMinimo, y rellenarlo de tal forma


que el stock mínimo valorado de cada artículo sea de 100€. El stock mínimo
valorado es el resultado de multiplicar el stock mínimo por el precio del producto.
No hace falta incluir estas sentencias en un procedimiento ya que sólo nos sirven
para preparar la tabla productos pero redactarlas en Transact-SQL para repasar
temas anteriores.

PRINT 'Empieza el ejercicio 1'


USE Gestion10;
Alter TABLE Productos ADD StockMinimo INTEGER DEFAULT 0;
GO
UPDATE Productos SET StockMinimo= 100/(CASE WHEN Precio IS NULL OR
Precio=0 THEN 100 ELSE Precio END);
-- Utilizamos CASE para que si el producto no tiene precio o un precio
igual a cero que asuma StockMinimo 1.

Seguidamente, escribir un procedimiento que se pueda ejecutar en cualquier


momento y que actualice una lista de productos bajo stock. Esta lista se guarda en
una tabla ProductosAPedir y contiene el código completo del producto, su
descripción, existencias y cantidad a pedir (para que el producto supere en 5
unidades su stock mínimo). Si un producto bajo mínimo (cuyas existencias no
llegan al stock mínimo) no está en esa tabla, insertarlo, si está actualizar el campo
CantidadAPedir, y si  ya no está bajo mínimo y está en la tabla, eliminar el registro
de ProductosAPedir.

PRINT 'Empieza el ejercicio 1 bis'


USE Gestion10
IF OBJECT_ID('ActualizaProductosAPedir','P') IS NOT NULL DROP PROC
ActualizaProductosAPedir
GO
CREATE PROC ActualizaProductosAPedir
AS
BEGIN
      IF OBJECT_ID('ProductosAPedir','U') IS NULL
        CREATE TABLE ProductosAPedir (
Afab CHAR(5),
Aproducto CHAR(10),
Adescripcion CHAR(20),
Aexistencias INT,
Apedir SMALLINT,
PRIMARY KEY (Afab,Aproducto)
)
     
      DELETE FROM ProductosAPedir WHERE EXISTS (SELECT * FROM
Productos WHERE idfab=Afab AND idproducto=Aproducto AND existencias >=
StockMinimo)
      --Borramos los ProductosAPedir de los productos que rebasan el
stock mínimo, si rebasa el stock el producto ya no tiene que estar en la
tabla
      UPDATE ProductosAPedir SET Apedir=StockMinimo+5-existencias
      FROM ProductosAPedir INNER JOIN Productos ON idfab=Afab AND
idproducto=Aproducto
      WHERE existencias < Stockminimo
      -- Actualizamos los ProductosAPedir de productos que no llegan
al stock mínimo
      INSERT INTO ProductosAPedir       
                  SELECT
Idfab,Idproducto,Descripcion,existencias,StockMinimo+5-existencias
                  FROM Productos
                  WHERE existencias < Stockminimo
                        AND NOT EXISTS (SELECT * FROM ProductosAPedir
WHERE idfab=Afab AND idproducto=Aproducto);
      -- Insertamos nuevos ProductosAPedir de productos que no llegan
al stock mínimo y que no están ya en ProductosAPedir (para no
repetirlos).
END;
GO
USE Gestion10
SELECT * FROM Productos WHERE existencias < Stockminimo
EXEC ActualizaProductosAPedir
SELECT * FROM ProductosAPedir

2. Obtener un listado de las oficinas de una determinada región con ventas


superiores a un determinado importe.

PRINT 'Empieza el ejercicio 2'


USE Gestion10
IF OBJECT_ID('Listado_Oficinas1','P') IS NOT NULL DROP PROCEDURE
Listado_Oficinas1
GO
CREATE PROCEDURE Listado_Oficinas1 @region CHAR(20),@ventas MONEY
AS
      SELECT * FROM oficinas WHERE region= @region AND ventas > @ventas
GO
EXEC Listado_Oficinas1 Este, 1000
EXEC Listado_Oficinas1 Este, 100000
EXEC Listado_Oficinas1 Norte, 0
EXEC Listado_Oficinas1 Norte, 10000

3. Crear un nuevo procedimiento parecido al anterior pero que nos devuelva


también el número de oficinas recuperadas. Utilizar un parámetro de salida.

PRINT 'Empieza el ejercicio 3'


USE Gestion10
IF OBJECT_ID('Listado_Oficinas2','P') IS NOT NULL DROP PROCEDURE
Listado_Oficinas2
GO
CREATE PROCEDURE Listado_Oficinas2 @region CHAR(20),@ventas MONEY,
@cuantas INT OUTPUT
AS
      SELECT * FROM oficinas WHERE region= @region AND ventas >
@ventas
      SET @cuantas=(SELECT COUNT(*) FROM oficinas WHERE region=
@region AND ventas > @ventas)
GO
DECLARE @resultado INT
EXEC Listado_Oficinas2 Este, 1000 ,@resultado OUTPUT
SELECT @resultado
EXEC Listado_Oficinas2 Este, 100000 ,@resultado OUTPUT
PRINT @resultado
EXEC Listado_Oficinas2 Norte, 0 ,@resultado OUTPUT
PRINT @resultado
EXEC Listado_Oficinas2 Norte, 10000 ,@resultado OUTPUT
PRINT @resultado

También podría gustarte