sql >> Base de Datos >  >> RDS >> MariaDB

Maximización de la eficiencia de las consultas de la base de datos para MySQL:segunda parte

Esta es la segunda parte de una serie de blogs de dos partes para maximizar la eficiencia de las consultas de bases de datos en MySQL. Puedes leer la primera parte aquí.

Uso de columna única, compuesto, prefijo e índice de cobertura

Las tablas que reciben mucho tráfico con frecuencia deben indexarse ​​correctamente. No solo es importante indexar su tabla, sino que también debe determinar y analizar cuáles son los tipos de consultas o tipos de recuperación que necesita para la tabla específica. Se recomienda enfáticamente que analice qué tipo de consultas o recuperación de datos necesita en una tabla específica antes de decidir qué índices se requieren para la tabla. Repasemos estos tipos de índices y cómo puede usarlos para maximizar el rendimiento de sus consultas.

Índice de una sola columna

La tabla InnoD puede contener un máximo de 64 índices secundarios. Un índice de una sola columna (o índice de columna completa) es un índice asignado solo a una columna en particular. Crear un índice para una columna en particular que contenga valores distintos es un buen candidato. Un buen índice debe tener una alta cardinalidad y estadísticas para que el optimizador pueda elegir el plan de consulta adecuado. Para ver la distribución de índices, puede verificar con la sintaxis MOSTRAR ÍNDICES como se muestra a continuación:

root[test]#> SHOW INDEXES FROM users_account\G

*************************** 1. row ***************************

        Table: users_account

   Non_unique: 0

     Key_name: PRIMARY

 Seq_in_index: 1

  Column_name: id

    Collation: A

  Cardinality: 131232

     Sub_part: NULL

       Packed: NULL

         Null: 

   Index_type: BTREE

      Comment: 

Index_comment: 

*************************** 2. row ***************************

        Table: users_account

   Non_unique: 1

     Key_name: name

 Seq_in_index: 1

  Column_name: last_name

    Collation: A

  Cardinality: 8995

     Sub_part: NULL

       Packed: NULL

         Null: 

   Index_type: BTREE

      Comment: 

Index_comment: 

*************************** 3. row ***************************

        Table: users_account

   Non_unique: 1

     Key_name: name

 Seq_in_index: 2

  Column_name: first_name

    Collation: A

  Cardinality: 131232

     Sub_part: NULL

       Packed: NULL

         Null: 

   Index_type: BTREE

      Comment: 

Index_comment: 

3 rows in set (0.00 sec)

También puede inspeccionar con las tablas information_schema.index_statistics o mysql.innodb_index_stats.

Índices compuestos (compuestos) o de varias partes

Un índice compuesto (comúnmente llamado índice compuesto) es un índice de varias partes compuesto por varias columnas. MySQL permite hasta 16 columnas limitadas para un índice compuesto específico. Superar el límite devuelve un error como el siguiente:

ERROR 1070 (42000): Too many key parts specified; max 16 parts allowed

Un índice compuesto proporciona un impulso a sus consultas, pero requiere que tenga una comprensión pura de cómo está recuperando los datos. Por ejemplo, una tabla con un DDL de...

CREATE TABLE `user_account` (

  `id` int(11) NOT NULL AUTO_INCREMENT,

  `last_name` char(30) NOT NULL,

  `first_name` char(30) NOT NULL,

  `dob` date DEFAULT NULL,

  `zip` varchar(10) DEFAULT NULL,

  `city` varchar(100) DEFAULT NULL,

  `state` varchar(100) DEFAULT NULL,

  `country` varchar(50) NOT NULL,

  `tel` varchar(16) DEFAULT NULL

  PRIMARY KEY (`id`),

  KEY `name` (`last_name`,`first_name`)

) ENGINE=InnoDB DEFAULT CHARSET=latin1

...que consta del índice compuesto `nombre`. El índice compuesto mejora el rendimiento de las consultas una vez que se hace referencia a estas claves como partes clave usadas. Por ejemplo, vea lo siguiente:

root[test]#> explain format=json select * from users_account where last_name='Namuag' and first_name='Maximus'\G

*************************** 1. row ***************************

