Implementación de diseño h2database BTree y pensamiento de optimización de consultas | Equipo técnico de JD Cloud

h2database es una base de datos de código abierto escrita en Java, compatible con ANSI-SQL89. No solo implementa el motor de almacenamiento convencional basado en BTree, sino que también es compatible con el motor de almacenamiento estructurado por registro. Las funciones son muy ricas (mecanismo de detección de puntos muertos, funciones de transacción, MVCC, herramientas de operación y mantenimiento, etc.), y el aprendizaje de la base de datos es un caso muy bueno.

Este artículo combina la teoría con la práctica, a través del diseño y la implementación del índice BTree, para comprender mejor los puntos de conocimiento y los principios de optimización relacionados con el índice de la base de datos.

clase de implementación Btree

El motor de almacenamiento MVStore utilizado por h2database de forma predeterminada, si desea utilizar el motor de almacenamiento basado en BTree, debe especificarlo (el siguiente código de ejemplo jdbcUrl).

Las siguientes son las clases clave relacionadas con el motor de almacenamiento general (estructura BTree).

  • org.h2.table.RegularTable
  • org.h2.index.PageBtreeIndex  (implementación de ontología de índice SQL)
  • org.h2.store.PageStore  (capa de almacenamiento, capa lógica de acoplamiento y sistema de archivos)

La estructura de datos de BTree se puede encontrar en una descripción detallada y una explicación en Internet, por lo que no entraré en demasiados detalles.

Lo que necesita una explicación especial es: PageStore. PageStore completa las cachés de claves, las lecturas de disco y los registros de deshacer de nuestra consulta y optimización de datos. La documentación detallada y una implementación completa se pueden encontrar aquí.

BTree agregar cadena de llamada de entrada de índice

Proporciona una nueva cadena de llamadas para datos de índice. Del mismo modo, la consulta y la eliminación del índice estarán involucradas, lo cual es conveniente para la referencia de depuración.

  1. org.h2.command.dml.Insert#insertRows ( Insertar datos de activadores SQL y adición de índice )
  2. org.h2.mvstore.db.RegularTable#addRow ( Fila de datos procesados, ejecutar nueva adición )
  3. org.h2.index.PageBtreeIndex#add ( la capa lógica agrega datos de índice )
  4. org.h2.index.PageDataIndex#addTry ( Añadir datos de índice en la capa de almacenamiento )
  5. org.h2.index.PageDataLeaf#addRowTry ( nueva implementación de la capa de almacenamiento )
// 示例代码
// CREATE TABLE city (id INT(10) NOT NULL AUTO_INCREMENT, code VARCHAR(40) NOT NULL, name VARCHAR(40) NOT NULL);
public static void main(String[] args) throws SQLException {
    // 注意:MV_STORE=false,MVStore is used as default storage
    Connection conn = DriverManager.getConnection("jdbc:h2:~/test;MV_STORE=false", "sa", "");
    Statement statement = conn.createStatement();
    // CREATE INDEX IDX_NAME ON city(code); 添加数据触发 BTree 索引新增
    // -- SQL 实例化为:IDX_NAME:16:org.h2.index.PageBtreeIndex
    statement.executeUpdate("INSERT INTO city(code,name) values('cch','长春')");
    statement.close();
    conn.close();
}

Información de código

En combinación con el código de ejemplo anterior, conozca las características del índice BTree y las precauciones de uso a partir de la implementación del nuevo proceso de índice. Realice la operación del índice de análisis desde la capa inferior y comprenda mejor el uso y la optimización de los índices SQL.

tabla agregar datos

 public void addRow(Session session, Row row) {
    // MVCC 控制机制,记录和比对当前事务的 id
    lastModificationId = database.getNextModificationDataId();
    if (database.isMultiVersion()) {
        row.setSessionId(session.getId());
    }
    int i = 0;
    try {
        // 根据设计规范,indexes 肯定会有一个聚集索引(h2 称之为scan index)。①
        for (int size = indexes.size(); i < size; i++) {
            Index index = indexes.get(i);
            index.add(session, row);
            checkRowCount(session, index, 1);
        }
        // 记录当前 table 的数据行数,事务回滚后会相应递减。
        rowCount++;
    } catch (Throwable e) {
        try {
            while (--i >= 0) {
                Index index = indexes.get(i);
                // 对应的,如果发生任何异常,会移除对应的索引数据。
                index.remove(session, row);
            }
        }
        throw de;
    }
}

① Al igual que el almacenamiento de datos Mysql InnoDB, RegularTable debe tener un solo índice agrupado. Utilice la clave principal (o el ID de incremento automático implícito) como clave para almacenar datos completos.

