UTN Córdoba Ingenieria en Sistemas de Información Cátedra: Diseño de Lenguajes de Consulta Ficha de clase número: 09 Fecha: Docente: Semana del 04/05 al 08/05 de 2009 Ing. Valerio Frittelli Tema Principal: Temas Particulares: 1.) Indexación de Archivos: Arboles B. Concepto de indexación. Indices basados en Arboles B. Reglas de formación de un árbol B. Indexación de archivos: conceptos básicos. Al almacenar registros en un archivo, tarde o temprano se presenta la necesidad de realizar una búsqueda de un registro particular en el archivo, y se pretende que esa búsqueda sea rápida. Hemos visto que si el archivo está ordenado podemos hacer una búsqueda binaria (y obtener un tiempo de búsqueda O(log(n), siendo n la cantidad de registros del archivo). Mejor aún: si organizamos el archivo como una tabla hash obtendremos tiempos de búsqueda aún mejores (en tiempo casi constante...) Entonces, no parece que haya nada que agregar al tema... Sin embargo, notemos que en las soluciones propuestas se debe reorganizar el contenido del archivo para favorecer una búsqueda rápida. En el caso de la búsqueda binaria, la reorganización consiste simplemente en ordenarlo. Y en el caso del hash, hay que replantear por completo la estructura interna del archivo, al punto que un recorrido secuencial del mismo pasa a carecer de sentido. El punto, entonces, no es menor: no siempre se querrá cambiar la estructura del archivo, o no siempre será de ayuda hacerlo, o directamente podría no ser posible hacerlo. ¿Qué pasaría si la estrategia para buscar rápido fuera mantener ordenado el archivo por un atributo, pero luego se necesitara buscar por otros atributos? ¿Qué pasaría si el archivo estuviera organizado como tabla hash y se requirieran recorridos secuenciales (ordenados o no...)? En definitiva: ¿qué hacer si el archivo se organiza de acuerdo a ciertas necesidades de consulta, pero luego se requieren vistas distintas del contenido del archivo? Piense que esto es lo que típicamente ocurre en una tabla de una base de datos: algunas aplicaciones necesitan buscar por legajo, otras por apellido y nombre, otras por dirección postal, etc. Sencillamente, no sería práctico que el archivo cambie su ordenamiento interno o su estructura cada vez que se dispara una consulta de naturaleza diferente, y mucho menos se puede aceptar que el archivo se "replique" varias veces (una por cada tipo de consulta posible...) La solución clásica para estos problemas de "vistas múltiples" para el mismo contenido del archivo, son los archivos índice, o simplemente, índices. En un sentido amplio, un índice es una estructura de datos (normalmente almacenada en memoria secundaria) que permite un rápido acceso a los elementos contenidos en otra estructura (que típicamente es otro archivo). Puede establecerse una analogía entre el índice de un libro y el libro mismo: si queremos saber en qué página está un tema dado, se busca en el índice (en lugar de hacer un recorrido secuencial página a página en el libro) y el índice nos dice en qué página buscar. Típicamente un índice se organiza como un árbol de búsqueda balanceado, de forma que garantice búsquedas de orden logarítmico. La idea es que si m es un archivo en el que se quiere hacer búsquedas mediante índices (o simplemente, busquedas indexadas) y t es un índice para m, entonces t contendrá un nodo por cada registro de m. Si cada registro contiene un atributo k mediante el cual se quiere hacer la búsqueda, entonces cada nodo de t, contendrá 1 UTN Córdoba Ingenieria en Sistemas de Información Cátedra: Diseño de Lenguajes de Consulta el valor del atributo k y mediante ese valor se ordenará el árbol. Además, cada nodo en t tendrá la dirección en m del registro con el valor respectivo del atributo k (el número relativo del registro que contiene a ese valor en k): 25 índice1: ordenado por legajo archivo 20 30 legajo 30 25 20 Ana Juan Pedro nombre 2000 1200 1500 sueldo índice2: Juan ordenado por nombre Ana Pedro Como se ve, se puede tener varios índices a la vez sobre el mismo archivo, organizando a cada índice por un atributo de búsqueda diferente, sin tocar la estructura del archivo original, y sin que esos índices dependan el uno del otro... Si bien en la gráfica mostramos a cada índice como un árbol binario, está claro que en la práctica un índice para un archivo debe cumplir ciertos requisitos esenciales, pues de otro modo el proceso de indexación fallaría o no tendría un tiempo de búsqueda aceptable. Por lo pronto, el árbol de búsqueda no debería ser binario: si el archivo a indexar tiene una gran cantidad de registros (lo cual en la práctica es lo común...) entonces el árbol tendría demasiados niveles y por lo tanto su altura sería también grande. Aún cuando se supone que el árbol implementa algún mecanismo de equilibrado, una gran altura va en contra de un tiempo aceptable de búsqueda. Y por otra parte, parece obvio que el árbol no debería organizarse en memoria principal: si el archivo es muy grande el índice podría no entrar en memoria, y aún si entra podría no quedar lugar para otros índices que se requieran para ese achivo. Hasta 1972 no se había ideado una estructura de índice que realmente fuera eficiente en el acceso rápido a archivos. Las diversas técnicas de equilibrado de árboles clásicas (como los árboles AVL), eran claramente insuficientes para indexar grandes archivos. Pero entonces algo cambió... 2 UTN Córdoba Ingenieria en Sistemas de Información Cátedra: Diseño de Lenguajes de Consulta 2.) Arboles B. En 1972, dos desarrolladores llamados R. Bayer y E. McCreight, trabajando para la Boeing Corporation, publicaron un artículo (con el título de "Organization and Maintenance of Large Ordered Files") en el número 1 de la revista especializada Acta Informática que pasaría a la historia como el artículo de presentación de los Arboles B. Dos o tres décadas más tarde, los árboles B eran ya el estándar indiscutido en cuanto a soporte de índices para acceso rápido a grandes archivos de registros. El contenido de la publicación original de estos dos autores, se anexa en un documento pdf adjunto a esta ficha. Un árbol B es esencialmente un árbol de búsqueda multicamino (n-ario) balanceado, almacenado en disco, y usado como soporte muy eficiente para índices de grandes archivos. Garantiza un tiempo de búsqueda de orden logarítmico, al mismo tiempo que logra un muy buen balance en cuanto al uso del espacio de almacenamiento del índice. Hoy en día los árboles B están en el corazón de todos los sistemas de indexación de bases de datos comerciales, aunque en forma de variantes respecto del planteo original de Bayer y McCreight. Anecdóticamente, digamos que sus creadores nunca dijeron porqué llamaron B al árbol B... Se ha especulado con que esa B viene de balanceado. Otras especulaciones sugieren que la B es una especie de propaganda subliminal a la firma Boeing para la que ambos trabajaban. Y muchos directamente han asumido que la B es por Bayer, llegando incluso a designar como árboles de Bayer a los árboles B... Pero si bien esto último parece un justo tributo a R. Bayer, la pregunta que nos hacemos es: ¿qué hubo de McCreight en ese caso? Como se dijo, un árbol B es un árbol de búsqueda de caminos múltiples. Esto implica que cada nodo del árbol puede tener varios hijos, y a su vez implica que cada nodo puede tener varias claves o valores (de hecho, la forma típica de implementar el conjunto de claves de un nodo es a través de un arreglo ordenado). En el léxico propio de los árboles B, un nodo se designa como página. La idea central es que un árbol B se graba en un archivo (el archivo índice) página por página. Cuando el índice se abre, se carga en memoria la página raiz del árbol, y sólo la página raiz. La clave deseada se busca en esa página, y si está en ella se detiene el proceso (que en este caso sólo llevó una operación de acceso a disco). Si la clave no está, se determina en cuál de las hijas de la raiz debería estar, y se carga esa página. Otra vez, se busca la clave, y así se prosigue, de forma que la raiz siempre esté en memoria y cada nueva página que se carga reemplace a la última cargada. Si la cantidad de claves que se almacena en cada página es adecuada, veremos que se puede garantizar que un árbol B encontrará la clave (o verá que no existe) en no más de dos accesos directos (seeking) a disco incluso si el archivo tiene un número tan desopilante como un millón de registros... Las características estructurales de un árbol B se resumen en cuatro reglas, que enunciamos: i.) ii.) Sea n un parámetro que designaremos como el orden del árbol. Entonces cada página del árbol (salvo eventualmente la página raiz) debe tener un mínimo de n claves en todo momento. El número máximo de claves que una página puede tener es 2*n. iii.) Hay sólo dos tipos de páginas: las hojas (que no tienen hijos) y las páginas que tienen exactamente m + 1 páginas hijas, siendo m el número de claves que efectivamente hay en esa página. iv.) Todas las páginas hoja deben aparecer juntas en el mismo nivel (que no puede ser otro que el último nivel... como es obvio...) 3 UTN Córdoba Ingenieria en Sistemas de Información Cátedra: Diseño de Lenguajes de Consulta Estas cuatro reglas indican en forma taxativa lo que se puede y no se puede hacer al ir formando un árbol B. Veremos a continuación, que cada una de ellas cumple un papel importante en cuanto al proceso de auto-balanceo del árbol: al igual que un árbol AVL, los árboles B insertan una clave y luego verifican si esa inserción provocó algún tipo de desbalance, procediendo a restaurar el equilibrio si fuera el caso. Supongamos que se quiere mostrar la forma en que va creciendo un árbol B de orden 2 (n=2) a medida que se insertan claves. Supongamos que el árbol arranca vacío, y que se quieren insertar las siguientes primeras cuatro claves: 10, 20, 40 y 30. Como el árbol está vacío, no contiene ninguna página aún. Debemos crear la página raiz entonces. La regla ii) nos dice que debemos prever lugar máximo para 2*n claves = 4 claves. Lo común es dibujar cada página como si fuera un arreglo de tamaño 2*n. Las claves que se insertan (al igual que en un árbol de búsqueda común) se insertan como parte de una hoja, y dentro de la hoja huésped se almacenan ordenadas de menor a mayor. La raiz quedaría así luego de crearla e insertar las primeras cuatro claves citadas (note que la regla i exige que una página siempre tenga al menos n = 2 claves, pero la raiz está exenta de esa regla: de otro modo, nunca podríamos tener un árbol B vacío o con menos de n claves cuando recién se crea...) 10 20 30 40 Supongamos que ahora queremos insertar la clave x = 25. La única página que tiene el árbol ya no tiene lugar para una nueva clave, por lo tanto debemos crear al menos una página nueva. Sin embargo, no podemos crear páginas de cualquier forma... Las reglas estructurales del árbol sugieren que hay sólo una forma de hacerlo: suponga que x = 25 realmente pudo insertarse. En ese caso, la secuencia ordenada quedaría así: promover al nivel superior 10 20 25 30 40 Puede verse que ahora el número de claves es impar (2*n + 1 claves) y por lo tanto el valor del medio de la secuencia es la mediana de la misma. Lo que se hace es tomar el valor mediano, y promoverlo a la página que estuviera en el nivel superior de esta que fue rebalsada. Si no hubiera ninguna página en el nivel superior (pues la página rabalsada es la raiz) entonces se crea una nueva raiz que sólo contendrá al valor promovido (el 25 en nuestro caso). Pero en la página original quedan exactamente 2*n claves, tales que la mitad son menores que el promovido y la otra mitad son mayores... se arman dos páginas con esos valores, que quedarán como hijas de la raiz: 25 10 20 menores que 25 30 40 mayores que 25 4 UTN Córdoba Ingenieria en Sistemas de Información Cátedra: Diseño de Lenguajes de Consulta Como se ve, en el árbol resultante se cumplen todas las reglas: las dos páginas hoja están juntas en el último nivel. Ninguna página (salvo la raiz) tiene menos de n = 2 claves. Y la única página que tiene hijos, tiene exactamente 2 hijos, los cual es igual al número m de claves que tiene esa página (la raiz, con m = 1 clave efectivamente contenida) más uno. Note que el árbol ha crecido un nivel: de altura h = 1 mientras sólo tenía cuatro claves, pasó a tener altura h = 2 con la quinta clave. Veremos que en el único caso en que el árbol gana altura es cuando se parte la raiz (lo que ocurrió en este caso...) Supongamos que ahora se quieren insertar las claves 5, 15 y 23. Las claves entran por la raiz, y bajan por el árbol buscando una hoja con lugar libre para insertarse. Las claves 5 y 15 no tendrán problemas, pero la clave 23 provocará un desborde de la página izquierda del 25: 25 5 10 20 15 30 23 40 mayores que 25 menores que 25 El 15 es el mediano en la hoja desbordada, y por lo tanto debe promoverse al nivel superior. En este caso, en ese nivel hay una página (la raiz) que tiene lugar libre. Por lo tanto, el 15 se alojará en esa página. Como ahora la raiz tendrá m = 2 claves efectivas, no podrá seguir teniendo sólo un hijo: deberá tener m + 1 = 3 hijos. Pero esto no es problema: la pagina desbordada se partió en dos, quedando la mitad de las claves menores que 15 y la otra mitad mayores. Esas claves arman dos páginas que quedarán como hijos del 15. La página con el 30 y el 40 no sufre cambios: 15 5 10 menores que 15 25 20 23 mayores que 15 y menores que 25 30 40 menores que 15 Observe que se partió una página pero el árbol no ganó altura: el impacto del quiebre de la página fue absorbido "a lo ancho" en el árbol (lo cual no produce merma en el rendimiento de una búsqueda: mientras no crezca la altura, el crecimiento horizontal no es problema). Podemos decir entonces que los árboles B son muy estables en cuanto a su crecimiento en altura: debe quebrarse la raiz para que la altura crezca, pero ese acontecimiento depende que primero se quiebre una larga serie de páginas inferiores antes de provocar el rebalsamiento en 5 UTN Córdoba Ingenieria en Sistemas de Información Cátedra: Diseño de Lenguajes de Consulta la raiz... De hecho, en este momento el árbol necesitaría el quiebre de tres páginas para que la raiz se quiebre a su vez... Para verlo, adelantémonos en el proceso. Supongamos que ya se han insertado las claves 45, 50 y 60 (provocando un quiebre y enviando el 45 a la raiz) y luego las claves 65, 70 y 80 (provocando otro quiebre y enviando el 65 a la raiz). El árbol quedaría así: 15 5 10 20 25 23 45 65 30 40 50 60 70 80 Si ahora se insertan las claves 85, 90 y 95, el valor 95 provocará el desborde de la última página de la derecha del árbol. En ese momento el 85 quedará como mediano y será promovido al nivel superior. Pero en ese nivel, la raiz no tiene lugar y se desborda a su vez, provocando que el 45 sea promovido hacia arriba: antes de las particiones, el árbol quedaría así: 15 5 10 20 25 23 45 65 30 40 85 50 60 70 80 85 90 La solución: se crea una nueva raiz con el 45. El árbol crece un nivel, y las páginas se reparten así: 45 15 5 10 20 25 23 65 30 40 50 60 85 70 80 90 95 En nuestro modelo hemos supuesto que el orden n del árbol es 2, y eso sirve a los efectos didácticos. Pero en la práctica, el orden del árbol se elige de forma tal que cada página pueda tener un número alto de claves: De hecho, se suele hacer que el orden elegido garantice que el tamaño final en bytes de una página sea un múltiplo de 512, de forma que el tamaño de 6 95 UTN Córdoba Ingenieria en Sistemas de Información Cátedra: Diseño de Lenguajes de Consulta una página coincida o sea múltiplo del factor de paginación usado por el sistema operativo huésped: de esta forma, se aprovecha al máximo la capacidad de lectura y grabación del sistema. Note que si el orden del árbol es n = 500, entonces una página puede tener hasta 1000 claves. Si la raiz tiene 1000 claves, deberá tener 1001 páginas hijas que pueden a su vez tener hasta 1000 claves cada una... Eso implica que el árbol podrá contener un millón de claves... en sólo dos niveles de árbol. En la práctica, la página raiz está siempre en memoria mientras el índice está abierto (sólo se accede a disco para cargarla la primera vez, cuando el índice se abre). Por lo tanto, encontrar una clave requerirá como máximo dos operaciones de acceso directo a disco (que en la práctica es sólo una... pues dijimos que la raiz sólo se levanta de disco una vez, al principio...) Esa notable eficiencia se ve incrementada con otras características destacables: la regla i estructural del árbol garantiza que al ir a disco a buscar una página, esa página tendrá un número razonable de claves (50% llena) que haga que valga la pena la pérdida de tiempo de ir al disco a buscarla. Y la regla ii nos protege de la posibilidad de levantar una página tan llena de claves que no quepa en memoria. La regla iii garantiza que no iremos a disco nunca a buscar una página vacía. Y la iv es la que finalmente fuerza a que el árbol se mantenga siempre en equilibrio, y se expanda hacia lo ancho en lugar de hacerlo hacia arriba. 3.) Variantes a los árboles B. Desde 1972 a la fecha se han estudiado a fondo las propiedades de los árboles B, y como no podía ser de otra forma, se plantearon muchas variantes estructurales. Una de ellas fue planteada por Knuth en 1973, y se llamó Arbol B* (B estrella o B star). Esencialmente se trata de una variante que garantiza que cada página tendrá al menos dos tercios de su capacidad ocupada (y no sólo la mitad) Otra variante muy conocida es la designada como árbol B+ (B más o B plus). La idea es simple: un árbol B ofrece un posibilidad rápida de acceder a un registro del archivo, pero no ofrece la propiedad de recorrer en forma secuencial ordenada ese mismo archivo. Los árboles B+ constituyen una extensión natural al árbol B permitiendo esa característica. En un árbol B+, todas las claves se almacenan en la hojas del último nivel. Las páginas intermedias contienen valores duplicados de esas claves, sólo para proporcionar rutas de recorrido de búsqueda que lleguen hasta las hojas. Hasta aquí, el árbol sigue sirviendo para búsquedas rápidas. Pero el detalle final es que las hojas del último nivel se enlazan entre ellas formando una lista, que puede recorrerse desde el principio (si entramos al archivo por la clave menor) o desde cualquier otra parte (entrando al índice por una clave cualquiera, llegamos hasta ella en la hoja del último nivel y recorremos desde alli en adelante en orden de lista): un árbol B+ 10 5 10 30 20 23 30 40 7 UTN Córdoba Ingenieria en Sistemas de Información Cátedra: Diseño de Lenguajes de Consulta Bibliografía: Si bien los profesores de la cátedra preparan una serie de fichas de consulta y guía para los temas de la asignatura, debe entenderse que para un dominio completo de estos temas y para el desarrollo óptimo de las tareas y ejercicios que se piden es fuertemente recomendable que los alumnos estudien e investiguen a fondo en otras fuentes. Va para ello la siguiente bibliografía de consulta y ampliación de temas: Deitel, H., Deitel, P. (2005 o posterior). "Java Cómo Programar" . México: Prentice Hall. ISBN: 970-26-0518-0 [disponible en biblioteca del Departamento de Sistemas] Drozdek, A. (2007). "Estructura de Datos y Algoritmos en Java". México D.F.: Thomson. ISBN: 9789706866110 [disponible en biblioteca del Departamento de Sistemas] Eckel, B. (2002 aunque existe edición posterior). "Piensa en Java". Madrid: Pearson Educación. ISBN: 9788489660342. [disponible en biblioteca del Departamento de Sistemas] Horstmann, C., y Cornell G. (2000). “Core Java 2 – Volume I: Fundamentals”. (Disponible en español) Upper Saddle River: Prentice Hall. ISBN: 84-205-4832-4 [disponible en biblioteca central] Horstmann, C., y Cornell G. (2001). “Core Java 2 – Volume II: Advanced Features”. (Disponible en español) Palo Alto: Prentice Hall. ISBN: 84-8322-310-4 [disponible en biblioteca central] Langsam, Y., Augenstein, M., y Tenenbaum, A. (1997). “Estructura de Datos con C y C++ (2da. Edición)”. México: Prentice Hall. ISBN: 968-880-798-2 [disponible en biblioteca central] Sedgewick, Robert (1995). “Algoritmos en C++”. Reading: Addison Wesley – Díaz de Santos. ISBN: 978-0-201-62574-5 [disponible en biblioteca central] Weiss, M. A. (2000). “Estructuras de Datos en Java – Compatible con Java 2”. Madrid: Addison Wesley. ISBN: 84-7829-035-4 [disponible en biblioteca central] 8