MySQL: delete… where..in () vs delete..from..join e tabelas bloqueadas em delete com subselect

9

Isenção de responsabilidade: desculpe meu desconhecimento sobre os dados internos do banco de dados. Aqui vai:

Executamos um aplicativo (não escrito por nós) que apresenta um grande problema de desempenho em um trabalho de limpeza periódica no banco de dados. A consulta fica assim:

delete from VARIABLE_SUBSTITUTION where BUILDRESULTSUMMARY_ID in (
       select BUILDRESULTSUMMARY_ID from BUILDRESULTSUMMARY
       where BUILDRESULTSUMMARY.BUILD_KEY = "BAM-1");

SQL simples, fácil de ler e padrão. Mas infelizmente muito lento. Explicar a consulta mostra que o índice existente no VARIABLE_SUBSTITUTION.BUILDRESULTSUMMARY_IDnão é usado:

mysql> explain delete from VARIABLE_SUBSTITUTION where BUILDRESULTSUMMARY_ID in (
    ->        select BUILDRESULTSUMMARY_ID from BUILDRESULTSUMMARY
    ->        where BUILDRESULTSUMMARY.BUILD_KEY = "BAM-1");
| id | select_type        | table                 | type            | possible_keys                    | key     | key_len | ref  | rows    | Extra       |
+----+--------------------+-----------------------+-----------------+----------------------------------+---------+---------+------+---------+-------------+
|  1 | PRIMARY            | VARIABLE_SUBSTITUTION | ALL             | NULL                             | NULL    | NULL    | NULL | 7300039 | Using where |
|  2 | DEPENDENT SUBQUERY | BUILDRESULTSUMMARY    | unique_subquery | PRIMARY,key_number_results_index | PRIMARY | 8       | func |       1 | Using where |

Isso torna muito lento (120 segundos e mais). Além disso, parece bloquear as consultas que tentam inserir na BUILDRESULTSUMMARYsaída de show engine innodb status:

---TRANSACTION 68603695, ACTIVE 157 sec starting index read
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 360, 1 row lock(s)
MySQL thread id 127964, OS thread handle 0x7facd0670700, query id 956555826 localhost 127.0.0.1 bamboosrv updating
update BUILDRESULTSUMMARY set CREATED_DATE='2015-06-18 09:22:05', UPDATED_DATE='2015-06-18 09:22:32', BUILD_KEY='BLA-RELEASE1-JOB1', BUILD_NUMBER=8, BUILD_STATE='Unknown', LIFE_CYCLE_STATE='InProgress', BUILD_DATE='2015-06-18 09:22:31.792', BUILD_CANCELLED_DATE=null, BUILD_COMPLETED_DATE='2015-06-18 09:52:02.483', DURATION=1770691, PROCESSING_DURATION=1770691, TIME_TO_FIX=null, TRIGGER_REASON='com.atlassian.bamboo.plugin.system.triggerReason:CodeChangedTriggerReason', DELTA_STATE=null, BUILD_AGENT_ID=199688199, STAGERESULT_ID=230943366, RESTART_COUNT=0, QUEUE_TIME='2015-06-18 09:22:04.52
------- TRX HAS BEEN WAITING 157 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 38 page no 30140 n bits 112 index `PRIMARY` of table `bamboong`.`BUILDRESULTSUMMARY` trx id 68603695 lock_mode X locks rec but not gap waiting
------------------
---TRANSACTION 68594818, ACTIVE 378 sec starting index read
mysql tables in use 2, locked 2
646590 lock struct(s), heap size 63993384, 3775190 row lock(s), undo log entries 117
MySQL thread id 127845, OS thread handle 0x7facc6bf8700, query id 956652201 localhost 127.0.0.1 bamboosrv preparing
delete from VARIABLE_SUBSTITUTION  where BUILDRESULTSUMMARY_ID in   (select BUILDRESULTSUMMARY_ID from BUILDRESULTSUMMARY where BUILDRESULTSUMMARY.BUILD_KEY = 'BLA-BLUBB10-SON')

