wtorek, 19 sierpnia 2014

Bucardo - replikacja master-master w PostgreSQLu

Bucardo jest aplikacją napisaną w perlu, służącą na przykład do replikacji danych w PostgreSQLu. Jest o tyle ciekawym rozwiązaniem, że pozwala na synchronizacje danych typu master-master.

Główne cechy Bucardo to:
  • Asynchroniczna kaskadowa replikacja master-slave i/lub master-master
  • Replikacja typy 'row based'
  • W swoim działania używa triggerów i asynchronicznych powiadomień LISTEN/NOTIFY/UNLISTEN (link do poprzedniego postu na ten temat tutaj)
  • Wymaga dedykowanej bazy danych. Działa jako demon programu napisanego w Perlu, który komunikuje się ze swoją bazą danych i wszystkimi innymi bazami związanymi z działającymi replikacjami (synchronizacjami)
  • Multimaster synchronizacja jest ograniczona tylko do dwóch baz danych (Od wersji 5.0 to ograniczenie już nie istnieje). Dla tego typu replikacji musi być zdefiniowany sposób rozwiązywania konfliktów. Typy definicji ich rozwiązywania:
    • source - wiersz na bazy 'source' zawsze wygrywa
    • target - wiersz na bazie 'target' zawsze wygrywa
    • skip - wiersze przy, których wynikł konflikt nie są zreplikowane
    • random - każdy z serwerów ma takie same szanse
    • latest - wiersz, który został niedawno zmieniony, wygrywa
    • abort - synchronizacja jest przerywana na konflikt
  • Replikacja master-slave umożliwia działanie jednego mastera z wieloma slavami
  • Jest możliwa kaskadowa synchronizacja
  • Nie ma możliwości replikacji zmian typu DDL ponieważ PostgreSQL nie ma triggerów na tabelach systemowych
  • Nie ma możliwości replikacji dużych obiektów
  • Możliwość dowolnego projektowania architektury naszej replikacji
  • Wymagania:
    • PostgreSQL:
      • Od wersji 8.X
      • zainstalowany język pl/perlu
      • zainstalowany język pl/pgsl
    • Perl + moduły (DBD::Pg, DBI, DBIx::Safe, ExtUtils::MakeMaker)
  • Bucardo w wersji 4 trzeba zdefiniować jeden z typów synchronizacji:
  • Demony działające w Bucardo:
    • MCP - Master Control Process - główny proces zarządzający synchronizującymi
    • CTL - Controller - śledzi i zabija procesy synchronizujące
    • KID - Proces synchronizujący
  • Bardzo prosta instalacja paczek perl-owych i Bucardo:

    perl Makefile.pl
    make
    make test
    sudo make install
    
    
    Instalacja Bucardo:

    [bucardo@bucardo ~]$ /usr/local/bin/bucardo install
    This will install the bucardo database into an existing Postgres cluster.
    Postgres must have been compiled with Perl support,
    and you must connect as a superuser
    
    Current connection settings:
    1. Host:           
    2. Port:           5432
    3. User:           postgres
    4. Database:       bucardo
    5. PID directory:  /var/run/bucardo
    Enter a number to change it, P to proceed, or Q to quit: 3
    
    Change the user to: bucardo
    
    Changed user to: bucardo
    Current connection settings:
    1. Host:           
    2. Port:           5432
    3. User:           bucardo
    4. Database:       bucardo
    5. PID directory:  /var/run/bucardo
    Enter a number to change it, P to proceed, or Q to quit: 4
    
    Change the database name to: test
    
    Changed database name to: test
    Current connection settings:
    1. Host:           
    2. Port:           5432
    3. User:           bucardo
    4. Database:       test
    5. PID directory:  /var/run/bucardo
    Enter a number to change it, P to proceed, or Q to quit: P
    
    Postgres version is: 9.3
    Attempting to create and populate the bucardo database and schema
    Database creation is complete
    
    Updated configuration setting "piddir"
    Installation is now complete.
    If you see errors or need help, please email bucardo-general@bucardo.org
    
    You may want to check over the configuration variables next, by running:
    /usr/local/bin/bucardo show all
    Change any setting by using: /usr/local/bin/bucardo set foo=bar
    
    
  • Konfiguracja Bucardo
    • Przykład dodania bazy danych

      [bucardo@bucardo ~]$ /usr/local/bin/bucardo add database amsterdam dbname=test host=188.226.xxx.xxx user=root pass=root
      Added database "amsterdam"
      
      [bucardo@bucardo ~]$ /usr/local/bin/bucardo add database niujork dbname=test host=107.170.xx.xx user=root pass=root
      Added database "niujork"
      
      

    • Przykład listy baz danych

      [bucardo@bucardo ~]$ /usr/local/bin/bucardo list dbs
      Database: amsterdam  Status: active  Conn: psql -U root -d test -h 188.226.xxx.xxx
      Database: niujork    Status: active  Conn: psql -U root -d test -h 107.170.xx.xx
      
      

    • Przykład dodania tabeli

      [bucardo@bucardo ~]$ /usr/local/bin/bucardo add table test1 db=amsterdam
      Added the following tables or sequences:
        public.test1
      [bucardo@bucardo ~]$ /usr/local/bin/bucardo add table test1 db=niujork
      Added the following tables or sequences:
        public.test1
      
      

    • Przykład usuwania tabel

      [bucardo@bucardo ~]$ /usr/local/bin/bucardo remove table test1
      Please use the full schema.table name
      [bucardo@bucardo ~]$ /usr/local/bin/bucardo remove table public.test1
      Removed the following tables:
        public.test1
      [bucardo@bucardo ~]$ /usr/local/bin/bucardo list tables
      No tables have been added yet
      
      

    • Przykład stworzenia synchronizacji

      [bucardo@bucardo ~]$ /usr/local/bin/bucardo add sync test1_sync dbs=niujork,amsterdam tables=test1 conflict_strategy=bucardo_source 
      WARNING:  Issuing rollback() due to DESTROY without explicit disconnect() of DBD::Pg::db handle dbname=test;host=107.170.39.26 at line 279.
      KONTEKST:  PL/Perl function "validate_sync"
      SQL statement "SELECT validate_sync('test1_sync')"
      PL/Perl function "validate_sync"
      Failed to add sync: DBD::Pg::st execute failed: ERROR:  DBD::Pg::db do failed: ERROR:  permission denied for database test at line 280. at line 30.
      KONTEKST:  PL/Perl function "validate_sync" at /usr/local/bin/bucardo line 4413.
      
      

      W bazach musi być zainstalowany język plperl:

      test=> CREATE EXTENSION plperl;
      CREATE EXTENSION
      test=> \dL
                               Lista języków
        Nazwa  | Właściciel | Zaufany |             Opis             
      ---------+------------+---------+------------------------------
       plperl  | root       | t       | PL/Perl procedural language
       plpgsql | postgres   | t       | PL/pgSQL procedural language
      (2 wiersze)
      
      test=> \q
      
      

      Stworzenie synchronizacji typu master-slave (pierwsza baza jest typu 'source', a druga (albo po prostu każde następne) 'target'):

      [bucardo@bucardo ~]$ /usr/local/bin/bucardo add sync test1_sync dbs=niujork,amsterdam tables=test1 conflict_strategy=bucardo_source 
      Added sync "test1_sync"
      Created a new relgroup named "test1_sync"
      Created a new dbgroup named "test1_sync"
      
      

      Stworzenie synchronizacji typu master-master (jawnie definiujemy typ bazy danych):

      [bucardo@bucardo ~]$ /usr/local/bin/bucardo add sync test1_sync dbs=niujork:source,amsterdam:source tables=test1 conflict_strategy=bucardo_source
      Added sync "test1_sync"
      Created a new relgroup named "test1_sync"
      Created a new dbgroup named "test1_sync_2"
      
      

    • Status synchronizacji

      [bucardo@bucardo ~]$ /usr/local/bin/bucardo status
      PID of Bucardo MCP: 22761
       Name         State    Last good    Time      Last I/D    Last bad    Time  
      ============+========+============+=========+===========+===========+=======
       test1_sync | Good   | 11:06:44   | 24m 32s | 0/373     | none      |       
      
      

      Szczegółowe informacje o synchronizacji:

      [bucardo@bucardo ~]$ /usr/local/bin/bucardo status test1_sync
      ======================================================================
      Last good                : Aug 12, 2014 11:06:43 (time to run: 2s)
      Rows deleted/inserted    : 0 / 373
      Sync name                : test1_sync
      Current state            : Good
      Source relgroup/database : test1_sync / amsterdam
      Tables in sync           : 1
      Status                   : Active
      Check time               : None
      Overdue time             : 00:00:00
      Expired time             : 00:00:00
      Stayalive/Kidsalive      : Yes / Yes
      Rebuild index            : No
      Autokick                 : Yes
      Onetimecopy              : No
      Post-copy analyze        : Yes
      Last error:              : 
      ======================================================================
      
      

