Siempre pienso que en, existe no indexa, y es despreciado por mis compañeros ...

Prefacio

Recientemente, hubo un requisito comercial. Dame una copia de los datos A y calcule la parte que existe en la base de datos B pero es más que A. Debido a los datos desordenados, simplifiqué el modelo aquí.

Entonces encontrarás, voy, esto no está en, no existe.

Entonces, la pregunta es, ¿cuál es la diferencia entre en, no en, existe, no existe y qué tan eficiente es?

Una vez escuché en Internet que en y existe no se indexará. ¿Es este realmente el caso?

Con dudas, seguimos estudiando.

Nota: Cuando se habla de este problema, no significa que la versión de MySQL sea falsa, yo uso 5.7.18 aquí.

 

Explicación de uso

Por conveniencia, creamos dos tablas t1 y t2. Y agregue algunos datos por separado. (Id es la clave principal, el nombre es el índice ordinario)

-- t1
DROP TABLE IF EXISTS `t1`;
CREATE TABLE `t1` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `address` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `idx_t1_name` (`name`(191)) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1009 DEFAULT CHARSET=utf8mb4;

INSERT INTO `t1` VALUES ('1001', '张三', '北京'), ('1002', '李四', '天津'), ('1003', '王五', '北京'), ('1004', '赵六', '河北'), ('1005', '杰克', '河南'), ('1006', '汤姆', '河南'), ('1007', '贝尔', '上海'), ('1008', '孙琪', '北京');