Isso diminui a velocidade do sistema e nos força a aumentar innodb_lock_wait_timeout.

Enquanto rodamos o MySQL, reescrevemos a consulta delete para usar "delete from join":

delete VARIABLE_SUBSTITUTION from VARIABLE_SUBSTITUTION join BUILDRESULTSUMMARY
   on VARIABLE_SUBSTITUTION.BUILDRESULTSUMMARY_ID = BUILDRESULTSUMMARY.BUILDRESULTSUMMARY_ID
   where BUILDRESULTSUMMARY.BUILD_KEY = "BAM-1";

Isso é um pouco menos fácil de ler, infelizmente, nenhum SQL padrão (até onde pude descobrir), mas muito mais rápido (0,02 segundos), pois ele usa o índice:

mysql> explain delete VARIABLE_SUBSTITUTION from VARIABLE_SUBSTITUTION join BUILDRESULTSUMMARY
    ->    on VARIABLE_SUBSTITUTION.BUILDRESULTSUMMARY_ID = BUILDRESULTSUMMARY.BUILDRESULTSUMMARY_ID
    ->    where BUILDRESULTSUMMARY.BUILD_KEY = "BAM-1";
| id | select_type | table                 | type | possible_keys                    | key                      | key_len | ref                                                    | rows | Extra                    |
+----+-------------+-----------------------+------+----------------------------------+--------------------------+---------+--------------------------------------------------------+------+--------------------------+
|  1 | SIMPLE      | BUILDRESULTSUMMARY    | ref  | PRIMARY,key_number_results_index | key_number_results_index | 768     | const                                                  |    1 | Using where; Using index |
|  1 | SIMPLE      | VARIABLE_SUBSTITUTION | ref  | var_subst_result_idx             | var_subst_result_idx     | 8       | bamboo_latest.BUILDRESULTSUMMARY.BUILDRESULTSUMMARY_ID |   26 | NULL                     |

Informação adicional:

mysql> SHOW CREATE TABLE VARIABLE_SUBSTITUTION;
| Table                 | Create Table |
| VARIABLE_SUBSTITUTION | CREATE TABLE `VARIABLE_SUBSTITUTION` (
  `VARIABLE_SUBSTITUTION_ID` bigint(20) NOT NULL,
  `VARIABLE_KEY` varchar(255) COLLATE utf8_bin NOT NULL,
  `VARIABLE_VALUE` varchar(4000) COLLATE utf8_bin DEFAULT NULL,
  `VARIABLE_TYPE` varchar(255) COLLATE utf8_bin DEFAULT NULL,
  `BUILDRESULTSUMMARY_ID` bigint(20) NOT NULL,
  PRIMARY KEY (`VARIABLE_SUBSTITUTION_ID`),
  KEY `var_subst_result_idx` (`BUILDRESULTSUMMARY_ID`),
  KEY `var_subst_type_idx` (`VARIABLE_TYPE`),
  CONSTRAINT `FK684A7BE0A958B29F` FOREIGN KEY (`BUILDRESULTSUMMARY_ID`) REFERENCES `BUILDRESULTSUMMARY` (`BUILDRESULTSUMMARY_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin |

mysql> SHOW CREATE TABLE BUILDRESULTSUMMARY;
| Table              | Create Table |
| BUILDRESULTSUMMARY | CREATE TABLE `BUILDRESULTSUMMARY` (
  `BUILDRESULTSUMMARY_ID` bigint(20) NOT NULL,
....
  `SKIPPED_TEST_COUNT` int(11) DEFAULT NULL,
  PRIMARY KEY (`BUILDRESULTSUMMARY_ID`),
  KEY `FK26506D3B9E6537B` (`CHAIN_RESULT`),
  KEY `FK26506D3BCCACF65` (`MERGERESULT_ID`),
  KEY `key_number_delta_state` (`DELTA_STATE`),
  KEY `brs_build_state_idx` (`BUILD_STATE`),
  KEY `brs_life_cycle_state_idx` (`LIFE_CYCLE_STATE`),
  KEY `brs_deletion_idx` (`MARKED_FOR_DELETION`),
  KEY `brs_stage_result_id_idx` (`STAGERESULT_ID`),
  KEY `key_number_results_index` (`BUILD_KEY`,`BUILD_NUMBER`),
  KEY `brs_agent_idx` (`BUILD_AGENT_ID`),
  KEY `rs_ctx_baseline_idx` (`VARIABLE_CONTEXT_BASELINE_ID`),
  KEY `brs_chain_result_summary_idx` (`CHAIN_RESULT`),
  KEY `brs_log_size_idx` (`LOG_SIZE`),
  CONSTRAINT `FK26506D3B9E6537B` FOREIGN KEY (`CHAIN_RESULT`) REFERENCES `BUILDRESULTSUMMARY` (`BUILDRESULTSUMMARY_ID`),
  CONSTRAINT `FK26506D3BCCACF65` FOREIGN KEY (`MERGERESULT_ID`) REFERENCES `MERGE_RESULT` (`MERGERESULT_ID`),
  CONSTRAINT `FK26506D3BCEDEEF5F` FOREIGN KEY (`STAGERESULT_ID`) REFERENCES `CHAIN_STAGE_RESULT` (`STAGERESULT_ID`),
  CONSTRAINT `FK26506D3BE3B5B062` FOREIGN KEY (`VARIABLE_CONTEXT_BASELINE_ID`) REFERENCES `VARIABLE_CONTEXT_BASELINE` (`VARIABLE_CONTEXT_BASELINE_ID`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin |

(algumas coisas omitidas, é uma mesa bastante ampla).

Então, eu tenho algumas perguntas sobre isso:

  • por que o otimizador de consulta não pode usar o índice para excluir quando a versão da subconsulta, enquanto está usando a versão de junção?
  • existe alguma maneira (idealmente compatível com os padrões) de induzi-lo a usar o índice? ou
  • existe uma maneira portátil de escrever um delete from join? O aplicativo suporta PostgreSQL, MySQL, Oracle e Microsoft SQL Server, usado via jdbc e Hibernate.
  • por que a exclusão do VARIABLE_SUBSTITUTIONbloqueio de inserções BUILDRESULTSUMMARYé usada apenas na subseleção?
0x89
fonte
Servidor Percona 5.6.24-72.2-1.jessie e 5.6.24-72.2-1.wheezy (no sistema de teste).
0x89
Sim, todo o banco de dados usa innodb.
0x89
Portanto, parece que o 5.6 não recebeu muita atenção na melhoria do otimizador. Você vai ter que esperar para o 5.7 (mas tente MariaDB se puder Seus aprimoramentos do otimizador foram feitas de volta em seus 5.3 e 5.5 versões..)
ypercubeᵀᴹ
@ypercube AFAIK no fork tem um aprimoramento para tornar a subconsulta de exclusão otimizada nem 5.7. Exclui otimizar diferente das instruções SELECT.
Morgan Tocker

Respostas:

7
  • por que o otimizador de consulta não pode usar o índice para excluir quando a versão da subconsulta, enquanto está usando a versão de junção?

Porque o otimizador é / era um pouco burro nesse sentido. Não apenas para DELETEe também UPDATEpara SELECTdeclarações, algo assim WHERE column IN (SELECT ...)não foi totalmente otimizado. O plano de execução geralmente envolvia a execução da subconsulta para todas as linhas da tabela externa ( VARIABLE_SUBSTITUTIONneste caso). Se essa mesa é pequena, está tudo bem. Se for grande, não há esperança. Em versões ainda mais antigas, uma INsubconsulta com uma INsubconsulta tornaria EXPLAINa execução por séculos.

O que você pode fazer - se deseja manter essa consulta - é usar as versões mais recentes que implementaram várias otimizações e testar novamente. Versões mais recentes: MySQL 5.6 (e 5.7 quando sai da versão beta) e MariaDB 5.5 / 10.0

(atualização) Você já usa o 5.6, que possui aprimoramentos de otimização, e este é relevante: Otimizando subconsultas com transformações de semi-junção
Sugiro adicionar um índice (BUILD_KEY)sozinho. Existe um composto, mas isso não é muito útil para esta consulta.

  • existe alguma maneira (idealmente compatível com os padrões) de induzi-lo a usar o índice?

Nada que eu possa pensar. Na minha opinião, não vale a pena tentar usar o SQL padrão. Existem tantas diferenças e peculiaridades menores que cada DBMS possui ( UPDATEe as DELETEinstruções são bons exemplos dessas diferenças) que, quando você tenta usar algo que funciona em qualquer lugar, o resultado é um subconjunto muito limitado de SQL.

  • existe uma maneira portátil de escrever uma exclusão da associação? O aplicativo suporta PostgreSQL, MySQL, Oracle e Microsoft SQL Server, usado via jdbc e Hibernate.

Mesma resposta que a pergunta anterior.

  • por que a exclusão de VARIABLE_SUBSTITUTION bloqueando inserções no BUILDRESULTSUMMARY, que é usada apenas na subseleção?

Não tenho 100% de certeza, mas acho que isso tem a ver com a execução da subconsulta várias vezes e com o tipo de bloqueios que está ocorrendo na tabela.

ypercubeᵀᴹ
fonte
"3775190 bloqueios de linha" de innodb_status (da transação de exclusão) é altamente sugestivo. Mas também "tabelas MySQL em utilização 2, bloqueado 2" não parece muito bom para mim ..
0x89
2

aqui estão as respostas para duas de suas perguntas

  • O Optimizer não pode usar o índice porque a cláusula where é alterada para cada linha. A instrução delete será mais ou menos assim depois de passar no otimizador

    delete from VARIABLE_SUBSTITUTION where EXISTS (
    select BUILDRESULTSUMMARY_ID from BUILDRESULTSUMMARY
    where BUILDRESULTSUMMARY.BUILD_KEY = BUILDRESULTSUMMARY_ID AND BUILDRESULTSUMMARY.BUILD_KEY = "BAM-1");

mas quando você faz a associação, o servidor consegue identificar as linhas sujeitas a exclusão.

  • O truque é usar uma variável para conter BUILDRESULTSUMMARY_IDe usar a variável em vez da consulta. Observe que a inicialização da variável e a consulta de exclusão devem ser executadas dentro de uma sessão. Algo assim.

    SET @ids = (SELECT GROUP_CONCAT(BUILDRESULTSUMMARY_ID) 
            from BUILDRESULTSUMMARY where BUILDRESULTSUMMARY.BUILD_KEY = "BAM-1" ); 
    delete from VARIABLE_SUBSTITUTION where FIND_IN_SET(BUILDRESULTSUMMARY_ID,@ids) > 0;

    você pode enfrentar problemas com isso se a consulta retornar muitos IDs e isso não for uma maneira padrão. É apenas uma solução alternativa.

    E eu não tenho uma resposta para suas outras duas perguntas :)

Masoud
fonte
Ok, você perdeu o meu ponto. Eu acho que você não considerar é que tanto VARIABLE_SUBSTITUTION e BUILDRESULTSUMMARY tem uma coluna chamada BUILDRESULTSUMMARY_ID, por isso deve ser: 'apagar da VARIABLE_SUBSTITUTION onde existe (selecione BUILDRESULTSUMMARY_ID de BUILDRESULTSUMMARY onde BUILDRESULTSUMMARY.BUILDRESULTSUMMARY_ID = VARIABLE_SUBSTITUTION.BUILDRESULTSUMMARY_ID E BUILDRESULTSUMMARY.BUILD_KEY = "BAM -1 "); '. Então faz sentido, e as duas consultas fazem o mesmo.
0x89
11
Sim, estou apenas perdendo uma referência à tabela externa. Mas esta não é a questão. Isso é apenas uma ilustração de como será tratado no otimizador.
Masoud
Com a pequena diferença de que o otimizador produzirá uma consulta equivalente.
0x89