Każda tabela w synchronizacji ma nałożone na sobie triggery. Oto przykładowa tabela:

test=# \d test1
                                 Tabela "public.test1"
 Kolumna |          Typ          |                     Modyfikatory                     
---------+-----------------------+------------------------------------------------------
 c1      | integer               | niepusty domyślnie nextval('test1_c1_seq'::regclass)
 c2      | character varying(20) | 
 c3      | integer               | 
Indeksy:
    "test1_pkey" PRIMARY KEY, btree (c1)
Wyzwalacze:
    bucardo_delta AFTER INSERT OR DELETE OR UPDATE ON test1 FOR EACH ROW EXECUTE PROCEDURE bucardo.delta_public_test1()
    bucardo_kick_test1_sync AFTER INSERT OR DELETE OR UPDATE OR TRUNCATE ON test1 FOR EACH STATEMENT EXECUTE PROCEDURE bucardo.bucardo_kick_test1_sync()
    bucardo_note_trunc_test1_sync AFTER TRUNCATE ON test1 FOR EACH STATEMENT EXECUTE PROCEDURE bucardo.bucardo_note_truncation('test1_sync')


Triggery:
  • bucardo.delta_public_test1 - wywołuje inserty z kluczem głównym 'zmienionego' wiersza do tabeli trakującej delta_public_test1

    BEGIN 
     IF (TG_OP = 'INSERT') THEN 
       INSERT INTO bucardo.delta_public_test1 VALUES (NEW."c1");
     ELSIF (TG_OP = 'UPDATE') THEN
       INSERT INTO bucardo.delta_public_test1 VALUES (OLD."c1");
       IF (OLD."c1" <> NEW."c1") THEN 
         INSERT INTO bucardo.delta_public_test1 VALUES (NEW."c1");
       END IF; 
     ELSE 
       INSERT INTO bucardo.delta_public_test1 VALUES (OLD."c1"); 
     END IF; 
     RETURN NULL;
     END;
    
  • bucardo.bucardo_kick_test1_sync - wywołanie NOTIFY do bucardo informując, że doszło do zmian.

    BEGIN
       EXECUTE $nn$NOTIFY bucardo, 'kick_sync_test1_sync'$nn$;
     RETURN NEW; 
    END;
    
  • bucardo.bucardo_note_truncation - wywołuje insert do tabeli trakującej, informując o wykonaniu polecenia TRUCATE. Czyści także tabele trakujące zmiany

