Como atualizar mais de 10 milhões de linhas na tabela única do MySQL o mais rápido possível?

32

Usando o MySQL 5.6 com o mecanismo de armazenamento InnoDB para a maioria das tabelas. O tamanho do buffer pool do InnoDB é de 15 GB e os índices Innodb DB + estão em torno de 10 GB. O servidor possui 32 GB de RAM e está executando o Cent OS 7 x64.

Eu tenho uma tabela grande que contém cerca de 10 milhões de registros.

Eu recebo um arquivo de despejo atualizado de um servidor remoto a cada 24 horas. O arquivo está no formato csv. Eu não tenho controle sobre esse formato. O arquivo tem ~ 750 MB. Tentei inserir dados em uma tabela MyISAM linha por linha e demorou 35 minutos.

Eu preciso pegar apenas 3 valores por linha de 10 a 12 do arquivo e atualizá-lo no banco de dados.

Qual é a melhor maneira de conseguir algo assim?

Eu preciso fazer isso diariamente.

Atualmente, o Flow é assim:

  1. mysqli_begin_transaction
  2. Ler arquivo de despejo linha por linha
  3. Atualize cada registro Linha por Linha.
  4. mysqli_commit

As operações acima levam cerca de 30 a 40 minutos para serem concluídas. Enquanto isso, outras atualizações acontecem, o que me fornece

Tempo limite de espera de bloqueio excedido; tente reiniciar a transação

Atualização 1

carregamento de dados em nova tabela usando LOAD DATA LOCAL INFILE. No MyISAM, 38.93 secenquanto no InnoDB, foram necessários 7 min e 5,21 segundos. Então eu fiz:

UPDATE table1 t1, table2 t2
SET 
t1.field1 = t2.field1,
t1.field2 = t2.field2,
t1.field3 = t2.field3
WHERE t1.field10 = t2.field10

Query OK, 434914 rows affected (22 hours 14 min 47.55 sec)

Atualização 2

mesma atualização com consulta de junção

UPDATE table1 a JOIN table2 b 
ON a.field1 = b.field1 
SET 
a.field2 = b.field2,
a.field3 = b.field3,
a.field4 = b.field4

(14 hours 56 min 46.85 sec)

Esclarecimentos de perguntas nos comentários:

  • Cerca de 6% das linhas da tabela serão atualizadas pelo arquivo, mas às vezes pode chegar a 25%.
  • Existem índices nos campos que estão sendo atualizados. Existem 12 índices na tabela e 8 índices incluem os campos de atualização.
  • Não é necessário fazer a atualização em uma transação. Pode levar tempo, mas não mais que 24 horas. Eu estou olhando para fazê-lo em 1 hora sem bloquear a tabela inteira, pois mais tarde eu tenho que atualizar o índice de esfinge que depende dessa tabela. Não importa se as etapas demoram mais tempo, desde que o banco de dados esteja disponível para outras tarefas.
  • Eu poderia modificar o formato csv em uma etapa de pré-processo. A única coisa que importa é a atualização rápida e sem travar.
  • A tabela 2 é MyISAM. É a tabela recém-criada do arquivo csv usando o carregamento de dados infile. O tamanho do arquivo MYI é de 452 MB. A tabela 2 é indexada na coluna field1.
  • MYD da tabela MyISAM é 663MB.

Atualização 3:

Aqui estão mais detalhes sobre as duas tabelas.

