Únase a la optimización de consultas y rendimiento

En la producción real, las preguntas sobre el uso de declaraciones de combinación generalmente se centran en las dos categorías siguientes:

  • Nuestro DBA no permite el uso de join, ¿cuál es el problema con el uso de join?
  • Si hay dos mesas con diferentes tamaños para unir, ¿qué mesa debe usarse como mesa de conducción?

Tome un ejemplo para decir algo:

CREATE TABLE `t2` (
  `id` int(11) NOT NULL,
  `a` int(11) DEFAULT NULL,
  `b` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `a` (`a`)
) ENGINE=InnoDB;

create table t1 like t2;

Se insertan 1000 filas de datos en la tabla t2 y 100 filas de datos se insertan en la tabla t1.

Unión de bucle anidado de índice

Existe una consulta de este tipo: seleccione * de t1 straight_join t2 on (t1.a = t2.a);

Si usa la declaración de unión directamente, el optimizador de MySQL puede elegir la tabla t1 o t2 como tabla de manejo, lo que afectará el proceso de ejecución de nuestro análisis de la declaración SQL. Por lo tanto, para facilitar el análisis de los problemas de rendimiento durante la ejecución, cambio a straight_join para permitir que MySQL use un método de conexión fijo para ejecutar la consulta, de modo que el optimizador solo se unirá de la manera que especifiquemos . En esta declaración, t1 es la tabla conducida y t2 es la tabla conducida.

Como puede ver, en esta declaración, hay un índice en el campo a de la tabla impulsada t2, y este índice se usa en el proceso de unión, por lo que el flujo de ejecución de esta declaración es el siguiente:

  1. Leer una fila de datos R de la tabla t1 y recuperar el campo a de la fila de datos R para buscar en la tabla t2;
  2. Saque las filas que cumplen las condiciones en la tabla t2 y forme una fila con R como parte del conjunto de resultados;
  3. Repita los pasos 1 y 2 hasta que finalice el ciclo de la tabla t1.

En la forma, este proceso es similar a la consulta anidada cuando escribimos el programa, y ​​se puede usar el índice de la tabla conducida, por lo que lo llamamos "Index Nested-Loop Join", o NLJ para abreviar. Durante todo el proceso de ejecución, el número total de líneas de exploración es 200.

Suponiendo que la unión no se usa, solo podemos usar una consulta de tabla única:

1. Ejecute select * from t1 para encontrar todos los datos en la tabla t1, hay 100 filas;

2. Recorra estas 100 filas de datos:

  • Obtenga el valor del campo a $ Ra de cada fila R;
  • 执行 seleccione * de t2 donde a = $ Ra ;
  • El resultado devuelto y R forman una fila del conjunto de resultados.

En este proceso de consulta, se escanearon 200 filas, pero se ejecutaron un total de 101 sentencias de selección, que son 100 interacciones más que la combinación directa. Además, el cliente tiene que empalmar sentencias SQL y resultados por sí mismo. Obviamente, es mejor usar join.

Durante la ejecución de esta declaración de combinación, la tabla inicial es un escaneo de tabla completo y la tabla conducida es una búsqueda de árbol.

Suponiendo que el número de filas en la tabla impulsada es M , cada vez que verifica una fila de datos en la tabla impulsada, primero debe buscar en el índice a y luego buscar en el índice de clave principal. La complejidad aproximada de buscar un árbol cada vez es el logaritmo de M con la base de 2, registrado como log2M, por lo que la complejidad de tiempo de buscar una fila en la tabla conducida es 2 * log2M (búsqueda de índice normal + búsqueda de índice de clave primaria) .

Suponiendo que el número de filas en la tabla conductora es N , el proceso de ejecución debe escanear las N filas de la tabla conductora, y luego para cada fila, coincide una vez en la tabla conducida. Por lo tanto, la complejidad aproximada de todo el proceso de ejecución es N + N * 2 * log2M. Obviamente, N tiene un mayor impacto en el número de filas de escaneo, por lo que la tabla pequeña debe usarse como tabla de manejo .

A través del análisis anterior, obtenemos dos conclusiones:

  • Al usar la declaración de unión, el rendimiento es mejor que el rendimiento de dividir a la fuerza en varias tablas individuales para ejecutar declaraciones SQL;
  • Si usa la declaración de unión, debe convertir la mesa pequeña en la mesa de trabajo.

Sin embargo, debe tener en cuenta que la premisa de esta conclusión es que "se puede utilizar el índice de la tabla conducida".

Unión simple de bucle anidado