Od wersji 5.0 dane zanim będą przeniesione do docelowego serwera, są usuwane (przy conflict_stategy = 'bucardo_source') przy pomocy klucza głównego, aby za pobiedź konfliktom danych. W wcześniejszej wersji były najpierw sprawdzane pojedynczo, co powodowało obciążenie serwera i bardzo duże opóźnienia.

DELETE FROM public.test1 WHERE c1 = ANY('{{"5"}}')

Przenoszenie danych:

COPY (SELECT * FROM public.test1 WHERE c1 IN ('6')) TO STDOUT

COPY public.test1("c1","c2","c3") FROM STDIN

Zdecydowanie polecam obecną wersje tego programu do replikacji danych zwłaszcza z powodu lepszej wydajności. Jeśli jednak potrzebujemy tylko replikacji typu master-slave wybrała bym wbudowaną replikację PostgreSQLa bo działa na poziomie plików binarnych i nie obciąża serwera dodatkowymi zapytaniami.

poniedziałek, 11 sierpnia 2014

Partycjonowanie w MySQL v5.5


Jakiś czas temu przyszło mi się zmierzyć z problemem optymalizacji zapytań na bardzo dużej ilości danych w tabeli. Tabela, o której pisze, ma wielkość około 40GB i 80 milionów rekordów. Oto jej schemat:

