Mysql
 sql >> Baza danych >  >> RDS >> Mysql

Optymalne ustawienia MySQL dla zapytań dostarczających duże ilości danych?

Coś musi być poważnie nie tak, aby wykonanie zapytania zajęło 2 godziny, podczas gdy mogę zrobić to samo w mniej niż 60 sekund na podobnym sprzęcie.

Niektóre z poniższych mogą okazać się pomocne...

Dostosuj MySQL do swojego silnika

Sprawdź konfigurację serwera i odpowiednio zoptymalizuj. Niektóre z poniższych zasobów powinny być przydatne.

A teraz mniej oczywiste...

Rozważ użycie procedury składowanej do przetwarzania po stronie serwera danych

Dlaczego nie przetworzyć wszystkich danych w MySQL, aby nie wysyłać ogromnych ilości danych do warstwy aplikacji? W poniższym przykładzie użyto kursora do zapętlenia i przetworzenia 50 mln wierszy po stronie serwera w czasie krótszym niż 2 minuty. Nie jestem wielkim fanem kursorów, zwłaszcza w MySQL, gdzie są one bardzo ograniczone, ale zgaduję, że zapętliłbyś zestaw wyników i wykonał jakąś formę analizy numerycznej, więc użycie kursora jest w tym przypadku uzasadnione.

Uproszczona tabela wyników myisam — klucze oparte na Twoich.

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;

Wygenerowałem 100 milionów wierszy danych z polami kluczowymi mającymi w przybliżeniu taką samą kardynalność jak w twoim przykładzie:

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 składowana

Stworzyłem prostą procedurę składowaną, która pobiera wymagane dane i przetwarza je (używa tego samego warunku, co w twoim przykładzie)

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 ; 

Zaobserwowano następujące czasy działania:

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, wydajność trochę rozczarowująca, więc przejdźmy do następnego pomysłu.

Rozważ użycie silnika innodb (szok horror)

Dlaczego innodb?? ponieważ ma klastrowane indeksy ! Zauważysz, że wstawianie jest wolniejsze przy użyciu innodb, ale miejmy nadzieję, że czytanie będzie szybsze, więc jest to kompromis, który może być tego wart.

Dostęp do wiersza za pośrednictwem indeksu klastrowego jest szybki, ponieważ dane wiersza znajdują się na tej samej stronie, na której prowadzi wyszukiwanie indeksu. Jeśli tabela jest duża, architektura indeksu klastrowego często zapisuje operację we/wy dysku w porównaniu z organizacjami pamięci masowej, które przechowują dane wierszy przy użyciu innej strony niż rekord indeksu. Na przykład MyISAM używa jednego pliku dla wierszy danych, a drugiego dla rekordów indeksu.

Więcej informacji tutaj:

Uproszczona tabela wyników innodb

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;

Jednym z problemów z innodb jest to, że nie obsługuje pól auto_increment, które stanowią część klucza złożonego, więc musisz samodzielnie podać wartość klucza inkrementacyjnego za pomocą generatora sekwencji, wyzwalacza lub innej metody - być może w aplikacji wypełniającej samą tabelę wyników ??

Ponownie wygenerowałem 100 milionów wierszy danych z polami kluczowymi mającymi w przybliżeniu taką samą kardynalność jak w twoim przykładzie. Nie martw się, jeśli te liczby nie pasują do przykładu myisam, ponieważ innodb szacuje liczności, więc nie będą one dokładnie takie same. (ale są - ten sam zbiór danych)

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 składowana

Procedura składowana jest dokładnie taka sama jak w powyższym przykładzie myisam, ale zamiast tego wybiera dane z tabeli innodb.

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

Wyniki są następujące:

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)

około 2–3 minuty szybciej niż implementacja silnika myisam ! (innodb FTW)

Dziel i zwyciężaj

Przetwarzanie wyników w procedurze składowanej po stronie serwera używającej kursora może nie być optymalnym rozwiązaniem zwłaszcza, że ​​MySQL nie obsługuje takich rzeczy jak tablice i złożone struktury danych, które są łatwo dostępne w językach 3GL, takich jak C# itp. lub nawet w innych bazach danych, takich jak jako Oracle PL/SQL.

Pomysł polega na tym, aby zwrócić partie danych do warstwy aplikacji (C# cokolwiek), która może następnie dodać wyniki do struktury danych opartej na kolekcji, a następnie przetworzyć dane wewnętrznie.

Procedura składowana

Procedura składowana przyjmuje 3 parametry rc, df_low i df_high, co pozwala wybrać zakres danych w następujący sposób:

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

oczywiście im wyższy zakres df, tym więcej danych będziesz wyodrębniać.

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 ; 

Podniosłem również wersję myisam, która jest identyczna, z wyjątkiem używanej tabeli.

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

Opierając się na powyższym przykładzie kursora, spodziewałbym się, że wersja innodb przewyższy wersję myisam.

Opracowałem szybki i brudny wielowątkowa aplikacja C#, która wywoła procedurę składowaną i doda wyniki do kolekcji w celu przetworzenia zapytania. Nie musisz używać wątków, to samo podejście do zapytań wsadowych może być wykonywane sekwencyjnie bez znacznej utraty wydajności.

Każdy wątek (QueryThread) wybiera zakres danych df, zapętla zestaw wyników i dodaje każdy wynik (wiersz) do kolekcji wyników.

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

Czas pracy obserwowany w następujący sposób:

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

Tak więc 50 milionów wierszy zostało pobranych i dodanych do kolekcji w mniej niż 60 sekund.

Próbowałem tego samego, używając procedury składowanej myisam, która trwała 2 minuty.

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

Przechodzenie do innodb

W moim uproszczonym systemie tabela myisam nie działa zbyt źle, więc migracja do innodb może nie być warta. Jeśli zdecydujesz się skopiować dane wynikowe do tabeli innodb, zrób to w następujący sposób:

start transaction;

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

commit;

Porządkowanie wyniku przez innodb PK przed wstawieniem i zapakowaniem całości w transakcję przyspieszy sprawę.

Mam nadzieję, że niektóre z nich okażą się pomocne.

Powodzenia




  1. Database
  2.   
  3. Mysql
  4.   
  5. Oracle
  6.   
  7. Sqlserver
  8.   
  9. PostgreSQL
  10.   
  11. Access
  12.   
  13. SQLite
  14.   
  15. MariaDB
  1. Jak zaimportować plik SQL za pomocą wiersza poleceń w MySQL?

  2. Jak uzyskać rekordy z ostatnich 15 dni w MySQL?

  3. Dlaczego wiersze zwracane przez wyjaśnienie nie są równe count()?

  4. SQL Performance UNION vs OR

  5. Jak mogę opóźnić kolumny w MySQL?