Diseño de bases de datos para todos los públicos Fernando Cano Espinosa Juan David González Cobas Mayo de 2003 1. Conceptos previos Antes de introducirnos en los problemas de diseño de bases de datos relacionales se hace necesario estudiar algunos conceptos básicos asociados fundamentalmente a las dependencias funcionales (DF). Vamos a definir una DF α → β, siendo α y β conjunto de atributos (columnas de una tabla), como una restricción que nos dice que cada valor concreto de los atributos α está asociado a un valor concreto de los atributos β. Es decir que si dos filas de una relación comparten el mismo valor para α entonces deben tener el mismo valor para β: t1 [α] = t2 [α] =⇒ t1 [β] = t2 [β] (1) Para ilustrar el concepto de DF supongamos que en una tabla tenemos una columna Profesor y otra Asignatura, entonces P rof esor → Asignatura nos dice que para un determinado Profesor sólo puede existir una Asignatura, en lenguaje más natural: un profesor sólo imparte una asignatura. O como decíamos antes, que si en dos filas aparece el nombre de un Profesor debe aparecer la misma asignatura. Con las DF podemos expresar cierto tipo de restricciones, no todas, que nos van a permitir estudiar la bondad de las tablas que contiene nuestra base de datos. Definamos L como el conjunto de restricciones que deben cumplir nuestros datos (columnas) expresadas mediante DFs. Casi siempre podremos deducir nuevas restricciones partiendo de un conjunto L inicial. Por ejemplo, si un código postal determina la localidad (CP → LOC) y la localidad determina la provincia (LOC → P ROV ) entonces podremos deducir por transitividad que un código postal sólo pertenece a una provincia (CP → P ROV ). Existen algoritmos (Anexo II) que nos permiten calcular el conjunto total de DFs que afectan a nuestro conjunto de datos. A dicho conjunto se le denomina cierre del conjunto de dependencias y se expresa como L+ . Lógicamente en L+ existirán 1 Diseño de bases de datos muchas DF redundantes (CP → P ROV ) y triviales (CP LOC → CP ), pero también contamos con un algoritmo (Anexo III) que nos permite calcular un conjunto minimal de DF. A estos conjuntos de DF donde ’no sobra nada’ se le denomina Recubrimiento canónico o Recubrimiento minimal. Por último vamos a definir el cierre de un conjunto de atributos, expresado como α+ como el conjunto de todos los atributos que dependen funcionalmente de α. Evidentemente también existe un algoritmo sencillo para calcularlo (Anexo I). Todos los algoritmos anteriormente mencionados pueden consultarse en cualquier libro clásico de fundamentos de bases de datos o de manera informal en los anexos, pero para el desarrollo de este tema el realmente útil es el cierre de un conjunto de atributos. 2. Un pequeño ejemplo Vamos a plantearnos un sencillo ejemplo para ilustrar los problemas que suelen presentarse a la hora de diseñar una base de datos relacional. Nuestro objetivo será definir un conjunto de tablas que nos permita almacenar, modificar y recuperar toda nuestra información de forma sencilla, partiendo de una serie de restricciones. Nuestro ejemplo consiste en una base de datos que contendrá información sobre alumnos, asignaturas, notas y profesores. Las restricciones de las que partimos son las siguientes: Un alumno sólo tiene asignado un profesor por asignatura Un alumno obtiene una única nota por asignatura Un profesor sólo puede dar clase de una asignatura Podríamos plantearnos mantener todos los datos en una única tabla. Utilizaremos el siguiente ejemplo: Alumno Luis Luis Carlos Marta Pepe Asignatura Física Filosofía Física Física Filosofía Profesor Newton Kant Einstein Newton Kant Nota 5 7 6 5 4 Llamaremos a este esquema R(T, L), para poner de manifiesto que estará compuesto por el conjunto de atributos T y el conjunto de restricciones L expresadas como dependencias funcionales. R(T, L) : T = L= {Alumno, Asignatura, Profesor, Nota} {Alumno Asignatura → Profesor, Alumno Asignatura → Nota, Profesor → Asignatura} Revisión 3 de mayo de 2007 Diseño de bases de datos Del conjunto de restricciones podemos deducir que el par (Alumno, Asignatura) funciona como clave candidata para el esquema, al igual que (Alumno, Profesor), ya que podemos deducir que Alumno Profesor → Asignatura ∈ Alumno Profesor → Nota ∈ L+ L+ De momento vamos a definir la tabla con (Alumno, Asignatura) como clave primaria, mediante la orden SQL: create table R ( alumno asignatura profesor nota primary key ); varchar(15) not null, varchar(15) not null, varchar(15) not null, integer, (alumno, asignatura) 3. Anomalías de actualización Si observamos nuestra tabla, podemos ver que nuestra decisión de mantener en ella toda la información conduce a varios inconvenientes. Repetición de información Dado que un profesor sólo imparte clases de una única asignatura, se produce una redundancia de datos. En nuestro ejemplo, la mención de que Marta cursa Física es redundante, puesto que sabemos que su profesor, Newton, sólo da clase de Física. Lo mismo ocurre con la asignatura de Filosofía que cursa Pepe. En la actualidad, dadas las altas prestaciones de los ordenadores y el precio del byte de Ram y de disco, el problema fundamental de la repetición de información no es el desperdicio de espacio sino otros problemas derivados del mantenimiento de información repetida, que enumeramos a continuación. Anomalías en la inserción y en la actualización Al insertar una fila nueva (en SQL, INSERT) o actualizar una ya existente (en SQL, UPDATE), tendremos que comprobar que la información que introducimos es consistente. Por ejemplo, si quiero insertar un alumno Dalmacio cuyo profesor es Newton, debo comprobar que cursa Física; de no hacerlo así, sería posible violar la dependencia funcional Profesor→Asignatura, es decir, permitir que Newton imparta más de una asignatura. Anomalías en el borrado El problema que se nos presenta aquí no es un problema de inconsistencia, sino de pérdida de información. Si, por ejemplo, borramos la única fila en la que aparece Einstein, desaparece la información de que éste es profesor de Física. Revisión 3 de mayo de 2007 Diseño de bases de datos Representación de Información Este problema está íntimamente ligado al anterior y se basa en que nuestro esquema no nos permite guardar algunos hechos. Por ejemplo, aunque sepamos qué profesores dan cada asignatura, no puedo guardar dicha información hasta que no tenga alumnos matriculados en ella. La opción de dejar valores nulos no es válida generalmente, ya que algunos campos que deberían quedar en blanco (no tenemos información de ellos) están declarados como not null por formar parte de la clave primaria, por lo tanto el sistema no me permite dejarlos ’en blanco’. Imposición de las restricciones Imponer la restricción de que un alumno sólo tiene un profesor por asignatura resulta tan sencillo como el declarar (Alumno, Asignatura) como clave primaria. Sin embargo, garantizar el cumplimiento de las dependencias funcionales cuya parte izquierda no es clave candidata no es así de sencillo. Por ejemplo, comprobar la dependencia Profesor→Asignatura requeriría establecer en SQL un check como este: alter table R add constraint PRO_ASIG check (not exists ( select profesor from R group by profesor having count(distinct asignatura) > 1)); Otra solución podría ser evitar que se inserten o actualicen tuplas que contradigan la restricción mediante triggers. En cualquier caso, nos vemos obligados a agregar más código, lo que es posiblemente innecesario y ralentiza la ejecución de nuestras modificaciones a la base de datos. Ante este tipo de anomalías, una solución consiste en descomponer nuestro esquema original en varios subesquemas de forma que dichas anomalías se eviten. Este proceso se conoce como normalización. No siempre es posible encontrar una descomposición que elimine todas las posibles anomalías. En algunos casos, la normalización resuelve ciertos problemas, pero introduce otros. Revisión 3 de mayo de 2007 Diseño de bases de datos 4. Descomposiciones Vamos a comenzar definiendo formalmente lo que es una descomposición. Sea R un esquema de relación. Un conjunto {R1 , R2 , . . . , Rn } de esquemas de relación es una descomposición de R si n [ Ri = R (2) i=1 es decir, cada atributo de R aparece por lo menos en uno de los Ri . En la práctica, cuando descomponemos un esquema en un conjunto de subesquemas, incluimos en éstos atributos comunes. El objetivo de esto es poder recuperar posteriormente la información que se ha guardado en distintas tablas a través de operaciones de join. Vamos a ver cómo una descomposición puede resolver algunos de los problemas mencionados en la sección ??. La propuesta es dividir nuestro esquema R(T, L) en dos subesquemas R1 (T1 , L1 ) y R2 (T2 , L2 ). R2 R1 Alumno Luis Luis Carlos Marta Pepe Asignatura Física Filosofía Física Física Filosofía Nota 5 7 6 5 4 Profesor Newton Kant Einstein Asignatura Física Filosofía Física Es evidente que esta descomposición nos resuelve algunos problemas: se repite menos información; a un profesor sólo se le puede asignar una asignatura (profesor es la clave primaria de R2 ); y podemos guardar información de la asignatura que imparte cada profesor aunque no tengamos alumnos matriculados. Pero nos aparece un problema nuevo: ya no sabemos qué profesor tiene cada alumno en una determinada asignatura. Ante la pregunta de quién da clase de Física a Luis, no podemos saber si es Newton o Einstein con la información que las tablas R1 y R2 nos proporcionan. Nos pasa igual con Marta, aunque no con los alumnos de Filosofía, ya que únicamente tenemos al profesor Kant impartiéndola. Este problema se conoce con el nombre de descomposición con pérdida de información, generalmente denominado como lossy-join. Cuando esta situación no se da, y toda la información que existía previamente puede recuperarse después de la descomposición, decimos que se cumple la propiedad losslessjoin o LJ (ciertamente, la “L” podría ser abreviatura tanto de “lossy” como de “lossless”, pero la vida es así de dura a veces). La aparición de la temible palabra join tiene que ver con la definición formal de la propiedad. Cuando la descomposición es con pérdida, al hacer la reunión Revisión 3 de mayo de 2007 Diseño de bases de datos de los subesquemas no obtenemos el esquema original. En nuestro ejemplo, para esta reunión la consulta apropiada sería: select from r1.alumno, r1.asignatura, r2.profesor, r1.nota r1 inner join r2 on r1.asignatura = r2.asignatura Y el resultado sería: R1 .Alumno Luis Luis Luis Carlos Carlos Marta Marta Pepe R1 .Asignatura Física Física Filosofía Física Física Física Física Filosofía R2 .Profesor Newton Einstein Kant Newton Einstein Newton Einstein Kant R1 .Nota 5 5 7 6 6 5 5 4 Y evidentemente nos aparecen tres filas más (en sombreado) que en la tabla R original. El problema radica en el hecho de que hay más de un profesor de Física, con lo que al reunir (realizar el producto natural) R1 y R2 , a cada alumno de Física se le empareja con todos los profesores de Física, dando lugar a filas espurias que contienen información incorrecta. Revisión 3 de mayo de 2007 Diseño de bases de datos 5. Descomposición con la propiedad LJ (Lossless-Join) Para formalizar lo anterior vamos a retomar el concepto formal de descomposición. Como anteriormente, sea R un esquema de una relación y consideremos una descomposición {R1 , R2 , . . . , Rn } (en el sentido de la definición ??). Sea r una instancia de la relación (los valores actuales del conjunto de filas que contiene la tabla) con esquema R; definimos las instancias ri de Ri como ri = Ri (r) = πTi (r). De esta forma, {R1 , R2 , . . . , Rn } es la base de datos que resulta de descomponer R en sus proyecciones sobre los conjuntos de atributos {T1 , T2 , . . . , Tn }. Pues bien, puede demostrarse que siempre se da la siguiente inclusión: n ⊲⊳ ri ⊇ r i=1 (3) es decir, las tuplas de la relación original siempre se recuperan realizando una reunión natural de las proyecciones ri . Desgraciadamente, también pueden aparecer otras nuevas. Se puede ver que en nuestro caso se cumple esta afirmación, ya que el conjunto de filas original es un subconjunto del obtenido tras de realizar el producto natural de r1 y r2 . A estas alturas está claro que nuestro objetivo es realizar una descomposición en la que ambos conjuntos sean iguales, es decir, que cumpla la propiedad LJ. Decimos que una descomposición {R1 , R2 , . . . , Rn } de un esquema R es sin pérdidas (lossless) cuando se verifica la identidad n ⊲⊳ ri = r i=1 (4) es decir, la relación original se recupera siempre realizando la reunión de las relaciones proyección que resultan de la descomposición. Para conseguir esto en nuestro ejemplo, sería necesario que al cruzar una fila de la tabla R1 sólo obtuviéramos una fila de la tabla R2 que cumpliera la condición R1 .Asignatura = R2 .Asignatura o dicho de otra forma, que cada asignatura de la tabla R2 sólo apareciera una vez. Esto también se puede formalizar: el hecho se conoce como Teorema 1. (Heath) Dado un esquema R(T, L), la descomposición R1 (R), R2 (R) es una descomposición sin pérdida (propiedad LJ) si y sólo si alguna de las siguientes dependencias funcionales se pueden deducir de L (pertenecen a L+ ): R1 ∩ R2 R1 ∩ R2 → R1 − R2 → R2 − R1 O lo que es lo mismo: Revisión 3 de mayo de 2007 R1 ∩ R2 → R1 R1 ∩ R2 → R2 Diseño de bases de datos Estas dependencias funcionales nos viene a decir que los atributos comunes a las dos tablas funcionan como clave (más exactamente, superclave) en alguna de ellas. Aprovechando este formalismo, vamos a dejar enunciado el siguiente teorema sobre descomposiciones con la propiedad LJ: Teorema 2. Sea el esquema R(T, L) y sea ρ = {R1 , R2 , . . . , Rk } una descomposición de R con la propiedad LJ respecto de L. Si τ = {S1 , S2 , . . . , Sk } es una descomposición de Ri con la propiedad LJ respecto de Li (proyección de L+ sobre Ti ), entonces γ = {R1 , R2 , . . . , Ri − 1, S1, S2, . . . , Sk, Ri + 1, . . . , Er} es una descomposición de R respecto L con la propiedad LJ. Siguiendo con nuestro ejemplo, vemos que Asignatura es el atributo común a ambas tablas y no es clave en ninguna de ellas. Por eso, la descomposición propuesta resultó ser con pérdidas. Otra descomposición alternativa que sí cumpliría la propiedad LJ sería la siguiente: Alumno Luis Luis Carlos Marta Pepe R1 Profesor Newton Kant Einstein Newton Kant Nota 5 7 6 5 4 R2 Profesor Asignatura Newton Física Kant Filosofía Einstein Física La descomposición es sin pérdida, ya que Profesor, el atributo común, es clave en R2 . En principio, nuestro problema parece resuelto, pero si miramos con atención aún pueden aparecer algunas complicaciones. Si alguien, por error, introduce una nueva fila en la tabla R1 con los valores (“Luis”, “Einstein”, 4) (puede hacerlo ya que no se viola la unicidad de la clave primaria), nos encontraremos con el dilema de si Luis tiene aprobada la asignatura de Física o no, además de no saber si su profesor es Newton o Einstein: Alumno Luis Luis Carlos Marta Pepe Luis R1 Profesor Newton Kant Einstein Newton Kant Einstein Nota 5 7 6 5 4 4 Profesor Newton Kant Einstein R2 Asignatura Física Filosofía Física Lo que sucede en este caso es que no estamos cumpliendo las restricciones de que un alumno tiene un único profesor y una única nota en cada asignatura (Alumno Asignatura → P rof esorN ota) Revisión 3 de mayo de 2007 Diseño de bases de datos No ocurre lo mismo con la dependencia Profesor→Asignatura, que se exige al declarar Profesor como clave primaria en R2 . Este nuevo problema se conoce como descomposición con pérdida de dependencias funcionales. 6. Descomposición sin pérdida de dependencias Cuando imponemos un conjunto de restricciones mediante dependencias funcionales debemos ser conscientes de las consecuencias que esto tiene a la hora de implementar la base de datos. Hacer que se cumplan ciertas restricciones en una tabla es casi siempre costoso. Cuando se trata de DFs que se traducen en definir claves candidatas (PRIMARY KEY o UNIQUE), la solución es sencilla desde el punto de vista del programador de la base de datos. Para otras DFs que involucran a atributos de una misma tabla también tenemos algunas alternativas (por ejemplo mediantes CHECKs). La cuestión tiene mayores inconvenientes cuando en las restricciones intervienen atributos que se encuentran localizados en distintas tablas. Esto suele realizarse mediante asertos (CREATE ASSERTION) de la base de datos, aunque, si es posible, es mejor evitarlos. No tenemos que olvidar que normalmente hablamos de sistemas multiusuario y las tablas pueden estar bloqueadas por otro usuario cuando nosotros necesitamos acceder a ellas para comprobar ciertas restricciones. Lo que se pretende, por tanto, es que las restricciones se exijan a nivel de tabla, es decir, que ante inserciones o modificaciones de datos en una tabla no sea necesario consultar otras. Visto esto formalicemos el problema. Sea el esquema R(T, L), donde L son las dependencias estipuladas para la relación R. Sea {R1 , R2 , . . . , Rk } la descomposición de R con esquemas Ri (Ti , Li ), construidos de modo que Li es la proyección de L+ sobre Ti , es decir Li = (X → Y ) ∈ L+ | (X ∪ Y ) ⊆ Ti (5) Por tanto cada Li será el conjunto de restricciones que involucra atributos únicamente de Ri , es decir, aquellas dependencias funcionales que se pueden comprobar de forma independiente en Ri . Hay que tener cuidado con la engañosa simplicidad de la definición anterior. Las dependencias que afectan a cada relación Ri pueden no resultar necesariamente de la proyección de las dependencias originales sobre los atributos Ti de Ri . Por ejemplo, en una relación R(A, B, C) con dependencias dadas por el recubrimiento L = {A → B, B → C} la proyección sobre R1 (A, C) debe satisfacer la dependencia A → C, aunque ésta no se obtiene proyectando el recubrimiento expresado en la definición de R: π(A,C) (L) = φ pero A → C ∈ L1 = π(A,C) (L+ ) de forma que es preciso deducir (un recubrimiento de) todas las dependencias que pueden afectar solamente a los atributos A y C para conocer L1 . Revisión 3 de mayo de 2007 Diseño de bases de datos Consideremos el conjunto L′ = ∪Li , es decir, la reunión de todas las dependencias impuestas a cada esquema Ri . En general L′ ⊆ L, pero puede darse el caso de que una dependencia del conjunto L pueda deducirse de L′ aun no apareciendo explícitamente en L′ ; es decir, se encuentre en (L′ )+ . Para que la descomposición siga imponiendo (a nivel de tabla) las dependencias originales, será entonces preciso que (L′ )+ = L+ . Si esto ocurre se dice que la descomposición preserva las dependencias. Traducido a lenguaje menos simbólico, la descomposición preserva dependencias si al imponer las dependencias relativas a cada tabla de la descomposición, todas las originales resultan impuestas también, al poder deducirse de ellas. Observemos qué es lo que sucede en nuestro ejemplo. Esquema de partida: Atributos: Dependencias: Descomposición: Atributos: R(T, L) T = {Alumno, Asignatura, Profesor, Nota} L = {Alumno Asignatura → Profesor, Alumno Asignatura → Nota, Profesor → Asignatura, Alumno Profesor → Nota} R1 (T1 , L1 ), R2 (T2 , L2 ) T1 = {Alumno, Profesor, Nota} T2 = {Profesor, Asignatura} Dependencias: L1 = {Alumno Profesor → Nota} L2 = {Profesor→Asignatura} En suma: L′ = {Alumno Profesor→Nota, Profesor→Asignatura} Como vemos la dependencia Alumno Asignatura→Profesor no aparece explícitamente en L′ ; igual ocurre con Alumno Asignatura→Nota. Pero, además, tampoco se pueden deducir de dicho conjunto. Por este motivo, si no establecemos procedimientos adicionales a la hora de realizar actualizaciones en nuestras tablas, nos podemos encontrar con inconsistencias como las mencionadas en la sección ??. Existe un mecanismo para comprobar si esto ocurre: cuando dudemos de si una restricción de la forma X → Y se exige o no, bastará con calcular el cierre X + de X respecto al conjunto L′ , y si Y aparece en dicho cierre, la dependencia se estará imponiendo. Aún más, hay un algoritmo que nos facilita este proceso sin la necesidad de calcular cada Li (cálculo que, como vimos, puede resultar equívoco), y es el que se presenta a continuación. El algoritmo se basa en la R-operación sobre el conjunto de atributos Z respecto de L como la sustitución de Z por Z ∪ ((Z ∩ Ti )+ ∩ Ti ). Repitiendo esta operación para cada uno de los conjuntos de atributos Ti hasta que el conjunto Z no cambie, obtenemos el algoritmo siguiente Revisión 3 de mayo de 2007 Diseño de bases de datos Z⇐X while Z cambie and not Y ∈ Z do for i = 1 to K do {siendo K el número de subesquemas} Z = Z ∪ ((Z ∩ Ti )+ ∩ Ti ) end for end while if Y 6∈ Z then Se pierde la dependencia X → Y else Se conserva la dependencia X → Y end if Aplicando este algoritmo se puede comprobar que nuestra descomposición de R es una descomposición con pérdida de dependencias. Revisión 3 de mayo de 2007 Diseño de bases de datos 7. Estudio de Normalización Llegados a este punto, la pregunta es si existe alguna otra alternativa que no presente ninguno de los anteriores problemas. La respuesta depende de cada base de datos en concreto, pero en nuestro caso la respuesta es negativa. No siempre es así, de hecho nuestro ejemplo ha sido elegido para ilustrar estos problemas. En general, el cumplir la propiedad LJ debe ser una exigencia básica que debe satisfacer cualquier descomposición, pero en el caso de descomposiciones con pérdidas habrá que analizar las ventajas e inconvenientes de las posibles descomposiciones y de las implicaciones de su implementación en un sistema real de gestión de bases de datos. Hasta el momento hemos visto qué tipos de problemas nos puede presentar una tabla y qué propiedades son deseables que cumpla una descomposición. Ahora vamos a centrarnos en el estudio de la calidad de un esquema R(T, L), lo que se conoce como estudio de normalización. Lo que se pretende es dar una clasificación, de mayor a menor, de la posibilidad de encontrarnos con problemas a la hora de implementar un esquema (tabla), teniendo en cuenta las restricciones que se deben cumplir impuestas como dependencias funcionales. Realmente es un estudio sobre la posibilidad de que nos aparezca redundancia de información. Cuanto mayor es el grado de repetición de información más problemas tendremos. Para esto se definen 4 formas normales: Primera Forma Normal (1FN), 2FN, 3FN y Forma Normal de Boyce-Codd (FNBC), en este orden. Cuanto más alta sea la forma normal que alcancemos, menor repetición de información tendremos a priori. Estas forma normales que vamos a ver están basadas en las Dependencias Funcionales. Existen otras formas normales superiores (4FN y 5FN) basadas en otro tipo de dependencias que no vamos a ver aquí. Lo que se pretende es analizar qué formas normales alcanzan las tablas que conforman nuestra base de datos. Antes de entrar a fondo con la normalización es importante recalcar que se trata de una recomendación general, pero que en bastantes ocasiones es muy aconsejable no respetarla. 7.1. Primera Forma Normal (1FN) Un esquema R(T, L) está en 1FN cuando todas sus columnas (atributos de T ) son simples. Dicho de otra forma, cada columna de una tabla debe estar definida sobre un tipo simple de datos (entero, real, cadena, etc.) y no un tipo estructurado (registro, lista, matriz, etc.). Realmente, no es necesario ser tan exigente; lo que se busca con esta forma normal es que cada atributo se trate como un valor atómico. Para ilustrarlo, vamos a suponer que tenemos un tipo de dato estructurado fecha, con los campos día, mes y año. Si el tratamiento que hago sobre la fecha es de forma conjunta podríamos decir que estamos cumpliendo la 1FN, pero si vamos a trabajar con cada uno de lo campos de forma independiente, no. Revisión 3 de mayo de 2007 Diseño de bases de datos Por ejemplo, si el sueldo de un trabajador depende funcionalmente del valor fecha.mes (Operario Fecha.Mes → Sueldo) entonces sería más recomendable tener tres columnas (día, mes y año) en lugar de la columna fecha. Como vemos, la 1FN es una recomendación, ya que muchos gestores de bases de datos nos permiten definir tipos de datos estructurados, tales como matrices. 7.2. Segunda forma normal (2FN) Un esquema R(T, L) en 1FN está en 2FN cuando todos sus atributos no principales (aquellos que no forman parte de ninguna clave candidata) tienen dependencia funcional total (completa) respecto de cada una de las claves candidatas. En algunos textos se habla únicamente de la clave primaria y no del resto de las claves candidatas. Para ver que esto no es así vamos a utilizar nuestro ejemplo con una pequeña modificación: vamos a incluir en T el atributo que nos diga el departamento al que pertenece el profesor. Tomemos el siguiente esquema: Esquema: R(T, L) Atributos: T = {Alumno, Asignatura, Profesor, Nota, Departamento } Dependencias:L = {Alumno Asignatura → Profesor, Alumno Asignatura → Nota, Profesor → Asignatura, Profesor → Departamento} Tomemos como clave primaria (Alumno Asignatura), de manera que Departamento tiene una dependencia funcional completa con respecto a ella. No ocurre lo mismo con la otra clave candidata (Alumno Profesor) ya que Departamento tiene una dependencia parcial de ella, porque sólo depende de Profesor. Es cierto que en este esquema se repite redundantemente la información del Departamento siempre que se repita un determinado profesor, y por tanto no cumple la 2FN. Consecuentemente, un esquema en 1FN cuyas claves candidatas están formadas por un solo atributo siempre está en 2FN. 7.3. Tercera Forma Normal (3FN) Un esquema R(T, L) en 2FN está en 3FN cuando ningún atributo no principal depende transitivamente de ninguna clave. Entre X y Z existe una dependencia transitiva (X ։ Z) si se cumple que Revisión 3 de mayo de 2007 X ∩Z ∃Y : X ∩ Y = = φ φ, Y ∩ Z = φ X → Y, Y 9 X y Y →Z Diseño de bases de datos Se dice que Z es una información sobre X, pero de forma indirecta, ya que realmente Z es una información sobre Y , e Y sobre X. Aunque nuestro esquema no cumple la 2FN, este tipo de transitividad también se presenta: Alumno Asignatura → Profesor Profesor 9 Alumno Asignatura Profesor → Departamento Luego: Alumno Asignatura ։ Departamento Esto sucede porque Departamento es una información propia del Profesor y no de la asignatura de un alumno. Por eso cada vez que se repite el profesor se repetirá la asignatura que imparte. Otra forma equivalente, aunque algo más sencilla, de comprobar si un esquema está en 3FN es la siguiente: un esquema estará en 3FN si para toda dependencia funcional no trivial X → A ( es decir que A * X ) se cumple alguna de las siguientes condiciones: 1. X es una superclave de R 2. A está contenida en una clave candidata de R Aunque se habla de superclaves, si trabajamos con un conjunto de dependencias sin atributos extraños (por ejemplo un recubrimiento no redundante) podemos hablar directamente de claves candidatas. En nuestro esquema R vemos que la dependencia Profesor → Departamento no cumple ninguna de las dos condiciones. 7.4. Forma Normal de Boyce-Codd (FNBC) Un esquema R(T, L) en 1FN está en BCFN cuando para toda dependencia funcional no trivial X → Y (es decir, Y * X , Y 6= φ), se cumple que: 1. X es una clave o superclave de R. Vemos que ahora somos más restrictivos y exigimos que se cumpla únicamente la condición 1 para estar en 3FN. Por lo tanto es evidente que todo esquema que esté en FNBC también estará en 3FN. Para ver la diferencia entre FNBC y 3FN vamos a retomar nuestro esquema original: R(T, L) : T = L= {Alumno, Asignatura, Profesor, Nota} {Alumno Asignatura → Profesor, Alumno Asignatura → Nota, Profesor → Asignatura} Claves Candidatas Revisión 3 de mayo de 2007 = {(Alumno Asignatura), (Alumno Profesor)} Diseño de bases de datos Ahora R sí que está en 3FN, ya que en las dos primeras dependencias su parte izquierda es una clave candidata (condición 1) y en la dependencia Profesor → Asignatura, Asignatura es un atributo principal (condición 2). Pero es por esta última dependencia por lo que R no alcanza la FNBC. De hecho, en R tenemos repetición de información, como ya se ha mencionado en varias ocasiones. Hay un aspecto de la FNBC que la hace muy interesante a la hora de implementar la base de datos. Si todas la tablas están en FNBC, podemos hacer cumplir las dependencias funcionales asociadas a cada tabla mediante la restricción PRIMARY KEY (para la clave primaria) y UNIQUE (para el resto de claves candidatas). Es decir con estas dos palabras reservadas de SQL no necesitamos implementar ningún otro procedimiento adicional. Sin embargo en 3FN las dependencias del tipo Profesor → Asignatura suelen tener un coste computacional elevado, como ya se mencionó anteriormente. Aún así, tenemos que ser conscientes de que el declarar un conjunto de atributos como clave candidata, ya sea mediante PRIMARY KEY o UNIQUE, también lleva un alto coste asociado. Los sistemas de gestión de bases de datos suelen construir automáticamente, cuando hacemos estas declaraciones, un índice con estos atributos (frecuentemente se utilizan árboles balanceados). Estos índices le permiten al sistema agilizar las comprobaciones de valores únicos ante inserciones o modificaciones de las columnas implicadas. Por otro lado debemos tener cierta confianza y pensar que los sistemas suelen hacer estas operaciones de forma muy optimizada y por tanto no parece mala la idea de que sean ellos los que se encarguen de estas comprobaciones. Resumiendo todo lo anteriormente visto, a la hora de realizar un diseño de una base de datos debemos plantearnos lo siguiente. Si un esquema o tabla no alcanza una forma normal satisfactoria, por ejemplo FNBC, que reduzca la repetición de información, entonces podemos realizar una descomposición de la misma. Esta descomposición debe cumplir: la propiedad LJ; que si es posible conserve dependencias; y que las tablas obtenidas alcancen una forma normal más adecuada. Esto no siempre es posible, y a veces lo que ganamos por un lado lo perdemos por otro. En este sentido existe un algoritmo que describimos a continuación y que nos asegura una descomposición con la propiedad LJ y que todas las tablas obtenidas alcanzan la FNBC. Algoritmo de descomposición en FNBC con la Propiedad LJ Partimos de R(T, L) donde L es un recubrimiento no redundante. Si existe una dependencia X →A tal que X no sea clave (por tanto no está en FNBC), proyectamos R en R1 = (T1 , L1 ) y R2 = (T2 , L2 ), con T1 T2 = = X ∪ {A} T − {A} y Podemos ver una de las condiciones para que la descomposición cumpla la propiedad LJ ( T1 ∩ T2 → T1 − T2 ) siempre se cumple, ya que (T1 ∩ T2 ) es X y Revisión 3 de mayo de 2007 Diseño de bases de datos (T1 − T2 ) es A, y por tanto, se trata de la propia dependencia X → A. Es decir, los atributos comunes (X) funcionan como clave por lo menos en uno de los subesquemas (T2 ). L1 y L2 son las proyecciones de L+ (cierre del conjunto L) sobre T1 y T2 respectivamente. Aquí es donde radica la dificultad del algoritmo, ya que normalmente no tenemos calculado L+ . Por tanto lo que debemos hacer es encontrar todas las posibles dependencias que impliquen únicamente a atributos de cada esquema y después obtener su recubrimiento no redundante. Si alguno de los esquemas obtenidos no alcanzan la FNBC volveremos a descomponerlo hasta que todos los esquemas resultantes cumplan la FNBC. En la práctica cuando en los esquemas aparecen dependencias de la forma X → A1 , X → A2 . . . X → An donde X no es clave candidata, se genera una tabla T1 con los atributos {X, A1 , A2 . . . An } y otra T2 con {T − {A1 , A2 . . . An }} Para ser realmente formales tenemos que añadir una pequeña sutileza: dado un esquema R que es descompuesto sucesivamente, cumpliendo la propiedad LJ en cada paso, hasta obtener un conjunto de subesquemas R1 , R2 . . . Rn por aplicación del Teorema 2, dicha descomposición también cumple la propiedad LJ, es decir, si reunimos de nuevo R1 , R2 . . . Rn obtendremos de nuevo R. Para no perder de vista nuestro sustancioso ejemplo vamos a ver cómo se aplica este algoritmo en un caso concreto. Partimos del siguiente esquema: R(T, L) : T = L= Claves Candidatas = {Alumno, Asignatura, Profesor, Nota} {Alumno Asignatura → Profesor, Alumno Asignatura → Nota, Profesor → Asignatura} {(Alumno Asignatura), (Alumno Profesor)} Vemos que la dependencia Profesor → Asignatura no está en FNBC, y la utilizamos para descomponer, obteniendo: R1 (T1 , L1 ) : Clave Candidata T1 = {Alumno, Profesor, Nota} L1 = {Alumno Profesor → Nota} = {(Alumno Profesor)} R2 (T2 , L2 ) : T2 = {Profesor, Asignatura} Clave Candidata L2 = = {Profesor → Asignatura, } {(Profesor)} El proceso es sencillo, pero incluso aquí vemos que el cálculo de L1 no es tan inmediato, ya que ninguna de las dependencias de L se pueden exigir en Revisión 3 de mayo de 2007 Diseño de bases de datos R1 pues involucras atributos de R2 y hemos tenido que encontrar una dependencia que se deduce de L (es decir que pertenece a L+ ) y que involucra únicamente a a tributos de T1 , la verdad es que ya sabíamos que (Alumno, Profesor) era clave candidata en R, luego debería seguir siéndolo en R1 . Es cierto que en otros casos esto es menos evidente. Como planteamiento práctico una estrategia puede ser: dado el conjunto de atributos Ti buscamos las dependencias funcionales que existen como si fuera el problema inicial, si ya lo hicimos con un conjunto de atributos mayor (T ) raro será que no lo consigamos de nuevo. Si hubiéramos partido del esquema que incluía el Departamento del Profesor, el atributo Departamento quedaría en el esquema R2 junto con Profesor y Asignatura. El resultado que hemos obtenido con todas las tablas en FNBC ya lo vimos anteriormente para mostrar que dicha descomposición no conservaba dependencias. Una vez más el resultado no parece óptimo, la elección última dependerá de otros factores de implementación que no se contemplan en esta teoría de diseño de bases de datos. Por ejemplo: si la consulta más frecuente es obtener listados de alumnos asignaturas y notas la descomposición no parece demasiado adecuada, si el número de actualizaciones de los campos Alumnos Asignatura Profesor no es muy elevado puede que el esquema original sea la mejor alternativa, si . . . Revisión 3 de mayo de 2007 Diseño de bases de datos Anexo I: Cierre de un conjunto de atributos El siguiente algoritmo, partiendo de un conjunto de DF L, nos permite calcular el conjunto de atributos que depende funcionalmente de uno dado (α). Dicho conjunto se expresa como α+ . α+ ⇐ α while α+ cambie do for each β → γ in F do if β ∈ α+ then α+ ⇐ α+ ∪ γ end if end for end while Anexo II: Cálculo del cierre de un conjunto de dependencias Ya hemos definido anteriormente L+ como el conjunto de todas las DF que se pueden inferir de un conjunto L dado. Existen diversos algoritmos para obtenerlo, pero aquí sólo vamos a mostrar uno sencillo ya que en la práctica no se suele utilizar. Dado un esquema R(T, L) for each X ⊆ T do Calcular X + for each A ∈ X + do incluir X → A en L end for end for Revisión 3 de mayo de 2007 Diseño de bases de datos Anexo II: Cálculo del recubrimiento canónico Dados dos conjuntos de dependencias funcionales L y M , tal que L+ = M , se dice que L recubre a M y M recubre a L, o que son mutuamente recubrimientos. Se dice que un recubrimiento no es redundante o es un recubrimiento canónica cuando: + Todas las dependencias son de la forma X → A siendo X un conjunto de atributos y A un único atributo. No hay atributos extraños. B ∈ X es extraño en la dependencia X → A, si al sustituir en L la dependencia X → A por (X − B) → A, el cierre no varía. Es decir: (L − {X → A}U {(X − B) → A})+ = L+ No hay DFs redundantes. X → A es redundante si al eliminarla del conjunto L se puede seguir infiriendo del resto. Es decir, que no aporta nada nuevo a L: (L − {X → A})+ = L+ El algoritmo que nos permite obtener un Recubrimiento canónico se basa en los siguientes pasos: 1. Para toda dependencia X → Y de L, la sustituimos por X → Ai , siendo A1 , ..., An el conjunto de atributos Y . Sea L1 el conjunto resultante. 2. Eliminamos atributos extraños. Para toda dependencia X → A ∈ L1 , si B → X , y Z = X − B, calculamos Z + respecto de L1 . Si A ∈ Z + , sustituimos X → A por la dependencia Z → A. Sea L2 el conjunto resultante. 3. Eliminamos dependencias redundantes. Para toda dependencia X → A ∈ L2 , calculamos X + respecto de L2 − {X → A}, si A ∈ X + , eliminamos X → A. Sea L3 el conjunto canónico resultante. Revisión 3 de mayo de 2007