sql >> Base de Datos >  >> RDS >> Mysql

¿Configuración óptima de MySQL para consultas que entregan grandes cantidades de datos?

Algo tiene que estar muy mal para que su consulta tarde 2 horas en ejecutarse cuando puedo hacer lo mismo en menos de 60 segundos en un hardware similar.

Algunos de los siguientes pueden resultar útiles...

Ajuste MySQL para su motor

Verifique la configuración de su servidor y optimícelo en consecuencia. Algunos de los siguientes recursos deberían ser útiles.

Ahora, para lo menos obvio...

Considere usar un procedimiento almacenado para procesar el lado del servidor de datos

¿Por qué no procesar todos los datos dentro de MySQL para no tener que enviar grandes cantidades de datos a su capa de aplicación? El siguiente ejemplo utiliza un cursor para realizar un bucle y procesar 50 millones de filas del lado del servidor en menos de 2 minutos. No soy un gran fanático de los cursores, especialmente en MySQL, donde son muy limitados, pero supongo que estarías haciendo un bucle en el conjunto de resultados y haciendo algún tipo de análisis numérico, por lo que el uso de un cursor está justificado en este caso.

Tabla de resultados de myisam simplificada:claves basadas en las suyas.

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;

Generé 100 millones de filas de datos con los campos clave que tienen aproximadamente la misma cardinalidad que en su ejemplo:

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   

Procedimiento almacenado

Creé un procedimiento almacenado simple que obtiene los datos requeridos y los procesa (usa la misma condición de su ejemplo)

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 ; 

Se observaron los siguientes tiempos de ejecución:

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, el rendimiento es un poco decepcionante, así que pasemos a la siguiente idea.

Considere usar el motor innodb (shock horror)

¿Por qué innodb? ¡porque tiene índices agrupados! Encontrará que la inserción es más lenta con innodb pero, con suerte, será más rápido de leer, por lo que es una compensación que podría valer la pena.

Acceder a una fila a través del índice agrupado es rápido porque los datos de la fila están en la misma página a la que lleva la búsqueda del índice. Si una tabla es grande, la arquitectura de índice agrupado a menudo guarda una operación de E/S de disco en comparación con las organizaciones de almacenamiento que almacenan datos de fila utilizando una página diferente del registro de índice. Por ejemplo, MyISAM usa un archivo para filas de datos y otro para registros de índice.

Más información aquí:

Tabla de resultados de 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;

Un problema con innodb es que no es compatible con los campos de incremento automático que forman parte de una clave compuesta, por lo que deberá proporcionar el valor de la clave incremental usted mismo mediante un generador de secuencias, un activador o algún otro método, tal vez en la aplicación que completa la tabla de resultados. ??

Nuevamente, generé 100 millones de filas de datos con los campos clave que tienen aproximadamente la misma cardinalidad que en su ejemplo. No se preocupe si estas cifras no coinciden con el ejemplo de myisam, ya que innodb estima las cardinalidades para que no sean exactamente iguales. (pero lo son, se usa el mismo conjunto de datos)

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   

Procedimiento almacenado

El procedimiento almacenado es exactamente el mismo que el ejemplo de myisam anterior, pero en su lugar selecciona datos de la tabla innodb.

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

Los resultados son los siguientes:

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 más rápido que la implementación del motor myisam! (innodb FTW)

Divide y vencerás

Procesar los resultados en un procedimiento almacenado del lado del servidor que usa un cursor podría no ser una solución óptima, especialmente porque MySQL no tiene soporte para cosas tales como matrices y estructuras de datos complejas que están disponibles en lenguajes 3GL como C #, etc. o incluso en otras bases de datos como como Oracle PL/SQL.

Entonces, la idea aquí es devolver lotes de datos a una capa de aplicación (C# lo que sea) que luego puede agregar los resultados a una estructura de datos basada en recopilación y luego procesar los datos internamente.

Procedimiento almacenado

El procedimiento almacenado toma 3 parámetros rc, df_low y df_high que le permiten seleccionar un rango de datos de la siguiente manera:

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, cuanto mayor sea el rango de df, más datos extraerá.

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 ; 

También preparé una versión de myisam que es idéntica excepto por la tabla que se usa.

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

Según el ejemplo del cursor anterior, esperaría que la versión de innodb supere a la de myisam.

Desarrollé un rápido y sucio Aplicación de C# de subprocesos múltiples que llamará al procedimiento almacenado y agregará los resultados a una colección para el procesamiento posterior a la consulta. No tiene que usar subprocesos, el mismo enfoque de consulta por lotes podría realizarse secuencialmente sin mucha pérdida de rendimiento.

Cada subproceso (QueryThread) selecciona un rango de datos df, repite el conjunto de resultados y agrega cada resultado (fila) a la colección 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();
        }
    }

Tiempo de ejecución observado de la siguiente manera:

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

Por lo tanto, se recuperaron 50 millones de filas y se agregaron a una colección en menos de 60 segundos.

Intenté lo mismo usando el procedimiento almacenado myisam que tardó 2 minutos en completarse.

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

Pasar a innodb

En mi sistema simplificado, la tabla myisam no funciona demasiado mal, por lo que podría no valer la pena migrar a innodb. Si decide copiar los datos de sus resultados en una tabla innodb, hágalo de la siguiente manera:

start transaction;

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

commit;

Ordenar el resultado por innodb PK antes de insertar y envolver todo en una transacción acelerará las cosas.

Espero que algo de esto resulte útil.

Buena suerte