Uma cena muito comum é esta: logo ao lançar a primeira versão de uma nova aplicação (ou de uma nova feature), ela funcionava como um tiro, respondia muito rápido. Depois de um tempo, com o aumento do uso dessa feature, você começa a perceber no seu software de APM (ou pior, os usuários começam a reclamar) que o tempo de resposta aumentou muito.  Pesquisa mais um pouco e descobre que a culpada é uma query que está levando muito tempo para executar.

Uma das formas de resolver o problema de performance da query é criando um ou mais índices. Mas a pergunta que fica é:

Qual índice criar para otimizar minha query?

A primeira tentação é criar todos os índices que possam estar envolvidos na query, mas em muitos casos isso, além de não resolver o problema, acaba introduzindo outros: a diminuição de performance nas operações de DML (INSERTUPDATE e DELETE em SQL, insertOne/insertManyupdateOne/updateManydeleteOne/deleteMany no MongoDB), uma vez que, além de alterar a tabela/collection, é necessário atualizar os índices da mesma; e o desperdício de espaço em disco, já que o índice estaria ocupando espaço sem necessidade.

Tá, então como eu descubro quais índices criar?

A melhor forma de analisar uma query para saber quais índices criar é através da análise dos filtros da query (cláusulas WHERE em SQL, o primeiro parâmetro do método find() do MongoDB ou o argumento $match do método aggregate() do MongoDB) o do seu plano de execução. Com essas informações em mãos, você consegue saber quais colunas estão sendo filtradas e quantas linhas estão sendo analisadas.

Para consultar o plano de execução de uma query, execute um dos comandos abaixo, de acordo com o banco de dados utilizado:

-- MySQL e PostgreSQL:
EXPLAIN [sua query]

-- MySQL com saída em formato JSON (contém informações adicionais) 
EXPLAIN FORMAT=json [sua query]

-- Oracle:
EXPLAIN PLAN FOR [sua query]

-- SQL Server:

SET SHOWPLAN_XML ON;
GO

[sua query]
GO

SET SHOWPLAN_XML OFF;
GO

-- MongoDB:
db.[nome da collection].find({[filtros]}).explain()

-- Exemplo em MySQL/Postgres:
EXPLAIN SELECT * FROM cidades WHERE uf = 'ES';

-- Exemplo em MongoDB:
db.cidades.find({ "uf": "ES" }).explain()

Suponha, por exemplo, a tabela pedidos, que possui 100.000 linhas e a seguinte estrutura (banco de dados MySQL 5.7):

mysql> desc pedidos;
+------------+--------------+------+-----+---------+-------+
| Field      | Type         | Null | Key | Default | Extra |
+------------+--------------+------+-----+---------+-------+
| id         | int(11)      | NO   | PRI | NULL    |       |
| id_cliente | int(11)      | NO   |     | NULL    |       |
| status     | varchar(20)  | NO   | MUL | NULL    |       |
| valor      | decimal(7,2) | NO   |     | NULL    |       |
+------------+--------------+------+-----+---------+-------+
4 rows in set (0.01 sec)

A coluna status possui 2 valores: ok (97% dos registros) e cancelado (3% dos registros).

A query que está com lentidão é essa:

SELECT * FROM pedidos WHERE status = 'cancelado';

Para analisar o plano de execução da query, rodamos o comando abaixo e recebemos o seguinte plano de execução:

mysql> EXPLAIN FORMAT=json SELECT * FROM pedidos WHERE status = 'cancelado';
+---------------------------------------------------------------------------+
| EXPLAIN                                                                   |
+---------------------------------------------------------------------------+
 { 
	"query_block": { 
		"select_id": 1, 
		"cost_info": { "query_cost": "19746.00" }, 
		"table": { 
			"table_name": "pedidos", 
			"access_type": "ALL", 
			"rows_examined_per_scan": 97605, 
			"rows_produced_per_join": 9760, 
			"filtered": "10.00", 
			"cost_info": { 
				"read_cost": "17793.90", 
				"eval_cost": "1952.10", 
				"prefix_cost": "19746.00", 
				"data_read_per_join": "381K" 
			},
			"used_columns": [ "id", "id_cliente", "status", "valor" ], 
			"attached_condition": "(`teste`.`pedidos`.`status` = 'cancelado')" 
		} 
	}
 } 
+---------------------------------------------------------------------------+

O valor ALL no campo access_type indica que não foi utilizado um índice e o campo rows_examined_per_scan indica que serão consultadas aproximadamente 97.605 linhas (é uma estimativa, não o valor real). O campo attached_condition indica o filtro utilizado na tabela (teste.pedidos.status = ‘cancelado’).

A partir desses dados, descobrimos que o índice deve ser criado na coluna status da tabela:

mysql> create index idx_pedidos_status on pedidos (status);
Query OK, 0 rows affected (0.11 sec)
Records: 0  Duplicates: 0  Warnings: 0

Executando novamente o plano de execução da query, temos o seguinte resultado:

mysql> EXPLAIN FORMAT=json SELECT * FROM pedidos WHERE status = 'cancelado';
+---------------------------------------------------------------------------+
| EXPLAIN                                                                   |
+---------------------------------------------------------------------------+
{
  "query_block": {
    "select_id": 1,
    "cost_info": {
      "query_cost": "1275.00"
    },
    "table": {
      "table_name": "pedidos",
      "access_type": "ref",
      "possible_keys": [
        "idx_pedidos_status"
      ],
      "key": "idx_pedidos_status",
      "used_key_parts": [
        "status"
      ],
      "key_length": "22",
      "ref": [
        "const"
      ],
      "rows_examined_per_scan": 3000,
      "rows_produced_per_join": 3000,
      "filtered": "100.00",
      "cost_info": {
        "read_cost": "675.00",
        "eval_cost": "600.00",
        "prefix_cost": "1275.00",
        "data_read_per_join": "117K"
      },
      "used_columns": [
        "id",
        "id_cliente",
        "status",
        "valor"
      ]
    }
  }
}
+---------------------------------------------------------------------------+

No exemplo acima, podemos ver no campo key que o índice idx_pedidos_status que acabamos de criar foi utilizado e que a quantidade de linhas percorridas (rows_examined_per_scan)reduziu de 97605 para 3000 (97% de redução), permitindo que a leitura desses dados seja consideravelmente mais rápida.

Nos próximos artigos eu falarei de estatísticas de tabelas e outras funcionalidades dos bancos de dados que afetam o uso de índices. Até lá!