Apuntes Base de Datos II

Page 1

Apuntes BASE DE DATOS II Recopilación Didáctica Ing. Wilfrido Martínez Cruz 17/06/2013


APUNTES DE BASES DE DATOS II 1.

Estructura de archivos y sistemas.

El objetivo de un DBMS es simplificar y facilitar el acceso a los datos. No se debe cargar innecesariamente a los usuarios del sistema con los detalles físicos de la implementación del sistema. Un factor importante para que el usuario quede satisfecho con un DBMS es su rendimiento, que depende de la eficiencia de la estructura de datos que se utiliza para representar la información en la BD y de la eficiencia del sistema para operar sobre esas estructuras de datos. Es preciso alcanzar un equilibrio no sólo entre el espacio y el tiempo, sino también entre la eficiencia de un tipo de operación y la de otro. 1.1.

Estructura general del sistema.

En esta sección dividimos un DBMS en módulos que tratan cada una de las responsabilidades del sistema global. El sistema operativo del computador puede proporcionar algunas de las funciones del DBMS. En la mayoría de los casos, sólo proporciona únicamente los servicios más básicos y el DBMS debe construir sobre esa base. Por tanto, nuestro tratamiento sobre el diseño del sistema incluirá la consideración del interfaz entre el DBMS y el sistema operativo. Un DBMS consta de varios componentes funcionales, entre los que están:       

El gestor de archivos. Se encarga de asignar el espacio en la memoria del disco y la estructura de datos que se usa para representar la información almacenada en el disco. El gestor de registros intermedios buffer. Es responsable de transferir la información entre la memoria del disco y la memoria principal. El analizador sintáctico (parser) de consultas. Traduce sentencias de un lenguaje de consulta a un lenguaje de nivel más bajo. El selector de estrategias. Intenta transformar la solicitud del usuario en una forma equivalente pero más eficiente. El gestor de autorización e integridad. Prueba la satisfacción de las restricciones de integridad y comprueba que el usuario está autorizado para acceder a los datos. El gestor de recuperaciones. Asegura que la BD permanezca en un estado consistente a pesar de que ocurran fallos en el sistema. El controlador de concurrencia. Asegura que las interacciones concurrentes con la BD se llevan a cabo sin conflictos entre ellas.

Además, se requieren varias estructuras de datos como parte de la implementación física:    

Archivo de datos. Almacena la BD. Diccionario de datos. Almacena información acerca de la estructura de la BD, y la información de autorización, como las restricciones de clave. Índices. Permiten el acceso rápido a datos que tienen determinados valores. Datos estadísticos. Almacenan información acerca de los datos de la BD. Esta información la utiliza el selector de estrategias.

En este capítulo estudiaremos como pueden implementarse el gestor de archivos, el gestor de buffer, los archivos de datos y el diccionario de datos. 1.2.

Medios de almacenamiento físico.

En la mayoría de los sistemas de computadores existen varios tipos de almacenamiento de datos. Estos medios de almacenamiento se clasifican según la velocidad con que pueden tener acceso a los datos, según el coste por unidad de datos, y según la fiabilidad que tienen. Entre los medios con que normalmente se cuenta están: 

Próximo (caché). Esta es la forma de almacenamiento más rápida y más costosa. El tamaño de la memoria caché es muy pequeño y el sistema operativo gestiona su utilización. No necesitaremos preocuparnos de la gestión de la memoria caché en el sistema de BD.

Apuntes Bases de Datos II

In g. Wilfrido Martínez Cruz


Memoria principal. Este es el medio de almacenamiento que se emplea para los datos disponibles sobre los cuales se va a operar. Las instrucciones de máquina de propósito general operan sobre la memoria principal. Aunque puede contener varios megabytes, casi siempre es demasiado pequeña para almacenar la BD entera. Normalmente el contenido de la memoria principal se pierde si hay un corte de corriente o si se cae el sistema. Almacenamiento en disco. Este es el medio principal para el almacenamiento de datos a largo plazo. Lo común es que toda la BD esté almacenada en disco. Los datos deben trasladarse del disco a la memoria principal para poder operar sobre ellos, después de realizar las operaciones, deben devolverse al disco. El almacenamiento en disco se denomina almacenamiento de acceso directo, porque es posible leer los datos en disco en cualquier orden. Normalmente, el almacenamiento en disco sobrevive a los cortes de energía y las caídas de sistema. Los dispositivos de almacenamiento en disco pueden fallar y destruir los datos, pero estos fallos son muy poco frecuentes. Almacenamiento en cinta. Este tipo de almacenamiento se usa principalmente para copias de seguridad y datos de archivo. Aunque la cinta es mucho más barata que el disco, el acceso a los datos es mucho más lento, puesto que la cinta debe leerse secuencialmente desde el principio. Por esta razón se denomina almacenamiento de acceso secuencial, y se usa principalmente para recuperarse de fallos en el disco. Los dispositivos de cinta son menos complejos que los de disco, por lo que son más fiables.

Puesto que el almacenamiento en disco es de vital importancia para la implementación de la BD, examinaremos las características de los discos con más detalle. La cabeza es un dispositivo que se coloca muy cerca de la superficie del plato y lee o escribe información codificada magnéticamente en el plato. El plato está organizado en pistas concéntricas de datos. El brazo puede colocarse sobre cualquiera de las pistas. El plato gira a alta velocidad. Para leer o escribir información, el brazo se coloca sobre la pista correcta, y cuando los datos a los que se va a acceder pasan por debajo de la cabeza, se realiza la operación de lectura o escritura. Puesto que el plato rota a gran velocidad, no se requiere mucho tiempo para que todo el contenido de una pista pase por debajo de la cabeza. Esta cantidad de tiempo se conoce como tiempo de latencia del disco. El tiempo en volver a colocar el brazo, el tiempo de búsqueda, aumenta conforme se incrementa la distancia a que debe moverse el brazo. Resulta útil almacenar información relacionada en la misma pista o en pistas cercanas físicamente siempre que sea posible para minimizar el tiempo de búsqueda. Existen discos de platos múltiples, denominados paquetes de discos, y de aquí en adelante, cuando usemos el término disco, nos referiremos a discos de platos múltiples. El impulsor, une todos los brazos y los mueve como una unidad. Cada brazo tiene dos cabezas, una para leer y escribir en la superficie superior del plato que está bajo la cabeza, y una para leer y escribir en la superficie inferior del disco que está sobre la cabeza. El conjunto de pistas sobre las que están colocadas las cabezas forma un cilindro. El cilindro tiene los datos que pueden accederse sin ningún movimiento del impulsor. Es decir, todos los datos del cilindro son accesibles dentro del tiempo de latencia del disco. Es eficiente almacenar datos relacionados en el mismo cilindro, o si no es posible, en cilindros próximos entre sí. Los datos se transfieren entre el disco y la memoria principal en unidades llamadas bloques. Un bloque es una secuencia contigua de bytes de una sola pista de un plato. Si es necesario transferir varios bloques de un cilindro a la memoria principal, podemos ahorrar tiempo pidiendo los bloques en el orden en que van a pasar por debajo de las cabezas. Si los bloques requeridos están en cilindros distintos, resulta ventajoso pedir los bloques en un orden que minimice el movimiento del impulsor.. La forma más sencilla de optimizar el tiempo de acceso al bloque es organizar los bloques en el disco de una forma que corresponda fielmente a la manera en la que esperamos que se acceda a los datos. Sin embargo, puede ser costoso mantener esta organización cuando se inserta o elimina información. 1.3.

Organización de archivos.

Un archivo está organizado lógicamente como una secuencia de registros. Estos registros se asignan a bloques del disco. Los archivos se dan como construcciones básicas en los sistemas operativos, por lo que supondremos que existe un sistema de archivos subyacente. Necesitamos considerar formas de representar modelos lógicos de datos en términos de archivos. Aunque los bloques son de tamaño fijo, los tamaños de los registros varían. En una BD relacional, las tuplas de relaciones distintas son ,por lo general, de tamaños distintos. En una BD de datos de red es probable que el tipo de registro propietario sea de distinto tamaño que el tipo de registro miembro. Una forma de enfocar la asignación de la BD a los archivos a almacenar en un archivo dado solamente registros de una longitud fija. Una alternativa es estructurar los archivos de una forma tal que podamos

Apuntes Bases de Datos II

In g. Wilfrido Martínez Cruz