CREATE TABLE `content` (
  `hash` char(40) CHARACTER SET ascii NOT NULL DEFAULT '',
  `title` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
  `og_name` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
  `keywords` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '',
  `files_count` smallint(5) unsigned NOT NULL DEFAULT '0',
  `more_files` smallint(5) unsigned NOT NULL DEFAULT '0',
  `files` varchar(255) COLLATE utf8_unicode_ci NOT NULL DEFAULT '0',
  `category` smallint(3) unsigned NOT NULL DEFAULT '600',
  `size` bigint(19) unsigned NOT NULL DEFAULT '0',
  `downloaders` int(11) NOT NULL DEFAULT '0',
  `completed` int(11) NOT NULL DEFAULT '0',
  `uploaders` int(11) NOT NULL DEFAULT '0',
  `creation_date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  `upload_date` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  `last_updated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
  `vote_up` int(11) unsigned NOT NULL DEFAULT '0',
  `vote_down` int(11) unsigned NOT NULL DEFAULT '0',
  `comments_count` int(11) NOT NULL DEFAULT '0',
  `imdb` int(8) unsigned NOT NULL DEFAULT '0',
  `video_sample` tinyint(1) NOT NULL DEFAULT '0',
  `video_quality` tinyint(2) NOT NULL DEFAULT '0',
  `audio_lang` varchar(127) CHARACTER SET ascii NOT NULL DEFAULT '',
  `subtitle_lang` varchar(127) CHARACTER SET ascii NOT NULL DEFAULT '',
  `verified` tinyint(1) unsigned NOT NULL DEFAULT '0',
  `uploader` int(11) unsigned NOT NULL DEFAULT '0',
  `anonymous` tinyint(1) NOT NULL DEFAULT '0',
  `enabled` tinyint(1) unsigned NOT NULL DEFAULT '0',
  `tfile_size` int(11) unsigned NOT NULL DEFAULT '0',
  `scrape_source` tinyint(1) unsigned NOT NULL DEFAULT '0',
  `record_num` int(11) unsigned NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`record_num`),
  UNIQUE KEY `hash` (`hash`),
  KEY `uploaders` (`uploaders`),
  KEY `tfile_size` (`tfile_size`),
  KEY `enabled_category_upload_date_verified_` (`enabled`,`category`,`upload_date`,`verified`),
  KEY `enabled_upload_date_verified_` (`enabled`,`upload_date`,`verified`),
  KEY `enabled_category_verified_` (`enabled`,`category`,`verified`),
  KEY `enabled_verified_` (`enabled`,`verified`),
  KEY `enabled_uploader_` (`enabled`,`uploader`),
  KEY `anonymous_uploader_` (`anonymous`,`uploader`),
  KEY `enabled_uploaders_upload_date_` (`enabled`,`uploaders`,`upload_date`),
  KEY `enabled_verified_category` (`enabled`,`verified`,`category`),
  KEY `verified_enabled_category` (`verified`,`enabled`,`category`)
) ENGINE=InnoDB AUTO_INCREMENT=7551163 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci ROW_FORMAT=FIXED


