Ф Indice 2. Introducción 3. Operaciones sobre una base de datos 3. Multiway-Tree 5. B-TREE 8. B+-TREE 9. Los algoritmos a. Búsqueda b. Inserción c. Eliminación 12.Costos de los algoritmos 14.B*-TREE 15.Bibliografía consultada 1 Ф Introducción Todos sabemos que una de las aplicaciones más importantes de las computadoras es la de guardar y manipular información, en especial, grandes cantidades de información. La manipulación de estos datos consiste en la inserción, lectura, modificación y borrado de los mismos. Los datos se guardan en la memoria secundaria de la máquina (el disco) y para poder llevar a cabo estos procesos, la información debe ser recuperada y llevada a la memoria principal (la RAM) para que el procesador pueda acceder a ella. Luego, para asegurar que el proceso de manipulación será veloz, es necesario organizar correctamente los datos. Normalmente los datos se guardan en una Base de Datos. Se define a ésta como una colección de datos relacionados entre sí. Las bases de datos guardan en sus tablas registros, los cuales contienen información clasificada en campos. Por ejemplo en una tabla de alumnos de la facultad, cada registro le corresponderá a un alumno, y dentro de éste se guardarán los datos: nombre, apellido, legajo, año, etc... Éstos son los campos. Una universidad guarda datos sobre miles de alumnos, luego si no organizamos esta información será muy difícil encontrar un registro en particular. Cuanto más rápida sea la base de datos, más rápido realizará la computadora la manipulación de la información y por lo tanto más tiempo tendrá para llevar a cabo otras operaciones. ¿Qué significa organizar la información? Organizar los datos es ordenarlos de la manera más útil, haciendo fácil y eficiente su manipulación. Si tuviésemos que organizar, volviendo al mismo ejemplo, una colección de registros de alumnos, podríamos por ejemplo colocar los registros en carpetas con las iniciales de los apellidos de éstos, o sea, ordenarlos por orden alfabético. En este caso tendríamos carpetas de la A a la Z y sería más fácil encontrar un alumno que si estuviesen todos los registros apilados en un solo cajón. Pero tambíen podríamos organizarlo de otra forma: ahora que están así, colocar las carpetas en cajones. Un cajón que tenga las carpetas de la A a la F, otro con las carpetas de la G a la N, de la O a la T y un último de la U a la Z. Ahora es más rápido buscar una carpeta que antes, y luego más veloz el acceso a un dato de algún alumno. Veamos: hemos creado con esto un índice (INDEX), lo que nos ha servido para organizar la información. Pero qué ocurre si nos ponemos a pensar, que una facultad tiene muchos años en su carrera, luego muchísimos registros de alumnos para guardar, y además, una universidad tiene muchas facultades. Podríamos entonces tener varios muebles con cajones, un mueble para cada año. Luego podríamos también agregar un nuevo nivel de indexación, teniendo un conjunto de muebles o archivos para cada facultad. En la computadora Este caso es concreto en la realidad. ¿Qué ocurre cuando diseñamos una base de datos para guardar la información en la computadora? Inicialmente hay que definir a cada registro de un conjunto de n registros como un ri al cual le corresponderá una clave(key) identificadora única ki . Al conjunto de datos asociados a un registro se lo definirá como ai. Recordemos que la información está guardada en la memoria secundaria y debe ser recuperada. Como la colección de datos es muy grande (en la mayoría de los 2 casos), será necesario un index file o archivo índice que funcione en paralelo con el archivo de datos para hacer más veloz la manipulación de este último. En el data file tendremos registros de la forma: ai = <valores de los campos del registro> Mientras que en el index file los registros de índice serán de la forma: ki = <ri , ai> Siendo ai una referencia al registro de datos que se encuentra en el data file. Habrá que tener en cuenta ciertas consideraciones: Para que funcione bien el proceso de recuperación de datos es necesaria una clave identificadora para cada registro. Es necesario que las claves tengan un orden natural entre sí, esto es importante a la hora de querer acceder secuencialmente a la colección de datos. El tamaño de los registros de índice es mucho menor (casi siempre) que el tamaño de un registro de datos, por eso se trabaja velozmente organizando los registros del archivo índice. Sería una locura ponerse a manipular directamente el archivo de datos, dado el gran tamaño que tiene cada uno de los registros. Ф Operaciones sobre una base de datos Las operaciones básicas que se pueden realizar sobre una base de datos son: Inserción (alta) Eliminación (baja) Búsqueda Next (siguiente) Acceso Secuencial Todas éstas tienen un costo de operación relativo a cómo tengamos organizados los datos. Un buen index file asegura la eficiencia de estas operaciones. Si nos ponemos a pensar en la velocidad de nuestro procesador, estas operaciones llevadas a cabo sobre un índice en memoria no tienen un costo relativamente alto, son bastante veloces, pero... A pesar de que manejaremos los registros a partir de su índice, estos pueden ser muy grandes debido a la gran cantidad de registros guardados, y luego no entrarán en la memoria principal. Deberán trabajarse entonces desde disco. Al tener algoritmos que se manejan con acceso a disco, todo lo que sea procesamiento en memoria tiene un coste despreciable, ya que el acceso a disco es millones de veces más lento que el trabajo en memoria. Pensemos que con un procesador de 1000 MHz, un ciclo de su reloj (los ciclos máquina son de algunos ciclos de reloj y cada operación básica tarda un cierto número de ciclo de máquina) es de 1 nano segundo. Comparando con la velocidad de un acceso a disco, ésta es del orden de los milisegundos. Es notable por qué se desprecia el trabajo del procesador sobre la memoria RAM en comparación con el acceso a disco. Ф Multiway Tree Planteémonos sabiendo cuáles son ahora nuestras necesidades, el diseño de una estructura de datos acorde. Imaginemos qué pasaría si utilizáramos una lista ordenada para esto. La búsqueda es secuencial, a lo sumo puede lograrse una búsqueda binaria, pero para cada comparación se realizaría un acceso a disco. No tiene mucho sentido que digamos... 3 Pensemos ahora en algo más avanzado: un BST o árbol de búsqueda binario. O mejor, un AVL. Supongamos que pudiésemos implementar esta estructura en disco. El AVL nos asegura que todos los algoritmos serán de orden logarítmico. Pero pensemos en un árbol AVL concreto. Supongamos que las claves de los registros son números enteros. Si quisiéramos buscar un registro cuya clave es el 315, siendo el árbol mostrado el index file, deberíamos hacer 4 accesos a disco. Imaginemos ahora que tenemos una base de datos, con una gran cantidad de registros, tal vez miles. La cantidad de accesos a disco para una búsqueda sería muy grande y luego la búsqueda terriblemente lenta. Definitivamente esta estructura no es la adecuada para lo que necesitamos. Aquí aparece la necesidad de implementar el concepto abstracto introducido anteriormente. El ejemplo de la universidad es un tipo de index que se conoce como Multilevel Index o índice multinivel, en el cual se hacen agrupaciones de los registros por niveles, un nivel es el índice del nivel que tiene más abajo, hasta llegar al último que tiene el acceso al data file. La traducción de un índice multinivel a la informática es un Multiway-Tree. Éste es un árbol M-Ario, o sea, cada nodo tiene como máximo M hijos. Veamos un multilevel index (VER FIGURA), en particular, uno de 2 niveles. En éste los registros del índice o index entry del 2º nivel tienen claves para la búsqueda y punteros a los entry del primer nivel. Los registros del primer nivel tienen las claves identificadoras y los punteros correspondientes a los registros en el data file. Pero veamos que un index entry es un nodo que contiene más de una clave, en este caso en particular, contiene 4 claves y 4 punteros. Supongamos que queremos buscar el registro de clave 71. Entonces levantamos a memoria el index entry del 2º nivel. Recordemos que la búsqueda en memoria sobre este registro es despreciable. Se busca y vemos que el valor 71 está entre el 55 y el 85, luego nos guiamos por el puntero correspondiente a levantar a memoria el index entry respectivo del 1º nivel. Aquí buscamos el 71 y el puntero correspondiente nos llevará directo al bloques de registros en el data file donde se encuentra el registro buscado. Veamos que hicimos una búsqueda con 2 accesos a disco en una base de 26 registros. Imagine la posibilidad de disminución de accesos en comparación con un 4 árbol binario. (Notar que al llegar al bloque deberá hacerse una búsqueda secuencial para encontrar el registro, luego en realidad serán 3 accesos para hallarlo). El árbol M-Ario es la implementación de un registro multilevel, y evoluciona a lo que llamamos B-TREE. Multilevel Index de 2 niveles Ф B-TREE En 1970 R. Bayer y E. McCreight descubrieron un tipo de M-Way Tree al que llamaron B-Tree, el cual logra actualizar y buscar sobre un archivo grande con “buena” eficiencia garantizada y utilizando algoritmos relativamente sencillos. El B-Tree de orden m satisface las siguientes condiciones: i) Todo nodo tiene como máximo m hijos ii) Todo nodo, excepto por la raíz y las hojas, tiene al menos m/2 hijos iii) La raíz tiene por lo menos 2 hijos (a menos que sea una hoja) iv) Todas las hojas se encuentran en el mismo nivel y no tienen información 5 v) Un nodo interno (no es hoja) con k hijos posee k-1 claves Definición en “Art Of Computer Programming, 3rd Edition – D. Knuth”, cuando usa la definición de Bayer y McCreight. Puede considerarse, como las hojas no tienen punteros a otros nodos, que no son parte del árbol, sino el último nivel de indexación antes del datafile. Ésta consideración es útil dado que los nodos hoja son diferentes a los nodos internos del árbol. Las características del B-Tree aseguran que se encuentra balanceado (ver característica iv) ). Esto se logra en los algoritmos de inserción y eliminación con dos algoritmos respectivos auxiliares: split y concatenation (separación y concatenación) que serán explicados más adelante. Elmasri y Navathe hacen una definición formal del B-Tree de búsqueda, pensado ya en la implementación para una base de datos: Un B-Tree de orden p se define: 1. Cada nodo interno es de la forma: <P1, <K1,Pr1>, P2, <K2,Pr2>, ..., <Kq-i,Prq-i>, Pq> 2. 3. 4. 5. donde q p . Cada Pi es un puntero a otro nodo, cada Pri es un puntero a un registro del data file al cual corresponde una clave Ki. Dentro de cada nodo, K1 < K2 < ... < Kq-1 Para todo valor X de búsqueda en el subárbol apuntado por Pi se encontrará: Ki-1 < X < Ki , para 1 < i < q ; X < Ki para i =1 ; Ki-1 < X para i = q Cada nodo tiene como máximo p punteros a hijos. Cada nodo, excepto la raíz y las hojas, tienen al menos ( p / 2) punteros a nodos. La raíz tiene por lo menos 2 punteros a menos que sea el único nodo en el árbol (es hoja). 6. Un nodo con q punteros a nodos, q p, tiene q-1 valores clave de búsqueda y por lo tanto q-1 punteros a registros de datos. 7. Las hojas se encuentran en el mismo nivel. Tienen la misma estructura que los nodos internos con excepción de que sus punteros Pi a nodos son todos null. “Fundamentals Of Database Systems – Elmasri & Navathe” 6 Veamos la estructura de un nodo de un B-Tree, según la definición mencionada: La estructura del B-Tree será: Este en particular es un B-Tree de p = 3. Vayamos de a poco observando... ¿Cuál es la ventaja del B-Tree? Si utilizamos un orden p grande, digamos p = 200, el árbol crecerá a lo ancho en vez de a lo alto, asegurando una baja cantidad de accesos a disco. Pero este B-Tree tiene un inconveniente a nivel de eficiencia. ¿Qué ocurre cuando eliminamos un registro de nuestra base? Digamos que queremos eliminar el registro de clave 8 del B-Tree mostrado. Esto necesitaría de una reorganización del árbol que en disco tendría un gran coste con un orden p elevado. Otra característica importante a considerar es el coste de la operación NEXT. Si trabajamos en un B-TREE, debemos conservar en una pila el camino recorrido para llegar al siguiente registro correspondiente y el coste de la operación será significativo dado que debemos volver a acceder a nodos a los que ya habíamos accedido para llegar a donde estábamos. Hay una mejora de este árbol que es el B+-TREE. Ф B+-TREE 7 La mayoría de las implementaciones utilizan éste árbol. En un B-TREE, toda clave aparece una sola vez en todo el árbol, y se encuentra con su correspondiente puntero al registro respectivo. En un B +-TREE, los punteros a registros de datos sólo se guardan en las hojas del árbol, luego la estructura de las hojas difiere de la estructura de los nodos internos. Otra característica interesante del B +-TREE es que las hojas están unidas unas a otras secuencialmente permitiendo un veloz acceso secuencial de la base de datos (recordar operación NEXT). En el caso del B+-TREE, algunos valores de clave se repetirán en los nodos internos para guiar la búsqueda. La estructura de un B+-TREE de orden p es la siguiente: 1. Cada nodo interno es de la forma: <P1, K1, P2, K2, ..., Pq-1, Kq-1, Pq> donde q p y cada Pi es un puntero a nodo. 2. Dentro de cada nodo interno, K1 < K2 < ... < Kq-1 3. Para todo valor X de búsqueda en el subárbol apuntado por Pi se encontrará: Ki-1 < X Ki , para 1 < i < q ; X Ki para i =1 ; Ki-1 < X para i = q 4. Cada nodo tiene como máximo p punteros a nodos hijos. 5. Cada nodo, excepto la raíz, tiene por lo menos ( p / 2) punteros a nodo. La raíz tiene por lo menos 2 punteros a hijos si es un nodo interno. 6. Un nodo interno con q punteros, q p, tiene q-1 claves de búsqueda. Estructura de un nodo interno del B+-TREE La estructura de los nodos hoja del B+-TREE es la siguiente: 1. Cada nodo hoja es de la forma: <<K1, Pr1>, <K2, Pr2>, ..., <Kq-1, Prq-1>, Pnext > donde q p, cada Pri es un puntero a registro de datos y Pnext apunta a la siguiente hoja en el B+-TREE. 2. Dentro de cada nodo hoja, K1 < K2 < ... < Kq-1 , q p 3. Cada Pri es un puntero al registro de datos cuya clave es K i. 4. Cada nodo hoja tiene al menos ( p / 2) valores. 5. Todos los nodos hoja están en el mismo nivel. 8 Estructura de un nodo hoja del B+-TREE Para un B+-TREE contruido sobre un campo clave, los punteros en los nodos internos apuntan a sectores del disco donde se guardan otros nodos; los punteros en los nodos hoja apuntan a sectores del disco donde se encuentran los registros de datos correspondientes, excepto por el Pnext que apunta a la siguiente hoja en el árbol. Si empezamos en la hoja más a la izquierda, podremos recorrer los registros como si la estructura fuese una lista ordenada. Puede además incluirse un puntero Pprevious para agregar funcionalidad y así poder realizar fácilmente la operación PREVIOUS. Ф Los algoritmos Los algoritmos de inserción y eliminación pueden ser algo complicados y tienen un coste que es relativamente alto, debido a que deben acceder algunas veces al disco. Igualmente se demuestra por análisis y simulación que luego de ingresar y eliminar numerosos valores al árbol, éste mantiene sus nodos internos un 69% llenos y desde ese punto hay muy pocas eliminaciones e inserciones sobre los nodos internos. Esto asegura una gran eficiencia en la inserción y eliminación (en el caso promedio) en el árbol. ATENCIÓN: La inserción o eliminación de un registro no implica esta operación sobre un nodo interno (Si esto no se comprende, revisar la definición de B+-TREE). Búsqueda La descripción del algoritmo de búsqueda es un poco trivial. Alcanza con analizar la característica 3. de la definición de B+-TREE. Dada una clave X a buscar, se empieza en la raíz. El valor X estará acotado como indica la característica 3., y luego a la búsqueda le corresponderá acceder a un nodo hijo en correspondencia a la acotación de X con respecto a las claves del nodo donde se está buscando. Por ejemplo: 9 Si queremos buscar el registro de clave 5, empezamos por la raíz. El 5 coincide con la clave K1 y luego seguiremos la búsqueda por el nodo apuntado por P 1. Aquí hallamos que 5 > K1 siendo K1 = 3. Luego seguimos la búsqueda por el nodo apuntado por P2, en este caso, una hoja. En esta hoja encontraremos la clave 5, o sea, K1 = 5 y le corresponde un puntero Pr1 que apunta al registro de datos de clave 5 en el datafile. Inserción El algoritmo de inserción es un poco más complicado. Veamos este caso. Al insertar en primer lugar el 8 y el 5, la raíz es el único nodo y es además un nodo hoja. Notemos algo: - Todo valor debe existir en el nivel de las hojas porque todo puntero a registros está en ellas. - Sólo algunos valores de clave están en los nodos internos, éstos sirven para guiar la búsqueda. - Cada clave que aparece en un nodo interno es además la clave más a la derecha del subárbol apuntado por el puntero a la izquierda de la clave interna mencionada. Cuando un nodo hoja hace overflow (posee más que p-1 claves) éste debe ser separado (split). Las primeras ( p 1) / 2 claves se mantienen en el nodo a separar y las demás se mueven hacia un nuevo nodo hoja. La clave ( p 1) / 2 -ésima se copia como clave hacia el nodo interno padre y sobre el padre también se agrega un puntero a la nueva hoja. Si la inserción de esta nueva clave en el padre lo lleva al overflow, entonces se debe hacer split sobre éste, con la diferencia de que la clave ( p 1) / 2 -ésima se mueve hacia el padre, no se copia. El split puede propagarse hacia arriba hasta la raíz, y si ésta hace overflow, entonces se separa, se crea una nueva raíz padre de la anterior y luego el B+-TREE adquiere un nuevo nivel. Aclaración: Éste algoritmo de inserción caracteriza al árbol como un “bottom-up tree”. Pues crece de abajo hacia arriba. La otra posibilidad sería ir separando los nodos en la bajada. O sea, si al ingresar una nueva clave y cuando esta va bajando se encuentra con un nodo lleno, entonces hacemos split sobre éste y seguimos bajando la clave hasta llegar a las hojas. Este sería un “top-down tree”, pues crece de arriba hacia abajo (nos referimos al sentido de propagación del split). Normalmente se 10 utiliza el bottom-up, pero en un B-TREE debido a que es muy “chato” la diferencia no es notable, por lo tanto puede usarse cualquier de los dos algoritmos consiguiendo la misma eficiencia. Eliminación Cuando un registro es borrado, siempre se elimina del nivel de hojas. Si el valor de clave que es borrado se encuentra en un nodo interno, también debe ser removido aquí. Esto último se logra cambiando la clave interna por la nueva clave más a la derecha en el subárbol apuntado por el puntero izquierdo a la clave interna mencionada. Cuando eliminamos una clave de un nodo, si éste queda con al menos p / 2 hijos no hay problema, el proceso ha finalizado. Pero puede pasar que se llegue a un underflow, o sea, que el nodo posea menos hijos que los necesarios: (NOTAR QUE EL NODO HOJA TIENE COMO MÁXIMO p-1 HIJOS) En este caso se intentan en orden: - Redistribución: Se toma un nodo hermano y se redistribuyen las claves hasta que ambos tengan al menos p / 2 hijos. Normalmente se intenta primero con el - hermano izquierdo, si no es posible se intenta con el derecho y en última instancia se hace una redistribución con ambos. Notar que siempre para un nodo dado, el puntero respectivo en su padre debe tener a su derecha una clave igual o mayor a la mayor clave del nodo mencionado. A veces la redistribución no es posible, pues los nodos quedarían en underflow. En este caso se lleva a cabo otra solución. Nota:La redistribución no se propaga. Concatenación: Es la unión del nodo con su hermano. Esto conlleva una disminución de 1 en la cantidad de nodos y por lo tanto una disminución de 1 en las claves y punteros del padre. Primero se intenta concatenar con el hermano izquierdo, si no es posible se lo hará con el derecho. Al disminuir la cantidad de claves del padre, la concatenación puede propagarse hasta que la raíz llegue a tener menos de 2 hijos, en cuyo caso se generará una disminución de la altura del árbol. Nota:A veces la concatenación puede generar un split. ATENCIÓN: El nodo hoja tiene como máximo p-1 punteros (no contamos el pnext), luego al aplicar el algoritmo de eliminación explicado sobre las hojas se debe interpretar: “Tener al menos p / 2 hijos” “tener al menos ( p 1) / 2 punteros” Aclaración: Este es un posible algoritmo. También existe otra variante de éste, que es un algoritmo un poco más eficiente. Al eliminar una clave, no necesariamente es eliminada de los nodos internos, ya que sigue sirviendo como guía para la búsqueda. Llegado el caso que se de una concatenación puede que se elimine un valor de clave en un nodo interno, pero no forzaremos esta situación si la clave eliminada coincide con una que se encuentra en el árbol, como se mencionó en el algoritmo anterior. Esto disminuye el coste del algoritmo de eliminación. 11 Ф Los costos de los algoritmos Llámese p ó m, es el orden del árbol al cual nos referimos, o sea, la cantidad máxima de hijos que posee un nodo. Pensemos que cuanto más grande sea nuestro p, más ancho será el árbol, luego menos accesos a disco serán necesarios para encontrar un nodo. Pero pensemos también, que al ser mayor p, más grandes serán los nodos que debemos levantar en memoria. Luego el tamaño no puede ser arbitrario, ya que el nodo debe entrar en memoria. Los costos de los algoritmos dependerán de la altura del árbol, y ésta depende del órden del mismo. Puede demostrarse (no viene al caso) que la altura (h) del árbol será: N 1 h log p / 2 2 Siendo N = cantidad de claves en el árbol Costo en la búsqueda: Para clasificar los costos se separarán los costos de lectura y escritura. Sean: f = cantidad de nodos accedidos = cantidad de escrituras realizadas Para la búsqueda tendremos: fmin 1 fmax h min 0 max 0 El análisis de esto es trivial. Luego, para un N=1.999.998 y un p=199, se requieren como máximo 3 accesos a nodos para hallar una clave. (Ver la fórmula de h) Normalmente se trabaja con la raíz en memoria para no tener que levantarla en cada operación, lo que significaría en este caso que necesitaríamos tan sólo 2 accesos a disco para hallar una clave. Pensemos que el índice maneja 2 millones de claves. Bastante eficiente, ¿no? 12 Veamos algunos valores: La tabla muestra en función del orden p y la cantidad de claves N, la cantidad de accesos a nodos necesarios para concretar una búsqueda. ¿Pero qué sucede cuando uno levanta un nodo en memoria? El nodo podría contener una lista enlazada de los elementos <K i, Pi>, luego debemos hacer una búsqueda interna de la clave en el nodo. Una característica de la implementación puede ser que el nodo guarde cuántas claves tiene en su interior, o sea, cuán lleno está. Puede entonces hacerse lo siguiente: - Si tiene pocas claves, se hace una búsqueda secuencial - De lo contrario, lo más veloz será una búsqueda binaria ¿A qué nos lleva todo esto? Empírica y analíticamente se llega a un intervalo de valores de p que son los óptimos, éste valor dependerá de las características de la computadora sobre la que queramos manejar nuestro árbol. La elección de un valor de p no es sencilla. Bayer y McCreight construyeron con buenos resultados una manera de elegir el orden del B-TREE, pero el desarrollo es bastante complejo y depende de los tiempos de acceso de la máquina, tanto a nivel de memoria primaria como secundaria. Coste de la inserción: fmin h fmax h min 1 max 2h+1 En el mejor caso, solo necesitamos recorrer el árbol y hacer la inserción, sin ninguna separación. El el peor caso, debemos separar el nodo y el overflow se propaga hasta la raíz, llevándonos a separala y creando un nuevo nivel para el árbol. Luego se necesitan los h accesos para recorrer el árbol más la escritura de dos nodos por cada uno separado (aparece uno nuevo en cada separación) y la creación de una nueva raíz (2h+1). Coste de la eliminación: fmin h fmax 2h-1 min 1 max h+1 En el mejor caso, recorremos el árbol y eliminamos la clave correspondiente. 13 En el peor caso, modificada. la concatenación se propaga hasta el hijo de la raíz y la raíz es Ф B*-TREE Knuth basándose en un concepto de Bayer y McCreight crea una variante del árbol B-TREE con algunas mejoras. Bayer y MacCreight proponían un concepto de manejo diferente del overflow. En vez de generar siempre un split se trata antes de redistribuir las claves con los hermanos, si esto no es posible, entonces forzar el split. Esto lleva a redefinir el árbol condicionando los nodos a estar 2/3 llenos siempre. La nueva construcción logra una búsqueda más rápida pero una inserción más costosa. 14 15 16 Ф Bibliografía consultada Libros: ► The Art of Computer Programming, 3rd Vol. – D. E. Knuth ► Fundamentals of Database Systems – Elmasri and Navathe ► Algorithms In C++ – R. Sedgewick Publicaciones: ► The Ubiquitous B-TREE – D. Comer ► Organization and Maintenance Of Large Ordered Indexes – Bayer and McCreight 17