acomodar registros de varias longitudes diferentes. Los archivos de registros de longitud fija son más fáciles de implementar. Registros de longitud fija. Como ejemplo, consideremos un archivo de registros depósito, definidos: type depósito = registro nombre_sucursal: char (20); número_cuenta: integer; nombre_cliente: char (20); saldo: real; end Si suponemos que cada carácter ocupa un byte, un entero cuatro, y un real ocho, el registro depósito tiene 52 bytes. Un enfoque sencillo es utilizar los primeros 52 bytes para el primer registro, los siguientes 52 para el segundo y así sucesivamente. Sin embargo este enfoque presenta dos problemas: 1.- Es difícil eliminar un registro de esta estructura. El espacio que ocupa el registro que se va a eliminar debe llenarse con otro registro, o debemos marcarlo para que sea ignorado. 2.- A menos que el tamaño del bloque sea múltiplo de 52, algunos registros pasarán los límites del bloque, lo que requeriría tener acceso a dos bloques para leer o escribir el registro. Cuando se elimina un registro podríamos mover el registro que le seguía un espacio para delante, y así sucesivamente, pero esto requiere mover muchos registros, o se podría mover el último registro al hueco libre. No es deseable mover registros para ocupar el espacio que deja libre un registro eliminado, ya que esto requiere tener acceso a bloques adicionales. Puesto que las inserciones suelen ser más frecuentes que las eliminaciones, es aceptable dejar abierto el espacio que ocupa el registro eliminado y esperar una inserción posterior para ocupar el espacio. No basta simplemente marcar el registro eliminado, ya que no es fácil encontrar el espacio disponible cuando se va a realizar una inserción. Por tanto, necesitamos introducir una estructura adicional. Al principio del archivo asignamos un cierto número de bytes como encabezamiento del archivo. El encabezamiento contendrá información diversa acerca del archivo. Por ahora, todo lo que hace falta almacenar aquí es la dirección del primer registro cuyo contenido se haya eliminado. Utilizamos este primer registro para almacenar la dirección del segundo registro disponible, y así, sucesivamente. Podemos considerar estas direcciones almacenadas como punteros. Al insertarse un nuevo registro, utilizamos el registro al que apunta el encabezamiento; y éste se cambia para que apunte al siguiente registro disponible. Si no hay espacio disponible añadimos el registro al final del archivo. El uso de punteros requiere mucho cuidado en la programación. La inserción y eliminación en archivos de registros de longitud fija es bastante fácil de implementar, porque el espacio que deja disponible un registro es exactamente el que se necesita para insertar otro. Si permitimos registros de longitud variable en un archivo, la situación cambiara, es posible que un registro insertado no quepa en el espacio que deja libre otro eliminado, o que llene solamente parte del espacio. Registros de longitud variable. Los registros de longitud variable se presentan en los DBMS de varias formas:   

Almacenamiento de varios tipos de registros en un archivo. Tipos de registros que permiten uno o más campos de longitudes variables. Tipos de registros que permiten campos repetidos.

Existen varias técnicas para implementar los registros de longitud variable. Para ilustrarlas utilizaremos el mismo ejemplo. El formato de registro es: type lista_depósito = record nombre_sucursal: char (20); info_cuenta: array [1..] of record número_cuenta: integer; nombre_cliente: char (20);

Apuntes Bases de Datos II

In g. Wilfrido Martínez Cruz


saldo: real; end end Definimos info_cuenta como un array con un número arbitrario de elementos de manera que el tamaño del registro no tiene límite. Representación en cadena de bytes. Un método sencillo para implementar los registros de longitud variable es añadir un símbolo especial de fin de registro al final de cada registro. Así podemos almacenar cada registro como una cadena de bytes consecutivos. La representación en cadena de bytes tiene varias desventajas, las más serias son: 1.- No es fácil volver a utilizar el espacio que ocupaba un registro eliminado. Aunque existen técnicas para gestionar la inserción y eliminación, resultan en un gran número de fragmentos pequeños de espacio que se desperdician. 2.- En general, los registros no disponen de espacio para crecer. Si un registro de longitud variable se hace más largo debe moverse, y si el registro está sujeto, el movimiento resulta costoso. Por tanto, la representación en cadena de bytes, normalmente, no se utiliza para almacenar registros de longitud variable. Representación de longitud fija. Para implementar los registros de longitud variable de manera eficiente en un sistema de archivos, utilizamos uno o más registros de longitud fija para representar un registro de longitud variable. Existen dos técnicas para implementar archivos de registros de longitud variable utilizando registros de longitud fija:  

Espacio reservado. Si hay una longitud máxima de registro que nunca se excede, podemos utilizar registros de longitud fija de esa longitud. El espacio no utilizado se llena con símbolo especial nulo o de fin de registro. Punteros. El registro de longitud variable se representa por una lista de registros de longitud fija encadenado por medio de punteros.

El método de espacio reservado es útil cuando gran parte de los registros es de longitud cercana al máximo. Si no es así, puede desperdiciarse una cantidad apreciable de espacio. Para representar el archivo empleando el método de punteros, añadimos un campo que corresponda al puntero. Utilizamos los punteros solamente para encadenar los registros de una determinada sucursal. Una desventaja del método de punteros es que desperdiciamos espacio en todos los registros menos en el primero de la cadena. Es preciso que el primero incluya el valor de nombre_sucursal, pero no los subsiguientes registros. El espacio desperdiciado es considerable, ya que esperamos que las sucursales tengan gran número de cuentas. Para resolver este problema permitimos dos tipos de bloques en el archivo:  

Bloque ancla. Contiene el primer registro de la cadena. Bloque de desbordamiento. Contiene registros que no son el primero.

Así, todos los registros dentro de un bloque tienen la misma longitud, aunque no todos los registros del archivo tienen la misma longitud. 1.4.

Organización de registros en bloques.