Cambiamos la instrucción SQL a esto: seleccione * de t1 straight_join t2 on (t1.a = t2.b);

Dado que no hay un índice en el campo b de la tabla t2, se requiere un escaneo completo de la tabla cada vez que se empareja t2. Si solo observa los resultados, este algoritmo es correcto, y este algoritmo también tiene un nombre llamado "Unión de bucle anidado simple".

De esta manera, esta solicitud SQL escaneará la tabla t2 hasta 1,000 veces y escaneará un total de 100 * 1000 = 100,000 filas. Si se trata de dos tablas grandes, este algoritmo parece demasiado "engorroso".

Por supuesto, MySQL no usa este algoritmo Simple Nested-Loop Join, pero usa otro algoritmo llamado "Block Nested-Loop Join", conocido como BNL.

Bloquear unión de bucle anidado

En este momento, no hay ningún índice disponible en la tabla conducida, y el flujo del algoritmo es así:

  • Lea los datos de la tabla t1 en la memoria de subprocesos join_buffer Dado que escribimos select * en esta declaración, colocamos toda la tabla t1 en la memoria;
  • Escanee la tabla t2, saque cada fila de la tabla t2, compárelas con los datos en join_buffer y devuelva las que cumplen las condiciones de unión como parte del conjunto de resultados.

Se puede ver que en este proceso, se realiza un escaneo de tabla completo en las tablas t1 y t2, por lo que el número total de filas escaneadas es 1100. Dado que join_buffer está organizado en una matriz desordenada, se requieren 100 juicios para cada fila en la tabla 2. El número total de juicios requeridos en la memoria es: 100 * 1000 = 100,000 veces.

En comparación con el anterior algoritmo simple de unión de bucle anidado, los 100.000 juicios de este algoritmo son operaciones de memoria, que son mucho más rápidas y tienen un mejor rendimiento.

Echemos un vistazo, en este caso, qué mesa debe seleccionarse como mesa de dirección. Suponiendo que el número de filas en la tabla pequeña es N y el número de filas en la tabla grande es M, entonces en este algoritmo:

  • Ambas tablas hacen un escaneo de tabla completo, por lo que el número total de filas escaneadas es M + N;
  • El número de juicios en la memoria es M * N.

Se puede ver que no hay diferencia entre M y N en estos dos cálculos, por lo que en este momento, ya sea para elegir una mesa grande o una mesa pequeña como mesa de conducción, el tiempo de ejecución es el mismo.

El tamaño de join_buffer lo establece el parámetro join_buffer_size, y el valor predeterminado es 256k. Si no puede poner todos los datos en la tabla t1, la estrategia es muy simple, que consiste en ponerlos en secciones. Cambié join_buffer_size a 1200 y luego lo ejecuté. El proceso de ejecución se convierte en:

  1. Escanee la tabla t1, lea las filas de datos secuencialmente y colóquelas en join_buffer. Después de colocar el join_buffer en la fila 88, continúe con el paso 2;
  2. Escanee la tabla t2, saque cada fila en t2, compárela con los datos en join_buffer y devuelva aquellos que cumplen las condiciones de unión como parte del conjunto de resultados;
  3. Borrar join_buffer;
  4. Continúe escaneando la tabla t1, lea secuencialmente las últimas 12 filas de datos en join_buffer y continúe con el paso 2.

Este proceso refleja el origen del "Bloque" en el nombre del algoritmo, que significa "unirse por bloque" . Puede verse que en este momento, debido a que la tabla t1 se divide en dos veces y se coloca en join_buffer, la tabla t2 se escaneará dos veces. Aunque join_buffer se divide en dos veces, el número de veces para determinar la condición de equivalencia permanece sin cambios, aún (88 + 12) * 1000 = 100,000 veces.

Veamos la selección de la tabla de controladores en este caso. Suponiendo que el número de filas de datos en la tabla impulsada es N, debe dividirse en K segmentos para completar el flujo del algoritmo, y el número de filas de datos en la tabla impulsada es M. Tenga en cuenta que K aquí no es una constante. Cuanto mayor sea N, mayor será K. Por lo tanto, K se expresa como λ * N, y el rango de valores de λ es (0,1). Por lo tanto, en la ejecución de este algoritmo: el número de líneas de exploración es N + λ * N * M; la memoria se juzga N * M veces .

Obviamente, el número de juicios de memoria no se ve afectado por qué mesa se selecciona como mesa de conducción. Teniendo en cuenta el número de líneas de exploración, cuando se determina el tamaño de M y N, N es menor y el resultado de todo el cálculo será menor. La mesa pequeña debe utilizarse como mesa de conducción .

