Mysql
 sql >> Base de Dados >  >> RDS >> Mysql

Configurações ideais do MySQL para consultas que fornecem grandes quantidades de dados?


Algo deve estar seriamente errado para que sua consulta demore 2 horas para ser executada quando posso fazer a mesma coisa em menos de 60 segundos em hardware semelhante.

Alguns dos seguintes podem ser úteis...

Ajuste o MySQL para seu mecanismo


Verifique a configuração do seu servidor e otimize de acordo. Alguns dos seguintes recursos devem ser úteis.

Agora o menos óbvio...

Considere usar um procedimento armazenado para processar o lado do servidor de dados


Por que não processar todos os dados dentro do MySQL para que você não precise enviar grandes quantidades de dados para sua camada de aplicação? O exemplo a seguir usa um cursor para fazer um loop e processar 50 milhões de linhas do lado do servidor em menos de 2 minutos. Eu não sou um grande fã de cursores, especialmente no MySQL, onde eles são muito limitados, mas eu acho que você estaria fazendo um loop no conjunto de resultados e fazendo alguma forma de análise numérica, então o uso de um cursor é justificável neste caso.

Tabela de resultados myisam simplificada - chaves baseadas na sua.
drop table if exists results_1mregr_c_ew_f;
create table results_1mregr_c_ew_f
(
id int unsigned not null auto_increment primary key,
rc tinyint unsigned not null,
df int unsigned not null default 0,
val double(10,4) not null default 0,
ts timestamp not null default now(),
key (rc, df)
)
engine=myisam;

Gerei 100 milhões de linhas de dados com os campos-chave com aproximadamente a mesma cardinalidade do seu exemplo:
show indexes from results_1mregr_c_ew_f;

Table                   Non_unique  Key_name    Seq_in_index    Column_name Collation   Cardinality Index_type
=====                   ==========  ========    ============    =========== =========   =========== ==========
results_1mregr_c_ew_f       0       PRIMARY         1               id          A       100000000   BTREE   
results_1mregr_c_ew_f       1       rc              1               rc          A               2   BTREE   
results_1mregr_c_ew_f       1       rc              2               df          A             223   BTREE   

Procedimento armazenado

Eu criei um procedimento armazenado simples que busca os dados necessários e os processa (usa a mesma condição where do seu exemplo)
drop procedure if exists process_results_1mregr_c_ew_f;

delimiter #

create procedure process_results_1mregr_c_ew_f
(
in p_rc tinyint unsigned,
in p_df int unsigned
)
begin

declare v_count int unsigned default 0;
declare v_done tinyint default 0;
declare v_id int unsigned;
declare v_result_cur cursor for select id from results_1mregr_c_ew_f where rc = p_rc and df > p_df;
declare continue handler for not found set v_done = 1;

open v_result_cur;

repeat
    fetch v_result_cur into v_id;

    set v_count = v_count + 1;
    -- do work...

until v_done end repeat;
close v_result_cur;

select v_count as counter;

end #

delimiter ; 

Os seguintes tempos de execução foram observados:
call process_results_1mregr_c_ew_f(0,60);

runtime 1 = 03:24.999 Query OK (3 mins 25 secs)
runtime 2 = 03:32.196 Query OK (3 mins 32 secs)

call process_results_1mregr_c_ew_f(1,60);

runtime 1 = 04:59.861 Query OK (4 mins 59 secs)
runtime 2 = 04:41.814 Query OK (4 mins 41 secs)

counter
========
23000002 (23 million rows processed in each case)

Hmmmm, desempenho um pouco decepcionante, então vamos para a próxima ideia.

Considere usar o mecanismo innodb (choque horror)


Por que innodb?? porque tem índices agrupados! Você achará a inserção mais lenta usando o innodb, mas espero que seja mais rápido de ler, então é uma troca que pode valer a pena.

O acesso a uma linha por meio do índice clusterizado é rápido porque os dados da linha estão na mesma página em que a pesquisa do índice leva. Se uma tabela for grande, a arquitetura de índice clusterizado geralmente salva uma operação de E/S de disco quando comparada a organizações de armazenamento que armazenam dados de linha usando uma página diferente do registro de índice. Por exemplo, MyISAM usa um arquivo para linhas de dados e outro para registros de índice.

Mais informações aqui:

Tabela de resultados innodb simplificada
drop table if exists results_innodb;
create table results_innodb
(
rc tinyint unsigned not null,
df int unsigned not null default 0,
id int unsigned not null, -- cant auto_inc this !!
val double(10,4) not null default 0,
ts timestamp not null default now(),
primary key (rc, df, id) -- note clustered (innodb only !) composite PK
)
engine=innodb;

Um problema com o innodb é que ele não suporta campos auto_increment que fazem parte de uma chave composta, então você teria que fornecer o valor da chave de incremento usando um gerador de sequência, gatilho ou algum outro método - talvez no aplicativo preenchendo a própria tabela de resultados ??

Novamente, gerei 100 milhões de linhas de dados com os campos-chave tendo aproximadamente a mesma cardinalidade do seu exemplo. Não se preocupe se esses números não corresponderem ao exemplo do myisam, pois o innodb estima as cardinalidades para que elas não sejam exatamente as mesmas. (mas eles são - mesmo conjunto de dados usado)
show indexes from results_innodb;

Table           Non_unique  Key_name    Seq_in_index    Column_name Collation   Cardinality Index_type
=====           ==========  ========    ============    =========== =========   =========== ==========
results_innodb      0       PRIMARY         1               rc          A                18     BTREE   
results_innodb      0       PRIMARY         2               df          A                18     BTREE   
results_innodb      0       PRIMARY         3               id          A         100000294     BTREE   