CREATE TABLE `service_numbers` (
  `template_id` bigint(20) NOT NULL,
  `template_info_id` bigint(20) NOT NULL,
  `mobile_number` varchar(255) DEFAULT NULL,
  `appeared_at` timestamp NULL DEFAULT NULL,
  `verified_at` timestamp NULL DEFAULT NULL,
  `alias_name` varchar(255) NOT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  `fake_id` int(11) NOT NULL AUTO_INCREMENT,
  `external_operator_id` int(11) DEFAULT NULL,
  `areacode` varchar(3) DEFAULT NULL,
  PRIMARY KEY (`fake_id`),
  KEY `idx_mn_ti_tii` (`mobile_number`(191),`template_id`,`template_info_id`),
  KEY `sn_by_template_then_appeared` (`template_id`,`template_info_id`,`appeared_at`),
  KEY `sn_by_template_then_verified` (`template_id`,`template_info_id`,`verified_at`),
  KEY `sn_by_tmeplate_alias_appeared` (`template_id`,`alias_name`(191),`template_info_id`,`appeared_at`),
  KEY `ssn_tid_arec_tiid_vat_idx` (`template_id`,`areacode`,`template_info_id`,`verified_at`),
  KEY `ssn_tid_oid_tiid_vat_idx` (`template_id`,`external_operator_id`,`template_info_id`,`verified_at`),
  KEY `idx_tmeplate_alias_verified` (`template_id`,`alias_name`(64),`template_info_id`,`verified_at`)
) ENGINE=InnoDB AUTO_INCREMENT=184473055 DEFAULT CHARSET=utf8mb4;

Zapytanie, które wymaga optymalizacji ma następującą konstrukcję:

select count(*) from service_numbers where template_id = ID1 and template_info_id = ID2 and verified_at < 'DATE';


Może przyjmować różnego rodzaju dodatkowe warunki, dlatego dla każdego rodzaju zapytania dodano indexy. Zawsze jednak głównym elementem jest tutaj kolumna template_id. Aby pomóc przyspieszyć wykonywanie tych zapytań, dobrze jest usunąć historyczne dane, aby zmniejszyć ich ilość. Jednak ze względu na specyfikę wykorzystania tych danych, nie jest to proste. Ale to w tym momencie pomijam. Tabela została uszczuplona na tyle na ile się da, ale nadal jest ona ogromna. Ale co jeśli wykorzystamy partycjonowanie?

Co to jest partycjonowanie?