Solo dijimos que cuanto mayor es N, mayor es el número de segmentos K. Entonces, cuando N es fijo, cuanto mayor sea el tamaño de join_buffer_size, menor será el número de segmentos y menos los escaneos de la tabla completa de la tabla conducida.

Por eso, es posible que vea algunas sugerencias que le indican que si su declaración de unión es muy lenta, aumente el tamaño de join_buffer_size .

Responde las dos preguntas iniciales

¿Puedo usar la declaración de unión?

  • Si se puede usar el algoritmo Index Nested-Loop Join, lo que significa que se puede usar el índice de la tabla conducida, en realidad no hay problema;
  • Si utiliza el algoritmo Block Nested-Loop Join, el número de líneas de exploración será demasiado. Especialmente para operaciones de unión en tablas grandes, esto puede tener que escanear la tabla conducida muchas veces, lo que consumirá una gran cantidad de recursos de CPU del sistema . Así que trate de no usar este tipo de unión.

Entonces, cuando decida si usar la declaración de combinación, simplemente mire el resultado de la explicación y vea si la palabra "Bloquear bucle anidado" aparece en el campo Extra .

Si desea utilizar join, ¿debería elegir una mesa grande como mesa de conducción o una mesa pequeña como mesa de conducción?

  • Si se trata del algoritmo Index Nested-Loop Join, se debe seleccionar una pequeña tabla como tabla inicial;
  • Si se trata del algoritmo Block Nested-Loop Join: cuando el join_buffer_size es lo suficientemente grande, es el mismo; cuando el join_buffer_size no es lo suficientemente grande (esta situación es más común), se debe seleccionar una tabla pequeña como la tabla inicial.

Por lo tanto, la conclusión de esta pregunta es que las tablas pequeñas siempre deben usarse como tablas de manejo .

¿Cómo juzgar quién es el "reloj pequeño" ?

1. seleccione * de t1 straight_join t2 en (t1.b = t2.b) donde t2.id <= 50;
    seleccione * de t2 straight_join t1 en (t1.b = t2.b) donde t2.id <= 50;

Tenga en cuenta que para evitar que las tablas controladas de las dos sentencias utilicen índices, los campos de combinación utilizan el campo b sin índice. Pero si se usa la segunda declaración, join_buffer solo necesita colocarse en las primeras 50 líneas de t2, lo que obviamente es mejor. Entonces, aquí, "las primeras 50 filas de t2" es la tabla relativamente pequeña, es decir, la "tabla pequeña".

2. seleccione t1.b, t2. * De t1 straight_join t2 en (t1.b = t2.b) donde t2.id <= 100;
    seleccione t1.b, t2. * de t2 straight_join t1 en (t1.b = t2.b) donde t2.id <= 100;

En este ejemplo, las tablas t1 y t2 tienen solo 100 filas que participan en la combinación. Sin embargo, los datos colocados en join_buffer en estas dos declaraciones son diferentes cada vez:

  • La tabla t1 solo verifica el campo b, por lo que si t1 se coloca en join_buffer, solo el valor de b debe colocarse en join_buffer;
  • La tabla t2 necesita verificar todos los campos, por lo que si coloca la tabla t2 en join_buffer, debe ingresar tres campos id, ay b.

Aquí, deberíamos elegir la tabla t1 como la tabla inicial, "sólo una columna de la tabla t1 que participa en la unión" es la tabla relativamente pequeña.

Por lo tanto, para ser más precisos, al decidir qué tabla será la tabla de manejo, las dos tablas deben filtrarse de acuerdo con sus respectivas condiciones. Una vez completado el filtrado, se calcula el volumen total de datos de cada campo que participa en la combinación, y la tabla con el volumen de datos más pequeño es Es la "tabla pequeña", que debe utilizarse como tabla de conducción.

Optimización MRR y optimización BKA

1. Optimización de lectura de rango múltiple (MRR). El objetivo principal de esta optimización es utilizar la lectura de disco secuencial tanto como sea posible.

Debido a que la mayoría de los datos se insertan en el orden creciente de la clave principal, podemos pensar que si la consulta está en el orden creciente de la clave principal, la lectura del disco está más cerca de la lectura secuencial, lo que puede mejorar el rendimiento de la lectura.

seleccione * de t1 donde a> = 1 y a <= 100; a es un índice normal. El flujo de ejecución de la declaración se convierte en este:

  1. De acuerdo con el índice a, ubique el registro que cumple la condición y coloque el valor de id en read_rnd_buffer;
  2. Ordene la identificación en read_rnd_buffer en orden ascendente;
  3. Después de ordenar la matriz de identificación, verifique los registros en el índice de identificación de la clave principal a su vez y regrese como resultado.