-- t2
DROP TABLE IF EXISTS `t2`;
CREATE TABLE `t2`  (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  `address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE,
  INDEX `idx_t2_name`(`name`(191)) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1014 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;

INSERT INTO `t2` VALUES (1001, '张三', '北京');
INSERT INTO `t2` VALUES (1004, '赵六', '河北');
INSERT INTO `t2` VALUES (1005, '杰克', '河南');
INSERT INTO `t2` VALUES (1007, '贝尔', '上海');
INSERT INTO `t2` VALUES (1008, '孙琪', '北京');
INSERT INTO `t2` VALUES (1009, '曹操', '魏国');
INSERT INTO `t2` VALUES (1010, '刘备', '蜀国');
INSERT INTO `t2` VALUES (1011, '孙权', '吴国');
INSERT INTO `t2` VALUES (1012, '诸葛亮', '蜀国');
INSERT INTO `t2` VALUES (1013, '典韦', '魏国');
Entonces, para el problema actual, es muy simple: puede usar not in o no existe para seleccionar la parte de los datos en la tabla t1 que es mayor que la tabla t2. (Por supuesto, no se cuenta la parte de t2 mayor que t1)

Se asume aquí que el nombre se usa para hacer coincidir los datos.

select * from t1 where name not in (select name from t2);
或者用
select * from t1 where not exists (select name from t2 where t1.name=t2.name);

Los resultados son los mismos.

Sin embargo, cabe señalar que aún existen diferencias entre no in y no existe.

Cuando use not in, debe asegurarse de que el campo coincidente de la subconsulta no esté vacío. Por ejemplo, el nombre de esta tabla t2 no debe estar vacío. De lo contrario, hará que todo el conjunto de resultados devuelto por not in esté vacío.

Por ejemplo, agrego un dato con un nombre vacío a la tabla t2.

INSERT INTO `t2` VALUES (1014, NULL, '魏国');

En este momento, el resultado de no volverá vacío.

Otra cosa que hay que entender es que el resultado devuelto por existe es un valor booleano de verdadero o falso, no un conjunto de resultados.

Porque no le importa cuáles son los datos específicos devueltos, pero la consulta externa necesita obtener este booleano Sí. Cuando se usa existe, si la subconsulta encuentra los datos, devuelve verdadero. Cuando use no existe, juzgue si la subconsulta no encuentra el valor.

Diferenciar datos, devuelve verdadero.

Porque a la subconsulta existente no le importa cuáles son los datos específicos devueltos. Por lo tanto, la declaración anterior se puede modificar completamente de la siguiente manera,

-- 子查询中 name 可以修改为其他任意的字段,如此处改为 1 。
select * from t1 where not exists (select 1 from t2 where t1.name=t2.name);

En términos de eficiencia de ejecución, 1> columna> *. Por lo tanto, se recomienda seleccionar 1. (Para ser precisos, debe ser un valor constante)

 

en, existe proceso de ejecución

1. Para la consulta in, la subconsulta se ejecuta primero, como la tabla t2 anterior, y luego el resultado de la consulta y la tabla externa t1 se toman como un producto cartesiano y luego se filtran por la condición (la condición aquí se refiere a si el nombre es igual), Agregue todos los datos elegibles al conjunto de resultados.

sql es el siguiente,

select * from t1 where name in (select name from t2);

El pseudocódigo es el siguiente:

for(x in A){
    for(y in B){
     if(condition is true) {result.add();}
    }
}

La condición aquí es realmente comparar si los nombres de las dos tablas son iguales.

2. Para existe, primero debe consultar y recorrer la tabla externa t1, y luego, cada vez que atraviesa, verificar si la tabla interna cumple las condiciones de coincidencia, es decir, verificar si hay datos con el mismo nombre.

sql es el siguiente,

select * from t1 where name exists (select 1 from t2);

El pseudocódigo es el siguiente:

for(x in A){
  if(exists condition is true){result.add();}
}

De acuerdo con este ejemplo, es atravesar la tabla t1 desde el id de 1001, y luego verificar si hay un nombre igual en t2 durante el recorrido.

Por ejemplo, cuando id = 1001, Zhang San existe en la tabla t2, devuelve verdadero, agrega el registro de Zhang San en t1 al conjunto de resultados y continúa con el siguiente ciclo. Cuando id = 1002, si Li Si no está en la tabla t2, se devuelve falso, no se realiza ninguna operación y continúa el siguiente ciclo. Hasta que se atraviese la tabla t1 completa.

 

¿Tomar el índice?

El in y existe no están indexados en Internet, entonces, ¿es cierto?

Verifiquémoslo en MySQL 5.7.18. (Preste atención al número de versión)

Consulta de tabla única

Primero, verifique el caso más simple de una sola tabla. Tomemos la tabla t1 como ejemplo, con id como clave principal y name como índice normal.

Ejecute las siguientes declaraciones respectivamente,

explain select * from t1 where id in (1001,1002,1003,1004);
explain select * from t1 where id in (1001,1002,1003,1004,1005);
explain select * from t1 where name in ('张三','李四');
explain select * from t1 where name in ('张三','李四','王五');

¿Por qué necesito comprobar la cantidad de ID diferentes por separado? Mira la captura de pantalla,

Se sorprenderá al descubrir que cuando id tiene cuatro valores, también se toma el índice de clave principal. Cuando id es de cinco valores, no se toma ningún índice. Esto es muy intrigante.

Mira la situación del nombre de nuevo

Una vez finalizada la misma tarea, no se tomará el índice.

Entonces, supongo que esto está relacionado con la longitud del campo coincidente. Según el cálculo de los caracteres chinos son tres bytes, y al diseño del programa le gusta usar la potencia de 2 a la potencia de n, aquí hay probablemente 16 bytes como punto de demarcación.

Sin embargo, utilicé los mismos datos para consultar en mi servidor (número de versión 5.7.22), y cuando encontré cuatro valores de identificación, no usé el índice. Por lo tanto, el valor crítico aquí se estima en 12 bytes.

En cualquier caso, esto muestra que debería haber un límite en la longitud de bytes de la consulta en MySQL. (No hay una declaración oficial exacta, por lo tanto, solo como referencia)

Varias tablas involucran subconsultas

Principalmente, observamos si en y existe están indexados al consultar las dos tablas en el ejemplo actual.

1. Ejecute las siguientes declaraciones respectivamente, el índice de clave principal (id) y el índice ordinario (nombre), ya sea para usar el índice bajo en, no en.

explain select * from t1 where id in (select id from t2); --1
explain select * from t1 where name in (select name from t2); --2
explain select * from t1 where id not in (select id from t2); --3
explain select * from t1 where name not in (select name from t2); --4

La captura de pantalla del resultado es la siguiente,

1. Utilice el índice en t1 y el índice en t2.

1

2. T1 no usa índice y t2 no usa índice. (En este caso, si el nombre se cambia a un índice único en la medición real, t1 también irá al índice)

2

3. T1 no usa índice, t2 usa índice.

3

4. T1 no usa índice, t2 no usa índice.

4

Caigo al cielo, este resultado se ve desordenado, parece que no puedo caminar por el índice, depende del estado de ánimo.

Sin embargo, encontramos que solo en el primer caso, es decir, el campo de índice de clave principal coincide, y en el caso de in, las dos tablas están indexadas.

¿Es esto regular? Para ser investigado, miremos hacia abajo.

En segundo lugar, la siguiente prueba, el índice de clave principal y el índice ordinario en existe y no existe. sql es el siguiente,

explain select * from t1 where exists (select 1 from t2 where t1.id=t2.id);
explain select * from t1 where exists (select 1 from t2 where t1.name=t2.name);
explain select * from t1 where not exists (select 1 from t2 where t1.id=t2.id);
explain select * from t1 where not exists (select 1 from t2 where t1.name=t2.name);

Este resultado es muy regular, veamos,

¿Ha descubierto que la tabla t1 no se indexará en ningún caso, y la tabla t2 se indexará si hay un índice? ¿Por qué pasó esto?

De hecho, el apartado anterior hablaba del proceso de ejecución de existe, que ya explicaba el problema.

Es la mesa exterior como la mesa de conducción, la atravesará de todos modos, por lo que escaneará toda la mesa. La tabla interna puede determinar rápidamente si el registro actual coincide con el índice.

 

¿Qué tan eficiente es?

En vista del hecho de que existe en Internet debe ser más eficiente que en, haremos una prueba.

Inserte datos de 100W y 200W en t1 y t2 respectivamente.

Aquí, uso una función personalizada para insertar en un bucle, la referencia de la declaración es la siguiente (el nombre de la tabla no está separado en una variable, porque no encontré una manera, vergonzoso)

-- 传入需要插入数据的id开始值和数据量大小,函数返回结果为最终插入的条数,此值正常应该等于数据量大小。
-- id自增,循环往 t1 表添加数据。这里为了方便,id、name取同一个变量,address就为北京。
delimiter // 
drop function if exists insert_datas1//
create function insert_datas1(in_start int(11),in_len int(11)) returns int(11)
begin  
  declare cur_len int(11) default 0;
  declare cur_id int(11);
  set cur_id = in_start;
 
  while cur_len < in_len do
     insert into t1 values(cur_id,cur_id,'北京');
  set cur_len = cur_len + 1;
  set cur_id = cur_id + 1;
  end while; 
  return cur_len;
end  
//
delimiter ;
-- 同样的,往 t2 表插入数据
delimiter // 
drop function if exists insert_datas2//
create function insert_datas2(in_start int(11),in_len int(11)) returns int(11)
begin  
  declare cur_len int(11) default 0;
  declare cur_id int(11);
  set cur_id = in_start;
 
  while cur_len < in_len do
     insert into t2 values(cur_id,cur_id,'北京');
  set cur_len = cur_len + 1;
  set cur_id = cur_id + 1;
  end while; 
  return cur_len;
end  
//
delimiter ;

Antes de eso, primero borre los datos de la tabla y luego ejecute la función,

select insert_datas1(1,1000000);

Haga lo mismo para t2, pero para que las dos tablas se superpongan, comience con 70W y luego inserte los datos de 200W.

select insert_datas2(700000,2000000);

El tiempo de ejecución real de la computadora en casa es de 36 segundos y 74 segundos, respectivamente.

Por alguna razón, la computadora en casa no es tan rápida como el script que se ejecuta en la máquina virtual Docker. . Daño, solo arreglártelas.

Cuando tenga dinero nuevo, lo cambiaré, tararear.

Del mismo modo, ejecute todos los planes de ejecución anteriores y compárelos. No publicaré fotos aquí.

en y existe, que es más rápido o más lento

Para mayor comodidad, tome principalmente los siguientes dos SQL para comparar y analizar.

select * from t1 where id in (select id from t2);
select * from t1 where exists (select 1 from t2 where t1.id=t2.id);

El resultado de la ejecución muestra que los dos sql ejecutan 1.3s y 3.4s respectivamente.

Tenga en cuenta que en este momento, el volumen de datos de la tabla t1 es 100W y el volumen de datos de la tabla t2 es 200W.

Según el dicho popular en Internet sobre la diferencia entre en y existe,

Si las dos tablas que se van a consultar son del mismo tamaño, entonces la diferencia entre in y existe no es grande; si una de las dos tablas es más pequeña y la otra es más grande, los usos de la tabla de subconsultas existen para la tabla de subconsultas más grande, y se usa in para la tabla de subconsultas pequeña;

Correspondiente a aquí es:

  • Cuando t1 es una tabla pequeña y t2 es una tabla grande, se debe usar existe, que es más eficiente.

  • Cuando t1 es una tabla grande y t2 es una tabla pequeña, se debe usar in, que es más eficiente.

Pero lo probé con datos reales y anulé la primera afirmación. Porque es obvio que t1 es una tabla pequeña, pero la velocidad de ejecución de in es más rápida que la existente.

Para continuar probando este punto de vista, cambié la relación entre la tabla interna y la tabla externa de las dos tablas, y dejé la tabla t2 como la tabla externa para comparar y consultar.

select * from t2 where id in (select id from t1);
select * from t2 where exists (select 1 from t1 where t1.id=t2.id);

El resultado de la ejecución muestra que los dos sql ejecutan 1.8s y 10.0s respectivamente.

¿No es interesante? se puede descubrir,

  • Por ejemplo, la relación entre la mesa grande y la mesa pequeña se intercambia entre las capas interior y exterior, y el tiempo de ejecución no es muy diferente. Uno es de 1,3 sy el otro de 1,8 s.

  • Pues existe, las tablas grandes y pequeñas han intercambiado las relaciones internas y externas, y el tiempo de ejecución es muy diferente, una es de 3.4s y la otra de 10.0s, que es el doble de lento.

1. Compare con las dimensiones del optimizador de consultas.

Para explorar las razones de este resultado. Fui a verificar el sql después de que fueron optimizados en el optimizador de consultas.

Con select * from t1 where id in (select id from t2);, por ejemplo, el orden de las dos siguientes declaraciones.

-- 此为 5.7 写法,如果是 5.6版本,需要用 explain extended ...
explain select * from t1 where id in (select id from t2);
-- 本意为显示警告信息。但是和 explain 一块儿使用,就会显示出优化后的sql。需要注意使用顺序。
show warnings;

La declaración que queremos se mostrará en el mensaje de resultado.

-- message 优化后的sql
select `test`.`t1`.`id` AS `id`,`test`.`t1`.`name` AS `name`,`test`.`t1`.`address` AS `address` from `test`.`t2` join `test`.`t1` where (`test`.`t2`.`id` = `test`.`t1`.`id`)

Se puede encontrar que aquí se convierte para unirse para su ejecución.

Aquí, on no se usa, pero where se usa, porque cuando solo hay unir, el siguiente on se puede reemplazar por where. Es decir, unirse es equivalente a unirse dónde.

PD:  Aquí también podemos encontrar que select * eventualmente se convertirá en campos específicos. Sepa por qué no recomendamos usar select *.

De manera similar, para consultas que usan la tabla t2 grande como tabla externa, verifique la declaración optimizada.

explain select * from t2 where id in (select id from t1);
show warnings;

Descubriremos que también se transformará en join.

select `test`.`t2`.`id` AS `id`,`test`.`t2`.`name` AS `name`,`test`.`t2`.`address` AS `address` from `test`.`t1` join `test`.`t2` where (`test`.`t2`.`id` = `test`.`t1`.`id`)

La conversión sql de existe ya no se publica aquí, de hecho, no ha cambiado mucho.

2. Compare con la dimensión del plan de ejecución.

Luego comparamos sus diferencias con la dimensión del plan de ejecución.

explain select * from t1 where id in (select id from t2);
explain select * from t2 where id in (select id from t1);
explain select * from t1 where exists (select 1 from t2 where t1.id=t2.id);
explain select * from t2 where exists (select 1 from t1 where t1.id=t2.id);

Los resultados de la ejecución son respectivamente,

1
2
3
4

Se puede encontrar que para in, ya sea que la tabla grande t2 se use como tabla externa o interna, se indexará, y cuando la tabla pequeña t1 se use como tabla interna, también se indexará. Si observa la columna de filas, también puede ver que los resultados de las dos primeras imágenes son los mismos.

Para existe, cuando la tabla pequeña t1 se usa como tabla externa, el escaneo de la tabla completa de t1 está cerca de 100W; cuando la tabla grande t2 se usa como la tabla externa, el escaneo de la tabla completa de t2 está cerca de 200W. Esta es también la razón por la cual la eficiencia de ejecución de t2 es muy baja cuando hace su aparición.

Debido a que para existe, la tabla externa siempre realizará un escaneo completo de la tabla, por supuesto, cuantos menos datos de la tabla, mejor.

Conclusión final:  utilícelo para la mesa grande exterior y la mesa pequeña interior. En la mesa pequeña exterior y la mesa grande interior, en y existe tienen aproximadamente la misma eficiencia (incluso en es más rápido que existe, y no se dice que existe en Internet es más eficiente que en).

no está y no existe, que es más rápido y más lento

Además, la medición real no se compara en y no existe.

explain select * from t1 where id not in (select id from t2);
explain select * from t1 where not exists (select 1 from t2 where t1.id=t2.id);
explain select * from t1 where name not in (select name from t2);
explain select * from t1 where not exists (select 1 from t2 where t1.name=t2.name);

explain select * from t2 where id not in (select id from t1);
explain select * from t2 where not exists (select 1 from t1 where t1.id=t2.id);
explain select * from t2 where name not in (select name from t1);
explain select * from t2 where not exists (select 1 from t1 where t1.name=t2.name);

En el caso de las mesas pequeñas por apariencia. Para la clave principal, no existe es más rápido que no en. Para los índices ordinarios, no en y no existe no son muy diferentes, incluso no en será un poco más rápido.

En el caso de tablas grandes como tablas externas, no es más rápido que no existe para claves primarias. Para los índices ordinarios, no en y no existe no son muy diferentes, incluso no en será un poco más rápido.

Los estudiantes interesados ​​pueden probarlo por sí mismos. Compare las dos dimensiones anteriores (optimizador de consultas y plan de ejecución) respectivamente.

Unión de bucle anidado de unión

Para entender por qué aquí se convierte en join, creo que es necesario comprender las tres combinaciones de bucle anidado de join.

1. Unión simple de bucle anidado, SNLJ para abreviar

Join es unión interna, unión interna, es un producto cartesiano, que utiliza un bucle de doble capa para atravesar dos tablas.

Sabemos que las tablas pequeñas se usan generalmente como tablas de manejo en sql. Por lo tanto, para las dos tablas A y B, si el conjunto de resultados de A es menor, colóquelo en el bucle exterior como tabla de conducción. Naturalmente, B circula en la capa interior como mesa conducida.

Los bucles anidados simples son el caso más simple, sin ninguna optimización.

Por lo tanto, la complejidad también es la más alta, O (mn). El pseudocódigo es el siguiente,

for(id1 in A){
    for(id2 in B){
        if(id1==id2){
            result.add();
        }
    }
}

2. Index Nested-Loop Join, INLJ para abreviar

Se puede ver en el nombre, que coincide con el índice. La tabla exterior coincide directamente con el índice de la tabla interior, por lo que no es necesario recorrer toda la tabla interior. El uso de índices reduce el número de coincidencias entre la tabla exterior y la tabla interior.

Por lo tanto, esta situación requiere un índice en las columnas de la tabla interna.

El pseudocódigo es el siguiente,

for(id1 in A){
    if(id1 matched B.id){
        result.add();
    }
}

3. Bloquear combinación de bucle anidado, denominado BNLJ

La conexión anidada de índice de bloque es almacenar en caché los datos de la tabla externa en el búfer de unión, y luego los datos en el búfer se comparan con los datos de la tabla interna en lotes, reduciendo así el número de bucles internos.

Tome el bucle de la capa externa 100 veces como ejemplo. Normalmente, el bucle de la capa interna necesita leer los datos de la capa externa 100 veces. Si cada 10 datos se almacenan en el búfer de caché y se pasan al bucle interno, el bucle interno solo necesita leerse 10 veces (100/10). Esto reduce el número de lecturas en el bucle interno.

La documentación oficial de MySQL también tiene instrucciones relacionadas, puede consultar: https://dev.mysql.com/doc/refman/5.7/en/nested-loop-joins.html#block-nested-loop-join-algorithm

Por lo tanto, esto se convierte en join, que se puede utilizar para indexar la conexión de bucle anidado, mejorando así la eficiencia de ejecución.

Descargo de responsabilidad: lo anterior se basa en los datos de prueba del autor y los resultados medidos. Es probable que los datos reales y los resultados de las pruebas sean diferentes.

No hay manera, pero la técnica se puede lograr; si no hay manera, termina con la técnica.

Bienvenidos a todos a seguir la cuenta pública de Java Way

Buen artículo, estoy leyendo ❤️

Supongo que te gusta

Origin blog.csdn.net/hollis_chuang/article/details/108613056
Recomendado
Clasificación