Índice agrupado para agregar datos

  • La clave en el índice es lo que busca la consulta, mientras que su valor puede ser una de dos cosas: puede ser una fila real (documento, vértice) o puede ser una referencia a una fila almacenada en otro lugar. En el último caso, el lugar donde se almacenan las filas se denomina  archivo heap y los datos se almacenan sin ningún orden en particular (relativo según el índice).
  • El salto adicional del índice al archivo heap es un impacto de rendimiento demasiado grande para las lecturas, por lo que puede ser deseable almacenar filas indexadas directamente en el índice. Esto se denomina índice agrupado.
  • En función del escaneo de clave principal, los datos se pueden determinar y obtener de forma única, y el rendimiento del índice agrupado es un escaneo menos que el del índice de clave no principal.
public void add(Session session, Row row) {
    // 索引key 生成 ②
    if (mainIndexColumn != -1) {
        // 如果主键非 long, 使用 org.h2.value.Value#convertTo 尝试把主键转为 long
        row.setKey(row.getValue(mainIndexColumn).getLong());
    } else {
        if (row.getKey() == 0) {
            row.setKey((int) ++lastKey);
            retry = true;
        }
    }

    // 添加行数据到聚集索引 ③
    while (true) {
        try {
            addTry(session, row);
            break;
        } catch (DbException e) {
            if (!retry) {
                throw getNewDuplicateKeyException();
            }
        }
    }
}

② En el caso de que haya una clave principal, se obtendrá el valor de la clave principal de la fila actual y se convertirá en un valor largo. En el caso de que no se especifique ninguna clave principal, la clave única se incrementa automáticamente a partir del atributo de índice agrupado actual lastKey.

Solo cuando se especifica la clave principal, se verificará la duplicación de datos (es decir, la clave de índice está duplicada y la última clave de incremento automático no tendrá el problema de los valores duplicados).

③ El índice agrupado PageDataIndex busca la posición clave correspondiente según la estructura BTree y almacena la Fila en la página según el orden de la clave principal/clave. El índice no agrupado PageBtreeIndex también es un flujo de procesamiento de este tipo.

Esto implica tres cuestiones:

  1. ¿Cómo encontrar la ubicación de la clave, es decir, el cálculo de la ubicación de BTree?
  2. ¿Cómo calcular las compensaciones en la página de almacenamiento de filas (datos reales)?
  3. ¿Cómo se escribe Row en el disco y cuándo se escribe?

Implementación de acceso a datos de índice

  • Los árboles B dividen una base de datos en  bloques  o  páginas de tamaño fijo , tradicionalmente de 4 KB de tamaño (a veces más grandes), y solo se puede leer o escribir una página a la vez.
  • Cada página se puede identificar mediante una dirección o ubicación, lo que permite que una página se refiera a otra, similar a un puntero, pero en el disco en lugar de en la memoria. (correspondiente a la base de datos h2 PageBtreeLeaf y PageBtreeNode)
  • A diferencia de PageDataIndex, PageBtreeIndex se almacena en el orden de column.value. El proceso de sumar es comparar y encontrar column.value, y determinar el subíndice x de las compensaciones en el bloque. Todo lo que queda es calcular el desplazamiento de los datos y almacenarlo en el subíndice x.
/**
 * Find an entry. 二分查找 compare 所在的位置。这个位置存储 compare 的offset。
 * org.h2.index.PageBtree#find(org.h2.result.SearchRow, boolean, boolean, boolean)
 * @param compare 查找的row, 对应上述示例 compare.value = 'cch'
 * @return the index of the found row
 */
int find(SearchRow compare, boolean bigger, boolean add, boolean compareKeys) {
    // 目前 page 持有的数据量 ④
    int l = 0, r = entryCount;
    int comp = 1;
    while (l < r) {
        int i = (l + r) >>> 1;
        // 根据 offsets[i],读取对应的 row 数据 ⑤
        SearchRow row = getRow(i);
        // 比大小 ⑥
        comp = index.compareRows(row, compare);
        if (comp == 0) {
            // 唯一索引校验 ⑦
            if (add && index.indexType.isUnique()) {
                if (!index.containsNullAndAllowMultipleNull(compare)) {
                    throw index.getDuplicateKeyException(compare.toString());
                }
            }
        }
        if (comp > 0 || (!bigger && comp == 0)) {
            r = i;
        } else {
            l = i + 1;
        }
    }
    return l;
}

④ Para cada bloque (página) entryCount, se inicializan dos métodos. De acuerdo con la asignación de bloques y la inicialización de la creación de instancias, PageStore lee el archivo de bloque y lo analiza a partir de los datos de la página.

⑤ En el proceso de deserialización, lea los datos del código de bytes del archivo de página (matriz de 4k bytes) de acuerdo con el protocolo y cree una instancia como un objeto de fila. Referencia: org.h2.index.PageBtreeIndex#readRow(org.h2.store.Data, int, boolean, boolean) .

