Mysql
 sql >> Base de données >  >> RDS >> Mysql

Paramètres MySQL optimaux pour les requêtes qui fournissent de grandes quantités de données ?

Quelque chose ne va pas du tout pour que votre requête prenne 2 heures à s'exécuter alors que je peux faire la même chose en moins de 60 secondes sur un matériel similaire.

Certains des éléments suivants pourraient s'avérer utiles...

Régler MySQL pour votre moteur

Vérifiez la configuration de votre serveur et optimisez-la en conséquence. Certaines des ressources suivantes devraient être utiles.

Passons maintenant au moins évident...

Envisagez d'utiliser une procédure stockée pour traiter le côté serveur de données

Pourquoi ne pas traiter toutes les données à l'intérieur de MySQL pour ne pas avoir à envoyer de grandes quantités de données à votre couche application ? L'exemple suivant utilise un curseur pour boucler et traiter 50 millions de lignes côté serveur en moins de 2 minutes. Je ne suis pas un grand fan des curseurs, en particulier dans MySQL où ils sont très limités, mais je suppose que vous boucleriez le jeu de résultats et feriez une forme d'analyse numérique, donc l'utilisation d'un curseur est justifiable dans ce cas.

Tableau de résultats myisam simplifié - clés basées sur les vôtres.

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;

J'ai généré 100 millions de lignes de données avec les champs clés ayant approximativement la même cardinalité que dans votre exemple :

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   

Procédure stockée

J'ai créé une procédure stockée simple qui récupère les données requises et les traite (utilise la même condition que votre exemple)

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 ; 

Les temps d'exécution suivants ont été observés :

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, les performances sont un peu décevantes, alors passons à l'idée suivante.

Envisagez d'utiliser le moteur innodb (Shock Horror)

Pourquoi innodb ?? car il a des index clusterisés ! Vous constaterez que l'insertion est plus lente avec innodb, mais j'espère que la lecture sera plus rapide, c'est donc un compromis qui en vaut la peine.

L'accès à une ligne via l'index clusterisé est rapide car les données de la ligne se trouvent sur la même page où mène la recherche d'index. Si une table est volumineuse, l'architecture d'index clusterisée enregistre souvent une opération d'E/S de disque par rapport aux organisations de stockage qui stockent les données de ligne à l'aide d'une page différente de l'enregistrement d'index. Par exemple, MyISAM utilise un fichier pour les lignes de données et un autre pour les enregistrements d'index.

Plus d'infos ici :

Tableau de résultats innodb simplifié

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 problème avec innodb est qu'il ne prend pas en charge les champs auto_increment qui font partie d'une clé composite, vous devez donc fournir vous-même la valeur de la clé d'incrémentation à l'aide d'un générateur de séquence, d'un déclencheur ou d'une autre méthode - peut-être dans l'application remplissant la table de résultats elle-même ??

Encore une fois, j'ai généré 100 millions de lignes de données avec les champs clés ayant approximativement la même cardinalité que dans votre exemple. Ne vous inquiétez pas si ces chiffres ne correspondent pas à l'exemple myisam, car innodb estime les cardinalités afin qu'elles ne soient pas exactement les mêmes. (mais ils sont - même jeu de données utilisé)

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   

Procédure stockée

La procédure stockée est exactement la même que l'exemple myisam ci-dessus mais sélectionne à la place les données de la table innodb.

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

Les résultats sont les suivants :

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)

environ 2-3 minutes plus rapide que l'implémentation du moteur myisam ! (innodb FTW)

Diviser pour régner

Le traitement des résultats dans une procédure stockée côté serveur qui utilise un curseur peut ne pas être une solution optimale, d'autant plus que MySQL ne prend pas en charge des éléments tels que les tableaux et les structures de données complexes qui sont facilement disponibles dans les langages 3GL tels que C#, etc. ou même dans d'autres bases de données telles que comme Oracle PL/SQL.

L'idée ici est donc de renvoyer des lots de données à une couche d'application (C# quel qu'il soit) qui peut ensuite ajouter les résultats à une structure de données basée sur une collection, puis traiter les données en interne.

Procédure stockée

La procédure stockée prend 3 paramètres rc, df_low et df_high ce qui permet de sélectionner une plage de données comme suit :

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

évidemment, plus la plage df est élevée, plus vous extrayez de données.

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 ; 

J'ai également mis au point une version myisam également qui est identique à l'exception de la table utilisée.

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

Sur la base de l'exemple de curseur ci-dessus, je m'attendrais à ce que la version innodb surpasse celle de myisam.

J'ai développé un rapide et sale Application C# multithread qui appellera la procédure stockée et ajoutera les résultats à une collection pour le traitement post-requête. Vous n'êtes pas obligé d'utiliser des threads, la même approche de requête par lots peut être effectuée de manière séquentielle sans trop de perte de performances.

Chaque thread (QueryThread) sélectionne une plage de données df, boucle le jeu de résultats et ajoute chaque résultat (ligne) à la collection de résultats.

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 observé comme suit :

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

Ainsi, 50 millions de lignes ont été récupérées et ajoutées à une collection en moins de 60 secondes.

J'ai essayé la même chose en utilisant la procédure stockée myisam qui a pris 2 minutes.

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

Passage à innodb

Dans mon système simplifié, la table myisam ne fonctionne pas trop mal, il ne vaut donc peut-être pas la peine de migrer vers innodb. Si vous avez décidé de copier vos données de résultat dans une table innodb, procédez comme suit :

start transaction;

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

commit;

Commander le résultat par le PK innodb avant d'insérer et d'envelopper le tout dans une transaction accélérera les choses.

J'espère que certaines de ces informations vous seront utiles.

Bonne chance