EXPLAIN: {

  "query_block": {

    "select_id": 1,

    "cost_info": {

      "query_cost": "1.20"

    },

    "table": {

      "table_name": "users_account",

      "access_type": "ref",

      "possible_keys": [

        "name"

      ],

      "key": "name",

      "used_key_parts": [

        "last_name",

        "first_name"

      ],

      "key_length": "60",

      "ref": [

        "const",

        "const"

      ],

      "rows_examined_per_scan": 1,

      "rows_produced_per_join": 1,

      "filtered": "100.00",

      "cost_info": {

        "read_cost": "1.00",

        "eval_cost": "0.20",

        "prefix_cost": "1.20",

        "data_read_per_join": "352"

      },

      "used_columns": [

        "id",

        "last_name",

        "first_name",

        "dob",

        "zip",

        "city",

        "state",

        "country",

        "tel"

      ]

    }

  }

}

1 row in set, 1 warning (0.00 sec

Las used_key_parts muestran que el plan de consulta ha seleccionado perfectamente nuestras columnas deseadas cubiertas en nuestro índice compuesto.

La indexación compuesta también tiene sus limitaciones. Ciertas condiciones en la consulta no pueden tomar todas las columnas como parte de la clave.

La documentación dice:"El optimizador intenta usar partes clave adicionales para determinar el intervalo siempre que el operador de comparación sea =, <=> o ES NULL. Si el operador es> , <,>=, <=, !=, <>, BETWEEN o LIKE, el optimizador lo usa pero no considera más partes clave. Para la siguiente expresión, el optimizador usa =de la primera comparación. También usa>=de la segunda comparación, pero no considera más partes clave y no usa la tercera comparación para la construcción de intervalos..." . Básicamente, esto significa que independientemente de que tenga un índice compuesto para dos columnas, una consulta de muestra a continuación no cubre ambos campos:

root[test]#> explain format=json select * from users_account where last_name>='Zu' and first_name='Maximus'\G

*************************** 1. row ***************************

EXPLAIN: {

  "query_block": {

    "select_id": 1,

    "cost_info": {

      "query_cost": "34.61"

    },

    "table": {

      "table_name": "users_account",

      "access_type": "range",

      "possible_keys": [

        "name"

      ],

      "key": "name",

      "used_key_parts": [

        "last_name"

      ],

      "key_length": "60",

      "rows_examined_per_scan": 24,

      "rows_produced_per_join": 2,

      "filtered": "10.00",

      "index_condition": "((`test`.`users_account`.`first_name` = 'Maximus') and (`test`.`users_account`.`last_name` >= 'Zu'))",

      "cost_info": {

        "read_cost": "34.13",

        "eval_cost": "0.48",

        "prefix_cost": "34.61",

        "data_read_per_join": "844"

      },

      "used_columns": [

        "id",

        "last_name",

        "first_name",

        "dob",

        "zip",

        "city",

        "state",

        "country",

        "tel"

      ]

    }

  }

}

1 row in set, 1 warning (0.00 sec)

En este caso (y si su consulta es más de rangos en lugar de constantes o tipos de referencia), evite usar índices compuestos. Simplemente desperdicia su memoria y búfer y aumenta la degradación del rendimiento de sus consultas.

Índices de prefijos

Los índices de prefijo son índices que contienen columnas a las que se hace referencia como un índice, pero solo toman la longitud inicial definida para esa columna, y esa parte (o datos de prefijo) son la única parte almacenada en el búfer. Los índices de prefijo pueden ayudar a disminuir los recursos de su grupo de búfer y también su espacio en disco, ya que no es necesario que tome la longitud completa de la columna. ¿Qué significa esto? Tomemos un ejemplo. Comparemos el impacto entre el índice de longitud completa y el índice de prefijo.

root[test]#> create index name on users_account(last_name, first_name);

Query OK, 0 rows affected (0.42 sec)

Records: 0  Duplicates: 0  Warnings: 0



root[test]#> \! du -hs /var/lib/mysql/test/users_account.*

12K     /var/lib/mysql/test/users_account.frm

36M     /var/lib/mysql/test/users_account.ibd

Creamos un índice compuesto completo que consume un total de 36 MiB de espacio de tablas para la tabla users_account. Dejémoslo y luego agreguemos un índice de prefijo.

root[test]#> drop index name on users_account;

Query OK, 0 rows affected (0.01 sec)

Records: 0  Duplicates: 0  Warnings: 0



root[test]#> alter table users_account engine=innodb;

Query OK, 0 rows affected (0.63 sec)

Records: 0  Duplicates: 0  Warnings: 0



root[test]#> \! du -hs /var/lib/mysql/test/users_account.*

12K     /var/lib/mysql/test/users_account.frm

24M     /var/lib/mysql/test/users_account.ibd






root[test]#> create index name on users_account(last_name(5), first_name(5));

Query OK, 0 rows affected (0.42 sec)

Records: 0  Duplicates: 0  Warnings: 0



root[test]#> \! du -hs /var/lib/mysql/test/users_account.*

12K     /var/lib/mysql/test/users_account.frm

28M     /var/lib/mysql/test/users_account.ibd

Usando el índice de prefijo, solo soporta hasta 28MiB y eso es menos de 8MiB que usando el índice de longitud completa. Es genial escuchar eso, pero no significa que sea eficaz y que sirva para lo que necesitas.

Si decide agregar un índice de prefijo, primero debe identificar qué tipo de consulta para la recuperación de datos necesita. La creación de un índice de prefijo lo ayuda a utilizar una mayor eficiencia con el grupo de búfer y, por lo tanto, ayuda con el rendimiento de su consulta, pero también necesita conocer su limitación. Por ejemplo, comparemos el rendimiento cuando se usa un índice completo y un índice de prefijo.

Vamos a crear un índice completo usando un índice compuesto,

root[test]#> create index name on users_account(last_name, first_name);

Query OK, 0 rows affected (0.45 sec)

Records: 0  Duplicates: 0  Warnings: 0



root[test]#>  EXPLAIN format=json select last_name from users_account where last_name='Namuag' and first_name='Maximus Aleksandre' \G

*************************** 1. row ***************************

EXPLAIN: {

  "query_block": {

    "select_id": 1,

    "cost_info": {

      "query_cost": "1.61"

    },

    "table": {

      "table_name": "users_account",

      "access_type": "ref",

      "possible_keys": [

        "name"

      ],

      "key": "name",

      "used_key_parts": [

        "last_name",

        "first_name"

      ],

      "key_length": "60",

      "ref": [

        "const",

        "const"

      ],

      "rows_examined_per_scan": 3,

      "rows_produced_per_join": 3,

      "filtered": "100.00",

      "using_index": true,

      "cost_info": {

        "read_cost": "1.02",

        "eval_cost": "0.60",

        "prefix_cost": "1.62",

        "data_read_per_join": "1K"

      },

      "used_columns": [

        "last_name",

        "first_name"

      ]

    }

  }

}

1 row in set, 1 warning (0.00 sec)



root[test]#> flush status;

Query OK, 0 rows affected (0.02 sec)



root[test]#> pager cat -> /dev/null; select last_name from users_account where last_name='Namuag' and first_name='Maximus Aleksandre' \G

PAGER set to 'cat -> /dev/null'

3 rows in set (0.00 sec)



root[test]#> nopager; show status like 'Handler_read%';

PAGER set to stdout

+-----------------------+-------+

| Variable_name         | Value |

+-----------------------+-------+

| Handler_read_first    | 0 |

| Handler_read_key      | 1 |

| Handler_read_last     | 0 |

| Handler_read_next     | 3 |

| Handler_read_prev     | 0 |

| Handler_read_rnd      | 0 |

| Handler_read_rnd_next | 0     |

+-----------------------+-------+

7 rows in set (0.00 sec)

El resultado revela que, de hecho, está usando un índice de cobertura, es decir, "using_index":verdadero y usa índices correctamente, es decir, Handler_read_key se incrementa y realiza un escaneo de índice a medida que se incrementa Handler_read_next.

Ahora, intentemos usar el índice de prefijo del mismo enfoque,

root[test]#> create index name on users_account(last_name(5), first_name(5));

Query OK, 0 rows affected (0.22 sec)

Records: 0  Duplicates: 0  Warnings: 0



root[test]#>  EXPLAIN format=json select last_name from users_account where last_name='Namuag' and first_name='Maximus Aleksandre' \G

*************************** 1. row ***************************

EXPLAIN: {

  "query_block": {

    "select_id": 1,

    "cost_info": {

      "query_cost": "3.60"

    },

    "table": {

      "table_name": "users_account",

      "access_type": "ref",

      "possible_keys": [

        "name"

      ],

      "key": "name",

      "used_key_parts": [

        "last_name",

        "first_name"

      ],

      "key_length": "10",

      "ref": [

        "const",

        "const"

      ],

      "rows_examined_per_scan": 3,

      "rows_produced_per_join": 3,

      "filtered": "100.00",

      "cost_info": {

        "read_cost": "3.00",

        "eval_cost": "0.60",

        "prefix_cost": "3.60",

        "data_read_per_join": "1K"

      },

      "used_columns": [

        "last_name",

        "first_name"

      ],

      "attached_condition": "((`test`.`users_account`.`first_name` = 'Maximus Aleksandre') and (`test`.`users_account`.`last_name` = 'Namuag'))"

    }

  }

}

1 row in set, 1 warning (0.00 sec)



root[test]#> flush status;

Query OK, 0 rows affected (0.01 sec)



root[test]#> pager cat -> /dev/null; select last_name from users_account where last_name='Namuag' and first_name='Maximus Aleksandre' \G

PAGER set to 'cat -> /dev/null'

3 rows in set (0.00 sec)



root[test]#> nopager; show status like 'Handler_read%';

PAGER set to stdout

+-----------------------+-------+

| Variable_name         | Value |

+-----------------------+-------+

| Handler_read_first    | 0 |

| Handler_read_key      | 1 |

| Handler_read_last     | 0 |

| Handler_read_next     | 3 |

| Handler_read_prev     | 0 |

| Handler_read_rnd      | 0 |

| Handler_read_rnd_next | 0     |

+-----------------------+-------+

7 rows in set (0.00 sec)

MySQL revela que sí usa el índice correctamente pero, notablemente, hay una sobrecarga de costos en comparación con un índice completo. Eso es obvio y explicable, ya que el índice de prefijo no cubre la longitud total de los valores de campo. El uso de un índice de prefijos no es un reemplazo ni una alternativa de la indexación completa. También puede crear resultados deficientes cuando se usa el índice de prefijo de manera inapropiada. Por lo tanto, debe determinar qué tipo de consulta y datos necesita recuperar.

Índices de cobertura

Cubrir índices no requiere ninguna sintaxis especial en MySQL. Un índice de cobertura en InnoDB se refiere al caso en que todos los campos seleccionados en una consulta están cubiertos por un índice. No es necesario realizar una lectura secuencial del disco para leer los datos de la tabla, sino que solo utiliza los datos del índice, lo que acelera significativamente la consulta. Por ejemplo, nuestra consulta anterior, es decir, 

select last_name from users_account where last_name='Namuag' and first_name='Maximus Aleksandre' \G

Como se mencionó anteriormente, es un índice de cobertura. Cuando tenga tablas muy bien planificadas sobre el almacenamiento de sus datos y el índice creado correctamente, intente hacer lo posible para que sus consultas estén diseñadas para aprovechar el índice de cobertura para que se beneficie del resultado. Esto puede ayudarlo a maximizar la eficiencia de sus consultas y obtener un excelente rendimiento.

Aproveche las herramientas que ofrecen asesores o seguimiento del rendimiento de las consultas

Las organizaciones a menudo tienden inicialmente a ir primero a github y encuentran software de código abierto que puede ofrecer grandes beneficios. Para obtener avisos simples que lo ayuden a optimizar sus consultas, puede aprovechar el kit de herramientas de Percona. Para un DBA de MySQL, el kit de herramientas de Percona es como una navaja suiza.

Para operaciones, necesita analizar cómo está usando sus índices, puede usar pt-index-usage.

Pt-query-digest también está disponible y puede analizar consultas de MySQL desde registros, listas de procesos y tcpdump. De hecho, la herramienta más importante que debe usar para analizar e inspeccionar consultas incorrectas es pt-query-digest. Utilice esta herramienta para agregar consultas similares e informar sobre aquellas que consumen más tiempo de ejecución.

Para archivar registros antiguos, puede usar pt-archiver. Al inspeccionar su base de datos en busca de índices duplicados, aproveche pt-duplicate-key-checker. También puede aprovechar pt-deadlock-logger. Aunque los interbloqueos no son la causa de una consulta ineficiente y de bajo rendimiento, sino una implementación deficiente, sin embargo, afecta la ineficiencia de la consulta. Si necesita mantenimiento de tablas y requiere que agregue índices en línea sin afectar el tráfico de la base de datos que va a una tabla en particular, entonces puede usar pt-online-schema-change. Alternativamente, puede usar gh-ost, que también es muy útil para las migraciones de esquemas.

Si está buscando funciones empresariales, combinadas con muchas funciones de rendimiento y monitoreo de consultas, alarmas y alertas, paneles o métricas que lo ayuden a optimizar sus consultas y asesores, ClusterControl puede ser la herramienta para usted. ClusterControl ofrece muchas funciones que le muestran las consultas principales, las consultas en ejecución y los valores atípicos de la consulta. Consulte este blog MySQL Query Performance Tuning que lo guía sobre cómo estar a la par para monitorear sus consultas con ClusterControl.

Conclusión

Como has llegado a la parte final de nuestro blog de dos series. Cubrimos aquí los factores que causan la degradación de las consultas y cómo resolverlo para maximizar las consultas de la base de datos. También compartimos algunas herramientas que pueden beneficiarlo y ayudarlo a resolver sus problemas.