Todo DBA já passou pelo momento em que precisou alterar uma query (majoritariamente por performance) e recebeu do time de aplicação a informação de que não é possível alterá-la, seja por aplicação legada ou por aplicação de terceiros. Embora a melhor opção para essas alterações seja sempre fazer na fonte (aplicação), há momentos em que teremos de realizar a alteração ao nível de banco de dados/proxy.
Caso esteja utilizando o ProxySQL, essa problemática é facilmente concornada com o ProxySQL Query Rewrite. Neste blog, vou exemplificar como reescrever uma query com o ProxySQL.
O problema
Há momentos em que a aplicação pode escolher o index incorreto para uma execução mais performática, ou até mesmo utilizar um hint para um index específico. Em casos como esse, forçar um índice pode ser uma possível correção para a aplicação.
Cenário
Para os testes abaixo, favor considerar o uso do MySQL 8.4 e do ProxySQL 2.7.
Criando a tabela orders no nosso MySQL:
|
1 2 3 4 5 6 7 8 9 10 |
MySQL > CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, customer_id INT NOT NULL, status VARCHAR(20) NOT NULL, created_at DATETIME NOT NULL, total DECIMAL(10,2) NOT NULL, KEY idx_status (status), KEY idx_customer (customer_id) ) ENGINE=InnoDB; Query OK, 0 rows affected (0.03 sec) |
Carregando 200 mil linhas, valor de status aproximadamente 50/50:
|
1 2 3 4 5 6 7 8 9 |
MySQL > INSERT INTO orders (customer_id, status, created_at, total) SELECT FLOOR(RAND()*5000)+1, ELT(FLOOR(RAND()*2)+1, 'shipped','pending'), NOW() - INTERVAL FLOOR(RAND()*525600) MINUTE, ROUND(RAND()*500,2) FROM information_schema.columns a, information_schema.columns b LIMIT 200000; Query OK, 200000 rows affected (1.73 sec) Records: 200000 Duplicates: 0 Warnings: 0 |
A query de exemplo da aplicação — últimos pedidos ‘shipped’ de um cliente (42):
|
1 2 3 4 5 |
SELECT id, customer_id, status, created_at, total FROM orders WHERE customer_id = 42 AND status = 'shipped' ORDER BY created_at DESC LIMIT 10; |
Plano ruim utilizando index idx_status
Para replicarmos o problema de escolha de index errado, vamos nesse caso utilizar o hint USE INDEX, para o index idx_status, o que nesse caso deverá escanear uma grande quantidade de linhas
|
1 2 3 4 5 6 7 8 9 10 |
MySQL > EXPLAIN SELECT id, customer_id, status, created_at, total FROM orders USE INDEX (idx_status) WHERE customer_id = 42 AND status = 'shipped' ORDER BY created_at DESC LIMIT 10; +----+-------------+--------+------------+------+---------------+------------+---------+-------+-------+----------+-----------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+--------+------------+------+---------------+------------+---------+-------+-------+----------+-----------------------------+ | 1 | SIMPLE | orders | NULL | ref | idx_status | idx_status | 82 | const | 99840 | 0.02 | Using where; Using filesort | +----+-------------+--------+------------+------+---------------+------------+---------+-------+-------+----------+-----------------------------+ 1 row in set, 1 warning (0.00 sec) |
Vale lembrar que o problema acima pode ocorrer devido a estatísticas incorretas; porém, há casos em que, mesmo com estatísticas corretas, o optimizer escolhe um índice menos performático.
Tempo de execução da query:
|
1 2 3 4 5 6 |
MySQL > pager md5sum PAGER set to 'md5sum' MySQL > SELECT id, customer_id, status, created_at, total FROM orders USE INDEX (idx_status) WHERE customer_id = 42 AND status = 'shipped' ORDER BY created_at DESC LIMIT 10; b804831f495e773852d24cb641a4bca3 - 10 rows in set (0.17 sec) |
Melhorando a execução
É possível melhorar a execução da query acima forçando o uso do index idx_customer:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
MySQL > EXPLAIN SELECT id, customer_id, status, created_at, total FROM orders FORCE INDEX (idx_customer) WHERE customer_id = 42 AND status = 'shipped' ORDER BY created_at DESC LIMIT 10; +----+-------------+--------+------------+------+---------------+--------------+---------+-------+------+----------+-----------------------------+ | id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra | +----+-------------+--------+------------+------+---------------+--------------+---------+-------+------+----------+-----------------------------+ | 1 | SIMPLE | orders | NULL | ref | idx_customer | idx_customer | 4 | const | 31 | 100.00 | Using where; Using filesort | +----+-------------+--------+------------+------+---------------+--------------+---------+-------+------+----------+-----------------------------+ 1 row in set, 1 warning (0.00 sec) MySQL > pager md5sum PAGER set to 'md5sum' MySQL > SELECT id, customer_id, status, created_at, total FROM orders FORCE INDEX (idx_customer) WHERE customer_id = 42 AND status = 'shipped' ORDER BY created_at DESC LIMIT 10; b804831f495e773852d24cb641a4bca3 - 10 rows in set (0.00 sec) |
Solução com ProxySQL Query Rewrite
No ProxySQL existe a tabela mysql_query_rules. Essa tabela será utilizada para reescrever a query. Para isso, utilizaremos os campos match_pattern e replace_pattern, aplicados por meio de regex.
Criando a regra de rewrite
Nesse caso, alteraremos o comando USE INDEX (idx_status) para FORCE INDEX (idx_customer).
|
1 2 |
#Login mysql -u admin -padmin -h 127.0.0.1 -P 6032 --prompt='ProxySQL Admin> ' |
Criando a regra 3 que fará alteração:
|
1 2 3 4 5 6 7 |
ProxySQL Admin > INSERT INTO mysql_query_rules (rule_id, active, match_pattern, replace_pattern, destination_hostgroup, apply) VALUES(3, 1, '(?s)^SELECT (.+)\s+FROM\s+orders\s+USE\s+INDEX\s+\(idx_status\)\s+WHERE', 'SELECT \1 FROM orders FORCE INDEX (idx_customer) WHERE',NULL, 0); ProxySQL Admin> LOAD MYSQL QUERY RULES TO RUNTIME; ProxySQL Admin> SAVE MYSQL QUERY RULES TO DISK; |
- rule_id 1: Sending to hostgroup 2
- rule_id 2: Sending to hostgroup 1
- rule_id 3: rewrite USE → FORCE
Campos importantes:
- match_pattern — regex RE2 com
(?s)^para multiline; - replace_pattern —
\1reinjeta as colunas capturadas antes doFROM.
Validar que o rewrite ocorreu
Enviar a query via ProxySQL
|
1 2 3 4 5 6 |
MySQL ProxySQL > SELECT id, customer_id, status, created_at, total -> FROM orders USE INDEX (idx_status) -> WHERE customer_id = 42 AND status = 'shipped' -> ORDER BY created_at DESC LIMIT 10; [...] 10 rows in set (0.01 sec) |
Verificar hits no ProxySQL
|
1 2 3 4 5 6 7 8 9 |
ProxySQL Admin > SELECT * FROM stats_mysql_query_rules; +---------+------+ | rule_id | hits | +---------+------+ | 1 | 5 | | 2 | 0 | | 3 | 3 | +---------+------+ 3 rows in set (0.00 sec) |
Podemos ver que os hits da regra número 3 aumentam a cada vez que a executamos via ProxySQL.
Validando o Plano via MySQL Slow Query Log
|
1 2 3 4 5 6 7 |
# Time: 2026-06-02T20:01:46.781473Z # User@Host: root[root] @ roberto-node2.roberto-anydbver [172.19.0.4] Id: 1837 # Schema: shop Last_errno: 0 Killed: 0 # Query_time: 0.000543 Lock_time: 0.000004 Rows_sent: 10 Rows_examined: 41 Rows_affected: 0 Bytes_sent: 772 SET timestamp=1780430506; SELECT id, customer_id, status, created_at, total FROM orders FORCE INDEX (idx_customer) WHERE customer_id = 42 AND status = 'shipped' ORDER BY created_at DESC LIMIT 10; |
Um ponto de atenção: regex pode ser perigoso; teste corretamente seu regex e verifique as queries que serão alteradas, para não alterar uma query incorreta.
Conclusão
Quando uma query lenta vem de uma aplicação que não pode ser alterada, reescrever o SQL no ProxySQL pode ser uma alternativa de workaround. O Query Rewrite troca pode incluir, remover ou alterar uma query via regex, sem tocar no código da aplicação. Podendo dar mais tempo para execução de uma melhor solução.