Aquí, el tamaño de read_rnd_buffer está controlado por el parámetro read_rnd_buffer_size. Si read_rnd_buffer está lleno en el paso 1, los pasos 2 y 3 se ejecutarán primero, y luego se borrará read_rnd_buffer. Luego continúe buscando el siguiente registro del índice a y continúe con el bucle.

Otra cosa a tener en cuenta es que si desea utilizar la optimización MRR de manera constante, debe configurar set optimizer_switch = "mrr_cost_based = off" (el documento oficial dice que es la estrategia actual del optimizador. Al juzgar el consumo, estará más inclinado no usar MRR).

Si del resultado de la explicación, podemos ver que el campo Extra tiene más Uso de MRR, lo que significa que se usa la optimización de MRR.

Resumen: El núcleo de que MRR puede mejorar el rendimiento es que esta declaración de consulta es una consulta de rango o una consulta de valores múltiples en el índice a, que puede obtener suficientes ID de clave primaria, de modo que después de ordenar, vaya al índice de clave principal para verificar el datos Refleja la ventaja de la "secuencia".

2. MySQL comenzó a introducir el algoritmo Batched Key Access (BKA) después de la versión 5.6. Este algoritmo BKA es en realidad una optimización del algoritmo NLJ.

Porque, la lógica ejecutada por el algoritmo NLJ es: buscar valores fila por fila de la tabla conducida y luego unirse a la tabla conducida. En otras palabras, para la tabla impulsada, se hace coincidir un valor cada vez. En este momento, no se utilizarán las ventajas de MRR. Además, sabemos que la función de join_buffer en el algoritmo BNL es almacenar temporalmente los datos de la tabla de unidades. Pero no es útil en el algoritmo NLJ. Luego, podemos simplemente reutilizar join_buffer en el algoritmo BKA.

Si desea utilizar el algoritmo de optimización BKA, debe establecer antes de ejecutar la instrucción SQL:

set optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';

Entre ellos, la función de los dos primeros parámetros es habilitar MRR. La razón de esto es que la optimización del algoritmo BKA depende de MRR.

Problemas de rendimiento y optimización del algoritmo BNL

Cuando se utiliza el algoritmo Block Nested-Loop Join (BNL), la tabla conducida se puede escanear varias veces. Si la mesa impulsada es una gran mesa de datos fría, además de causar una alta presión IO, ¿qué impacto tendrá en el sistema?

Como se mencionó anteriormente, InnoDB optimizó el algoritmo LRU de Bufffer Pool. Sin embargo, si una declaración de unión que utiliza el algoritmo BNL escanea una tabla fría varias veces y el tiempo de ejecución de la declaración excede 1 segundo, las páginas de datos de la tabla fría se moverán al encabezado de la lista enlazada LRU cuando la tabla fría se escanea de nuevo. Esta situación corresponde a la situación en la que el volumen de datos de la mesa fría es inferior a 3/8 de todo el Buffer Pool y se puede colocar completamente en el área anterior.

Si la mesa fría es muy grande, se producirá otra situación: la página de datos a la que normalmente accede la empresa no tiene posibilidad de entrar en la zona joven. Porque es necesario volver a acceder a la página de datos a la que se accede normalmente después de 1 segundo para entrar en el área joven. Sin embargo, debido a que nuestra declaración de unión lee el disco en un bucle y elimina las páginas de memoria, es probable que las páginas de datos que ingresan al área anterior se eliminen en 1 segundo. De esta manera, las páginas de datos en el área joven del Buffer Pool de esta instancia de MySQL no se eliminan razonablemente durante este período.

Aunque la operación de unión de tablas grandes tiene un impacto en IO, el impacto en IO terminará después de que se ejecute la instrucción. Sin embargo, el impacto en el grupo de búfer es continuo y requiere solicitudes de consulta posteriores para restaurar lentamente la tasa de aciertos de la memoria.

En resumen, el impacto del algoritmo BNL en el sistema incluye principalmente tres aspectos:

  • La tabla conducida se puede escanear varias veces, ocupando recursos de E / S del disco;
  • Para determinar la condición de unión, necesita realizar comparaciones M * N (M y N son el número de filas en las dos tablas respectivamente) Si es una tabla grande, consumirá muchos recursos de CPU;
  • Puede hacer que se eliminen los datos calientes del Buffer Pool y afectar la tasa de aciertos de la memoria.

