¡Vacas! Base de datos de decenas de millones de filas de una sola tabla: como notas de optimización de búsqueda

Lectura recomendada:

A menudo usamos el operador LIKE en la base de datos para completar la búsqueda aproximada de los datos, el operador LIKE se usa para buscar el patrón especificado en la columna de la cláusula WHERE.

Si necesita encontrar todos los datos cuyo apellido es "Zhang" en la tabla de clientes, puede utilizar la siguiente declaración SQL:

SELECT * FROM Customer WHERE Name LIKE '张%'

Si necesita encontrar todos los datos en la tabla de clientes cuyo número de teléfono móvil es "1234", puede utilizar la siguiente instrucción SQL:

SELECT * FROM Customer WHERE Phone LIKE '%123456'

Si necesita encontrar todos los datos en la tabla de clientes que contiene "mostrar" en el nombre, puede usar la siguiente instrucción SQL:

SELECT * FROM Customer WHERE Name LIKE '%秀%'

Los tres anteriores corresponden a: coincidencia de prefijo izquierdo, coincidencia de sufijo derecho y consulta difusa, y corresponden a diferentes métodos de optimización de consultas.

Resumen de datos

Ahora hay una tabla de datos llamada tbl_like, que contiene todas las oraciones de los cuatro clásicos, con decenas de millones de datos:

Optimización de consultas de coincidencia de prefijo izquierdo

Si desea consultar todas las oraciones que comienzan con "Monkey King", puede utilizar la siguiente instrucción SQL:

SELECT * FROM tbl_like WHERE txt LIKE '孙悟空%'

La base de datos de SQL Server es relativamente potente y tarda más de 800 milisegundos, lo que no es rápido:

Podemos construir un índice en la columna txt para optimizar la consulta:

CREATE INDEX tbl_like_txt_idx ON [tbl_like] ( [txt] )

Después de aplicar el índice, la velocidad de la consulta se acelera enormemente, solo 5 milisegundos:

De esto se puede ver que para la coincidencia del prefijo izquierdo, podemos acelerar la consulta aumentando el índice.

Optimización de consultas de coincidencia de sufijo derecho

En la consulta de coincidencia de sufijo correcto, el índice anterior no es efectivo para la coincidencia de sufijo correcto. Utilice la siguiente instrucción SQL para consultar todos los datos que terminan en "Monkey King":

SELECT * FROM tbl_like WHERE txt LIKE '%孙悟空'

La eficiencia es muy baja y tarda 2,5 segundos:

Podemos utilizar el enfoque de "espacio para el tiempo" para resolver el problema de la baja eficiencia en la consulta de coincidencia de sufijo correcto.

En términos simples, podemos invertir la cadena y convertir la coincidencia del sufijo derecho en una coincidencia del prefijo izquierdo. Tome "Gu Hai de regreso y luego atrape al Rey Mono" como ejemplo, la cadena de caracteres después de invertirla es "Kong Wu Sun atrapa hacia adelante y hacia atrás en el mar y defiende". Cuando necesite encontrar los datos que terminan con "Monkey King", simplemente busque los datos que comienzan con "Kong Wu Sun".

El método específico es: agregar la columna "txt_back" a la tabla, invertir el valor de la columna "txt", completar la columna "txt_back" y finalmente agregar un índice para la columna "txt_back".

ALTER TABLE tbl_like ADD txt_back nvarchar(1000);-- 增加数据列
UPDATE tbl_like SET txt_back = reverse(txt); -- 填充 txt_back 的值
CREATE INDEX tbl_like_txt_back_idx ON [tbl_like] ( [txt_back] );-- 为 txt_back 列增加索引

Después de ajustar la tabla de datos, nuestra declaración SQL también debe ajustarse:

SELECT * FROM tbl_like WHERE txt_back LIKE '空悟孙%'

Después de esta operación, la velocidad de ejecución es muy rápida:

Puede verse a partir de esto que: para la coincidencia del sufijo derecho, podemos crear un campo de orden inverso para cambiar la coincidencia del sufijo derecho por la coincidencia del prefijo izquierdo para acelerar la consulta.

