MariaDB
 sql >> Base de données >  >> RDS >> MariaDB

Maximiser l'efficacité des requêtes de base de données pour MySQL - Deuxième partie

Ceci est la deuxième partie d'un blog en deux parties pour maximiser l'efficacité des requêtes de base de données dans MySQL. Vous pouvez lire la première partie ici.

Utilisation d'un index à colonne unique, composé, préfixé et couvrant

Les tables qui reçoivent fréquemment un trafic élevé doivent être correctement indexées. Il est non seulement important d'indexer votre table, mais vous devez également déterminer et analyser quels sont les types de requêtes ou les types de récupération dont vous avez besoin pour la table spécifique. Il est fortement recommandé d'analyser le type de requêtes ou d'extraction de données dont vous avez besoin sur une table spécifique avant de décider quels index sont requis pour la table. Passons en revue ces types d'index et comment vous pouvez les utiliser pour optimiser les performances de vos requêtes.

Index à une seule colonne

La table InnoD peut contenir un maximum de 64 index secondaires. Un index à colonne unique (ou index à colonne complète) est un index affecté uniquement à une colonne particulière. La création d'un index vers une colonne particulière contenant des valeurs distinctes est un bon candidat. Un bon index doit avoir une cardinalité et des statistiques élevées pour que l'optimiseur puisse choisir le bon plan de requête. Pour afficher la distribution des index, vous pouvez vérifier avec la syntaxe SHOW INDEXES comme ci-dessous :

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)

Vous pouvez également inspecter avec les tables information_schema.index_statistics ou mysql.innodb_index_stats.

Index composés (composites) ou en plusieurs parties

Un index composé (communément appelé index composite) est un index en plusieurs parties composé de plusieurs colonnes. MySQL autorise jusqu'à 16 colonnes délimitées pour un index composite spécifique. Le dépassement de la limite renvoie une erreur comme ci-dessous :

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

Un index composite donne un coup de pouce à vos requêtes, mais il nécessite que vous ayez une compréhension pure de la façon dont vous récupérez les données. Par exemple, une table avec 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

...qui se compose de l'index composite `name`. L'index composite améliore les performances des requêtes une fois que ces clés sont référencées en tant que parties de clé utilisées. Par exemple, consultez ce qui suit :

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

Les used_key_parts montrent que le plan de requête a parfaitement sélectionné les colonnes souhaitées couvertes dans notre index composite.

L'indexation composite a également ses limites. Certaines conditions de la requête ne peuvent pas inclure toutes les colonnes dans la clé.

La documentation indique :"L'optimiseur tente d'utiliser des éléments clés supplémentaires pour déterminer l'intervalle tant que l'opérateur de comparaison est =, <=> ou IS NULL. Si l'opérateur est> , <,>=, <=, !=, <>, BETWEEN ou LIKE, l'optimiseur l'utilise mais ne considère plus d'éléments clés. Pour l'expression suivante, l'optimiseur utilise =à partir de la première comparaison. Il utilise également>=de la deuxième comparaison, mais ne considère pas d'autres éléments clés et n'utilise pas la troisième comparaison pour la construction d'intervalle…" . En gros, cela signifie que peu importe que vous ayez un index composite pour deux colonnes, un exemple de requête ci-dessous ne couvre pas les deux champs :

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)

Dans ce cas (et si votre requête porte davantage sur des plages que sur des types constants ou de référence), évitez d'utiliser des index composites. Cela gaspille simplement votre mémoire et votre mémoire tampon et augmente la dégradation des performances de vos requêtes.

Index des préfixes

Les index de préfixe sont des index qui contiennent des colonnes référencées en tant qu'index, mais ne prennent que la longueur de départ définie pour cette colonne, et cette partie (ou données de préfixe) est la seule partie stockée dans le tampon. Les index de préfixe peuvent aider à réduire les ressources de votre pool de mémoire tampon ainsi que votre espace disque car il n'est pas nécessaire de prendre toute la longueur de la colonne. Qu'est-ce que cela signifie ? Prenons un exemple. Comparons l'impact entre l'index complet et l'index de préfixe.

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

Nous avons créé un index composite complet qui consomme un total de 36 Mo d'espace de table pour la table users_account. Laissons tomber, puis ajoutons un index de préfixe.

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

En utilisant l'index de préfixe, il ne contient que 28 Mo et c'est moins de 8 Mo qu'en utilisant l'index complet. C'est agréable à entendre, mais cela ne signifie pas qu'il est performant et qu'il répond à vos besoins.