Antes de ejecutar la declaración, debemos confirmar si usamos el algoritmo BNL a través del análisis teórico y ver los resultados de la explicación. Si se confirma que el optimizador utilizará el algoritmo BNL, se necesita optimización. El método de optimización común es agregar un índice al campo de combinación de la tabla conducida para convertir el algoritmo BNL al algoritmo BKA.

Si esta declaración es una declaración SQL de baja frecuencia al mismo tiempo, sería un desperdicio crear un índice en el campo b de la tabla t2 para esta declaración. Sin embargo, si usa el algoritmo BNL para unirse, como se mencionó anteriormente, el algoritmo BNL debe juzgarse en la memoria, lo que aumentará considerablemente con la tabla grande, y el tiempo es demasiado lento.

En este momento, podemos considerar el uso de tablas temporales. La idea general de usar tablas temporales es:

  • Primero coloque los datos que cumplan con las condiciones en la tabla grande en la tabla temporal;
  • Para permitir que join use el algoritmo BKA, agregue índices a los campos de la tabla temporal;
  • Deje que la mesa de conducción y la mesa temporal hagan una operación conjunta.

Esto reduce la cantidad de juicios de memoria al escanear una tabla grande.

En general, ya sea agregando un índice en la tabla original o usando una tabla temporal indexada, nuestra idea es permitir que la declaración de unión use el índice en la tabla conducida para activar el algoritmo BKA y mejorar el rendimiento de la consulta.

Unión de 1 hash de extensión

Si lo que se mantiene en join_buffer no es una matriz desordenada sino una tabla hash, entonces una gran cantidad de juicios se convierte en una pequeña cantidad de búsquedas hash y la velocidad de ejecución de toda la declaración es mucho más rápida. Dado que la versión actual de MySQL no es compatible con la combinación de hash, puede ser simulada por el lado de la aplicación. Teóricamente, el efecto es mejor que la solución de tabla temporal.

El proceso de implementación es aproximadamente el siguiente:

  • seleccione * de t1; Obtenga todos los datos de fila de la tabla pequeña t1 y almacene una estructura hash en el lado comercial, como estructuras de datos como las establecidas en C ++ y la matriz en PHP.
  • seleccione * de t2 donde ... Obtenga varias filas de datos en la tabla t2 que cumplan las condiciones. Lleve estas múltiples filas de datos línea por línea hasta el final del negocio y busque datos coincidentes en la tabla de datos de estructura hash. La fila de datos que cumple las condiciones de coincidencia se considera una fila del conjunto de resultados.

Unión de mesa múltiple de extensión 2

如 : seleccione * de t1  unirse t2 en  (t1.a = t2.a)  unirse t3 en  (t2.b = t3.b)  donde   t1.c> = X y t2.c> = Y y t3.c> = Z;

Si se reescribe como straight_join, cómo especificar el orden de conexión y cómo crear índices para las tres tablas.

El primer principio es utilizar el algoritmo BKA tanto como sea posible . Cabe señalar que cuando se utiliza el algoritmo BKA, no se trata de "calcular primero el resultado de la unión de dos tablas y luego unir la tercera tabla", sino que anida directamente la consulta.

La implementación específica es: entre las tres condiciones t1.c> = X, t2.c> = Y, t3.c> = Z, seleccione la tabla con la menor cantidad de datos después de filtrar como la primera tabla de conducción . En este momento, pueden ocurrir las dos situaciones siguientes.

En el primer caso, si se selecciona la tabla t1 o t3, la parte restante es fija.

  • Si la mesa conductora es t1, la secuencia de conexión es t1-> t2-> t3, y el índice debe crearse en el campo de la mesa conducida, es decir, t2.a y t3.b;
  • Si la tabla de conducción es t3, la secuencia de conexión es t3-> t2-> t1 y es necesario crear índices en t2.by t1.a.

Al mismo tiempo, también necesitamos crear un índice en el campo c de la primera tabla de manejo.

El segundo caso es que si la primera mesa de conducción seleccionada es la mesa t2, es necesario evaluar el efecto de filtrado de las otras dos condiciones.

En definitiva, la idea general es intentar que el conjunto de datos de la mesa de conducción que participa en la unión sea lo más pequeño posible, porque de esta forma nuestra mesa de conducción será más pequeña.

 

Fuente del contenido: Lin Xiaobin "45 conferencias sobre combate real de MySQL"

 

 

Supongo que te gusta

Origin blog.csdn.net/qq_24436765/article/details/112857658
Recomendado
Clasificación