MariaDB
 sql >> Database >  >> RDS >> MariaDB

Massimizzare l'efficienza delle query del database per MySQL - Parte seconda

Questa è la seconda parte di una serie di blog in due parti per massimizzare l'efficienza delle query del database in MySQL. Puoi leggere la prima parte qui.

Utilizzo di una singola colonna, composito, prefisso e indice di copertura

Le tabelle che ricevono frequentemente un traffico elevato devono essere indicizzate correttamente. Non è solo importante indicizzare la tabella, ma è anche necessario determinare e analizzare quali sono i tipi di query o i tipi di recupero necessari per la tabella specifica. Si consiglia vivamente di analizzare il tipo di query o il recupero di dati necessari su una tabella specifica prima di decidere quali indici sono necessari per la tabella. Esaminiamo questi tipi di indici e come utilizzarli per massimizzare le prestazioni delle query.

Indice a colonna singola

La tabella InnoD può contenere un massimo di 64 indici secondari. Un indice a colonna singola (o indice a colonna intera) è un indice assegnato solo a una determinata colonna. La creazione di un indice per una particolare colonna che contiene valori distinti è un buon candidato. Un buon indice deve avere una cardinalità e statistiche elevate in modo che l'ottimizzatore possa scegliere il piano di query corretto. Per visualizzare la distribuzione degli indici, puoi controllare con la sintassi SHOW INDEXES proprio come di seguito:

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)

Puoi anche ispezionare con le tabelle information_schema.index_statistics o mysql.innodb_index_stats.

Indici composti (compositi) o multi-parte

Un indice composto (comunemente chiamato indice composto) è un indice multiparte composto da più colonne. MySQL consente fino a 16 colonne delimitate per uno specifico indice composito. Il superamento del limite restituisce un errore simile al seguente:

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

Un indice composito fornisce una spinta alle tue query, ma richiede che tu abbia una comprensione pura di come stai recuperando i dati. Ad esempio, una tabella con un DDL di...

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

...che consiste nell'indice composito `name`. L'indice composito migliora le prestazioni della query una volta che queste chiavi sono referenziate come parti chiave utilizzate. Ad esempio, vedere quanto segue:

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

Le used_key_parts mostrano che il piano di query ha selezionato perfettamente le colonne desiderate coperte nel nostro indice composito.

Anche l'indicizzazione dei compositi ha i suoi limiti. Alcune condizioni nella query non possono accettare tutte le colonne come parte della chiave.

La documentazione dice, "L'ottimizzatore tenta di utilizzare parti chiave aggiuntive per determinare l'intervallo purché l'operatore di confronto sia =, <=> o IS NULL. Se l'operatore è> , <,>=, <=, !=, <>, BETWEEN o LIKE, l'ottimizzatore lo usa ma non considera più parti chiave. Per l'espressione seguente, l'ottimizzatore usa =dal primo confronto. Usa anche>=dal secondo confronto, ma non considera ulteriori parti chiave e non utilizza il terzo confronto per la costruzione dell'intervallo…” . Fondamentalmente, ciò significa che, indipendentemente dal fatto che tu abbia un indice composto per due colonne, una query di esempio di seguito non copre entrambi i campi:

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)

In questo caso (e se la tua query è più di intervalli anziché di tipi costanti o di riferimento), evita di utilizzare indici compositi. Spreca solo memoria e buffer e aumenta il degrado delle prestazioni delle tue query.

Indici prefissi

Gli indici di prefisso sono indici che contengono colonne referenziate come un indice, ma prendono solo la lunghezza iniziale definita per quella colonna e quella parte (o dati di prefisso) sono l'unica parte memorizzata nel buffer. Gli indici dei prefissi possono aiutare a ridurre le risorse del pool di buffer e anche lo spazio su disco in quanto non è necessario occupare l'intera lunghezza della colonna. Cosa significa? Facciamo un esempio. Confrontiamo l'impatto tra l'indice a lunghezza intera e l'indice del prefisso.

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

Abbiamo creato un indice composito a lunghezza intera che consuma un totale di 36 MiB di tablespace per la tabella users_account. Rilasciamolo e poi aggiungiamo un indice di prefisso.

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

Utilizzando l'indice del prefisso, può contenere fino a 28 MiB e questo è inferiore a 8 MiB rispetto all'utilizzo dell'indice a lunghezza intera. È fantastico da sentire, ma non significa che sia performante e serva ciò di cui hai bisogno.