Optimización de consultas difusas

Al consultar todas las declaraciones que contienen "Wukong", utilizamos la siguiente declaración SQL:

SELECT * FROM tbl_like WHERE txt LIKE '%悟空%'

La declaración no puede utilizar el índice, por lo que la consulta es muy lenta y requiere 2,7 segundos:

Desafortunadamente, no tenemos una manera fácil de optimizar esta consulta. Pero no existe una manera fácil, y eso no significa que no haya manera. Una de las soluciones es: segmentación de palabras + índice invertido.

La segmentación de palabras es el proceso de recombinar secuencias de palabras consecutivas en secuencias de palabras de acuerdo con ciertas especificaciones. Sabemos que en la escritura en inglés, los espacios entre palabras se utilizan como delimitadores naturales, mientras que solo las palabras, oraciones y párrafos en chino pueden delimitarse simplemente con delimitadores obvios, pero las palabras no tienen un delimitador formal. Aunque el inglés también tiene el problema de dividir frases, a nivel de palabras, el chino es mucho más complicado y difícil que el inglés.

El índice invertido surge de la necesidad de encontrar registros basados ​​en el valor de los atributos en aplicaciones prácticas. Cada elemento de esta tabla de índice incluye un valor de atributo y la dirección de cada registro con el valor de atributo. Dado que el valor del atributo no está determinado por el registro, sino que la posición del registro está determinada por el valor del atributo, se denomina índice invertido. Un archivo con un índice invertido se denomina archivo de índice invertido, o archivo invertido para abreviar.

Los dos textos desconcertantes anteriores son de Baidu Baike, puedes optar por ignorarlos como yo.

No necesitamos excelentes habilidades de segmentación de palabras, debido a las características del chino, solo necesitamos segmentación de palabras "binarias".

La denominada segmentación de palabras binarias significa que cada dos caracteres del texto de un párrafo se utilizan como palabra para segmentar palabras. Tomemos como ejemplo la frase "protegerse contra Gu Hai y luego atrapar al Rey Mono". Después de la segmentación binaria, el resultado es: protegerse del mar antiguo, volver al mar antiguo, volver al mar, volver, volver, coger de nuevo, coger al nieto, Rey Mono, Wukong. Use C # para implementarlo brevemente:

public static List<String> Cut(String str)
{
       var list = new List<String>();
       var buffer = new Char[2];
       for (int i = 0; i < str.Length - 1; i++)
       {
             buffer[0] = str[i];
             buffer[1] = str[i + 1];
             list.Add(new String(buffer));
       }
       return list;
}

Pruebe los resultados:

Necesitamos una tabla de datos para hacer coincidir las entradas segmentadas con los datos originales. Para obtener una mayor eficiencia, también utilizamos un índice de cobertura:

CREATE TABLE tbl_like_word (
  [id] int identity,
  [rid] int NOT NULL,
  [word] nchar(2) NOT NULL,
  PRIMARY KEY CLUSTERED ([id])
);
CREATE INDEX tbl_like_word_word_idx ON tbl_like_word(word,rid);-- 覆盖索引(Covering index)

La declaración SQL anterior crea una tabla de datos llamada "tbl_like_word" y agrega un índice conjunto a sus columnas "word" y "rid". Esta es nuestra tabla invertida y el siguiente paso es llenarla con datos.

Necesitamos usar la función de enlace de base de datos incorporada de LINQPad para enlazar a la base de datos, y luego podemos interactuar con la base de datos en LINQPad. Primero lea los datos en la tabla tbl_like en lotes de 3000 entradas en el orden de Id, segmente el valor del campo txt para generar los datos requeridos para tbl_like_word y luego almacene los datos en lotes. El código completo de LINQPad es el siguiente:

