Mysql
 sql >> Database >  >> RDS >> Mysql

Impostazioni MySQL ottimali per query che forniscono grandi quantità di dati?

Qualcosa deve essere seriamente sbagliato perché la tua query richieda 2 ore per essere eseguita quando posso fare la stessa cosa in meno di 60 secondi su hardware simile.

Alcuni dei seguenti potrebbero rivelarsi utili...

Regola MySQL per il tuo motore

Controlla la configurazione del tuo server e ottimizza di conseguenza. Alcune delle seguenti risorse dovrebbero essere utili.

Ora per il meno ovvio...

Considera l'utilizzo di una procedura memorizzata per elaborare il lato server dei dati

Perché non elaborare tutti i dati all'interno di MySQL in modo da non dover inviare grandi quantità di dati al livello dell'applicazione? Nell'esempio seguente viene utilizzato un cursore per eseguire il ciclo ed elaborare 50 milioni di righe lato server in meno di 2 minuti. Non sono un grande fan dei cursori, specialmente in MySQL dove sono molto limitati, ma suppongo che avresti eseguito il loop del set di risultati e fatto una qualche forma di analisi numerica, quindi l'uso di un cursore è giustificabile in questo caso.

Tabella dei risultati di myisam semplificata:chiavi basate sulla tua.

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;

Ho generato 100 milioni di righe di dati con i campi chiave aventi all'incirca la stessa cardinalità del tuo esempio:

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   

Procedura memorizzata

Ho creato una semplice procedura memorizzata che recupera i dati richiesti e li elabora (usa la stessa condizione dove del tuo esempio)

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 ; 

Sono stati osservati i seguenti tempi di esecuzione:

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, le prestazioni sono un po' deludenti, quindi alla prossima idea.

Considera l'utilizzo del motore innodb (shock horror)

Perché innodb?? perché ha indici cluster! Troverai un inserimento più lento usando innodb, ma si spera che sia più veloce da leggere, quindi è un compromesso che potrebbe valerne la pena.

L'accesso a una riga tramite l'indice cluster è rapido perché i dati della riga si trovano nella stessa pagina in cui conduce la ricerca nell'indice. Se una tabella è di grandi dimensioni, l'architettura dell'indice cluster spesso salva un'operazione di I/O del disco rispetto alle organizzazioni di archiviazione che archiviano i dati delle righe utilizzando una pagina diversa dal record dell'indice. Ad esempio, MyISAM utilizza un file per le righe di dati e un altro per i record di indice.

Maggiori informazioni qui:

Tabella dei risultati di innodb semplificata

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;

Un problema con innodb è che non supporta i campi auto_increment che fanno parte di una chiave composita, quindi dovresti fornire tu stesso il valore della chiave incrementale usando un generatore di sequenze, un trigger o qualche altro metodo, forse nell'applicazione che popola la tabella dei risultati stessa ??

Ancora una volta, ho generato 100 milioni di righe di dati con i campi chiave aventi approssimativamente la stessa cardinalità del tuo esempio. Non preoccuparti se queste cifre non corrispondono all'esempio di myisam poiché innodb stima le cardinalità in modo che non siano esattamente le stesse. (ma sono - stesso set di dati utilizzato)

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   

Procedura memorizzata

La procedura memorizzata è esattamente la stessa dell'esempio myisam sopra, ma seleziona invece i dati dalla tabella innodb.

declare v_result_cur cursor for select id from results_innodb where rc = p_rc and df > p_df;

I risultati sono i seguenti:

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)

circa 2-3 minuti più veloce rispetto all'implementazione del motore myisam! (innodb FTW)

Dividi e conquista

L'elaborazione dei risultati in una procedura memorizzata lato server che utilizza un cursore potrebbe non essere una soluzione ottimale, soprattutto perché MySQL non ha supporto per cose come array e strutture di dati complesse che sono prontamente disponibili in linguaggi 3GL come C# ecc. o anche in altri database come come Oracle PL/SQL.

Quindi l'idea qui è di restituire batch di dati a un livello dell'applicazione (C# qualunque) che può quindi aggiungere i risultati a una struttura di dati basata sulla raccolta e quindi elaborare i dati internamente.

Procedura memorizzata

La procedura memorizzata accetta 3 parametri rc, df_low e df_high che consentono di selezionare un intervallo di dati come segue:

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...

ovviamente più alto è l'intervallo df, più dati verranno estratti.

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 ; 

Ho anche inventato una versione myisam che è identica tranne che per la tabella che viene utilizzata.

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);

Sulla base dell'esempio del cursore sopra, mi aspetto che la versione di innodb superi quella di myisam.

Ho sviluppato un veloce e sporco applicazione C# multithread che chiamerà la stored procedure e aggiungerà i risultati a una raccolta per l'elaborazione successiva alla query. Non è necessario utilizzare i thread, lo stesso approccio alla query in batch potrebbe essere eseguito in sequenza senza una notevole perdita di prestazioni.

Ogni thread (QueryThread) seleziona un intervallo di dati df, esegue il loop del set di risultati e aggiunge ogni risultato (riga) alla raccolta dei risultati.

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();
        }
    }

Runtime osservato come segue:

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

Quindi 50 milioni di righe sono state recuperate e aggiunte a una raccolta in meno di 60 secondi.

Ho provato la stessa cosa usando la procedura memorizzata myisam che ha richiesto 2 minuti per essere completata.

50000000 results fetched and looped in 00:01:59.2144880 secs

Spostamento su innodb

Nel mio sistema semplificato la tabella myisam non funziona male, quindi potrebbe non valere la pena migrare su innodb. Se hai deciso di copiare i dati dei risultati in una tabella innodb, procedi come segue:

start transaction;

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

commit;

Ordinare il risultato da innodb PK prima di inserire e avvolgere il tutto in una transazione accelererà le cose.

Spero che alcuni di questi si rivelino utili.

Buona fortuna