Se decidi di aggiungere un indice di prefisso, devi prima identificare il tipo di query per il recupero dei dati di cui hai bisogno. La creazione di un indice di prefisso consente di utilizzare una maggiore efficienza con il pool di buffer e quindi aiuta con le prestazioni della query, ma è anche necessario conoscerne i limiti. Ad esempio, confrontiamo il rendimento quando si utilizza un indice a lunghezza intera e un indice di prefisso.

Creiamo un indice completo usando un indice composto,

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)

Il risultato rivela che in effetti utilizza un indice di copertura, ad esempio "using_index":true e utilizza gli indici correttamente, ovvero Handler_read_key viene incrementato ed esegue una scansione dell'indice mentre Handler_read_next viene incrementato.

Ora, proviamo a utilizzare l'indice del prefisso con lo stesso approccio,

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 rivela che utilizza l'indice in modo corretto, ma è evidente che c'è un sovraccarico dei costi rispetto a un indice a lunghezza intera. Questo è ovvio e spiegabile, poiché l'indice del prefisso non copre l'intera lunghezza dei valori del campo. L'uso di un indice di prefisso non è un sostituto, né un'alternativa, dell'indicizzazione a lunghezza intera. Può anche creare risultati scadenti quando si utilizza l'indice del prefisso in modo inappropriato. Quindi devi determinare quale tipo di query e dati devi recuperare.

Indici di copertura

La copertura degli indici non richiede alcuna sintassi speciale in MySQL. Un indice di copertura in InnoDB si riferisce al caso in cui tutti i campi selezionati in una query sono coperti da un indice. Non è necessario eseguire una lettura sequenziale sul disco per leggere i dati nella tabella ma utilizzare solo i dati nell'indice, velocizzando notevolmente la query. Ad esempio, la nostra query precedente, ovvero 

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

Come accennato in precedenza, è un indice di copertura. Quando disponi di tabelle molto ben pianificate per l'archiviazione dei dati e la creazione dell'indice correttamente, cerca di rendere possibile che le tue query siano progettate per sfruttare l'indice di copertura in modo da trarre vantaggio dal risultato. Questo può aiutarti a massimizzare l'efficienza delle tue query e ottenere un ottimo rendimento.

Utilizzare strumenti che offrono consulenti o interrogare il monitoraggio delle prestazioni

Le organizzazioni spesso inizialmente tendono ad andare per prime su github e trovare software open source in grado di offrire grandi vantaggi. Per semplici avvisi che ti aiutano a ottimizzare le tue query, puoi sfruttare Percona Toolkit. Per un DBA MySQL, Percona Toolkit è come un coltellino svizzero.

Per le operazioni, devi analizzare come stai usando i tuoi indici, puoi usare pt-index-usage.

Pt-query-digest è anche disponibile e può analizzare le query MySQL da log, processlist e tcpdump. In effetti, lo strumento più importante che devi utilizzare per analizzare e ispezionare le query errate è pt-query-digest. Utilizza questo strumento per aggregare query simili e generare rapporti su quelle che consumano più tempo di esecuzione.

Per archiviare i vecchi record, puoi usare pt-archiver. Ispezionando il tuo database per indici duplicati, sfrutta pt-duplicate-key-checker. Potresti anche sfruttare pt-deadlock-logger. Sebbene i deadlock non siano la causa di una query con prestazioni insufficienti e inefficienti, ma di una scarsa implementazione, tuttavia influiscono sull'inefficienza della query. Se è necessaria la manutenzione della tabella e è necessario aggiungere indici online senza influire sul traffico del database che passa a una tabella particolare, è possibile utilizzare pt-online-schema-change. In alternativa, puoi usare gh-ost, che è anche molto utile per le migrazioni di schemi.

Se stai cercando funzionalità aziendali, in bundle con molte funzionalità dalle prestazioni e monitoraggio delle query, allarmi e avvisi, dashboard o metriche che ti aiutano a ottimizzare le tue query e consulenti, ClusterControl potrebbe essere lo strumento per Voi. ClusterControl offre molte funzionalità che mostrano le query principali, le query in esecuzione e i valori anomali delle query. Dai un'occhiata a questo blog MySQL Query Performance Tuning che ti guida su come essere alla pari per il monitoraggio delle tue query con ClusterControl.

Conclusione

Come sei arrivato alla parte finale del nostro blog di due serie. Abbiamo trattato qui i fattori che causano il degrado delle query e come risolverlo al fine di massimizzare le query del database. Abbiamo anche condiviso alcuni strumenti che possono avvantaggiarti e aiutarti a risolvere i tuoi problemi.