Partycjonowanie jest fizycznym dzieleniem tabeli na części, do których jednak nie masz bezpośredniego dostępu (nie ma możliwości wykonania SELECT'a tylko na wskazanej partycji, ale możesz np. wskazać, którą partycję wyczyścić). Każda partycja jest zarządzana przez engine bazodanowy, ma własne indexy, które zostały zdefiniowane na tabeli.

Ważne informacje o partycjonowaniu w MySQLu


Najpierw sprawdźmy, czy nasz serwer wspiera partycjonowanie - oto jeden ze sposobów:

mysql (root@partition_test)> SHOW VARIABLES LIKE '%partition%';
+-------------------+-------+
| Variable_name     | Value |
+-------------------+-------+
| have_partitioning | YES   |
+-------------------+-------+
1 row in set (0.00 sec)


Jak traktowane są operacji bazodanowych:
  • SELECT - Otwierana jest warstwa partycjonowania i lokowane są wszystkie partycje. Optymalizator decyduje, które z partycji są ignorowane, bo warunki selektu ich nie dotyczą. Wtedy warstwa partycjonowania wywołuje API do silnika bazodanowego, który zarządza partycjami.

    mysql (root@partition_test)> explain partitions select * from test1;
    +----+-------------+-------+-------------+------+---------------+------+---------+------+------+-------+
    | id | select_type | table | partitions  | type | possible_keys | key  | key_len | ref  | rows | Extra |
    +----+-------------+-------+-------------+------+---------------+------+---------+------+------+-------+
    |  1 | SIMPLE      | test1 | p0,p1,p2,p3 | ALL  | NULL          | NULL | NULL    | NULL |    4 |       |
    +----+-------------+-------+-------------+------+---------------+------+---------+------+------+-------+
    1 row in set (0.00 sec)
    
    mysql (root@partition_test)> explain partitions select * from test1 where tid = 1;
    +----+-------------+-------+------------+------+----------------+----------------+---------+-------+------+-------------+
    | id | select_type | table | partitions | type | possible_keys  | key            | key_len | ref   | rows | Extra       |
    +----+-------------+-------+------------+------+----------------+----------------+---------+-------+------+-------------+
    |  1 | SIMPLE      | test1 | p0         | ref  | idx_tid_tnid_c | idx_tid_tnid_c | 5       | const |    1 | Using where |
    +----+-------------+-------+------------+------+----------------+----------------+---------+-------+------+-------------+
    1 row in set (0.07 sec)
    
    

  • INSERT - Otwierana jest warstwa partycjonowania i lokowane są wszystkie partycje, a następnie określa, która partycja ma otrzymać nowy wiersz. Później przekazuje ten wiersz do tej partycji.
  • DELETE - Otwierana jest warstwa partycjonowania i lokowane są wszystkie partycje. Określa gdzie znajduje się wiersz i wysyła żądanie jego usunięcia do tej partycji.
  • UPDATE - Otwierana jest warstwa partycjonowania i lokowane są wszystkie partycje aby określić gdzie znajduje się szukany wiersz. Jest on pobierany i modyfikowany. Określane jest gdzie po modyfikacji ma zostać zlokalizowany 'nowy' wiersz, następnie jest tam wysyłany. Do partycji źródłowej wiersza jest wysyłane żądanie usunięcia.
Oczywiście operacje te mogą być wspierane przez mechanizm nazywany pruning. Czyli w momencie gdy wykonywana jest operacja, tylko kilku lub jednej partycji, to na reszcie z nich jest ignorowana lock.

W jakich przypadkach najlepiej używać partycjonowania?
  • tabela jest tak duża, że nie mieści się już pamięci lub gdy najważniejsze dane są samym końcu tabelki, a sama tabela posiada bardzo dużo starych danych
  • utrzymanie tabel partycjonowanych jest o wiele łatwiejsze np. przy usuwaniu danych historycznych i odzyskiwaniu wolnej przestrzeni dyskowej
  • może być użyte przy zapobieganiu niektórych specyficznych obciążeniach, takich jak muteksy na indexach InnoDB
  • możliwy jest backup i ponowne załadowanie pojedynczej partycji dla bardzo dużej ilości danych
  • partycje mogą być rozłożone fizycznie na różnych dyskach
Ograniczenia:
  • maksymalna ilość partycji to 1,024 dla pojedynczej tabeli
  • dla MySQL v5.1 wyrażenie potrzebne do partycjonowania danych musi zwrócić wartość typu integer. Od wersji 5.5 możemy partycjonować także poprzez kolumny, które nie są tego typu
  • każdy klucz główny i index unikalny musi uwzględniać wszystkie kolumny w wyrażeniu partycjonującym
  • nie ma możliwości użycia kluczy obcych
  • lista funkcji, których można używać do wyrażenia partycjonującego jest dostępna pod tą stroną http://dev.mysql.com/doc/refman/5.5/en/partitioning-limitations-functions.html

Typy partycjonowania:
  • RANGE - dla każdej partycji zdefiniowane jest wyrażenie, które zwraca wartość typu integer:

    mysql (root@partition_test)> create table test_range (`id` int(11) NOT NULL, `ms` varchar(255) DEFAULT NULL, `tid` int(11) DEFAULT NULL, `tnid` int(11) DEFAULT NULL, `c` varchar(255) DEFAULT NULL) PARTITION BY RANGE (tnid) (PARTITION p0 VALUES LESS THAN (2), PARTITION p1 VALUES LESS THAN (3), PARTITION p2 VALUES LESS THAN (9), PARTITION p3 VALUES LESS THAN (10), PARTITION p4 VALUES LESS THAN MAXVALUE);
    Query OK, 0 rows affected (0.96 sec)
    
    mysql (root@partition_test)> show create table test_range \G
    *************************** 1. row ***************************
           Table: test_range
    Create Table: CREATE TABLE `test_range` (
      `id` int(11) NOT NULL,
      `ms` varchar(255) DEFAULT NULL,
      `tid` int(11) DEFAULT NULL,
      `tnid` int(11) DEFAULT NULL,
      `c` varchar(255) DEFAULT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=latin1
    /*!50100 PARTITION BY RANGE (tnid)
    (PARTITION p0 VALUES LESS THAN (2) ENGINE = InnoDB,
     PARTITION p1 VALUES LESS THAN (3) ENGINE = InnoDB,
     PARTITION p2 VALUES LESS THAN (9) ENGINE = InnoDB,
     PARTITION p3 VALUES LESS THAN (10) ENGINE = InnoDB,
     PARTITION p4 VALUES LESS THAN MAXVALUE ENGINE = InnoDB) */
    1 row in set (0.00 sec)
    
    
    W powyższym przykładzie, w partycji p0 będę umieszczone wszystkie wiersze gdzie wartość w kolumnie tnid jest mniejsze od 2, a w partycji p1 wszystkie wiersze gdzie kolumna tnid przyjmuje wartości równe 2 i mniejsze od 3.

  • LIST - jest bardzo podobne do powyższego sposobu partycjonowania - każda partycja musi być zdefiniowana. Różnica wynika z tego, że wyrażenie definiujące partycję jest listą wartości typu integer. Powyższą tabelę możemy zdefiniować następująco:

    mysql (root@partition_test)> create table test_list (`id` int(11) NOT NULL, `ms` varchar(255) DEFAULT NULL, `tid` int(11) DEFAULT NULL, `tnid` int(11) DEFAULT NULL, `c` varchar(255) DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=latin1 PARTITION BY LIST (tnid) (PARTITION p0 VALUES IN (0,1), PARTITION p1 VALUES IN (2), PARTITION p2 VALUES IN (3,4,5,6,7,8), PARTITION p3 VALUES IN (9), PARTITION p4 VALUES IN (10,11,12,13));
    Query OK, 0 rows affected (0.56 sec)
    
    mysql (root@partition_test)> show create table test_list \G
    *************************** 1. row ***************************
           Table: test_list
    Create Table: CREATE TABLE `test_list` (
      `id` int(11) NOT NULL,
      `ms` varchar(255) DEFAULT NULL,
      `tid` int(11) DEFAULT NULL,
      `tnid` int(11) DEFAULT NULL,
      `c` varchar(255) DEFAULT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=latin1
    /*!50100 PARTITION BY LIST (tnid)
    (PARTITION p0 VALUES IN (0,1) ENGINE = InnoDB,
     PARTITION p1 VALUES IN (2) ENGINE = InnoDB,
     PARTITION p2 VALUES IN (3,4,5,6,7,8) ENGINE = InnoDB,
     PARTITION p3 VALUES IN (9) ENGINE = InnoDB,
     PARTITION p4 VALUES IN (10,11,12,13) ENGINE = InnoDB) */
    1 row in set (0.00 sec)
    
    
    Znając strukturę danych jesteśmy świadomi jakie dane będą przechowywane w kolumnie tnid (Wartości typu integer od 0 do 13 - nie ma możliwości aby wstawić wiersz, dla którego kolumn tnid będzie przyjmować inne wartość, a także i NULL. Czytaj dalej.).
  • COLUMNS - jest nowym typem partycjonowania od wersji 5.5. Może być stosowana nie tylko dla kolumn typu integer. Wyróżniamy dwa sposoby podziału: RANGE lub LIST aby zwrócić wartości przedziałów lub listy. Dodatkowo w porównaniu do swoich podstawowych typów partycjonowania (COLUMN RANGE -> RANGE i COLUMN LIST -> LIST) możliwe jest zdefiniowanie podziału tabeli przez wiele kolumn. Link do dokumentacji: http://dev.mysql.com/doc/refman/5.5/en/partitioning-columns.html

  • HASH - używane głównie do równomiernego rozkładania wierszy w partycji między z góry określoną liczbą ścieżek. Wyrażenie definiująca partycję musi zwracać wartość typu integer. Przykład definicji tak partycjonowanej tabeli:

    CREATE TABLE t1 (c1 INT, c2 VARCHAR(255), c3 TIMESTAMP)
        PARTITION BY HASH( MONTH(c3))
        PARTITIONS 4;
    
    

    Tabela t1 będzie miała 4 partycje gdzie dane będą rozlokowane przez następujący wzór: MOD(MONTH(c3),4)
  • KEY - bardzo podobna metoda do HASH, ale tutaj to MySQL narzuca wyrażenie partycjonujące przy pomocy funkcji hashującej.
  • Subpartitiong (composite partitiong czyli złożone partycjonowanie) - jest dalszym podziałem partycji na jeszcze mniejsze partycje. Zainteresowanych odsyłam do dokumentacji: http://dev.mysql.com/doc/refman/5.5/en/partitioning-subpartitions.html
  • Wartość NULL nie są wartościami typu integer (http://db-diary.blogspot.com/2013/12/wartosc-null-w-bazach-danych.html). Każdy tym partycjonowania może inaczej traktować wartości NULL i niektóre z nich muszą być specjalnie przygotowane na tą ewentualność wartości danych.
    Dla partycjonowania typu RANGE wiersz jest ustawiany w pierwszej (najniżej) partycji. W ten sam sposób zostanie potraktowany wiersz dla partycji zdefiniowanej przy pomocy wyrażenia z funkcją SQL. Dla typu LIST partycja musi mieć zdefiniowaną wartość NULL aby można było wstawić do niej wiersz (UWAGA: ta sama zasada odnosi się także do innych 'poprawny' wartości). Dla powyższej tabeli test_list spróbujemy wstawić wiersz, którego wartość nie istnieje w liście wartości partycjonujących:

    mysql (root@partition_test)> insert into test_list (id, ms, tid, tnid, c) values (1, '11111', 22, 14, 'sfsfdfa');
    ERROR 1526 (HY000): Table has no partition for value 14
    mysql (root@partition_test)> insert into test_list (id, ms, tid, tnid, c) values (1, '11111', 22, NULL, 'sfsfdfa');
    ERROR 1526 (HY000): Table has no partition for value NULL
    
    
Teraz jak już wiemy mniej więcej na czym partycjonowanie polega, porównajmy czasy wykonania kilku zapytań dla przedstawionej na samym początku tabeli service_numbers (baza danych: test) i nowej tabeli service_numbers (baza danych: test2), która została przeze mnie partycjonowania:

mysql (root@test2)> show create table service_numbers \G
*************************** 1. row ***************************
       Table: service_numbers
Create Table: CREATE TABLE `service_numbers` (
  `template_id` bigint(20) NOT NULL,
  `template_info_id` bigint(20) NOT NULL,
  `mobile_number` varchar(255) DEFAULT NULL,
  `appeared_at` timestamp NULL DEFAULT NULL,
  `verified_at` timestamp NULL DEFAULT NULL,
  `alias_name` varchar(255) NOT NULL,
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  `fake_id` int(11) NOT NULL AUTO_INCREMENT,
  `external_operator_id` int(11) DEFAULT NULL,
  `areacode` varchar(3) DEFAULT NULL,
  KEY `idx_mn_ti_tii` (`mobile_number`(191),`template_id`,`template_info_id`),
  KEY `sn_by_template_then_appeared` (`template_id`,`template_info_id`,`appeared_at`),
  KEY `sn_by_template_then_verified` (`template_id`,`template_info_id`,`verified_at`),
  KEY `sn_by_tmeplate_alias_appeared` (`template_id`,`alias_name`(191),`template_info_id`,`appeared_at`),
  KEY `ssn_tid_arec_tiid_vat_idx` (`template_id`,`areacode`,`template_info_id`,`verified_at`),
  KEY `ssn_tid_oid_tiid_vat_idx` (`template_id`,`external_operator_id`,`template_info_id`,`verified_at`),
  KEY `idx_tmeplate_alias_verified` (`template_id`,`alias_name`(64),`template_info_id`,`verified_at`),
  KEY `idx_fake_id` (`fake_id`)
) ENGINE=InnoDB AUTO_INCREMENT=184435432 DEFAULT CHARSET=utf8mb4
/*!50100 PARTITION BY KEY (template_id)
PARTITIONS 100 */
1 row in set (0.00 sec)

Mała uwaga do zmienionej struktury tabeli: Pamiętajmy o ograniczeniach przy zmianie schematu tabeli, gdy mamy indexy unikalne lub klucz główny. W moim przypadku musiałam usunąć klucz główny i zamienić go na index, a kolumnę, na której działał zmodyfikować. Te operację wymagają czasu i przestrzeni dyskowej, dlatego lepiej dokonywać zmiany schematu tabeli na pustej tabeli (przed rozpoczęciem oczywiście exportujemy dane do pliku):

truncate table service_numbers;
alter table service_numbers MODIFY fake_id int(11) NOT NULL;
alter table service_numbers drop primary key;
alter table service_numbers add index idx_fake_id (fake_id);
alter table service_numbers MODIFY fake_id int(11) NOT NULL auto_increment;
alter table service_numbers PARTITION BY KEY(template_id) PARTITIONS 100;


Testujemy nasze partycjonowanie

Przykładowe zapytanie na nie zmienionym schemacie tabeli:

mysql (root@test)> select count(*) from service_numbers where template_id = 6401 and template_info_id = 10 and verified_at < '2013-05-01';
+----------+
| count(*) |
+----------+
|   608136 |
+----------+
1 row in set (11.32 sec)


mysql (root@test)> explain partitions select count(*) from service_numbers where template_id = 6401 and template_info_id = 10 and verified_at < '2013-05-01' \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: service_numbers
   partitions: NULL
         type: ref
possible_keys: sn_by_template_then_appeared,sn_by_template_then_verified,sn_by_tmeplate_alias_appeared,ssn_tid_arec_tiid_vat_idx,ssn_tid_oid_tiid_vat_idx,idx_tmeplate_alias_verified
          key: ssn_tid_oid_tiid_vat_idx
      key_len: 8
          ref: const
         rows: 1227206
        Extra: Using where; Using index
1 row in set (0.00 sec)


Nie wiem czy zwróciliście uwagę, ale optymalizator wybrał 'gorszy' index do wykonania tego zapytania. Poprawmy jego statystyki i wykonajmy explain ponownie:
mysql (root@test)> analyze table service_numbers;
+----------------------+---------+----------+----------+
| Table                | Op      | Msg_type | Msg_text |
+----------------------+---------+----------+----------+
| test.service_numbers | analyze | status   | OK       |
+----------------------+---------+----------+----------+
1 row in set (1.39 sec)

mysql (root@test)> explain partitions select count(*) from service_numbers where template_id = 6401 and template_info_id = 10 and verified_at < '2013-05-01' \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: service_numbers
   partitions: NULL
         type: range
possible_keys: sn_by_template_then_appeared,sn_by_template_then_verified,sn_by_tmeplate_alias_appeared,ssn_tid_arec_tiid_vat_idx,ssn_tid_oid_tiid_vat_idx,idx_tmeplate_alias_verified
          key: sn_by_template_then_verified
      key_len: 21
          ref: NULL
         rows: 922120
        Extra: Using where; Using index
1 row in set (0.02 sec)


Tym razem jednak było znacznie lepiej. I czas wykonania zapytania jest lepszy:
mysql (root@test)> select count(*) from service_numbers where template_id = 6401 and template_info_id = 10 and verified_at < '2013-05-01';
+----------+
| count(*) |
+----------+
|   608136 |
+----------+
1 row in set (3.96 sec)


To samo zapytanie wykonajmy na tabeli ze zmienionym schematem:

mysql (root@test2)> select count(*) from service_numbers where template_id = 6401 and template_info_id = 10 and verified_at < '2013-05-01';
+----------+
| count(*) |
+----------+
|   608136 |
+----------+
1 row in set (2.26 sec)


mysql (root@test2)> explain partitions select count(*) from service_numbers where template_id = 6401 and template_info_id = 10 and verified_at < '2013-05-01' \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: service_numbers
   partitions: p3
         type: range
possible_keys: sn_by_template_then_appeared,sn_by_template_then_verified,sn_by_tmeplate_alias_appeared,ssn_tid_arec_tiid_vat_idx,ssn_tid_oid_tiid_vat_idx,idx_tmeplate_alias_verified
          key: sn_by_template_then_verified
      key_len: 21
          ref: NULL
         rows: 454688
        Extra: Using where; Using index
1 row in set (0.00 sec)


Ilość wierszy, które optymalizator musi odczytać jest o wiele mniejsza, więc i zapytanie trwa o wiele krócej.

To chyba jednak mało wiarogodny test na lepsze działanie. Dlatego przygotowałam plik z ponad 600 zapytaniami, które na serwerze produkcyjnym złapały się do slow logów. Mój serwer testowy miał włączone slow logi, które teraz chce wam przedstawić (Wykresy zostały wygenerowane przy pomocy narzędzia: mysql-slow-query-log-visualizer, jeśli ktoś jest zainteresowany).

Czas trwania wykonywania wszystkich zapytań dla schematu niepartycjonowanego:


a dla schematu partycjonowanego:



Podsumowanie tego testu:

Niepartycjonowany schemat tabeliPartycjonowany schemat tabeli
Ilość zapytań 'złapanych' w slow logach:632429
Czas działania testu:+ 5 godz+ 1 godz
Czas najdłużej wykonanego zapytania:399 sekundy31 sekundy
Czas wykonania zapytania, które musiało 'przeczyta' największą ilość wierszy (2662877):85 sekundy25 sekundy
Dodatkowe informacje:Optymalizator bierze pod uwagę gorsze indexy-



No to tyle. Dla mnie okazało się to wybawieniem i w końcu rozwiązaniem problemu optymalizacji działania zapytań na tak dużej tabeli.