Un archivo puede considerarse como una colección de registros. Sin embargo, como los datos se transfieren entre el disco y la memoria en unidades de bloque, vale la pena asignar los registros a los bloques de tal forma que un simple bloque contenga registros relacionados entre si. El acceso a disco es generalmente el cuello de botella en el rendimiento del sistema, una asignación con cuidado de registros a los bloques puede mejorar significativamente el rendimiento. Anteriormente describimos una estructura de archivos en la que toda la información relativa alas cuentas de una sucursal aparecía en un registro (longitud variable9. Es fácil agrupar registros de esta manera si la BD

Apuntes Bases de Datos II

In g. Wilfrido Martínez Cruz


nunca cambia. Sin embargo, supóngase que se abre una nueva cuenta. Si estamos utilizando esta estructura, desearíamos añadir el registro que corresponde a esta cuenta al mismo bloque en que están el resto de las cuentas de la sucursal. Sin embargo, lo más probable es que el bloque ya esté lleno con registros de cuentas de otras sucursales. Debemos mover algunos de estos registros o abandonar el objetivo de agrupar los registros que representan a un único registro de longitud variable. Ninguna de estas opciones es deseable. Como alternativa, consideremos una estructura que ocupa un espacio un poco mayor, pero que permite mejorar la eficiencia de acceso a la información. A cada valor de nombre_sucursal le asignamos una cadena de bloques. Cada cadena de bloques contiene el registro de longitud variable completo para una sucursal. Una cadena consta de tantos bloques como sean necesarios para representar los datos, pero dos cadenas diferentes nunca comparten bloques. Las cadenas tienen una estructura ligeramente diferente de las demás estructuras de registros de longitud fija que hemos utilizado:  

El primer registro de la cadena contiene el valor nombre_sucursal de la cadena. Los siguientes registros contienen los campos que se repiten. No hace falta repetir nombre_sucursal.

Obsérvese que hay dos longitudes de registro diferentes en cada cadena. El primer registro y todos los demás. Puesto que todas las cadenas deben tener únicamente un registro que contenga el valor nombre_sucursal, no hay problema para realizar inserciones y eliminaciones. Al crecer una cadena por inserción de registros, puede que se necesite añadirle nuevos bloques. Al eliminar registros, un bloque podría quedar vacío. Como hicimos en el caso de los registros borrados, podemos mantener una cadena de bloques disponibles y volver a utilizarlos para otras cadenas que se extiendan más allá de los bloques que ocupan. Esta estrategia no es la mejor desde el punto de vista de rapidez de acceso. Cuando se hace una busqueda en una cadena se deben leer todos los bloques. Para minimizar el tiemo que se lleva una búsqueda necesitamos minimizar el tiempo que lleva transferir estos bloques a la memoria. Por tanto, conviene que los bloques de una cubeta estén almacenados en el mismo cilindro del disco o en cilindros adyacentes. Una cubeta es un conjunto de uno o más bloques encadenados. Si quedara vacío el bloque, sería preferible que volviera a ser utilizado por la misma cadena que lo contenía anteriormente. En la práctica, no es posible mantener una colocación perfecta de los bloques en el disco sin dejar vacía una gran cantidad de espacio. Tarde o temprano, las cadenas rebosarán su cilindro y puede que no exista espacio disponible en cilindros cercanos. Si la cadena queda tan fragmentada que el rendimiento empieza a disminuir, puede reorganizarse la BD. La BD se copia en cinta y se vuelve a cargar con los bloques cambiados de sitio de forma que las cadenas no queden fragmentadas, y exista un espacio razonable para su crecimiento. Generalmente es necesario prohibir el acceso de los usuarios a la BD durante estas reorganizaciones. 1.5.

Archivos secuenciales.

Un archivo secuencial esta diseñado para procesar de manera eficiente registros en un determinado orden basándose en alguna clave de búsqueda. Para permitir una recuperación rápida los registros se encadenan por medio de punteros. El puntero de cada registro apunta al siguiente registro en el orden de la clave de búsqueda. Además para minimizar el número de accesos a bloque en el procesamiento de archivos secuenciales, los registros se almacenan físicamente en el orden de la clave de búsqueda o tan cerca de éste como sea posible. Es difícil mantener el orden secuencial físico cuando se insertan o eliminan registros, ya que es costoso mover muchos registros. La eliminación puede manejarse utilizando cadenas de punteros, como vimos anteriormente. Para la inserción aplicamos las siguientes reglas: 1.- Localizar el registro en el archivo que precede al registro que se va a insertar en el orden de la clave de búsqueda. 2.- Si existe algún registro libre dentro del mismo bloque, se inserta allí el nuevo registro. Si no, se inserta un nuevo bloque de desbordamiento. En cualquier caso, se ajustan los punteros para que los registros queden encadenados en el orden de la clave de búsqueda. Si los registros que se necesitan almacenar en bloques de desbordamiento son relativamente pocos, este enfoque funciona bien. Sin embargo, puede llegar a ocurrir que se pierda totalmente la correspondencia entre

Apuntes Bases de Datos II

In g. Wilfrido Martínez Cruz


el orden de la clave de búsqueda y el orden físico, y el procesamiento secuencial llegue a ser mucho menos eficiente. En este punto es preciso reorganizar el archivo para que quede otra vez en orden secuencial físicamente. Estas reorganizaciones son costosas y se hacen en momentos en los que la carga del sistema es baja. 1.6.

Asignación (mapping) de datos relacionales a archivos.

Muchos DBMS relacionales almacenan cada relación en un archivo separado. Esto permite al DBMS aprovechar al máximo el sistema de archivos que forma parte del sistema operativo. Normalmente las tuplas de una relación pueden representarse como registros de longitud fija, por tanto, las relaciones pueden asignarse a una estructura simple de archivos. Esta implementación sencilla se adapta bien a computadores personales, ya que el tamaño de la BD es pequeño, por lo que no se aprovecharía una estructura de datos sofisticada. Además, en algunos computadores personales es fundamental que el código objeto del DBMS no ocupe mucho espacio, si la estructura de archivos es simple, se reduce la cantidad de código necesaria para implementar el sistema. Este enfoque sencillo de la implementación de BD relacionales se vuelve menos satisfactorio al aumentar el tamaño de la BD. Hemos visto que puede mejorarse el rendimiento si se tiene cuidado al asignar registros a los bloques y al organizarlos. Así, es aparente que una estructura de archivos más complicada pueda resultar ventajosa. Sin embargo, muchos DBMS a gran escala no dependen del sistema operativo subyacente para la gestión de archivos. En vez de ello se asigna un archivo grande del sistema operativo al DBMS. Todas las relaciones se almacenan en este archivo y se deja la gestión de éste al DBMS. Para ver la ventaja de almacenar muchas relaciones en un archivo, considérese la siguiente consulta SQL para la BD bancaria: select número_cuenta, nombre_cliente, calle, ciudad_cliente from depósito, cliente where depósito,nombre_cliente = cliente.nombre_cliente Esta consulta calcula un producto de las relaciones depósito y cliente. Así, para cada tupla de depósito, el sistema debe localizar las tuplas de cliente que tengan el mismo en nombre_cliente. Lo ideal sería localizar estos registros con la ayuda de índices, que se estudian en el tema siguiente, pero, sin importar la forma en que se hayan localizado estos registros, es necesario transferirlos del disco a la memoria. En el peor de los casos, cada registro residirá en un bloque diferente, obligando a leer un bloque por cada registro que requiera la consulta. Como un ejemplo concreto, considérense las relaciones depósito y cliente. Una estructura de archivo diseñada para ejecutar de manera eficiente consultas que impliquen a depósito|x|cliente, almacena las tuplas depósito de cada nombre_cliente, cerca de la tupla cliente del nombre_cliente correspondiente. Esta estructura mezcla tuplas de dos relaciones, pero permite el procesamiento eficiente del producto. Cuando se lee una tupla de la relación cliente, el bloque completo que contiene esa tupla se copia del disco a la memoria. Puesto que las tuplas depósito correspondientes están almacenadas en el disco cerca de la tupla cliente, el bloque que contiene la tupla cliente contiene las tuplas de la relación préstamo que se necesitan para procesar la consulta. Si un cliente tiene tantas cuentas que los registros depósito no caben en un bloque, los registros restantes aparecerán en bloques cercanos. Esta estructura de archivos, llamada agrupación, nos permite leer muchos de los registros que se requieren leyendo sólo un bloque. El uso de la agrupación mejora el procesamiento de un producto específico, pero resulta en un procesamiento más lento de otros tipos de consulta, ya que para localizar todas las tuplas de una única relación, necesitamos encadenar todos sus registros por medio de punteros. Para determinar cuando conviene utilizar la agrupación hay que tener en cuenta cuales son los tipos de consulta que el diseñador de la BD cree que van a ser más frecuentes. Si se utiliza con cuidado la agrupación, puede mejorar considerablemente el rendimiento en el procesamiento de consultas. 1.7.

Almacenamiento de diccionario de datos.

Hasta ahora sólo hemos considerado la representación de las relaciones mismas. Un DBMS relacional necesita mantener datos acerca de las relaciones. Esta información se denomina diccionario de datos o catálogo de sistema. Entre los tipos de información que el sistema debe almacenar están: los nombres de las relaciones, los nombres de los atributos de las relaciones, los dominios de los atributos, los nombres de las vistas definidas y su definición y las restricciones de integridad de cada relación. Además de esto, muchos sistemas conservan los datos de los usuarios: nombre de los usuarios autorizados e información contable sobre ellos. Además, en los sistemas que utilizan estructuras altamente sofisticadas, pueden conservarse

Apuntes Bases de Datos II

In g. Wilfrido Martínez Cruz


datos estadísticos y descriptivos acerca de las relaciones: número de tuplas por relación y método de almacenamiento para cada relación. En el siguiente capítulo, veremos que es necesario almacenar información acerca de cada índice de cada relación: nombre del índice, nombre de la relación que indexa, atributos sobre los que está el índice y tipo de índice. Toda esta información constituye una base de datos en miniatura. Algunos DBMS almacenan esta información empleando estructuras de datos y código de propósito especial. Generalmente es preferible almacenar los datos acerca de la BD en la propia BD. Si se utiliza la BD para almacenar datos del sistema, simplificamos la estructura global del sistema y permitimos aprovechar toda la capacidad de aquella para agilizar el acceso a los datos del sistema. La elección precisa de como se van a representar los datos por medio de relaciones debe hacerla el diseñador del sistema. Una posible representación es: esquema_catálogo_sistemas = (nombre_relación, numero_de_atributos) esquema_atributo = (nombre_atributo, nombre_relación, tipo_dominio, posición) esquema_usuario = (nombre_usuario, clave_codificada, grupo) esquema_índice = (nombre_índice, nombre_relación, tipo_índice, atributos_índice) esquema_vista = (nombvre_vista, definición). 1.8.

Gestión de registros intermedios (buffer).

Ya hemos mencionado la necesidad de utilizar almacenamiento en disco para la BD, y la necesidad de transferir bloques de datos entre la memoria y el disco. Un objetivo principal de las estructuras de archivo que se presentaron es minimizar el número de bloques a los que se debe acceder. Otra forma de reducir el número de accesos a disco es mantener tantos bloques en memoria como sea posible. El objetivo es incrementar al máximo la posibilidad de que, cuando se necesite acceder a un bloque, éste ya esté en la memoria, y por lo tanto no se requiera acceder al disco. Puesto que no es posible mantener todos los bloques en memoria, es preciso gestionar la asignación del espacio disponible en memoria para almacenamiento de bloques. Los registros intermedios (buffer) son aquella parte de la memoria principal disponible para almacenar copias de bloques de disco. Siempre se guarda una copia de todos los bloques en el disco, pero la copia en el disco puede ser una versión antigua de la del bloque distinta de la versión del buffer. El responsable del subsistema para la asignación de espacio en el buffer se denomina gestor del buffer. El gestor de buffer intercepta todas las solicitudes que hace el resto del sistema pidiendo bloques de la BD. Si el bloque ya está en el buffer, se pasa al solicitante la dirección del bloque en la memoria. Si el bloque no está en el buffer, el gestor lo lee del disco y lo escribe en el buffer, y pasa la dirección del bloque al solicitante. Así, el gestor de buffer es transparente para aquellos programas del sistema que solicitan bloques del disco. Con el fin de servir adecuadamente al sistema, el gestor de buffer debe utilizar técnicas sofisticadas de gestión del buffer: 

Estrategia de reemplazo. Cuando no queda espacio libre en el buffer, debe sacarse un bloque antes de que pueda escribirse uno nuevo. Los sistemas operativos más comunes emplean el esquema de “menos recientemente utilizado” (LRU). Este enfoque sencillo puede mejorarse en una aplicación de BD. Bloques sujetos. Para que el DBMS pueda recuperarse de caídas, es necesario restringir las oportunidades en las que se puede grabar un bloque en el disco. Un bloque al que no se permite que se vuelva a grabar en el disco se dice que está sujeto. Aunque muchos sistemas operativos no soporten bloques sujetos, una característica así es esencial para la implementación de un DBMS que sea resistente a las caídas. Salida forzada de bloques. Existen situaciones en las que es necesario escribir el bloque en el disco, aunque no se necesite el espacio que ocupa en el buffer. Esto se denomina salida forzada de un bloque. El requisito se debe al hecho de que los contenidos de la memoria, y por tanto del buffer, se pierden en una caída mientras que los datos en disco generalmente sobreviven a ella.

El objetivo de una estrategia de reemplazo de bloques en el buffer es la minimización de accesos a disco. Los sistemas operativos utilizan el patrón de referencias anteriores a bloques para predecir las referencias que se harán en el futuro según el principio de localidad. La suposición que se hace es que es probable que se vuelva a hacer referencia a los bloques a los que se hizo referencia recientemente. Por tanto, si se debe sustituir un bloque, se sustituye aquel al que se hizo referencia menos recientemente. Esto se denomina esquema de reemplazo de bloques LRU.

Apuntes Bases de Datos II

In g. Wilfrido Martínez Cruz


El LRU es un esquema de reemplazo aceptable en sistemas operativos. Sin embargo, un DBMS puede predecir el patrón de referencias futuras con más exactitud que un sistema operativo. Las solicitudes que hacen los usuarios al sistema operativo implican varios pasos, pero muchas veces el DBMS puede determinar por adelantado qué bloques va a necesitar cada uno de los pasos. Así, los DBMS pueden tener información referente, por lo menos, del futuro a corto plazo. Considérese el procesamiento de un producto natural de dos relaciones, y que para procesarlo, se procesa la segunda relación entera por cada tupla de la primera relación. Supóngase que las dos relaciones están almacenadas en archivos separados.. Por tanto, una vez que se procese un bloque de la primera relación, no se va a volver a necesitar hasta que se procese toda la segunda relación, por lo que se puede eliminar de la memoria, a pesar de que se uso recientemente. El gestor del buffer debe recibir instrucciones de dejar libre el espacio que ocupa ese bloque. Esta estrategia de gestión de buffer se conoce como desechar de inmediato. Considérense ahora los bloques que contienen tuplas de la segunda relación. Necesitamos examinar todos estos bloques una vez por cada tupla de la primera relación. Cuando se termina el procesamiento de un bloque de la segunda relación, sabemos que no volverá a ser accedido hasta que todos los demás bloques de esta segunda relación sean procesados. Por tanto el bloque de esta relación que se utilizó el último será el que tarde más en ser referenciado, y bloque más antiguo será el primero en ser llamado. Esto es exactamente lo opuesto a la estrategia LRU. De hecho, la estrategia óptima para sustituir bloques es la estrategia “mas recientemente utilizado” (MRU). Si es preciso sacar un bloque del buffer, la estrategia MRU elige el bloque que se utilizó más recientemente. Para que la estrategia MRU funcione correctamente en este ejemplo, el sistema debe sujetar el bloque, de la segunda relación, que se está procesando en ese momento. Después de que se haya procesado la última tupla del bloque, se libera y se convierte en el bloque utilizado más recientemente. El gestor del buffer puede utilizar información estadística referente a la probabilidad de que una solicitud haga referencia a una determinada relación. El diccionario de datos es una de las partes de la BD a la que se accede más frecuentemente. Por ello, el gestor de buffer debe procurar no sacar los bloques del diccionario de la memoria. A menos que otros factores dicten lo contrario. Como puede ser que se acceda al índice con mayor frecuencia que al mismo archivo, el gestor del buffer no deberá, en general, sacar los bloques de índice de la memoria mientras tenga alguna alternativa. La estrategia ideal de reemplazo de bloques de la BD necesita conocer las operaciones que se están realizando en ella. No se conoce una sola estrategia que maneje bien todas las situaciones posibles. De hecho, un gran número de sistemas utiliza la estrategia LRU a pesar de sus fallos. La estrategia que utiliza el gestor de buffer para sustituir los bloques se ve influenciada por otros factores. Si el sistema está procesando en forma concurrente solicitudes de varios usuarios, puede que el subsistema de control de concurrencia tenga que retrasar ciertas solicitudes para garantizar la conservación de la consistencia de los datos. Si el subsistema de control de concurrencia proporciona información al gestor del bufffer acerca de cuales son las solicitudes que se van a retrasar, el gestor del buffer puede utilizar esta información para alterar su estrategia de reemplazo de bloques. Los bloques que requieren las solicitudes activas (no retrasadas) pueden conservarse en el buffer a expensas de los bloques que necesitan las solicitudes retrasadas. El subsistema de recuperación de caídas impone restricciones muy estrictas sobre la sustitución de bloques. Si se ha modificado algún bloque, no se permitirá que el gestor del buffer escriba en el disco la nueva versión del bloque del buffer, ya que esto destruiría la versión antigua. En vez de ello, el gestor de bloques debe pedir la autorización del subsistema de recuperación de caídas antes de grabar el bloque. Es posible que el subsistema de recuperación de caídas exija la salida forzada de determinados bloques antes de permitir que se grabe el bloque solicitado. 2.

Indexación y asociatividad (hashing).

Muchas consultas hacen referencia a sólo una pequeña parte de los registros de un archivo. Es ineficiente que el sistema tenga que leer todos los registros. Lo ideal es que el sistema pueda localizar directamente estos registros. Para permitir estas formas de acceso diseñamos estructuras adicionales que asociamos con archivos. Consideraremos dos formas generales de atacar éste problema: la construcción de índices y la construcción de funciones de asociatividad (hash). 2.1.

Conceptos básicos.

Apuntes Bases de Datos II

In g. Wilfrido Martínez Cruz


Un índice de un archivo funciona de manera similar a un catálogo en una biblioteca. Si estamos buscando un libro por un autor determinado, buscamos en autores y una tarjeta de catálogo nos dice dónde encontrar el libro. Para facilitarnos la búsqueda, las tarjetas se guardan en orden alfabético, de forma que no tenemos que comprobar todas para encontrar la que queremos. En las BD es posible que estos tipos de índices sean demasiado grandes para manejarse eficientemente. En vez de ello, pueden utilizarse técnicas de indexación más sofisticadas. Como alternativa a la indexación se utilizan funciones de asociatividad. Consideraremos varias técnicas tanto de asociatividad como de indexación. Ninguna de ellas es la mejor, sino que cada una es más apropiada para una aplicación específica de BD. Cada técnica debe evaluarse en base a:    

Tiempo de acceso. El tiempo que se tarda en encontrar un dato determinado. Tiempo de inserción. El tiempo que se tarda en insertar un dato nuevo. Esto incluye el tiempo que se tarda en encontrar el lugar correcto, así como el que se tarda en actualizar la estructura de indexación. Tiempo de eliminación. El tiempo que se tarda en eliminar un dato. Esto incluye el tiempo que se tarda en encontrar el dato, así como el que se tarda en actualizar la estructura de indexación. Espacio extra. El espacio adicional que ocupa la estructura de indexación. Siempre que este espacio no sea muy grande, merece la pena sacrificar el espacio por una mejora en el rendimiento.

Muchas veces queremos tener más de un índice o función de aosciatividad para un archivo. El atributo o conjunto de atributos que se usa para buscar registros en un archivo se llama clave de búsqueda.. Obsérvese que ésta definición de clave difiere de las de clave primaria, clave candidata y superclave. 2.2.

Indexación.

Para permitir el acceso aleatorio rápido a los registros de un archivo se utiliza una estructura de índice. Cada estructura de índice está asociada con una clave de búsqueda determinada. Si el archivo está ordenado secuencialmente y elegimos incluir varios índices en diferentes claves de búsqueda, el índice cuya clave de búsqueda especifica el orden secuencial del archivo es el índice primario. Los demás se llaman índices secundarios. La clave de búsqueda de un índice primario es normalmente la clave primaria. En esta sección suponemos que todos los archivos están ordenados secuencialmente y , por tanto, tienen una clave de búsqueda primaria. Dichos archivos, junto con un índice primario, se llaman archivos de índices secuenciales. Se encuentran entre los esquemas de indexación más antiguos usados en los BDMS. Están diseñados para aplicaciones que requieren tanto un procesamiento secuencial del archivo completo como un acceso aleatorio a registros individuales.

Hay dos tipos de índices que pueden usarse:  

Índice denso. Aparece un registro índice para cada valor de la clave de búsqueda en el archivo. El registro contiene el valor de la clave de búsqueda y un puntero al registro. Índice escaso. Se crean registros índices solamente para algunos de los registros. Para localizar un registro, encontramos el registro índice con el valor de la clave de búsqueda más grande que sea menor o igual que el valor que estamos buscando. Empezamos en el registro al que apunta el registro índice y seguimos los punteros del archivo hasta encontrar el registro deseado.

Índice primario. Generalmente es más rápido localizar un registro con un índice denso que con uno escaso. Sin embargo, los índices escasos requieren menos espacio e imponen menos mantenimiento adicional para inserciones y eliminaciones. El diseñador del sistema debe lograr un equilibrio entre el tiempo de acceso y el espacio extra. Un buen compromiso es tener un índice escaso con una entrada de índice por bloque. Para que esta técnica sea completamente general, debemos considerar el caso en el que los registros para un valor de la clave de búsqueda ocupan varios bloques. Es fácil modificar el esquema para manejar esta situación. Aún cuando utilizamos un índice escaso, el índice puede llegar a ser demasiado grande para un procesamiento eficiente. En la práctica, no es raro tener un archivo con 100.000 registros. Con 10 registros

Apuntes Bases de Datos II

In g. Wilfrido Martínez Cruz


por bloque. Si tenemos un registro índice por bloque, el índice tiene 10.000 registros. Los registros índice son más pequeños que los de datos, por lo que podemos suponer que entran 100 por bloque, así pues el índice ocupa 100 bloques. Si un índice es lo bastante pequeño como para guardarlo en memoria, el tiempo de búsqueda es corto. Sin embargo, si le índice es tan grande que debe guardarse en disco, una búsqueda puede ser costosa. Para resolver este problema, tratamos el índice como cualquier otro archivo secuencial, y construimos un índice escaso sobre el índice primario, que puede almacenarse en memoria. Utilizando los dos niveles de indexación, hemos leído únicamente un bloque de índices en vez de 100. Si suponemos que el índice externo ya está en la memoria. Si el fichero es extremadamente grande, es posible que ni siquiera el índice exterior quepa en memoria principal, en este caso, podemos crear otro nivel de indexación. En la práctica, lo normal es que basten dos niveles. Frecuentemente, cada nivel de índice corresponde a una unidad de almacenamiento físico. Así, podemos tener índices en los niveles de pista, cilindro y disco. Sin importar cual sea la forma de índice que se utilice, se deben actualizar todos los índices cada vez que se inserta o elimina un registro del archivo. A continuación describimos algoritmos para actualizar índices de un sólo nivel: 

Eliminación. Para eliminar un registro, es necesario buscar el registro que se va a eliminar. Si el registro eliminado era el último que quedaba con ese valor particular de la clave de búsqueda, entonces eliminamos el valor de la clave de búsqueda del índice. Para índices densos, eliminamos un valor de la clave de búsqueda de la misma manera que se suprime en un archivo. Para índices escasos, eliminamos un valor de clave sustituyendo su entrada en el índice por el siguiente valor de la clave de búsqueda. Si el siguiente valor ya tiene una entrada de índice, eliminamos la entrada. Inserción. Se hace una búsqueda usando el valor de la clave de búsqueda que aparece en el registro que se va a insertar. Si el índice es denso y el valor de la clave de búsqueda no aparece en el índice, lo inserta. Si el índice es escaso no se necesita hacer ningún cambio en el índice a menos que se cree un nuevo bloque. En este caso, el primer valor de la clave de búsqueda que aparezca en el nuevo bloque se inserta en el índice.

Índices secundarios. Los índices secundarios pueden estructurarse de forma diferente a los índices primarios. Los punteros en el índice secundario no señalan directamente al archivo, en vez de ello, cada uno de esos punteros señala a una cubeta que contiene punteros al archivo.

Este enfoque permite almacenar juntos todos los punteros de un valor de clave de búsqueda secundaria determinado. Un enfoque así es útil en ciertos tipos de consultas para los que podemos hacer una parte considerable de procesamiento usando únicamente los punteros. Para las claves primarias, podemos obtener todos los punteros para un valor de la clave de búsqueda primaria determinado utilizando una revisión secuencial. Una revisión secuencial en orden de clave primaria es eficiente porque los registros están almacenados físicamente en un orden que se aproxima al orden de la clave primaria. Sin embargo, no podemos almacenar un archivo físicamente ordenado tanto por la clave primaria como por una clave secundaria. Como el orden de clave secundaria y el de clave física son distintos, si intentamos examinar el archivo secuencialmente en orden de clave secundaria, es probable que la lectura de cada registro requiera la lectura de un nuevo bloque de disco. Almacenando punteros en una cubeta, eliminamos la necesidad de punteros adicionales en los registros mismos y de revisiones secuenciales en orden de clave secundaria. El índice secundario puede ser denso o escaso. Si es denso, entonces el puntero de cada cubeta individual señala a los registros con el valor de la clave de búsqueda apropiado. Si el índice secundario es escaso, entonces el puntero de cada cubeta individual señala a los valores de la clave de búsqueda en un rango apropiado. En este caso, cada entrada de cubeta es un puntero único o bien un registro que consta de dos campos: un valor de la clave de búsqueda y un puntero a algún registro de archivo. Si las cubetas contienen únicamente punteros, debemos leer todos los registros a los que apunta la cubeta.

Apuntes Bases de Datos II

In g. Wilfrido Martínez Cruz


Asociando un valor de la clave de búsqueda con cada puntero de la cubeta, eliminamos la necesidad de leer registros con un valor de la clave de búsqueda secundaria distinta del que estamos buscando. La estructura de la cubeta puede eliminarse si el índice secundario es denso y los valores de la clave primaria forman parte de la clave de búsqueda. El procedimiento descrito para la inserción y eliminación puede aplicarse a un archivo con múltiples índices. Cada vez que se modifique el archivo es necesario actualizar todos los índices. Los índices secundarios mejoran el rendimiento en las consultas que utilizan claves que no son primarias, sin embargo, implican un gasto extra considerable en la modificación de la BD. El diseñador de una BD decide que índices secundarios son deseables, basándose en una estimación de la frecuencia relativa de consultas y modificaciones. 2.3.

Archivos indexados de árboles B+.

La desventaja principal de la organización de archivo secuencial indexado es que el rendimiento baja al crecer el archivo: la estructura de archivo de árbol B+ es la más ampliamente utilizada de varias estructuras de archivo que mantienen su eficiencia a pesar de inserciones y eliminaciones. Un índice de árbol toma B+ la forma de un árbol equilibrado en el que cualquier camino desde la raíz del árbol hasta una hoja tiene la misma longitud. Todos los nodos del árbol tienen entre [n/2] (entero, redondeo superior) y n hijos, donde n es fijo para un determinado árbol. Veremos que la estructura de árbol B+ impone un cierto gasto extra durante la inserción y la eliminación, además de requerir un determinado espacio extra. No obstante, esto es aceptable en el caso de archivos con una frecuencia alta de modificación, ya que se evita el coste de la reorganización del archivo. Un índice de árbol B+ tiene varios niveles, pero tiene una estructura que difiere de la del archivo secuencial de índices de varios niveles. Un nodo típico de un árbol B+ es de la forma: P1 K1 P2 … Pn-1 Kn-1 Pn Contiene hasta n-1 valores de clave de búsqueda K, y n punteros P. Los valores de la clave de búsqueda dentro de un nodo se guardan en un determinado orden, así, si i<j, entonces Ki<Kj. Consideramos primero la estructura de los nodos hoja. Para 1i<n, Pi apunta a cualquier registro del archivo con un valor de clave de búsqueda Ki o a una cubeta de punteros, cada uno de los cuales apunta a un registro del archivo con valor clave de búsqueda Ki. La estructura de cubeta se utiliza solamente si la clave de búsqueda no forma una clave primaria y el archivo no está ordenado en el orden del valor de la clave de búsqueda. El puntero Pn tiene un propósito general que indicaremos después. Ahora que hemos visto la estructura de un nodo hoja, consideremos como se asignan valores de clave de búsqueda a nodos específicos. Cada nodo hoja puede tener hasta n-1 valores, y se permite que tengan un mínimo de [(n-1)/2] valores, además, los rangos de valores de cada hoja no se solapan. Así, si Li y Lj son nodos hoja e i<j, entonces todos los valores de la clave de búsqueda en Li son menores que cualquiera de la clave Lj. El conjunto de nodos hoja de un árbol B+ debe formar un índice denso de manera que cada valor de la clave de búsqueda aparezca en algún nodo hoja. Ahora podemos explicar el uso del puntero Pn. Ya que existe un orden lineal de las hojas, basado en los valores de clave de búsqueda que contienen, usamos Pn para encadenar los nodos hoja en orden de clave de búsqueda. Esto permite un procesamiento secuencial del archivo en forma eficiente. Los nodos del árbol B+ que no son hojas, forman un índice escaso de varios niveles. La estructura de los nodos que no son hoja es la misma que la de las hojas, excepto que todos los punteros apuntan a nodos del árbol. Un nodo puede tener hasta n punteros, pero debe tener por lo menos [n/2] punteros. Consideremos un nodo que contiene m punteros. Si 1<i<m, el puntero Pi apunta la estructura que contiene los valores de la clave de búsqueda menores que Ki y mayores o iguales a Ki-1. El puntero Pm apunta a la parte del subárbol que contiene aquellos valores de las claves mayores que o iguales a Km-1, y el puntero P1 apunta a la parte del subárbol que contiene aquellos valores de la clave de búsqueda menores que K1. El requisito de que cada nodo tenga por lo menos [n/2] punteros es obligatorio en todos los niveles del árbol excepto en la raíz.

Apuntes Bases de Datos II

In g. Wilfrido Martínez Cruz


Un árbol B+ para el archivo depósito, con n=3, suprimiendo los punteros al archivo por simplicidad, será de la forma: Siempre es posible construir un árbol B+, para cualquier n, en el que todos los nodos distintos del raíz contienen por lo menos [n/2] punteros. Todos los ejemplos que hemos dado de árboles B+ han sido equilibrados. Es decir, la longitud de cualquier camino desde la raíz hasta un nodo hoja es la misma. Esta propiedad es un requisito de un árbol B+. De hecho, la B significa “balanced” (equilibrado). La propiedad de equilibrio de los árboles B+ es la que asegura un buen rendimiento en las búsquedas, inserciones y eliminaciones. Supóngase que queremos encontrar todos los registros con un valor de clave de búsqueda k. Primero examinamos el nodo raíz y buscamos el valor de clave de búsqueda más pequeño mayor que k. Supóngase que el valor de la clave de búsqueda es Ki. Seguimos el puntero Pi-1 a otro nodo. Si tenemos m punteros en el nodo, y KKm-1, entonces seguimos Pm a otro nodo. Una vez más buscamos el valor de la clave de búsqueda más pequeño mayor que k y seguimos el correspondiente puntero. Finalmente llegamos a un nodo hoja, donde el puntero nos señala en registro o la cubeta deseada. Así, al procesar una consulta se atraviesa un camino en el árbol desde la raíz a una hoja. Si hay K valores de la clave de búsqueda en el archivo, el camino no es más largo que log[n/2](K). En la práctica, significa que sólo se necesita tener acceso a unos pocos nodos aunque el archivo sea muy grande. En la mayoría de los casos, un nodo se hace para que tenga el mismo tamaño de un bloque del disco. La inserción y la eliminación son más complicadas que la búsqueda, ya que puede ser necesario partir un nodo, o combinar dos si se hacen demasiado pequeños. Además, cuando se parte un nodo o se combinan un par de nodos, debemos asegurarnos de que el equilibrio se conserva. Para introducir la idea de inserción y eliminación en un árbol B+, supongamos temporalmente que los nodos nunca llegan a ser demasiado grandes o demasiado pequeños. Bajo esta suposición, la inserción y la eliminación son de la forma: 

Inserción. Utilizando la misma técnica de la búsqueda, encontramos el nodo hoja en el que aparecería l valor de la clave de búsqueda. Si el valor de la clave de búsqueda ya aparece en el nodo hoja, añadimos el nuevo registro al archivo, y si es necesario un puntero a la cubeta. Si el valor de la clave de búsqueda no aparece, insertamos el valor en el nodo hoja y lo posicionamos de forma que las claves de búsqueda estén todavía en orden. Entonces, insertamos el nuevo registro en el archivo, y si es necesario, creamos una nueva cubeta con el puntero apropiado. Eliminación. Utilizando la misma técnica de la búsqueda, encontramos el registro que se va a eliminar y lo quitamos del archivo. El valor de la clave de la búsqueda se quita del nodo hoja si no hay ninguna cubeta asociada con el valor de la clave de búsqueda o si la cubeta queda vacía como resultado de la eliminación.

Ahora consideremos un ejemplo en el que es necesario partir un nodo. Supóngase que queremos insertar un registro con un valor nombre_sucursal=”Clearview” en el árbol B+ de la figura anterior. Empleando el algoritmo de búsqueda vemos que Clearview aparecería en el nodo que contiene Brighton y Downttown., y no hay espacio para insertarlo. Por tanto, el nodo se parte en dos, quedando en uno Brighton y Clearview y en el otro Downtown. En general tomamos los n valores de clave de búsqueda (los n-1 de la hoja más el que se inserta) y ponemos el primero en el nodo existente y los valores restantes en el nuevo nodo. Habiendo partido un nodo hoja, debemos insertar el nodo hoja nuevo en la estructura del árbol B+. En nuestro ejemplo, el nodo nuevo tiene a Downtown como valor más pequeño de la clave de búsqueda. Necesitamos insertar este valor de clave de búsqueda en el padre del nodo hoja que se partió. En nuestro ejemplo se inserta el valor Downtown en el padre, porque hay espacio disponible de clave de búsqueda. Si no hubiera sido así se hubiera tenido que partir el padre. En el peor de los casos, se deben partir todos los nodos a lo largo del camino hacia la raíz. Si la misma raíz se parte, el árbol se hace más profundo. La técnica general de inserción en un árbol B+ es determinar el nodo hoja l en el que se debe hacer la inserción. Si tiene lugar una partición, se inserta el nuevo nodo en el padre del nodo l. Si esta inserción causa una partición, se procede recursivamente hasta que o bien una inserción no cause una partición o se cree una nueva raíz. Ahora consideramos las eliminaciones que causan que tres nodos contengan muy pocos punteros. Primero eliminemos Downtown del árbol B+ resultado de la inserción anterior. Localizamos la entrada de Downtown usando el algoritmo de búsqueda. Cuando eliminamos la entrada de Downtown de su nodo hoja, la hoja queda vacía. Puesto que el ejemplo n=3, y 0<[(n-1)/2], se debe eliminar este nodo del árbol B+. Para eliminar un nodo hoja, debemos eliminar el puntero que le apunta desde el padre. Éste deja el nodo padre, que tenía

Apuntes Bases de Datos II

In g. Wilfrido Martínez Cruz


tres punteros, con sólo dos punteros. Puesto que 2[n/2], el nodo todavía es suficientemente grande y se ha finalizado la operación de eliminación. Cuando se hace una eliminación en un padre de un nodo hoja, es posible que el nodo padre se vuelva demasiado pequeño. Esto es exactamente lo que ocurre si eliminamos Perryridge. Cuando eliminamos el puntero a este nodo hoja en su padre, el padre queda con sólo un apuntador. Puesto que n=3, [n/2=2], y, por tanto, un único puntero es muy poco. Sin embargo, como el nodo contiene información útil, no podemos simplemente eliminarlo. En vez de ello, examinamos el nodo hermano. Este nodo hermano tiene espacio para incluir la información del nodo que ahora es demasiado pequeño, por lo que podemos juntar estos nodos, de forma que el nodo hermano ahora contiene las claves Mianus y Redwood. El otro nodo, el primero, ahora contiene información redundante y puede eliminarse de su padre. Obsérvese que la raíz quedo vacía después de la eliminación, por lo que se redujo en un nivel la profundidad del árbol B+. No siempre es posible fusionar nodos, lo que ocurre cuando el nodo hermano ya contiene el máximo de punteros. La solución en este caso es redistribuir los punteros de forma que cada hermano tenga los mismos punteros. Obsérvese que la redistribución de valores necesita un cambio de un valor de clave de búsqueda en el padre de los dos hermanos. En general, para eliminar un nodo en el árbol B+, realizamos una búsqueda del valor y lo eliminamos. Si el nodo es demasiado pequeño, lo eliminamos de su padre. Aunque las operaciones de inserción y eliminación en árboles B+ son complicadas, requieren relativamente pocas operaciones. Puede demostrarse que el número de operaciones que se necesita en el peor de los casos para una inserción o eliminación es proporcional al logaritmo del número de claves de búsqueda. La velocidad de las operaciones en los árboles B+ es la que hace que se utilicen frecuentemente como estructuras de índices en implementaciones de BD. 2.4.

Archivos indexados de árboles B.

Los índices de árboles B son similares a los índices de árboles B+. La principal diferencia entre los dos enfoques es que un árbol B elimina el almacenamiento redundante de valores de clave de búsqueda. Todos los valores de la clave de búsqueda aparecen en algún nodo hoja. Un árbol B permite que los valores de clave de búsqueda aparezcan una sola vez. Puesto que no se repiten las claves de búsqueda en el árbol B, podemos almacenar el índice utilizando menos nodos que en el árbol B+ correspondiente. Sin embargo, puesto que las claves de búsqueda que aparecen en los nodos no aparecen en ningún otro sitio del árbol B, estamos obligados a incluir un campo de puntero adicional para cada clave de búsqueda en un nodo que no sea hoja. Estos punteros adicionales apuntan a registros de archivos o cubetas para la clave de búsqueda asociada. Los árboles B ofrecen una ventaja adicional con respecto a los árboles B+ aparte de eliminar el almacenamiento redundante de claves de búsqueda. En una búsqueda en un árbol B+, siempre es necesario recorrer un camino desde la raíz del árbol hasta algún nodo hoja. Sin embargo, en un árbol B, a veces es posible encontrar el valor deseado antes de leer un nodo hoja. Así, la búsqueda es ligeramente más rápida en un árbol B, aunque en general el tiempo de búsqueda todavía es proporcional al logaritmo del número de claves de búsqueda. Estas ventajas del árbol B sobre el B+ se compensan con varias desventajas:  

2.5.

Los nodos hojas y los que no son hoja tienen el mismo tamaño en un árbol B+. En un árbol B, los nodos que no son hoja son más grandes, lo que complica la gestión del almacenamiento del índice. La eliminación de un árbol B es más complicada. En un árbol B+, la entrada eliminada siempre aparece en una hoja. En un árbol B, la entrada puede aparecer en un nodo que no sea hoja. Se debe seleccionar del subárbol del nodo que contiene la entrada eliminada el valor apropiado para sustituirlo. De manera específica, si se elimina la clave de búsqueda Ki, la clave de búsqueda más pequeña que aparece en el subárbol del puntero Pi+1 debe pasarse al campo que antes ocupaba Ki. Las ventajas de los árboles B son de poca importancia para índices grandes. Así, muchos implementadores de DBMS prefieren la simplicidad estructural de los árboles B+. Funciones de asociación (hash) estática.

Una desventaja de los esquemas de índices es que debemos acceder a una estructura de índices para localizar datos. La técnica de asociación (hashing) nos permite evitar el acceso a una estructura de índices. Suponemos que el índice denso está dividido entre un número de diferentes cubetas. La dirección de la cubeta que contiene un puntero al dato deseado se obtiene directamente calculando una función sobre el valor de la clave de búsqueda del registro deseado.

Apuntes Bases de Datos II

In g. Wilfrido Martínez Cruz


Formalmente, K representa el conjunto de todos los valores de la clave de búsqueda, y B el conjunto de todas las direcciones de la cubeta. Una función de asociación (hash) h es una función de K a B. El principio básico de la asociatividad es que, aunque el conjunto K de las claves sea grande, el conjunto {K1, K2, …, Kn} de valores de clave de búsqueda realmente almacenados en la BD es mucho más pequeño que K. En el momento de hacer el diseño no conocemos los valores de la clave de búsqueda que se almacenarán, pero sabemos que hay demasiados valores posibles para justificar la asignación de una cubeta a cada uno de ellos. Sin embargo, sabemos en el momento de hacer el diseño, aproximadamente cuantos valores de clave de búsqueda van a ser almacenados en la BD. Elegimos el número de cubetas para hacer la correspondencia con el número de valores de la clave de búsqueda que esperamos tener almacenados. La función de asociación es la que define la asignación de valores de la clave de búsqueda a cubetas específicas. Las funciones de asociación deben diseñarse con cuidado. Una función de asociación deficiente puede resultar en una búsqueda que tarde un tiempo proporcional al número de claves de búsqueda en el archivo. Una función bien diseñada da un tiempo de búsqueda promedio que es una constante (pequeña), independiente del número de claves de búsqueda en el archivo. Esto se logra asegurándose de que, en promedio, los registros se distribuyen uniformemente entre las cubetas. Sea h una función. Para realizar una búsqueda del valor de clave Ki, simplemente calculamos h(Ki) y buscamos la cubeta con esa dirección. Supóngase que dos claves de búsqueda, K5 y K7, tienen el mismo valor de asociación, es decir, h(K5)=h(K7). Si buscamos K5, la cubeta h(K5) contiene registros con valores de la clave de búsqueda K5 y de K7. Así, tenemos que comprobar el valor de la clave de búsqueda de todos los registros de la cubeta para verificar que el registro es uno de los que queremos. La peor función de asociación posible asigna todos los valores de clave de búsqueda a la misma cubeta. Esto es indeseable, ya que el índice denso completo se guarda en la misma cubeta, y , por tanto, la búsqueda requiere la revisión del índice completo. Una función de asociación ideal asigna cada valor de la clave de búsqueda a una cubeta distinta. Una función así es ideal porque cada registro de la cubeta que se examina, como resultado de una búsqueda tiene el valor de clave de búsqueda deseado. Puesto que en el momento de hacer el diseño no sabemos con precisión los valores de clave de búsqueda, queremos elegir una función de asociación que asigne valores de la clave de búsqueda a las cubetas, de manera que:  

La distribución sea uniforma. Es decir, se asigne a cada cubeta el mismo número de valores de la clave de búsqueda del conjunto de todos los valores posibles de la clave de búsqueda. La distribución sea al azar. Es decir, en el caso promedio, cada cubeta tendrá casi el mismo número de valores asignados.

Intentemos elegir una función de asociación para el archivo depósito utilizando la clave de búsqueda nombre_sucursal. Supóngase que decidimos tener 26 cubetas y definimos una función de asociación que asigna nombres que empiezan con la letra i del alfabeto a la cubeta i-ésima. Esta función de asociación es muy sencilla, pero falla en la distribución uniforme, ya que esperamos más nombres de sucursales que empiecen con B y R que con Q y X. Las funciones de asociación típicas realizan algún cálculo sobre la representación binaria interna a la máquina de los caracteres de la clave de búsqueda. Una función de asociación sencilla de este tipo es calcular la suma, el módulo del número de cubetas de la representación binaria de los caracteres de una clave que se han asignado. La inserción es casi tan simple como la búsqueda. Si el valor de la clave de búsqueda del registro que se va a insertar es Ki, calculamos h(Ki) para localizar la cubeta para ese registro. La eliminación es tan directa como la inserción. Si el valor de la clave de búsqueda del registro que se va a eliminar es Ki, calculamos h(Ki) y buscamos la cubeta correspondiente para ese registro. La forma de estructura de asociatividad que hemos descrito a veces se conoce como asociatividad abierta. Bajo un enfoque alternativo, llamado asociatividad cerrada, se almacenan todos los registros en una cubeta y la función de asociación calcula las direcciones dentro de la cubeta. La asociatividad cerrada se utiliza frecuentemente en la construcción de tablas de símbolos para compiladores y ensambladores, pero se prefiere la asociatividad abierta para DBMS. La razón es que la eliminación resulta problemática cuando se emplea asociatividad cerrada. Una desventaja importante de la forma de asociatividad que acabamos de

Apuntes Bases de Datos II

In g. Wilfrido Martínez Cruz


describir es que la función de asociación debe elegirse cuando implementamos el sistema y no se puede cambiar fácilmente después. Puesto que la función h asigna valores de la clave de búsqueda a un conjunto fijo B de direcciones de cubetas, desperdiciamos si B es excesivamente grande. Si B es demasiado pequeño, las cubetas contendrán registros de muchos valores diferentes de la clave de búsqueda y el rendimiento disminuirá. Normalmente, eligiendo el tamaño de B el doble del número de valores de la clave de búsqueda en el archivo puede lograrse un buen equilibrio entre espacio y rendimiento. 2.6.

Funciones de asociación (hash) dinámica.

Como hemos visto, la necesidad de fijar el conjunto B de direcciones de cubetas es un problema serio de la técnica de asociación estática. La mayoría de las BD crecen conforme pasa el tiempo. Si vamos a usar asociación estática es una BD de este tipo, se presentan tres clases de opciones:   

Elegir una función de asociación basada en el tamaño actual del archivo. Dará como resultado una degradación del rendimiento conforme crezca la BD. Elegir una función de asociación basada en el tamaño anticipado del archivo en el mismo punto en el futuro. Aunque se evita la degradación, al principio se desperdicia mucho espacio. Reorganizar periódicamente la estructura de asociación como respuesta al crecimiento del archivo. Una reorganización así implica la elección de una nueva función de asociación, volver a calcular la función de asociación para cada uno de los registros y generar nuevas asignaciones de las cubetas, lo que conlleva un elevado consumo de tiempo. Además es necesario prohibir el acceso al archivo durante la reorganización.

Existen varias técnicas de asociación que permiten modificar de manera dinámica la función de asociación para compensar el crecimiento o reducción de la BD. Estas técnicas se llaman funciones de asociación (hash) dinámica. A continuación describimos una forma de asociación dinámica llamada asociación extensible. La asociación extensible maneja los cambios en el tamaño de la BD dividiendo y fusionando cubetas conforme la BD crece y se reduce. Como resultado se mantiene la eficiencia de espacio. Además, puesto que la reorganización se realiza cada vez únicamente en una cubeta, el tiempo extra requerido es aceptablemente bajo. Con la asociación extensible elegimos una función de asociación h con las propiedades deseables de uniformidad y aleatoriedad. Sin embargo, esta función de asociación genera valores en un rango relativamente grande de enteros binarios de b bytes. No creamos una cubeta para cada valor de asociación, creamos cubetas según la demanda, según se insertan registros. Inicialmente no usamos los b bytes de la asociación. En un momento dado utilizamos i bytes, donde 0i b. Estos i bytes representan una posición relativa en una tabla adicional de direcciones de cubetas. El valor de i aumenta y disminuye con el tamaño de la BD. Aunque se requieren i bytes varias entradas consecutivas de la tabla pueden apuntar a la misma cubeta. Todas esas entradas tendrán un prefijo común de asociación, pero la longitud de este prefijo puede ser menor que i. Por tanto, con cada cubeta asociamos un entero dando la longitud del prefijo común de asociación. Para localizar la cubeta que tiene el valor de la clave de búsqueda Kl tomamos los primeros i bytes de h(Kl), vemos la entrada de la tabla que corresponde a esa cadena da bytes, y seguimos el puntero de la cubeta en la entrada de la tabla. Para insertar un registro con valor de clave de búsqueda Kl, seguimos el mismo procedimiento que antes para la búsqueda, terminando en alguna cubeta, llamémosla j. Si hay sitio en la cubeta insertamos la información apropiada y después insertamos el registro en el archivo. Si la cubeta está llena, debemos partirla y redistribuir los registros actuales más el nuevo. Para partir la cubeta, primero debemos determinar si necesitamos aumentar el número de bytes que usamos en la asociación. 

Si i=ij. Entonces solamente una entrada en la tabla de direcciones de cubetas apunta a la cubeta j. Por tanto, necesitamos aumentar el tamaño de la tabla de direcciones de cubetas de forma que podamos incluir punteros en las dos cubetas que resultan de la partición de j. Hacemos esto considerando un bit adicional de la asociación. Incrementamos el valor de i en 1, duplicando así el tamaño de la tabla. Cada entrada se sustituye por dos, cada una de las cuales contiene el mismo

Apuntes Bases de Datos II

In g. Wilfrido Martínez Cruz


puntero que la original. Ahora dos entradas en la tabla de direcciones de cubetas apuntan a la cubeta j. Asignamos una nueva cubeta (z, por ejemplo) y hacemos que la segunda entrada apunte a la nueva cubeta. Ponemos ij e iz a i. A continuación, se reasocia cada registro de la cubeta j y dependiendo de los i (ahora vale iinicial+1) o se guarda en la cubeta j o bien se asigna a la cubeta z recién creada. Ahora reintentamos la inserción del nuevo registro, y normalmente tendrá éxito, sin embargo, si todos los registros de la cubeta j y el nuevo registro tienen el mismo prefijo de valor de asociación, será necesario volver a partir una cubeta. Si la función de asociación se elige con cuidado, no es probable que una única inserción requiera que una cubeta se parta más de una vez. Si i>ij. Entonces más de una entrada en la tabla apuntan a la cubeta j. Así, podemos partir la cubeta j sin aumentar el tamaño de la tabla de direcciones de cubetas. Obsérvese que todas las entradas que apuntan a la cubeta j corresponden a prefijos de asociación que tienen el mismo valor en los bytes ij más ala izquierda.

Asignamos una nueva cubeta (digamos z) y ponemos ij e iz al valor que resulta de añadir 1 al valor ij original. A continuación necesitamos ajustar las entradas en la tabla de direcciones de cubetas que anteriormente apuntaban a la cubeta j. Dejamos la primera mitad de las entradas como estaban y ponemos todas las demás en la recién creada z. A continuación, como en el caso que vimos antes, se reasocia cada registro de la cubeta j y se reasignan a j o z según corresponda. Se reintenta la inserción En el caso menos probable de que vuelva a fallar, aplicamos uno de estos dos casos de nuevo. Obsérvese que en ambos caos necesitamos volver a calcular la función de asociación únicamente en los registros de la cubeta j. Para eliminar un registro con valor de clave de búsqueda Kl, seguimos el mismo procedimiento que antes para la búsqueda, terminando en alguna cubeta, digamos j. Sacamos la clave de búsqueda de la cubeta y el registro del archivo. La cubeta también se elimina si queda vacía, en este momento se pueden unir varias cubetas y el tamaño de la tabla de direcciones de cubetas se puede partir en dos. Examinemos ahora las ventajas y desventajas de la asociación extensible comparada con las otras planificaciones que se han estudiado. La ventaja principal de la asociación extensible es que el rendimiento no disminuye conforme crece el archivo. Además, se requiere un mínimo espacio adicional. Aunque la tabla de direcciones de cubetas ocupa un espacio extra adicional, contiene un puntero para cada valor de asociación con la longitud del prefijo actual, así pues la tabla es pequeña, y no es necesario reservar cubetas para un crecimiento futuro; las cubetas pueden asignarse de manera dinámica. Una desventaja de la asociación extensible es que la búsqueda implica un nivel de indirección especial, ya que debemos tener acceso a la tabla de direcciones de cubetas antes de acceder a la cubeta en sí. Esta referencia extra sólo tiene un impacto de poca importancia en el rendimiento, impacto que aumenta al llenarse la BD. Así pues, la asociación extensible parece ser una técnica muy atractiva, siempre que se quiera aceptar la complejidad añadida que implica su implementación. 2.7.

Comparación de indexación y asociación (hash).

La mayor parte de los DBMS utilizan sólo unas pocas o una forma de indexación o de asociación. Para tomar la decisión apropiada, el implementador o el diseñador de la BD deben considerar los siguientes factores:    

¿ Es aceptable el coste de la reorganización periódica del índice o estructura de asociación? ¿ Cuál es la frecuencia relativa de inserciones y eliminaciones? ¿ Es deseable optimizar el tiempo de acceso promedio a expensas de aumentar el tiempo de acceso en el peor de los casos? ¿ Qué tipos de consultas harán normalmente los usuarios?

Ya hemos examinado los tres primeros factores en la revisión de los mériots relativos de las técnicas especificadas de asociación. El cuarto factor, el tipo esperado de consulta, es crítico para la elección de indexación o asociación. Si la mayor parte de las consultas son de la forma: select A1, A2, …, An from r where Ai=c Entonces, al procesar esta consulta, el sistema realizará una búsqueda en el índice o estructura de asociación correspondiente al atributo Ai para el valor c. Para consultas de esta forma es preferible un esquema de asociación. La búsqueda en un índice requiere un tiempo proporcional al logaritmo del número de valores de Ai en r, sin embargo, en una estructura de asociación, el tiempo promedio de búsqueda es una constante independiente del tamaño de la BD. La única ventaja del índice para esta forma de consulta es que el tiempo

Apuntes Bases de Datos II

In g. Wilfrido Martínez Cruz


de búsqueda en el peor de los casos es proporcional al logaritmo del número de valores de Ai en r, en cambio, con la asociación, el tiempo de búsqueda en el peor de los casos es proporcional al número de valores de Ai en r. Las técnicas de indexación son preferibles a la asociación en los casos en los que se especifica un rango de valores en la consulta. Una consulta de este tipo tiene la forma siguiente: select A1, A2, …, An from r where Aic2 and Aic1 En otras palabras, esta consulta encuentra todos los registros con valores de Ai entre c1 y c2. Utilizando un índice, primero buscamos el valor c1, y seguimos la cadena de punteros del índice para leer la siguiente cubeta en orden alfabético y continuar de esta manera hasta alcanzar c2. Si tenemos una estructura de asociación, podemos buscar c1 y localizar la cubeta correspondiente, pero no es fácil determinar cuál es la siguiente cubeta. La dificultad surge del hecho de que una función de asociación buena asigna valores a las cubetas aleatoriamente. Si queremos atender consultas de rangos utilizando una estructura de asociación, debemos elegir una función de asociación que conserve el orden, es decir, si K1 y K2 son valores de la clave de búsqueda y K1<K2, entonces h(K1)<h(K2). Una función de este tipo asegura que las cubetas estén en orden de clave. Es difícil encontrar una función de asociación que conserve el orden y cumpla los requisitos de aleatoriedad y uniformidad. Debido a la dificultad para encontrar una buena función de asociación que conserve el orden, la mayor parte de los sistemas utilizan indexación.

2.8.

Definición de índice en SQL.

El estándar permite al compilador de SQL la libertad de elegir cómo implementar el cumplimiento de las claves. Las implementaciones típicas hacen cumplir una declaración de clave creando un índice con la clave declarada como la clave de búsqueda del índice. Algunas implementaciones de SQL incluyen órdenes específicas de definición de datos para crear y truncar índices, entre las que están el Sequel original y el SAA-SQL de IBM. A continuación presentamos las órdenes de índice del SAA-SQL. Un índice se crea mediante la orden create index, que tiene la forma: create index <nom_índice> on <nom_relación> (<lista_de_atributos>) La lista_de_atributos es la lista de atributos de las relaciones que forman la clave de búsqueda para el índice. Para definir un índice en la relación sucursal con clave de búsqueda nombre_sucursal, escribimos: create index índice_b on sucursal (nombre_sucursal) Si deseamos declarar que la clave de búsqueda es una clave candidata, añadimos el atributo unique a la definición de índice, entre create e index. Si en el momento en que se introduce create unique index, el atributo no es una clave candidata, se presentará un mensaje de error y fallará el intento de crear el índice. Si el intento de crear el índice tiene éxito, cualquier intento subsiguiente de insertar una tupla que viole la declaración de clave fallará. El nombre de índice especificado para un índice se requiere de forma que sea posible truncar índices. La orden drop index tiene la forma: drop index <nom_índice> 2.9.

Acceso por claves múltiples.

Para ciertos tipos de consultas resulta útil usar índices múltiples, si existen. Supóngase que el archivo depósito tiene dos índices, uno por nombre_sucursal y otro por nombre_cliente.

Apuntes Bases de Datos II

In g. Wilfrido Martínez Cruz


Considérese encontrar todas las cuentas de un cliente en una sucursal. Existen tres posibles estrategias para procesar esta consulta:   

Utilizar el índice de nombre_sucursal para encontrar todas las cuentas de la sucursal y examinar todos esos registros para ver cuales son los de el cliente. Utilizar el índice de nombre_cliente para encontrar todas las cuentas del cliente y examinar todos esos registros para ver cuales son los de la sucursal. Tomar la intersección de estos dos conjuntos de punteros. Aquellos punteros que estén en la intersección apuntan a registros pertenecientes tanto al cliente como a la sucursal.

La tercera estrategia es la única de las tres que aprovecha la existencia de índices múltiples. Sin embargo. Incluso esta estrategia puede ser pobre si se cumplen las siguientes condiciones: existen muchos registros para una sucursal, existen muchos registros para el cliente, hay pocos registros que pertenezcan al cliente y a la sucursal. Si se cumplen estas condiciones, debemos examinar un gran número de punteros para producir un resultado pequeño. Para acelerar el procesamiento de consultas con múltiples claves de búsqueda pueden mantenerse varias estructuras especiales. Consideraremos dos de estas estructuras: la estructura de rejilla y las funciones de asociación divididas. Estructura de rejilla. Una estructura de rejilla para estructuras que usan dos claves de búsqueda es un array bidimensional indexado por los valores de las claves de búsqueda. Para realizar una búsqueda, buscamos una de las claves en las columnas y la otra en las filas. Esa entrada contiene punteros a todos los registros en los que coinciden las claves de búsqueda pedidas. No es necesario realizar cálculos especiales, y solamente se accede a los registros necesarios para responder a la consulta. La estructura de rejilla también es apropiada para consultas que implican una sola clave de búsqueda., los punteros que aparecen en toda la fila o columna de la clave buscada son la respuesta a la consulta. Conceptualmente es sencillo extender el enfoque de estructura de rejilla a cualquier número de claves de búsqueda. Las estructuras de rejilla proporcionan una mejora importante en el tiempo de procesamiento para las consultas de claves múltiples. Sin embargo, requieren cierta cantidad de espacio y tiempo extra en las inserciones y eliminaciones de registros. Función de asociación dividida. Otra manera de enfocar las consultas de claves múltiples es usando una función de asociación dividida. Supóngase que deseamos construir una estructura adecuada para consultas en el archivo depósito que implican tanto a nombre_cliente como a nombre_sucursal. Construimos una estructura de asociación para la clave (nombre_cliente, nombre_sucursal). La única diferencia entre la estructura que vamos a crear y las que vimos anteriormente es que imponemos una restricción adicional sobre la función de asociación h. Los valores de asociación se dividen en dos partes. La primera parte depende sólo del valor de nombre_cliente y la segunda depende sólo de nombre_sucursal. La función de asociación se denomina dividida porque los valores de asociación se dividen en segmentos que dependen de cada elemento de la clave. Así, si utilizamos valores de búsqueda de seis bits, los tres primeros dependen del valor de nombre_cliente y los tres últimos de nombre_sucursal. Como era el caso del archivo de rejilla, la asociación dividida se extiende a un número arbitrario de atributos. Podemos hacer varias mejoras en la asociación dividida si sabemos con que frecuencia especificará el usuario cada uno de los atributos en una consulta. Existen otras varias técnicas híbridas para procesar consultas de claves múltiples. Tales técnicas pueden ser útiles en aplicaciones en las que la persona que implementa el sistema sabe que la mayoría de las consultas serán de una forma restringida.

Apuntes Bases de Datos II

In g. Wilfrido Martínez Cruz


Turn static files into dynamic content formats.

Create a flipbook
Issuu converts static files into: digital portfolios, online yearbooks, online catalogs, digital photo albums and more. Sign up and create your flipbook.