Procedimento armazenado

O procedimento armazenado é exatamente o mesmo do exemplo myisam acima, mas seleciona os dados da tabela innodb.
declare v_result_cur cursor for select id from results_innodb where rc = p_rc and df > p_df;

Os resultados são os seguintes:
call process_results_innodb(0,60);

runtime 1 = 01:53.407 Query OK (1 mins 53 secs)
runtime 2 = 01:52.088 Query OK (1 mins 52 secs)

call process_results_innodb(1,60);

runtime 1 = 02:01.201 Query OK (2 mins 01 secs)
runtime 2 = 01:49.737 Query OK (1 mins 50 secs)

counter
========
23000002 (23 million rows processed in each case)

aproximadamente 2-3 minutos mais rápido do que a implementação do motor myisam! (innodb FTW)

Dividir e conquistar


Processar os resultados em um procedimento armazenado do lado do servidor que usa um cursor pode não ser uma solução ideal, especialmente porque o MySQL não tem suporte para coisas como matrizes e estruturas de dados complexas que estão prontamente disponíveis em linguagens 3GL como C# etc ou mesmo em outros bancos de dados como como Oracle PL/SQL.

Portanto, a ideia aqui é retornar lotes de dados para uma camada de aplicativo (C#, qualquer que seja) que possa adicionar os resultados a uma estrutura de dados baseada em coleção e processar os dados internamente.

Procedimento armazenado

O procedimento armazenado leva 3 parâmetros rc, df_low e df_high que permite selecionar um intervalo de dados da seguinte forma:
call list_results_innodb(0,1,1); -- df 1
call list_results_innodb(0,1,10); -- df between 1 and 10
call list_results_innodb(0,60,120); -- df between 60 and 120 etc...

obviamente, quanto maior o intervalo df, mais dados você extrairá.
drop procedure if exists list_results_innodb;

delimiter #

create procedure list_results_innodb
(
in p_rc tinyint unsigned,
in p_df_low int unsigned,
in p_df_high int unsigned
)
begin
    select rc, df, id from results_innodb where rc = p_rc and df between p_df_low and p_df_high;
end #

delimiter ; 

Eu também criei uma versão do myisam que é idêntica, exceto pela tabela que é usada.
call list_results_1mregr_c_ew_f(0,1,1);
call list_results_1mregr_c_ew_f(0,1,10);
call list_results_1mregr_c_ew_f(0,60,120);

Com base no exemplo do cursor acima, eu esperaria que a versão innodb superasse a do myisam.

Desenvolvi um rápido e sujo aplicativo C# multithread que chamará o procedimento armazenado e adicionará os resultados a uma coleção para processamento de pós-consulta. Você não precisa usar threads, a mesma abordagem de consulta em lote pode ser feita sequencialmente sem muita perda de desempenho.

Cada thread (QueryThread) seleciona um intervalo de dados df, faz um loop no conjunto de resultados e adiciona cada resultado (linha) à coleção de resultados.
class Program
    {
        static void Main(string[] args)
        {
            const int MAX_THREADS = 12; 
            const int MAX_RC = 120;

            List<AutoResetEvent> signals = new List<AutoResetEvent>();
            ResultDictionary results = new ResultDictionary(); // thread safe collection

            DateTime startTime = DateTime.Now;
            int step = (int)Math.Ceiling((double)MAX_RC / MAX_THREADS) -1; 

            int start = 1, end = 0;
            for (int i = 0; i < MAX_THREADS; i++){
                end = (i == MAX_THREADS - 1) ? MAX_RC : end + step;
                signals.Add(new AutoResetEvent(false));

                QueryThread st = new QueryThread(i,signals[i],results,0,start,end);
                start = end + 1;
            }
            WaitHandle.WaitAll(signals.ToArray());
            TimeSpan runTime = DateTime.Now - startTime;

            Console.WriteLine("{0} results fetched and looped in {1} secs\nPress any key", results.Count, runTime.ToString());
            Console.ReadKey();
        }
    }

Tempo de execução observado da seguinte forma:
Thread 04 done - 31580517
Thread 06 done - 44313475
Thread 07 done - 45776055
Thread 03 done - 46292196
Thread 00 done - 47008566
Thread 10 done - 47910554
Thread 02 done - 48194632
Thread 09 done - 48201782
Thread 05 done - 48253744
Thread 08 done - 48332639
Thread 01 done - 48496235
Thread 11 done - 50000000
50000000 results fetched and looped in 00:00:55.5731786 secs
Press any key

Assim, 50 milhões de linhas foram buscadas e adicionadas a uma coleção em menos de 60 segundos.

Eu tentei a mesma coisa usando o procedimento armazenado myisam que levou 2 minutos para ser concluído.
50000000 results fetched and looped in 00:01:59.2144880 secs

Mover para innodb


No meu sistema simplificado, a tabela myisam não funciona muito mal, então pode não valer a pena migrar para o innodb. Se você decidiu copiar seus dados de resultado para uma tabela innodb, faça da seguinte forma:
start transaction;

insert into results_innodb 
 select <fields...> from results_1mregr_c_ew_f order by <innodb primary key>;

commit;

Ordenar o resultado pelo innodb PK antes de inserir e envolver tudo em uma transação irá acelerar as coisas.

Espero que um pouco disso seja útil.

Boa sorte