CREATE TABLE `content_csv_dump_temp` (
  `hash` char(40) CHARACTER SET ascii NOT NULL DEFAULT '',
  `title` varchar(255) COLLATE utf8_unicode_ci NOT NULL,
  `category_id` int(11) unsigned NOT NULL DEFAULT '0',
  `uploaders` int(11) unsigned NOT NULL DEFAULT '0',
  `downloaders` int(11) unsigned NOT NULL DEFAULT '0',
  `verified` tinyint(1) unsigned NOT NULL DEFAULT '0',
  PRIMARY KEY (`hash`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci

e aqui está a consulta de atualização que atualiza a contenttabela usando dados decontent_csv_dump_temp

UPDATE content a JOIN content_csv_dump_temp b 
ON a.hash = b.hash 
SET 
a.uploaders = b.uploaders,
a.downloaders = b.downloaders,
a.verified = b.verified

atualização 4:

todos os testes acima foram feitos na máquina de teste., mas agora fiz os mesmos testes na máquina de produção e as consultas são muito rápidas.

mysql> UPDATE content_test a JOIN content_csv_dump_temp b
    -> ON a.hash = b.hash
    -> SET
    -> a.uploaders = b.uploaders,
    -> a.downloaders = b.downloaders,
    -> a.verified = b.verified;
Query OK, 2673528 rows affected (7 min 50.42 sec)
Rows matched: 7044818  Changed: 2673528  Warnings: 0

Eu peço desculpas pelo meu erro. É melhor usar a junção em vez de cada atualização de registro. agora estou tentando melhorar o mpre usando o índice sugerido por rick_james, será atualizado assim que a marcação de banco de dados for concluída.

AMB
fonte
Você tem um composto INDEX(field2, field3, field4) (em qualquer ordem)? Por favor, mostre-nos SHOW CREATE TABLE.
Rick James
1
Os índices 12 e 8 são uma parte séria do seu problema. MyISAM é outra parte séria. O InnoDB ou o TokuDB têm um desempenho muito melhor com vários índices.
Rick James
Você tem dois diferentes UPDATEs . Diga-nos exatamente como é a declaração direta para atualizar a tabela a partir dos dados csv. Então nós podemos ser capazes de ajudá-lo a desenvolver uma técnica que atenda às suas necessidades.
Rick James
@RickJames só há um update, e verifique questão atualizado, obrigado.
AMB

Respostas:

17

Com base na minha experiência, eu usaria LOAD DATA INFILE para importar seu arquivo CSV.

A instrução LOAD DATA INFILE lê linhas de um arquivo de texto em uma tabela em uma velocidade muito alta.

Exemplo que encontrei na internet Exemplo de dados de carregamento . Eu testei este exemplo na minha caixa e funcionou bem

Tabela de exemplo

CREATE TABLE example (
  `Id` int(11) NOT NULL AUTO_INCREMENT,
  `Column2` varchar(14) NOT NULL,
  `Column3` varchar(14) NOT NULL,
  `Column4` varchar(14) NOT NULL,
  `Column5` DATE NOT NULL,
  PRIMARY KEY (`Id`)
) ENGINE=InnoDB

Arquivo CSV de exemplo

# more /tmp/example.csv
Column1,Column2,Column3,Column4,Column5
1,A,Foo,sdsdsd,4/13/2013
2,B,Bar,sdsa,4/12/2013
3,C,Foo,wewqe,3/12/2013
4,D,Bar,asdsad,2/1/2013
5,E,FOObar,wewqe,5/1/2013

Declaração de importação a ser executada no console do MySQL

LOAD DATA LOCAL INFILE '/tmp/example.csv'
    -> INTO TABLE example
    -> FIELDS TERMINATED BY ','
    -> LINES TERMINATED BY '\n'
    -> IGNORE 1 LINES
    -> (id, Column3,Column4, @Column5)
    -> set
    -> Column5 = str_to_date(@Column5, '%m/%d/%Y');

Resultado

MySQL [testcsv]> select * from example;
+----+---------+---------+---------+------------+
| Id | Column2 | Column3 | Column4 | Column5    |
+----+---------+---------+---------+------------+
|  1 |         | Column2 | Column3 | 0000-00-00 |
|  2 |         | B       | Bar     | 0000-00-00 |
|  3 |         | C       | Foo     | 0000-00-00 |
|  4 |         | D       | Bar     | 0000-00-00 |
|  5 |         | E       | FOObar  | 0000-00-00 |
+----+---------+---------+---------+------------+

IGNORE simplesmente ignora a primeira linha que é o cabeçalho da coluna.

Após IGNORE, estamos especificando as colunas (ignorando a coluna2), a serem importadas, que correspondem a um dos critérios da sua pergunta.

Aqui está outro exemplo diretamente do Oracle: Exemplo de LOAD DATA INFILE

Isso deve ser suficiente para você começar.

Craig Efrein
fonte
eu poderia usar dados de carga para o carregamento de dados na tabela temporária e, em seguida, usar outras consultas para atualizá-lo na tabela principal, obrigado.
AMB
14

À luz de todas as coisas mencionadas, parece que o gargalo é a junção em si.

ASPECTO Nº 1: Tamanho do Buffer de Ingresso

Com toda a probabilidade, seu join_buffer_size provavelmente é muito baixo.

De acordo com a documentação do MySQL sobre como o MySQL usa o cache de buffer de junção

Armazenamos apenas as colunas usadas no buffer de junção, não as linhas inteiras.

Sendo esse o caso, mantenha as chaves do buffer de junção na RAM.

Você tem 10 milhões de linhas vezes 4 bytes para cada chave. Isso é cerca de 40 milhões.

Tente aumentar a sessão para 42 milhões (um pouco maior que 40 milhões)

SET join_buffer_size = 1024 * 1024 * 42;
UPDATE table1 a JOIN table2 b 
ON a.field1 = b.field1 
SET 
a.field2 = b.field2,
a.field3 = b.field3,
a.field4 = b.field4;

Se isso funcionar, prossiga para adicioná-lo ao my.cnf

[mysqld]
join_buffer_size = 42M

Reiniciar o mysqld não é necessário para novas conexões. Apenas corra

mysql> SET GLOBAL join_buffer_size = 1024 * 1024 * 42;

ASPECTO Nº 2: Operação de Ingresso

Você pode manipular o estilo da operação de junção, apertando o otimizador

De acordo com a documentação do MySQL sobre junções de acesso em bloco aninhado e chave em lote

Quando BKA é usado, o valor de join_buffer_size define o tamanho do lote de chaves em cada solicitação para o mecanismo de armazenamento. Quanto maior o buffer, maior o acesso seqüencial à tabela à direita de uma operação de junção, o que pode melhorar significativamente o desempenho.

Para que o BKA seja usado, o sinalizador batched_key_access da variável de sistema optimizer_switch deve estar ativado. BKA usa MRR, portanto, o sinalizador mrr também deve estar ativado. Atualmente, a estimativa de custo para MRR é muito pessimista. Portanto, também é necessário desativar o mrr_cost_based para que o BKA seja usado.

Esta mesma página recomenda fazer isso:

mysql> SET optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';

ASPECTO Nº 3: Gravando atualizações no disco (OPCIONAL)

A maioria esquece de aumentar o innodb_write_io_threads para gravar páginas sujas fora do buffer pool mais rapidamente.

[mysqld]
innodb_write_io_threads = 16

Você precisará reiniciar o MySQL para esta alteração

DE UMA CHANCE !!!

RolandoMySQLDBA
fonte
Agradável! +1 para a ponta do buffer de junção ajustável. Se você precisar participar, junte-se na memória. Boa dica!
Peter Dixon-Moses
3
  1. CREATE TABLE que corresponde ao CSV
  2. LOAD DATA nessa mesa
  3. UPDATE real_table JOIN csv_table ON ... SET ..., ..., ...;
  4. DROP TABLE csv_table;

A etapa 3 será muito mais rápida que linha por linha, mas ainda bloqueará todas as linhas da tabela por um período não trivial. Se esse tempo de bloqueio for mais importante do que quanto tempo leva todo o processo, ...

Se nada mais estiver escrevendo na tabela, então ...

  1. CREATE TABLEque corresponda ao CSV; sem índices, exceto o que é necessário no JOINno UPDATE. Se único, faça-o PRIMARY KEY.
  2. LOAD DATA nessa mesa
  3. copie real_tablepara new_table( CREATE ... SELECT)
  4. UPDATE new_table JOIN csv_table ON ... SET ..., ..., ...;
  5. RENAME TABLE real_table TO old, new_table TO real_table;
  6. DROP TABLE csv_table, old;

A etapa 3 é mais rápida que a atualização, especialmente se índices desnecessários forem deixados de lado.
O passo 5 é "instantâneo".

Rick James
fonte
digamos no exemplo de segundos, após a etapa 3, estamos executando a etapa 4, então os novos dados são inseridos na tabela real, para que possamos perder esses dados na tabela nova? qual é a solução alternativa para isso? obrigado
AMB
Veja o que pt-online-schema-digest; cuida de tais problemas através de um TRIGGER.
Rick James
Você provavelmente não precisa de nenhum índice na tabela LOAD DATA. Adicionar índices desnecessários é caro (com o tempo).
Rick James
Com base nas informações mais recentes, estou inclinado para o arquivo CSV sendo carregado em uma tabela MyISAM com apenas uma AUTO_INCREMENT, e depois dividindo 1K linhas por vez, com base no PK. Mas preciso ver todos os requisitos e o esquema da tabela antes de tentar detalhar os detalhes.
Rick James
Eu configurei o hash como PRIMARY index, mas enquanto a divisão em 50k usando a consulta de pedidos leva mais tempo., seria melhor se eu criar incremento automático? e defini-lo como PRIMARY index?
AMB
3

Você disse:

  • As atualizações afetam de 6 a 25% da sua tabela
  • Você deseja fazer isso o mais rápido possível (<1 hora)
  • sem travar
  • não precisa estar em uma única transação
  • ainda (no comentário da resposta de Rick James), você expressa preocupação com as condições da corrida

Muitas dessas declarações podem ser contraditórias. Por exemplo, grandes atualizações sem bloquear a tabela. Ou evitando as condições de corrida sem o uso de uma transação gigante.

Além disso, como sua tabela é fortemente indexada, inserções e atualizações podem ser lentas.


Evitando condições de corrida

Se você conseguir adicionar um carimbo de data / hora atualizado à sua tabela, poderá resolver as condições de corrida e também evitar o registro de meio milhão de atualizações em uma única transação.

Isso libera você para executar atualizações linha por linha (como você faz atualmente), mas com confirmação automática ou lotes de transações mais razoáveis.

Você evita as condições de corrida (enquanto atualiza linha por linha) executando uma verificação de que uma atualização posterior ainda não ocorreu ( UPDATE ... WHERE pk = [pk] AND updated < [batchfile date])

E, o mais importante, isso permite executar atualizações paralelas .


Correndo o mais rápido possível - paralelizando

Com este carimbo de data e hora, verifique agora:

  1. Divida o arquivo em lotes em alguns pedaços de tamanho razoável (por exemplo, 50.000 linhas / arquivo)
  2. Em paralelo, leia um script em cada arquivo e produza um arquivo com 50.000 instruções UPDATE.
  3. Paralelamente, assim que (2) terminar, mysqlexecute cada arquivo sql.

(por exemplo, bashveja splite xargs -Pencontre maneiras de executar facilmente um comando de várias maneiras paralelas. O grau de paralelismo depende de quantos threads você deseja dedicar à atualização )

Peter Dixon-Moses
fonte
Tenha em mente que "linha por linha" é susceptível de ser 10x mais lento do que fazer as coisas em lotes de pelo menos 100.
Rick James
Você precisaria compará-lo neste caso para ter certeza. Atualizando 6-25% de uma tabela (com 8 índices envolvidos nas colunas atualizadas), eu consideraria a possibilidade de que a manutenção do índice se torne o gargalo.
Peter Dixon-Moses
Em alguns casos, pode ser mais rápido descartar índices, atualizar em massa e recriá-los depois ... mas o OP não quer tempo de inatividade.
Peter Dixon-Moses
1

Grandes atualizações são vinculadas à E / S. Eu sugeriria:

  1. Crie uma tabela distinta que armazene seus 3 campos atualizados com freqüência. Vamos chamar uma tabela de assets_static na qual você mantém, bem, dados estáticos e a outra assets_dynamic que armazenará uploaders, downloaders e verificados.
  2. Se você puder, use o mecanismo MEMORY para a tabela assets_dynamic . (backup em disco após cada atualização).
  3. Atualize seu assets_dynamic leve e ágil conforme sua atualização 4 (por exemplo, LOAD INFILE ... INTO temp; UPDATE assets_dynamic a JOIN temp b em a.id = b.id SET [o que deve ser atualizado]. minuto (no nosso sistema, assets_dynamic possui 95 milhões de linhas e as atualizações afetam ~ 6 milhões de linhas, em pouco mais de 40 anos).
  4. Ao executar o indexador do Sphinx, JOIN assets_static e assets_dynamic (supondo que você queira usar um desses campos como um atributo).
user3127882
fonte
0

Para UPDATEque você corra rápido, você precisa

INDEX(uploaders, downloaders, verified)

Pode estar em qualquer mesa. Os três campos podem estar em qualquer ordem.

Isso facilitará a UPDATEcapacidade de corresponder rapidamente as linhas entre as duas tabelas.

E torne os tipos de dados iguais nas duas tabelas (ambas INT SIGNEDou ambas INT UNSIGNED).

Rick James
fonte
isso realmente atrasou a atualização.
AMB
Hmmm ... Por favor, forneça EXPLAIN UPDATE ...;.
Rick James