void Main()
{
       var maxId = 0;
       const int limit = 3000;
       var wordList = new List<Tbl_like_word>();
       while (true)
       {
             $"开始处理:{maxId} 之后 {limit} 条".Dump("Log");
             //分批次读取
             var items = Tbl_likes
             .Where(i => i.Id > maxId)
             .OrderBy(i => i.Id)
             .Select(i => new { i.Id, i.Txt })
             .Take(limit)
             .ToList();
             if (items.Count == 0)
             {
                    break;
             }
             //逐条生产
             foreach (var item in items)
             {
                    maxId = item.Id;
                    //单个字的数据跳过
                    if (item.Txt.Length < 2)
                    {
                           continue;
                    }
                    var words = Cut(item.Txt);
                    wordList.AddRange(words.Select(str => new Tbl_like_word {  Rid = item.Id, Word = str }));
             }
       }
       "处理完毕,开始入库。".Dump("Log");
       this.BulkInsert(wordList);
       SaveChanges();
       "入库完成".Dump("Log");
}
// Define other methods, classes and namespaces here
public static List<String> Cut(String str)
{
       var list = new List<String>();
       var buffer = new Char[2];
       for (int i = 0; i < str.Length - 1; i++)
       {
             buffer[0] = str[i];
             buffer[1] = str[i + 1];
             list.Add(new String(buffer));
       }
       return list;
}

El script LINQPad anterior usa Entity Framework Core para conectarse a la base de datos y hace referencia al paquete NuGet "EFCore.BulkExtensions" para realizar la inserción masiva de datos.

Después de eso, puede organizar la consulta, consultar primero el índice invertido y luego asociarlo a la tabla principal:

SELECT TOP 10 * FROM tbl_like WHERE id IN (
SELECT rid FROM tbl_like_word WHERE word IN ('悟空'))

La velocidad de consulta es muy rápida, solo una docena de milisegundos:

Debido a que hemos dividido todas las oraciones en frases de dos caracteres, es una solución más económica usar LIKE directamente cuando se necesita una consulta difusa de un solo carácter. Si hay más de dos caracteres para consultar, es necesario segmentar la palabra de consulta. Si necesita consultar el término "East Tu Datang", la oración de consulta construida puede verse así:

SELECT TOP 10*FROM tbl_like WHERE id IN (
SELECT rid FROM tbl_like_word WHERE word IN ('东土','土大','大唐'))

Sin embargo, la consulta no cumple con nuestras expectativas, porque también filtrará las oraciones que solo contienen "suelo grande":

Podemos tomar algunos trucos para resolver este problema, como GROUP first:

SELECT TOP
    10 *
FROM
    tbl_like
WHERE
    id IN (
    SELECT
        rid
    FROM
        tbl_like_word
    WHERE
        word IN ( '东土', '土大', '大唐' )
    GROUP BY
        rid
    HAVING
    COUNT ( DISTINCT ( word ) ) = 3
    )

En la declaración SQL anterior, hemos agrupado el rid y filtrado que el número de frases únicas es tres (es decir, el número de nuestras palabras de consulta). Por lo tanto, podemos obtener el resultado correcto:

A partir de esto podemos ver: Para consultas difusas, podemos optimizar la velocidad de consulta por segmentación de palabras + índice invertido.

posdata

Aunque en la presentación se usa la base de datos SQL Server, la experiencia de optimización anterior es común a la mayoría de las bases de datos relacionales, como MySQL y Oracle.

Si usa la base de datos PostgreSQL en un trabajo real como el autor, puede usar directamente el tipo de matriz y configurar el índice GiN al realizar la indexación invertida para obtener una mejor experiencia de desarrollo y uso. Cabe señalar que, aunque PostgreSQL admite índices funcionales, si el resultado de la función se filtra LIKE, el índice no se activará.

Para bases de datos pequeñas como SQLite, la búsqueda difusa no puede usar el índice, por lo que los métodos de optimización de búsqueda de prefijo izquierdo y búsqueda de sufijo derecho no tienen efecto. Sin embargo, generalmente no usamos SQLite para almacenar grandes cantidades de datos, aunque el método de optimización de segmentación de palabras + índice invertido también se puede implementar en SQLite.

Supongo que te gusta

Origin blog.csdn.net/weixin_45784983/article/details/108411887
Recomendado
Clasificación