⑥ Todos los tipos admiten la comparación de tamaño, las reglas específicas se refieren a: org.h2.index.BaseIndex#compareRows

⑦ Si hay valores clave duplicados en los datos, no puede crear un índice único, una restricción ÚNICA o una restricción PRIMARY KEY. h2database es compatible con múltiples modos de base de datos, MySQL NULL no es único, MSSQLServer NULL es único y solo se permite una aparición.

private int addRow(SearchRow row, boolean tryOnly) {
	// 计算数据所占字节的长度
	int rowLength = index.getRowSize(data, row, onlyPosition);
	// 块大小,默认 4k
	int pageSize = index.getPageStore().getPageSize();
	// 块文件可用的 offset 获取
	int last = entryCount == 0 ? pageSize : offsets[entryCount - 1];
	if (last - rowLength < start + OFFSET_LENGTH) {
		// 校验和尝试分配计算,这其中就涉及到分割页面生长 B 树的过程 ⑧
	}
	// undo log 让B树更可靠 ⑨
	index.getPageStore().logUndo(this, data);
	if (!optimizeUpdate) {
		readAllRows();
	}

	int x = find(row, false, true, true);
	// 新索引数据的offset 插入到 offsets 数组中。使用 System.arraycopy(x + 1) 来挪动数据。
	offsets = insert(offsets, entryCount, x, offset);
	// 重新计算 offsets,写磁盘就按照 offsets 来写入数据。
	add(offsets, x + 1, entryCount + 1, -rowLength);
	// 追加实际数据 row
	rows = insert(rows, entryCount, x, row);
	entryCount++;
	// 标识 page.setChanged(true);
	index.getPageStore().update(this);
	return -1;
}

⑧ Si desea agregar una nueva clave, debe encontrar la página cuyo alcance puede contener la nueva clave y agregarla a esa página. Si no hay suficiente espacio libre en la página para la nueva clave, se divide en dos páginas llenas hasta la mitad y la página principal se actualiza para reflejar la nueva partición del rango de claves.

⑨Para permitir que la base de datos maneje escenarios de fallas anormales, las implementaciones de árbol B generalmente tienen una estructura de datos de disco duro adicional: registro preescrito (WAL, es decir, registro de escritura anticipada, también conocido como  registro de rehacer o registro de rehacer) . Este es un archivo de solo anexar en el que se debe escribir cada modificación del árbol B antes de que se pueda aplicar a las páginas del árbol mismo. Cuando la base de datos se recupere después de un bloqueo, este registro se utilizará para que el árbol B vuelva a un estado coherente.

Resumen de práctica

  • La optimización de consultas es esencialmente la optimización de la cantidad de datos a los que se accede y la optimización de la E/S del disco.

  • Si todos los datos se almacenan en caché en la memoria, en realidad es la optimización de la cantidad de cálculo y la optimización del uso de la CPU.

  • El índice está ordenado, lo que en realidad significa que los desplazamientos en el archivo de bloque se representan en forma de matriz. En particular , en h2database, los elementos de la matriz de compensaciones también están ordenados (por ejemplo: [4090, 4084, 4078, 4072, 4066, 4060, 4054, 4048, 4042]), lo que debería ser conveniente para la lectura secuencial del disco y evitar la fragmentación del disco . .

  • Teóricamente, la E/S de exploración del índice agrupado es mayor que la del índice BTree, porque en el mismo archivo de bloque, el índice BTree almacena una mayor cantidad de datos y ocupa menos archivos de bloque. Si una columna de la tabla es lo suficientemente pequeña, la exploración del índice agrupado es más eficiente.

    Debe tener cuidado al crear una tabla, y la longitud del campo de cada columna debe ser lo más corta posible para ahorrar espacio en la página .

  • Uso razonable de consultas de índice de cobertura para evitar consultas de vuelta a la tabla.  Como en el ejemplo anterior, select id from city where code = 'cch' escanee el índice BTree una vez para obtener el resultado. Si  select name from city where code = 'cch'necesita escanear el índice BTree una vez para obtener la clave de índice (clave principal), recorra y escanee el índice agrupado para obtener el resultado de acuerdo con la clave.

  • Use la memoria caché razonablemente para minimizar el impacto de la E/S del disco.  Por ejemplo, configure razonablemente el tamaño de la memoria caché y distinga entre consultas de datos calientes y frías.

Otros puntos de conocimiento

  • Un árbol de cuatro niveles de páginas de 4 KB con un factor de ramificación de 500 puede almacenar hasta 256 TB de datos). (El número de referencias a subpáginas en una página en un árbol B se denomina  factor de ramificación .

referencia

ddia/ch3.md árbol B

Autor: JD Logística Yang Pan

Fuente de contenido: comunidad de desarrolladores de JD Cloud

Supongo que te gusta

Origin blog.csdn.net/JDDTechTalk/article/details/131396216
Recomendado
Clasificación