Si vous décidez d'ajouter un index de préfixe, vous devez d'abord identifier le type de requête de récupération de données dont vous avez besoin. La création d'un index de préfixe vous aide à utiliser plus d'efficacité avec le pool de mémoire tampon et donc cela aide avec les performances de votre requête, mais vous devez également connaître sa limitation. Par exemple, comparons les performances lors de l'utilisation d'un index de longueur complète et d'un index de préfixe.

Créons un index complet à l'aide d'un index composite,

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)

Le résultat révèle qu'il utilise en fait un index couvrant, c'est-à-dire "using_index":vrai et utilise les index correctement, c'est-à-dire que Handler_read_key est incrémenté et effectue un balayage d'index lorsque Handler_read_next est incrémenté.

Maintenant, essayons d'utiliser l'index de préfixe de la même approche,

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 révèle qu'il utilise l'index correctement, mais notablement, il y a un surcoût par rapport à un index complet. C'est évident et explicable, puisque l'index de préfixe ne couvre pas toute la longueur des valeurs de champ. L'utilisation d'un index de préfixe n'est pas un remplacement, ni une alternative, à l'indexation complète. Cela peut également créer des résultats médiocres lors de l'utilisation inappropriée de l'index de préfixe. Vous devez donc déterminer le type de requête et les données que vous devez récupérer.

Index de couverture

La couverture des index ne nécessite aucune syntaxe spéciale dans MySQL. Un index de couverture dans InnoDB fait référence au cas où tous les champs sélectionnés dans une requête sont couverts par un index. Il n'a pas besoin d'effectuer une lecture séquentielle sur le disque pour lire les données de la table, mais utilise uniquement les données de l'index, ce qui accélère considérablement la requête. Par exemple, notre requête précédente : 

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

Comme mentionné précédemment, est un index de couverture. Lorsque vous avez des tables très bien planifiées lors du stockage de vos données et de l'index créé correctement, essayez de faire en sorte que vos requêtes soient conçues pour tirer parti de l'index couvrant afin que vous puissiez bénéficier du résultat. Cela peut vous aider à maximiser l'efficacité de vos requêtes et à obtenir d'excellentes performances.

Utiliser des outils offrant des conseillers ou des requêtes de surveillance des performances

Les organisations ont souvent tendance à commencer par github et à trouver des logiciels open source qui peuvent offrir de grands avantages. Pour des conseils simples qui vous aident à optimiser vos requêtes, vous pouvez tirer parti de la boîte à outils Percona. Pour un administrateur de base de données MySQL, le Percona Toolkit est comme un couteau suisse.

Pour les opérations, vous devez analyser la façon dont vous utilisez vos index, vous pouvez utiliser pt-index-usage.

Pt-query-digest est également disponible et peut analyser les requêtes MySQL à partir des journaux, processlist et tcpdump. En fait, l'outil le plus important que vous devez utiliser pour analyser et inspecter les mauvaises requêtes est pt-query-digest. Utilisez cet outil pour regrouper des requêtes similaires et générer des rapports sur celles qui consomment le plus de temps d'exécution.

Pour archiver les anciens enregistrements, vous pouvez utiliser pt-archiver. Inspectez votre base de données pour les index en double, tirez parti de pt-duplicate-key-checker. Vous pouvez également tirer parti de pt-deadlock-logger. Bien que les blocages ne soient pas la cause d'une requête sous-performante et inefficace, mais d'une mauvaise implémentation, ils ont néanmoins un impact sur l'inefficacité de la requête. Si vous avez besoin d'une maintenance de table et que vous devez ajouter des index en ligne sans affecter le trafic de la base de données vers une table particulière, vous pouvez utiliser pt-online-schema-change. Alternativement, vous pouvez utiliser gh-ost, qui est également très utile pour les migrations de schéma.

Si vous recherchez des fonctionnalités d'entreprise, regroupées avec de nombreuses fonctionnalités telles que la performance et la surveillance des requêtes, des alarmes et des alertes, des tableaux de bord ou des métriques qui vous aident à optimiser vos requêtes, et des conseillers, ClusterControl peut être l'outil pour tu. ClusterControl offre de nombreuses fonctionnalités qui vous montrent les requêtes principales, les requêtes en cours d'exécution et les valeurs aberrantes des requêtes. Consultez ce blog MySQL Query Performance Tuning qui vous explique comment être à la hauteur pour surveiller vos requêtes avec ClusterControl.

Conclusion

Comme vous êtes arrivé à la fin de notre blog en deux séries. Nous avons couvert ici les facteurs qui provoquent la dégradation des requêtes et comment les résoudre afin de maximiser vos requêtes de base de données. Nous avons également partagé quelques outils qui peuvent vous être utiles et vous aider à résoudre vos problèmes.