piątek, 31 stycznia 2014

Odzyskiwanie nieużywanej przestrzeni dyskowej w MySQL-u

Na pewno mieliście kiedyś problemy z odzyskiwaniem zajętej przestrzeni dyskowej przez MySQL, mimo iż większość danych została wcześniej usunięta. Jest to problem, przed którym stanie każdy administrator baz danych.


Aby sprawdzić jaka tabela ile miejsca zajmuje, a ile z tego miejsca można odzyskać, możemy użyć zapytania:

SELECT 
  table_schema as "Database",
  table_name AS "Tables",  
  round(((data_length) / (1024 * 1024 * 1024)), 2) "Size in GB", 
  round(((index_length) / (1024 * 1024 * 1024)), 2) "Index Size in GB", 
  round(((data_free) / (1024 * 1024 * 1024)), 2) "Data Free Size in GB"
FROM 
  information_schema.TABLES 
where table_schema = 'smsanalytics' 
order by data_length + index_length desc;

W zależności od konfiguracji naszego serwera i dostępności wolnego miejsca na dysku, nasze działa na odzyskanie nieużywanej przestrzeni, mogą być różne.

Jeśli mamy wystarczająco dużo wolnego miejsca i mamy włączoną zmienną innodb_file_per_table, możemy użyć polecenia OPTIMIZE TABLE.

mysql (root@ela_db)> SELECT 
 table_schema as "Database", 
 table_name AS "Tables", 
 round(((data_length) / (1024 * 1024)), 2) "Size in MB", 
 round(((index_length) / (1024 * 1024)), 2) "Index Size in MB", 
 round(((data_free) / (1024 * 1024)), 2) "Data Free Size in MB" 
FROM information_schema.TABLES 
where table_schema = 'smsanalytics' 
order by data_length + index_length desc 
limit 1;
+--------------+--------------------+------------+------------------+----------------------+
| Database     | Tables             | Size in MB | Index Size in MB | Data Free Size in MB |
+--------------+--------------------+------------+------------------+----------------------+
| database1    | test               |      60.59 |            49.66 |                 5.00 |
+--------------+--------------------+------------+------------------+----------------------+
5 rows in set (0.69 sec)

mysql (root@ela_db)> optimize table sub_grouprecipient;
+---------------------------------+----------+----------+-------------------------------------------------------------------+
| Table                           | Op       | Msg_type | Msg_text                                                          |
+---------------------------------+----------+----------+-------------------------------------------------------------------+
| database1.test                  | optimize | note     | Table does not support optimize, doing recreate + analyze instead |
| database1.test                  | optimize | status   | OK                                                                |
+---------------------------------+----------+----------+-------------------------------------------------------------------+
2 rows in set (38.57 sec)

mysql (root@ela_db)> SELECT 
 table_schema as "Database", table_name AS "Tables", 
 round(((data_length) / (1024 * 1024)), 2) "Size in MB", 
 round(((index_length) / (1024 * 1024)), 2) "Index Size in MB",
 round(((data_free) / (1024 * 1024)), 2) "Data Free Size in MB" 
FROM information_schema.TABLES 
 where table_schema = 'smsanalytics' 
order by data_length + index_length desc 
limit 1;
+--------------+--------------------+------------+------------------+----------------------+
| Database     | Tables             | Size in MB | Index Size in MB | Data Free Size in MB |
+--------------+--------------------+------------+------------------+----------------------+
| database1    | test               |      55.59 |            49.66 |                 6.00 |
+--------------+--------------------+------------+------------------+----------------------+
5 rows in set (0.32 sec)

Zmienna innodb_file_per_table od wersji 5.6 jest domyślnie włączona. Może być przydatna w wielu sytuacjach.

Zalety:
  • każda tabela jest zapisywana w oddzielnym pliku z rozszerzeniem .idb
  • przestrzeń dyskowa jest automatycznie zwraca do systemu operacyjnego po usunięciu (DROP TABLE ...) lub wyczyszczeniu (TRUNCATE TABLE ...) tabeli
  • operacja czyszczenia (TRANCATE TABLE ...) jest szybsza
  • dla optymalizacji tabeli można wykonać polecenie OPTIMIZE TABLE
  • możliwość przenoszenia plików tabeli i łatwiejsze ich backupowanie
  • ograniczenia na każdy plik tabeli jest taki sam jak dla pliku, w którym wcześniej trzymane były wszystkie tabele enginu InnoDB (64TB)
Możliwe wady:
  • operacja fsync - musi działać na każdej otwartej tabeli niż na pojedynczym pliku
  • mysqld musi utrzymywać na każdą tabelę otwarty uchwyt do jego pliku
  • jeśli rozmiar tabel bardzo rośnie, rośnie też ryzyko fragmentacji danych, które zmniejsza wydajność skanowania tabeli
Więcej informacji możecie przeczytać w dokumentacji : InnoDB File-Per-Table Mode.


Jeśli jednak, nie mamy wystarczająco dużo miejsca lub zmienna innodb_file_per_table jest wyłączona, musimy:
  1. Zdumpować tabelę (polecenie mysqldump)
  2. Usunąć tabelę (DROP TABLE)
  3. Przywrócić dane z dump-a (mysql)
Dodatkowo odsyłam do ciekawych postów:

niedziela, 26 stycznia 2014

Instalacja servera i replikacji master-master w MySQL v5.5

Pracując z bazami danych zawsze mamy do czynienia z replikacjami. Nie ma teraz chyba systemu, w którym replikacji by nie było. Może ona służyć jako kopia zapasowa (backup), maszyna dla celów raportowych (przy replikacji master-slave) lub dodatkowy master serwer aby odciążyć innego mastera (replikacja master-master).

Podstawowy schemat przepływu plików replikacji master-slave (master-master będzie lustrzany) będzie następujący:

  1. Slave łączy się z masterem
  2. Wątek I/O pyta o dane
  3. Wątek Binlog dump na masterze wysyła zawartość do wątku I/O
  4. Wątek SQL zatwierdza dane (zmiany)

Poniżej krótko opiszę jak zestawić replikację master-master dla MySQL v.5.5 w następujących krokach:
  1. Konfiguracja serwerów - plik my5.5.cnf
  2. Instalacja serwera MySQL v5.5
  3. Ustawienie replikacji
Na tych maszynach zainstalowano już inne wersje serwerów MySQL dlatego opis obejmuję instalacji wersji binarnych na niestandardowych ścieżkach i porcie 3309.

Serwery bazodanowe będą stać pod adresami:
  • 192.168.100.167 - server ID = 1
  • 192.168.100.166 - server ID = 2

Konfiguracja MySQL'a (my5.5.cnf)

Chce aby klient łączył się na porcie 3309 przy kodowaniu utf8. Wskazałam także ścieżkę do socket-u:

[client]
port                            = 3309
socket                          = /tmp/mysql5.5.sock
default-character-set           = utf8

Ustawienia serwera:


[mysqld]
user                            = mysql
skip-name-resolve
innodb_file_per_table
skip-external-locking
report-port                     = 3309
tmpdir                          = /home/mysql/mysql5.5/mysqltmp
event_scheduler                 = 0
pid-file                        = /home/mysql/mysql5.5/mysql.pid
socket                          = /tmp/mysql5.5.sock
datadir                         = /home/mysql/mysql5.5/myisam
port                            = 3309
server-id                       = 1
character-set-server            = utf8
auto_increment_offset     = 1
auto_increment_increment    = 2
log-output     = file
general_log                     = 1
general_log_file                = /home/mysql/mysql5.5/log/mysql.log
log_error                       = /home/mysql/mysql5.5/log/mysql.err
log-warnings     = 1
long_query_time                 = 1
slow_query_log                  = 1
slow_query_log_file             = /home/mysql/mysql5.5/slow.log
log_queries_not_using_indexes   = 1
log-bin                         = /home/mysql/mysql5.5/replication/binlog
binlog-format                   = ROW
relay-log                       = /home/mysql/mysql5.5/replication/relay-bin

#default-table-type              = InnoDB
innodb_data_home_dir            = /home/mysql/mysql5.5/innodb/
innodb_data_file_path           = ibdata/ibdata1:50M:autoextend
innodb_log_group_home_dir       = /home/mysql/mysql5.5/innodb/iblogs
innodb_flush_log_at_trx_commit  = 2
innodb_fast_shutdown

Z ustawień serwera przeczytamy, że:
  • nasłuchuję na porcie 3309, jest to także port dla połączenia ze slavem
  • logi bazodanowe możemy znaleźć w katalogu: /home/mysql/mysql5.5/log
  • slow logi są pod ścieżką: /home/mysql/mysql5.5/slow.log
  • katalog dla tabel tymczasowych jest pod ścieżką: /home/mysql/mysql5.5/mysqltmp
  • każdy z serwerów podpiętych do replikacji musi mieć unikalną wartość dla server-id, w tym przypadku jest ustawione na 1
  • dla replikacji master-master musimy dobrze ustawić zmienne odpowiadające za kontrolowanie auto incrementów kluczy głównych. Zmienna auto_increment_increment mówi jak mają być zmieniane wartość, a auto_increment_offset od jakiej wartości zaczynamy. Dla dwóch serwerów w replikacji master-master ustawimy, że wartości mają się zmieniać co 2 zaczynając od 1 dla serwera ID 1 i zaczynając od 2 dla serwera ID 2
  • dla enginu InnoDB będziemy przechowywać dane w katalogu: /home/mysql/mysql5.5/innodb. Przed uruchomieniem serwera stworzymy podkatalogi:
    • ibdata -pod zmienną innodb_data_file_path znajdziecie ścieżkę do plików danych związanych z tym enginem odraz ich rozmiar. Pełna ścieżka do tego katalogu jest zapisana pod zmienną innodb_data_home_dir.
    • iblogs -katalog, do którego ścieżka została zdefiniowana przez zmienną innodb_log_group_home_dir. Przechowane są tam redo log-i, czyli struktury danych na dysku używane w czasie odzyskiwania danych po niespodziewanym zamknięciu serwera aby odzyskać poprawnie zapisane dane przy niedokończonych transakcjach.
  • pliki dla replikacji, które można znaleźć w katalogu /home/mysql/mysql5.5/replication/ pod nazwami zaczynającymi się od:
    • binlog - pliki binlogów, które zawierają informację o numerze pliku, zdarzenia o zmianach w bazie danych i plik indexu z listą wszystkich używanych plików
    • relay-bin - składa się ze zdarzeń odczytywanych z binary logów z mastera i zapisanych przez wątek I/O slav-a. Zdarzenia w tych plikach są wykonywane na slave jako część wątku SQL.
  • typ replikacji (w tym przypadku ROW):
    • ROW - master wysyła zdarzenia, które identyfikują indywidualne zmiany wierszy które zostały dokonane
    • STATEMENT - replikowane są zapytania SQL (dla naszej wersji serwera jest to domyślna opcja)
    • MIXED - połączenie dwóch poprzednich typów replikacji.
  • zmienna innodb_file_per_table - jest włączona aby dla każdej tabeli, dane i index-y były w oddzielnych plikach. 

Instalacja serwera MySQL 5.5

Przed rozpoczęciem instalacji, pobrałam wybraną przeze mnie wersję serwera ze storny http://downloads.mysql.com/archives/community/. Po rozpakowaniu pliku tar.gz znajdziecie plik INSTALL-BINARY, w których opisany jest proces instalacji, dlatego tylko krótko przedstawię swoje kroki instalacji.

W pierwszej kolejności dodaję grupy i użytkownika mysql (jeśli jeszcze nie istnieje). W lokalizacji /usr/local rozpakowuje plik z serwerem, a następnie stworzony katalog podlinkuje pod symlinka mysql5.5.

sudo groupadd mysql
sudo useradd -g mysql mysql
cd /usr/local
sudo gunzip < /home/ela/mysql/mysql-5.5.23-linux2.6-x86_64.tar.gz | sudo tar xvf -
sudo ln -s /usr/local/mysql-5.5.23-linux2.6-x86_64.tar.gz mysql5.5

Domyślnie mysql szuka pliku konfiguracyjnego w katalogu /etc. Na tej maszynie istnieje już działający serwer MySQL, dlatego nasz plik konfiguracyjny będzie można znaleźć pod /etc/my5.5.cnf

cd mysql5.5
sudo chown -R mysql .
sudo chgrp -R mysql .
sudo cp /home/ela/mysql/my.cnf /etc/my5.5.cnf

Po zainstalowaniu plików serwera, musimy zainicjować katalog z danymi i stworzyć system tabel. Ponieważ mamy niestandardowe ścieżki, musimy je podać:

sudo scripts/mysql_install_db --defaults-file=/etc/my5.5.cnf --user=mysql --builddir=/usr/local/mysql5.5/bin --pid-file=/home/mysql/mysql5.5/mysql.pid
sudo chown -R root .

Uruchamiamy zainstalowany serwer i jeśli wszystko zakończy się dobrze, musimy stworzyć super użytkownika, któremu nadamy hasło:

bin/mysqld_safe --defaults-file=/etc/my5.5.cnf --user=mysql --ledir=/usr/local/mysql5.5/bin --pid-file=/home/mysql/mysql5.5/mysql.pid &
./bin/mysqladmin -u root password 'root' --host=127.0.0.1 --port=3309


Ustawienie replikacji

Ponieważ dopiero co zainstalowaliśmy nasze serwery, nie ma potrzeby ich wyrównywać. W przeciwnym przypadku musieli byśmy zrobić dump serwera i zrestować dane na drugim serwerze.

Aby replikacja mogła działać, na każdym z serwerów stworzymy użytkownika z prawami do replikacji:

CREATE USER 'replication'@'%' IDENTIFIED BY 'replication';
GRANT REPLICATION SLAVE ON *.* TO 'replication'@'%';

Jeśli na masterze mamy dane, które chcemy synchronizować przed uruchomieniem replikacji, musimy zatrzymać działające zapytania na masterze aby otrzymać aktualne binary logi, zdumpować je, zrestartować na slave i pozwolić na kontynuowanie działania przerwanych zapytań (Polecenie na masterze: UNLOCK TABLES).
Aktualne binlogi otrzymamy przez zapytanie:

FLUSH TABLES WITH READ LOCK;


Sprawdzamy jaki jest status binary logów (nazwa i pozycja) dla serwerów (poniżej przykład wyniku dla serwera ID 1) aby sprawdzić od którego pliku i jakiej pozycji ma się rozpocząć replikacja:

mysql (root@(none))> show master status;
+---------------+----------+--------------+------------------+
| File          | Position | Binlog_Do_DB | Binlog_Ignore_DB |
+---------------+----------+--------------+------------------+
| binlog.000003 |      502 |              |                  |
+---------------+----------+--------------+------------------+
1 row in set (0.00 sec)

Ustawiamy parametry replikacji na każdym masterze (Szczegóły w dokumentacji: http://dev.mysql.com/doc/refman/5.5/en/change-master-to.html lub http://dev.mysql.com/doc/refman/5.5/en/replication-howto-slaveinit.html).

Dla serwera ID 1 (192.168.100.167):
mysql (root@(none))>CHANGE MASTER TO
MASTER_HOST='192.168.100.166',
MASTER_PORT=3309,
MASTER_USER='replication',
MASTER_PASSWORD='replication',
MASTER_LOG_FILE='binlog.000005',
MASTER_LOG_POS=107;

Dla serwera ID 2 (192.168.100.166):
mysql (root@(none))>CHANGE MASTER TO
MASTER_HOST='192.168.100.167',
MASTER_PORT=3309,
MASTER_USER='replication',
MASTER_PASSWORD='replication',
MASTER_LOG_FILE='binlog.000003',
MASTER_LOG_POS=502;

Uruchamiany replikację na każdym serwerze:
mysql (root@(none))> SLAVE START;

Status slave z servera id 1 (192.168.100.167) weryfikuję:
mysql (root@(none))> SHOW slave STATUS \G
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: 192.168.100.166
                  Master_User: replication
                  Master_Port: 3309
                Connect_Retry: 60
              Master_Log_File: binlog.000005
          Read_Master_Log_Pos: 107
               Relay_Log_File: relay-bin.000002
                Relay_Log_Pos: 250
        Relay_Master_Log_File: binlog.000005
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
              Replicate_Do_DB:
          Replicate_Ignore_DB:
           Replicate_Do_Table:
       Replicate_Ignore_Table:
      Replicate_Wild_Do_Table:
  Replicate_Wild_Ignore_Table:
                   Last_Errno: 0
                   Last_Error:
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 107
              Relay_Log_Space: 400
              Until_Condition: None
               Until_Log_File:
                Until_Log_Pos: 0
           Master_SSL_Allowed: No
           Master_SSL_CA_File:
           Master_SSL_CA_Path:
              Master_SSL_Cert:
            Master_SSL_Cipher:
               Master_SSL_Key:
        Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error:
               Last_SQL_Errno: 0
               Last_SQL_Error:
  Replicate_Ignore_Server_Ids:
             Master_Server_Id: 2
1 row in set (0.00 sec)

Status slave z servera id 2 (192.168.100.166) weryfikuję:
mysql (root@(none))> show slave status \G
*************************** 1. row ***************************
               Slave_IO_State: Waiting for master to send event
                  Master_Host: 192.168.100.167
                  Master_User: replication
                  Master_Port: 3309
                Connect_Retry: 60
              Master_Log_File: binlog.000003
          Read_Master_Log_Pos: 502
               Relay_Log_File: relay-bin.000002
                Relay_Log_Pos: 250
        Relay_Master_Log_File: binlog.000003
             Slave_IO_Running: Yes
            Slave_SQL_Running: Yes
              Replicate_Do_DB:
          Replicate_Ignore_DB:
           Replicate_Do_Table:
       Replicate_Ignore_Table:
      Replicate_Wild_Do_Table:
  Replicate_Wild_Ignore_Table:
                   Last_Errno: 0
                   Last_Error:
                 Skip_Counter: 0
          Exec_Master_Log_Pos: 502
              Relay_Log_Space: 400
              Until_Condition: None
               Until_Log_File:
                Until_Log_Pos: 0
           Master_SSL_Allowed: No
           Master_SSL_CA_File:
           Master_SSL_CA_Path:
              Master_SSL_Cert:
            Master_SSL_Cipher:
               Master_SSL_Key:
        Seconds_Behind_Master: 0
Master_SSL_Verify_Server_Cert: No
                Last_IO_Errno: 0
                Last_IO_Error:
               Last_SQL_Errno: 0
               Last_SQL_Error:
  Replicate_Ignore_Server_Ids:
             Master_Server_Id: 1
1 row in set (0.00 sec) 

Mam nadzieję, że Wam także się udało. Przy pomocy stworzonej replikacji będę chciała sprawdzać kilka rzeczy, które mogą się przyda wiedzieć dla programisty. Ale to do następnego posta.

piątek, 24 stycznia 2014

Funkcja do szukania wierszy potomnych w PostgreSQL-u

Jakiś czas temu spotkałam się z problemem szukania wierszy, które były 'dziećmi' innego wiersza. Najczęstszym przykładem jest firma, która może mieć podfirmy albo sama może być podfirmą.
Gdy struktura takiej firmy jest duża, wyciąganie każdego poziomu drzewa tej struktury, może być dość uciążliwe. Dlatego stworzyłam funkcję, która zwróci wszystkie ID wierszy dzieci i ID rodzica.

Poniższy diagram przedstawia testową strukturę drzewa rodzic-dzieci.


Przygotowanie danych testowych:


postgres@test(localhost) # create table test5 (id integer, parent_id integer, name varchar(25));
CREATE TABLE
postgres@test(localhost) # insert into test5 (id, parent_id, name) values (1,null, 'id: 1, parent: null'), (2, null, 'id: 2, parent: null'), (3, null, 'id: 3, parent: null');
INSERT 0 3
postgres@test(localhost) # insert into test5 (id, parent_id, name) values (4,1, 'id: 4, parent: 1'), (5, 1, 'id: 5, parent: 1'), (6, 2, 'id: 6, parent: 2'), (7, 2, 'id: 7, parent: 2'), (8, 3, 'id: 8, parent: 3'), (9, 3, 'id: 9, parent: 3'),(10, 3, 'id: 10, parent: 3');
INSERT 0 7
postgres@test(localhost) # insert into test5 (id, parent_id, name) values (11,4, 'id: 11, parent: 4'), (12, 4, 'id: 12, parent: 4'), (13, 6, 'id: 13, parent: 6'), (14, 7, 'id: 14, parent: 7'), (15, 7, 'id: 15, parent: 7'), (16, 9, 'id: 16, parent: 9');
INSERT 0 6
postgres@test(localhost) # insert into test5 (id, parent_id, name) values (17,13, 'id: 17, parent: 13'), (18, 15, 'id: 18, parent: 15'), (19, 15, 'id: 19, parent: 15');
INSERT 0 3
postgres@test(localhost) # insert into test5 (id, parent_id, name) values (20,17, 'id: 20, parent: 17'), (21, 18, 'id: 21, parent: 18'), (22, 18, 'id: 22, parent: 18');
INSERT 0 3
postgres@test(localhost) # insert into test5 (id, parent_id, name) values (23,22, 'id: 23, parent: 22');
INSERT 0 1
postgres@test(localhost) # \i /home/ela/get_childern_by_root.sql
DROP FUNCTION
CREATE FUNCTION



Funkcji wymaga tylko jednego argumentu: ID rodzica. Wszystkie operacje wykonywane są na tablicach:
  • zmienna := ARRAY(select_statement) - przypisanie do zmiennej tablicy, której dane są wynikiem działania select_statement
  • zmienna INTEGER[] - deklaracja zmiennej typu tablica integer-ów
  • SELECT .... WHERE parent_id = ANY(my_array) - szukanie wierszy gdzie parent_id jest jedną z wartości w tablicy my_array
  • funkcja array_cat - łączenie dwóch tablic
  • funkcja array_dims - tekstowa reprezentacja rozmiaru tabeli
Tablice są bardzo przydatne w programowaniu w PostgreSQL-a, dlatego zachęcam do czytania dokumentacji: http://www.postgresql.org/docs/9.1/static/functions-array.html

Przejdziemy do samej funkcji:


DROP FUNCTION IF EXISTS get_childern_by_root(INTEGER);

CREATE OR REPLACE FUNCTION get_childern_by_root(INTEGER) RETURNS integer[] AS $$
DECLARE 
 update_state BOOLEAN DEFAULT true;
 flag BOOLEAN DEFAULT true;
 children_count INTEGER DEFAULT 0;
 root_id ALIAS FOR $1;
 children_ids INTEGER[];
 level_row_ids INTEGER[];
 temp_level_row_ids INTEGER[];
BEGIN
 level_row_ids := ARRAY(SELECT root_id);
 children_ids := ARRAY(SELECT root_id);

 WHILE flag LOOP
 BEGIN
  RAISE NOTICE 'Elements: %',level_row_ids;
  SELECT INTO children_count count(*) FROM test5 WHERE parent_id = ANY(level_row_ids);
  -- RAISE NOTICE 'Elements count: %',children_count;
  IF children_count > 0 THEN
   temp_level_row_ids := level_row_ids;
   level_row_ids := ARRAY(SELECT id FROM test5 WHERE parent_id = ANY(temp_level_row_ids));
   children_ids := array_cat(children_ids,level_row_ids);
  ELSE
   flag := false;
  END IF;
 EXCEPTION
  WHEN others THEN
  RAISE NOTICE 'ERROR, sql code: %, sql error message %', SQLSTATE,SQLERRM;
  RETURN children_ids;
 END;
 END LOOP;

 BEGIN
  RAISE NOTICE 'Founded % ROWS', array_dims(children_ids);
 EXCEPTION
  WHEN others THEN
  RAISE NOTICE 'ERROR, sql code: %, sql error message %', SQLSTATE,SQLERRM;
  RETURN children_ids;
 END;


 RETURN children_ids;
END;
$$ LANGUAGE plpgsql;

Testujemy funkcje sprawdzając wynik z diagramem wyżej:

postgres@test(localhost) # select get_childern_by_root(2);
NOTICE:  Elements: {2}
NOTICE:  Elements: {6,7}
NOTICE:  Elements: {13,14,15}
NOTICE:  Elements: {17,18,19}
NOTICE:  Elements: {20,21,22}
NOTICE:  Elements: {23}
NOTICE:  Founded [1:13] ROWS
         get_childern_by_root          
---------------------------------------
 {2,6,7,13,14,15,17,18,19,20,21,22,23}
(1 row)

postgres@test(localhost) # select get_childern_by_root(1);
NOTICE:  Elements: {1}
NOTICE:  Elements: {4,5}
NOTICE:  Elements: {11,12}
NOTICE:  Founded [1:5] ROWS
 get_childern_by_root 
----------------------
 {1,4,5,11,12}
(1 row)

postgres@test(localhost) # select get_childern_by_root(11);
NOTICE:  Elements: {11}
NOTICE:  Founded [1:1] ROWS
 get_childern_by_root 
----------------------
 {11}
(1 row)

postgres@test(localhost) # select get_childern_by_root(15);
NOTICE:  Elements: {15}
NOTICE:  Elements: {18,19}
NOTICE:  Elements: {21,22}
NOTICE:  Elements: {23}
NOTICE:  Founded [1:6] ROWS
 get_childern_by_root 
----------------------
 {15,18,19,21,22,23}
(1 row)

postgres@test(localhost) # select get_childern_by_root(3);
NOTICE:  Elements: {3}
NOTICE:  Elements: {8,9,10}
NOTICE:  Elements: {16}
NOTICE:  Founded [1:5] ROWS
 get_childern_by_root 
----------------------
 {3,8,9,10,16}
(1 row)

Mam nadzieję, że przyda się wam może kiedyś ta prosta funkcja.

wtorek, 21 stycznia 2014

EXPLAIN plan w PostgreSQL v9.1 - Zrozumieć co dzieje się z naszym zapytaniem

W poprzednim poście (EXPLAIN plan w PostgreSQL v9.1 - Wprowadzenie), dowiedzieliśmy się jakie opcje może przyjmować polecenie EXPLAIN i jak może wyglądać rezultat działania tej instrukcji. Teraz jednak chcieli byśmy wiedzieć co oznacza ten wynik. Czy nasze zapytanie zostało dobrze napisane? Czy działa efektywnie?

Przykładowy wynik działa instrukcji EXPLAIN:

postgres@test(127.0.0.1) # explain select * from event e left join event_2 ee on e.c4 = ee.c1 where (e.c4 < 16 AND e.c5 = 90) and ee.c2 > 10;
                                       QUERY PLAN                                        
-----------------------------------------------------------------------------------------
 Hash Join  (cost=6809.87..12892.11 rows=1661 width=56)
   Hash Cond: (e.c4 = ee.c1)
   ->  Bitmap Heap Scan on event e  (cost=6806.56..12864.79 rows=1974 width=40)
         Recheck Cond: ((c5 = 90) AND (c4 < 16))
         ->  BitmapAnd  (cost=6806.56..6806.56 rows=1974 width=0)
               ->  Bitmap Index Scan on idx_c5  (cost=0.00..177.89 rows=12473 width=0)
                     Index Cond: (c5 = 90)
               ->  Bitmap Index Scan on idx_c4  (cost=0.00..6627.44 rows=211049 width=0)
                     Index Cond: (c4 < 16)
   ->  Hash  (cost=2.25..2.25 rows=85 width=16)
         ->  Seq Scan on event_2 ee  (cost=0.00..2.25 rows=85 width=16)
               Filter: (c2 > 10)
(12 rows)

lub graficzny z pgAdmin III:


Jeśli będziemy wiedzieć jak odczytywać wynik działania polecenia EXPLAIN, będziemy też wiedzieć:
  • jaka metoda skanowania została użyta (Scan Methods)
    • Sequential Scan
    • Index Scan
    • Bitmap Scan
  • jaka metoda łączenia została użyta (Join Methods)
    • Nested Loop
      • With Inner Sequential Scan
      • With Inner Index Scan
    • Hash Join
    • Merge Join
  • jaki kierunek łączenia został zastosowany (Join Order)
  • jaki index został użyty
  • jaki filtry danych zostały zastosowane
  • oraz informacje, które zostały omówione we wprowadzeniu (koszty, użycie bufora, czas trwania i inne (które można zaleźć pod postem: EXPLAIN plan w PostgreSQL v9.1 - Wprowadzenie))


Metody skanowania


Sequential Scan

Najgorszy sposób skanowania danych. Optymalizator przechodzi przez tabelę, aby wybrać wiersze, które go interesują.

Ten sposób jest wybierany głównie gdy:
  • na kolumnie na której został nałożony warunek, nie mamy index-u
  • optymalizator zdecydował, że nasz warunek dotyczy większości danych w tabeli
Przykład:
postgres@test(127.0.0.1) # explain (ANALYZE, buffers true) select c5 from event offset 10 limit 10;
                                                   QUERY PLAN                                                   
----------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.29..0.58 rows=10 width=4) (actual time=0.044..0.066 rows=10 loops=1)
   Buffers: shared read=1
   ->  Seq Scan on event  (cost=0.00..38333.33 rows=1333333 width=4) (actual time=0.020..0.040 rows=20 loops=1)
         Buffers: shared read=1
 Total runtime: 0.097 ms
(5 rows)


Index Scan



Skanuje index, a następnie wybiera dane z tabeli aby je zwrócić.

Koło informacji, że index jest skanowany widzimy, który index został użyty i na jakiej tabeli:

Index Scan using idx_c5 on event

Przykład:


postgres@test(127.0.0.1) # explain select c5 from event order by c5 offset 10 limit 10;
                                      QUERY PLAN                                      
--------------------------------------------------------------------------------------
 Limit  (cost=16.31..32.62 rows=10 width=4)
   ->  Index Scan using idx_c5 on event  (cost=0.00..2174766.57 rows=1333333 width=4)
(2 rows)


Bitmap Scan



Tym razem chcemy wybrać dane, gdzie warunek jest spełniony przez dwa index-y.

Optymalizator wykonuje następujące kroki:
  1. Buduje bitmapę wierszy, które to spełniają pierwszy warunek, używa do tego index-u, który spełnia ten warunek (-> Bitmap Index Scan on idx_c5)
  2. Buduje bitmapę wierszy, które to spełniają drugi warunek, także, używa do tego index-u, który spełnia ten warunek (-> Bitmap Index Scan on idx_c4)
  3. Łączy powstałe w 1 i 2 krokach bitmap-y przy pomocy operatora użytego w warunkach zapytania (BitmapOr dla operatora OR, BitmapAnd dla operatora AND) -> BitmapOr
  4. Przegląda tabelę docelową z danymi (-> Bitmap Heap Scan on event) i ponownie sprawdza czy spełniony został nasz warunek (-> Recheck Cond: ((c4 = 16) OR (c5 = 30)))
Przykład:
postgres@test(127.0.0.1) # explain select * from event where (c4 = 16 OR c5 = 30);
                                   QUERY PLAN                                    
---------------------------------------------------------------------------------
 Bitmap Heap Scan on event  (cost=631.46..27250.57 rows=28181 width=40)
   Recheck Cond: ((c4 = 16) OR (c5 = 30))
   ->  BitmapOr  (cost=631.46..631.46 rows=28328 width=0)
         ->  Bitmap Index Scan on idx_c4  (cost=0.00..389.03 rows=12328 width=0)
               Index Cond: (c4 = 16)
         ->  Bitmap Index Scan on idx_c5  (cost=0.00..228.34 rows=16000 width=0)
               Index Cond: (c5 = 30)
(7 rows)


Metody łączenia


Każdy sposób łączenia przyjmuje dwa argumenty, jakimi są: lewa i prawa relację.

Nested Loop With Inner Sequential Scan




Dla bardzo dużych tabel, ten sposób łączenia może być bardzo kosztowny gdyż w takim przypadku optymalizator: dla każdego elementu relacji lewej, sprawdza każdy wiersz relacji prawej. Jeśli warunek łączenia jest spełniony, zwraca wiersz jako wynik łączenia. W przeciwnym przypadku, wiersz jest pomijany.



Przykład:


postgres@test(127.0.0.1) # explain select * from (select * from event limit 10) as foo, event_2 limit 10;
                                       QUERY PLAN                                       
----------------------------------------------------------------------------------------
 Limit  (cost=0.00..0.15 rows=10 width=56)
   ->  Nested Loop  (cost=0.00..14.91 rows=1000 width=56)
         ->  Seq Scan on event_2  (cost=0.00..2.00 rows=100 width=16)
         ->  Materialize  (cost=0.00..0.44 rows=10 width=40)
               ->  Limit  (cost=0.00..0.29 rows=10 width=40)
                     ->  Seq Scan on event  (cost=0.00..38333.33 rows=1333333 width=40)
(6 rows)


Nested Loop With Inner Index Scan



Tym razem jednak do wybierana danych z tabeli prawej, używany jest index.








Przykład:
postgres@test(127.0.0.1) # explain select * from event where c1 IN (select c1 from (select count(*) as c, min(c1) as c1, c4 from event where c3 > '2014-01-01' group by c4 having count(*) > 1500) as foo);
                                      QUERY PLAN                                       
---------------------------------------------------------------------------------------
 Nested Loop  (cost=43606.41..43734.71 rows=15 width=40)
   ->  HashAggregate  (cost=43606.41..43606.56 rows=15 width=4)
         ->  HashAggregate  (cost=43606.03..43606.22 rows=15 width=8)
               Filter: (count(*) > 1500)
               ->  Seq Scan on event  (cost=0.00..41666.66 rows=193937 width=8)
                     Filter: (c3 > '2014-01-01 00:00:00'::timestamp without time zone)
   ->  Index Scan using event_pkey on event  (cost=0.00..8.53 rows=1 width=40)
         Index Cond: (c1 = (min(public.event.c1)))
(8 rows)


Hash Join



Prawa relacja jest skanowana i ładowana do tabeli hash-y w taki sposób, że atrybuty łączenia są kluczami hash-y. Następnie lewa relacja jest skanowana i odpowiednie wartości są znalezione poprzez klucze hash-y (relacje prawą).

Algorytm jest bardzo szybki ale wymaga wystarczająco dużo pamięci, aby pomieścić prawą relację.Nie gwarantuje także, że sortowanie wejściowe będzie zachowane w wyniku łączenia.

Przykład:


postgres@test(127.0.0.1) # explain select * from t3 left join t1 on t1.c1 = t3.c1;
                            QUERY PLAN                             
-------------------------------------------------------------------
 Hash Left Join  (cost=35.43..96.25 rows=2140 width=50)
   Hash Cond: (t3.c1 = t1.c1)
   ->  Seq Scan on t3  (cost=0.00..31.40 rows=2140 width=8)
   ->  Hash  (cost=21.30..21.30 rows=1130 width=42)
         ->  Seq Scan on t1  (cost=0.00..21.30 rows=1130 width=42)
(5 rows)


Merge Join



Zanim dojdzie do łączenia, relacje (prawa i lewa) są sortowane.
Następnie skanuje równolegle dopasowując równe wartości.

Zazwyczaj algorytm skanuje wiersz tylko raz, ale pod warunkiem, że w relacji lewej nie ma duplikatów.


Przykład:
postgres@test(127.0.0.1) # explain select * from (select * from event order by c2 desc) e1 join (select * from event order by c4 desc) e2 on e1.c5 = e2.c4;
                                       QUERY PLAN                                       
----------------------------------------------------------------------------------------
 Merge Join  (cost=828400.83..134185000.82 rows=8888884444 width=80)
   Merge Cond: (public.event.c4 = public.event.c5)
   ->  Sort  (cost=283355.83..286689.17 rows=1333333 width=40)
         Sort Key: public.event.c4
         ->  Seq Scan on event  (cost=0.00..38333.33 rows=1333333 width=40)
   ->  Materialize  (cost=545045.00..551711.67 rows=1333333 width=40)
         ->  Sort  (cost=545045.00..548378.33 rows=1333333 width=40)
               Sort Key: public.event.c5
               ->  Sort  (cost=283355.83..286689.17 rows=1333333 width=40)
                     Sort Key: public.event.c2
                     ->  Seq Scan on event  (cost=0.00..38333.33 rows=1333333 width=40)
(11 rows)



To jaką decyzje podejmie optymalizator, który algorytm użyć, jak wybrać dane, jest uzależnione od statystyk. Dlatego ważne jest aby było one zawsze aktualne.
Ale o samych statystykach może kiedy indziej.

Miłego czytania!

czwartek, 16 stycznia 2014

EXPLAIN plan w PostgreSQL v9.1 - Wprowadzenie

Jakiś czas temu omawiałam jak wygląda EXPLAIN plan w MySQL-u (http://db-diary.blogspot.com/2014/01/exaplain-plan-w-mysql-v55.html). Teraz przyszedł czas na PostgreSQL-a. Będę używać wersji 9.1, która dla instrukcji EXPLAIN, nieznacznie różni się od innych wersji 9.x i ma dodatkowe opcje w stosunku do wersji 8.x.

Poniżej opiszę tylko wywołanie instrukcji EXPLAIN, a do opracowania informacji, jak czytać wynik tego polecenia, aby optymalizować naszej komendy, przejdę w następnym poście. Wszystkie te informacje możecie także znaleźć w dokumentacji :http://www.postgresql.org/docs/9.1/static/sql-explain.html.

Wywołanie instrukcji EXPLAIN:
EXPLAIN [ ( option [, ...] ) ] statement
EXPLAIN [ ANALYZE ] [ VERBOSE ] statement

where option can be one of:

    ANALYZE [ boolean ]
    VERBOSE [ boolean ]
    COSTS [ boolean ]
    BUFFERS [ boolean ]
    FORMAT { TEXT | XML | JSON | YAML } 

Opcje instrukcji EXPLAIN z przykładami:
  • ANALYZE - wykonuje polecenie i zwraca rzeczywisty czas wykonania komendy. Domyślana wartość parametru: FALSE
postgres@test(127.0.0.1) # explain (ANALYZE, costs false) select * from t1 where c1 IN (1,7);
                                  QUERY PLAN                                  
------------------------------------------------------------------------------
 Bitmap Heap Scan on t1 (actual time=0.023..0.025 rows=2 loops=1)
   Recheck Cond: (c1 = ANY ('{1,7}'::integer[]))
   ->  Bitmap Index Scan on t1_pkey (actual time=0.017..0.017 rows=2 loops=1)
         Index Cond: (c1 = ANY ('{1,7}'::integer[]))
 Total runtime: 0.054 ms
(5 rows)

  • VERBOSE - wyświetla dodatkowe informacje. Domyślna wartość parametru: FALSE
postgres@test(127.0.0.1) # explain (VERBOSE, costs false) select * from t1 where c1 IN (1,7);
                       QUERY PLAN                       
--------------------------------------------------------
 Bitmap Heap Scan on public.t1
   Output: c1, c2
   Recheck Cond: (t1.c1 = ANY ('{1,7}'::integer[]))
   ->  Bitmap Index Scan on t1_pkey
         Index Cond: (t1.c1 = ANY ('{1,7}'::integer[]))
(5 rows)

  • COSTS - wyświetla informacje o :
    • szacowanym koszcie (np. cost=0.00..10.44 to cost=[koszta startowy]..[koszt całkowity jeśli wszystkie wiersze zostaną zwrócone]) dla każdego fragmentu planu
    • szacowanej ilość wierszy zwróconych
    • średniej długości (w bajtach) każdego wiersza
    Domyślna wartość parametru: TRUE
  • BUFFERS - zwraca informację o użyciu bufora. Zawiera:
    • Shared Hit Blocks: liczbę 'trafionych' bloków współdzielonych, np. użytych do łączenia
    • Shared Read Blocks: liczbę przeczytanych bloków współdzielonych
    • Shared Written Blocks: liczbę zapisanych bloków współdzielonych
    • Local Hit Blocks: liczbę bloków lokalnych użytych np. do łączenia
    • Local Read Blocks: liczbę przeczytanych bloków lokalnych
    • Local Written Blocks: liczbę zapisanych bloków lokalnych
    • Temp Read Blocks: liczbę czytanych bloków tymczasowych
    • Temp Written Blocks: liczbę zapisanych bloków tymczasowych
    Wyświetla tylko wartości niezerowe dla formatu TEXT. Wartość domyślna parametru: FALSE. Ta opcja wymaga odpalenia polecenia, dlatego trzeba ją wykonać z opcją ANALYZE.
postgres@test(127.0.0.1) # explain (ANALYZE,VERBOSE, buffers true, FORMAT TEXT, costs false) select * from t1, t3 where t1.c1 = t3.c1;
                                 QUERY PLAN                                  
-----------------------------------------------------------------------------
 Hash Join (actual time=0.039..0.045 rows=2 loops=1)
   Output: t1.c1, t1.c2, t3.c1, t3.c2
   Hash Cond: (t3.c1 = t1.c1)
   Buffers: shared hit=2
   ->  Seq Scan on public.t3 (actual time=0.006..0.008 rows=2 loops=1)
         Output: t3.c1, t3.c2
         Buffers: shared hit=1
   ->  Hash (actual time=0.019..0.019 rows=6 loops=1)
         Output: t1.c1, t1.c2
         Buckets: 1024  Batches: 1  Memory Usage: 1kB
         Buffers: shared hit=1
         ->  Seq Scan on public.t1 (actual time=0.003..0.008 rows=6 loops=1)
               Output: t1.c1, t1.c2
               Buffers: shared hit=1
 Total runtime: 0.081 ms
(15 rows)

  • FORMAT - wyróżnia format wyjściowy. Domyślna wartość parametru: TEXT. Poniżej przykład instrukcji EXPLAIN w formacie JSON:
postgres@test(127.0.0.1) # explain (ANALYZE,VERBOSE, buffers true, FORMAT JSON) select * from t1 where c1 IN (1,7);
                          QUERY PLAN                           
---------------------------------------------------------------
 [                                                            +
   {                                                          +
     "Plan": {                                                +
       "Node Type": "Bitmap Heap Scan",                       +
       "Relation Name": "t1",                                 +
       "Schema": "public",                                    +
       "Alias": "t1",                                         +
       "Startup Cost": 8.52,                                  +
       "Total Cost": 13.86,                                   +
       "Plan Rows": 2,                                        +
       "Plan Width": 42,                                      +
       "Actual Startup Time": 0.027,                          +
       "Actual Total Time": 0.030,                            +
       "Actual Rows": 2,                                      +
       "Actual Loops": 1,                                     +
       "Output": ["c1", "c2"],                                +
       "Recheck Cond": "(t1.c1 = ANY ('{1,7}'::integer[]))",  +
       "Shared Hit Blocks": 3,                                +
       "Shared Read Blocks": 0,                               +
       "Shared Written Blocks": 0,                            +
       "Local Hit Blocks": 0,                                 +
       "Local Read Blocks": 0,                                +
       "Local Written Blocks": 0,                             +
       "Temp Read Blocks": 0,                                 +
       "Temp Written Blocks": 0,                              +
       "Plans": [                                             +
         {                                                    +
           "Node Type": "Bitmap Index Scan",                  +
           "Parent Relationship": "Outer",                    +
           "Index Name": "t1_pkey",                           +
           "Startup Cost": 0.00,                              +
           "Total Cost": 8.52,                                +
           "Plan Rows": 2,                                    +
           "Plan Width": 0,                                   +
           "Actual Startup Time": 0.019,                      +
           "Actual Total Time": 0.019,                        +
           "Actual Rows": 2,                                  +
           "Actual Loops": 1,                                 +
           "Index Cond": "(t1.c1 = ANY ('{1,7}'::integer[]))",+
           "Shared Hit Blocks": 2,                            +
           "Shared Read Blocks": 0,                           +
           "Shared Written Blocks": 0,                        +
           "Local Hit Blocks": 0,                             +
           "Local Read Blocks": 0,                            +
           "Local Written Blocks": 0,                         +
           "Temp Read Blocks": 0,                             +
           "Temp Written Blocks": 0                           +
         }                                                    +
       ]                                                      +
     },                                                       +
     "Triggers": [                                            +
     ],                                                       +
     "Total Runtime": 0.058                                   +
   }                                                          +
 ]
(1 row)

statement:  jest dowolnym poleceniem SELECT, INSERT, UPDATE, DELETE, VALUES, EXECUTE, DECLATE, CREATE TABLE AS ...

Musimy pamiętać, że użycie opcji ANALYZE do polecień INSERT, UPDATE, DELETE, CREATE TABLE AS albo EXECUTE spowoduje zmiany w naszej bazie. Aby się przed tym uchronić, musimy nasze poleceni zawrzeć w transakcji, która zostanie później ROLLBACK-owana.
postgres@test(127.0.0.1) # select * from t1;
 c1 |   c2    
----+---------
  1 | 
  2 | 
  3 | asdfsdf
  5 | asdfsdf
(4 rows)

postgres@test(127.0.0.1) # explain (Analyze,buffers true) insert into t1 (c1,c2) values (6,'test'), (7,'test');
                                                  QUERY PLAN                                                  
--------------------------------------------------------------------------------------------------------------
 Insert on t1  (cost=0.00..0.03 rows=2 width=42) (actual time=0.044..0.044 rows=0 loops=1)
   Buffers: shared hit=6
   ->  Values Scan on "*VALUES*"  (cost=0.00..0.03 rows=2 width=42) (actual time=0.003..0.005 rows=2 loops=1)
 Total runtime: 0.076 ms
(4 rows)

postgres@test(127.0.0.1) # begin; explain (Analyze,buffers true) insert into t1 (c1,c2) values (8,'test'), (9,'test'); rollback;
BEGIN
                                                  QUERY PLAN                                                  
--------------------------------------------------------------------------------------------------------------
 Insert on t1  (cost=0.00..0.03 rows=2 width=42) (actual time=0.048..0.048 rows=0 loops=1)
   Buffers: shared hit=6
   ->  Values Scan on "*VALUES*"  (cost=0.00..0.03 rows=2 width=42) (actual time=0.003..0.006 rows=2 loops=1)
 Total runtime: 0.083 ms
(4 rows)

ROLLBACK
postgres@test(127.0.0.1) # select * from t1;
 c1 |   c2    
----+---------
  1 | 
  2 | 
  3 | asdfsdf
  5 | asdfsdf
  6 | test
  7 | test
(6 rows)

Zawarta tutaj informacja jest wstępem do analizowania, co się dzieje z naszym zapytaniem. Znając te podstawy, będziemy mogli przejść dalej.

Powodzenia w nauce :-)

wtorek, 14 stycznia 2014

Zrozumieć constraint-y na przykładach z PostgreSQL-a i MySQL-a

Jak sama nazwa wskazuje, constraint-y są ograniczeniami. Są regułami, które wymuszają pewne reguły na danych w kolumnach.

W SQL-u mamy następujące typy constraint-ów:
  • NOT NULL - wymusza aby w kolumnie zawsze występowała jakaś wartość
  • UNIQUE - wymaga, aby w kolumnie występowały tylko i wyłącznie wartości unikalne
  • PRIMARY KEY - wymaga spełnienia następujących reguł:
    • unikalność wartości w kolumnie (UNIQUE)
    • w tabeli może być tylko jedna kolumna tego typu
    • nie może zawierać wartości NULL
    • aby klucz główny automatycznie posiadał wartość domyślną, trzeba przy jego definicji podać opcje auto increment (W PostgreSQL-u za ten mechanizm odpowiada seqwencja)
  • FOREIGN KEY - Kolumna z tą opcją, wskazuje na klucz główny (PRIMARY KEY) w innej tabeli. Jeśli nie ma nałożonych innych ograniczeń, kolumna może zawierać wartości tylko, które będą odpowiadać wartością klucza głównego w innej tabeli, do której się odwołuje. Constraint może dotyczyć także grupy kolumn i odwoływać się do tej liczby i typów kolumn innej tabeli.
  • CHECK - wymusza limitację wartości danych w kolumnie. Np. CHECK (c1 > 10) -  tylko wartości większe od 10. Niestety tego typu ograniczenie nie jest wspierane przez bazę MySQL
  • DEFAULT - jest używane do wstawianie wartości domyślnych. W poniższych przykładach zobaczycie, że wartość domyślna dla stringów jest wstawiana tylko wtedy, gdy kolumna jest pomijana.
  • EXCLUDE (PostgreSQL) - gwarantuje, że 2 wiersze są porównywane na specjalnej kolumnie lub wyrażeniu, używające specjalnego operatora.
Przykład poniżej przestawia definicję i zawartość tabeli z kołami, gdzie można wstawić tylko i wyłącznie koła, które nie nachodzą na siebie:
postgres@test(127.0.0.1) # CREATE TABLE circles (
    c circle,
    EXCLUDE USING gist (c WITH &&)
);

postgres@test(127.0.0.1) # select * from circles;
       c       
---------------
 <(0,2),1>
 <(0,0),0.5>
 <(0,-10),0.5>
 <(-1,-1),0.5>
(4 rows)

postgres@test(127.0.0.1) # insert into circles (c) values (circle(point '(-2,-2)', 1));
ERROR:  conflicting key value violates exclusion constraint "circles_c_excl"
DETAIL:  Key (c)=(<(-2,-2),1>) conflicts with existing key (c)=(<(-1,-1),0.5>).
Przykłady

Przejdźmy do konkretnych przykładów, omówionych powyżej constraint-ów. Definicja tabela test3 zawierająca wszystkie typy constraint-ów (MySQL):
mysql (root@test)> create table test3 (c1 integer primary key, c2 varchar(255) default 'default text', c3 varchar(10) UNIQUE, c4 timestamp default CURRENT_TIMEstamp, c5 integer not null, c6 integer, check (c5 > 10), foreign key (c6) references test(c1));
Query OK, 0 rows affected (0.31 sec)

mysql (root@test)> show create table test3 \G
*************************** 1. row ***************************
       Table: test3
Create Table: CREATE TABLE `test3` (
  `c1` int(11) NOT NULL,
  `c2` varchar(255) DEFAULT 'default text',
  `c3` varchar(10) DEFAULT NULL,
  `c4` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `c5` int(11) NOT NULL,
  `c6` int(11) DEFAULT NULL,
  PRIMARY KEY (`c1`),
  UNIQUE KEY `c3` (`c3`),
  KEY `c6` (`c6`),
  CONSTRAINT `test3_ibfk_1` FOREIGN KEY (`c6`) REFERENCES `test` (`c1`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
W tabeli test, używanej już innych postach, mamy 4 wiersze:
mysql (root@test)> select * from test;
+----+-------+---------------------+
| c1 | c2    | c3                  |
+----+-------+---------------------+
|  2 |  test | 2013-12-23 14:37:27 |
|  1 | test  | 2013-12-23 14:37:21 |
|  3 | test  | 2013-12-23 14:37:33 |
|  4 | TEST  | 2013-12-23 14:37:39 |
+----+-------+---------------------+
4 rows in set (0.00 sec)
Wprowadźmy teraz kilka danych do nowej tabeli i zobaczmy co się stanie.

MySQL:
Próba wstawienie null pod klucz główny lub do kolumny z NOT NULL:
mysql (root@test)> insert into test3 (c1,c2,c3,c4,c5,c6) values (null,null, null, null, null, null);
ERROR 1048 (23000): Column 'c1' cannot be null
Error (Code 1048): Column 'c1' cannot be null
Error (Code 1105): Unknown error
mysql (root@test)> insert into test3 (c1,c2,c3,c4,c5,c6) values (1,null, null, null, null, null);
ERROR 1048 (23000): Column 'c5' cannot be null
Error (Code 1048): Column 'c5' cannot be null
Error (Code 1105): Unknown error
mysql (root@test)> insert into test3 (c1,c2,c3,c4,c5,c6) values (1,null, null, null, 1, null);
Query OK, 1 row affected (0.03 sec)

mysql (root@test)> insert into test3 (c1,c2,c3,c4,c5,c6) values (2,null, null, null, 1, 2);
Query OK, 1 row affected (0.03 sec)
Klucz główny wymaga unikalności:
mysql (root@test)> insert into test3 (c1,c2,c3,c4,c5,c6) values (2,null, null, null, 1, 2);
ERROR 1062 (23000): Duplicate entry '2' for key 'PRIMARY'
mysql (root@test)> insert into test3 (c1,c2,c3,c4,c5,c6) values (3,null, 3, null, 1, 2);
Query OK, 1 row affected (0.04 sec)
mysql (root@test)> insert into test3 (c1,c2,c3,c4,c5,c6) values (4,null, 3, null, 1, 2);
ERROR 1062 (23000): Duplicate entry '3' for key 'c3'
Dla klucza obcego (FOREIGN KEY), nie jest możliwe wstawienie wartości, które nie istnieje w kluczu głównym:
mysql (root@test)> insert into test3 (c1,c2,c3,c4,c5,c6) values (4,null, 4, null, 1, 6);
ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`test`.`test3`, CONSTRAINT `test3_ibfk_1` FOREIGN KEY (`c6`) REFERENCES `test` (`c1`))
mysql (root@test)> insert into test3 (c1,c2,c3,c4,c5,c6) values (4,null, 4, null, 1, 4);
Query OK, 1 row affected (0.03 sec)

mysql (root@test)> insert into test3 (c1,c3,c4,c5,c6) values (5,5, null, 1, 4);
Query OK, 1 row affected (0.05 sec)
MySQL nie wspiera constarint-a typu CHECK dlatego w kolumnie c5 widzimy 'niedozwolone' wartości:
mysql (root@test)> select * from test3;
+----+--------------+------+---------------------+----+------+
| c1 | c2           | c3   | c4                  | c5 | c6   |
+----+--------------+------+---------------------+----+------+
|  1 | NULL         | NULL | 2014-01-13 12:03:57 |  1 | NULL |
|  2 | NULL         | NULL | 2014-01-13 12:04:26 |  1 |    2 |
|  3 | NULL         | 3    | 2014-01-13 12:05:08 |  1 |    2 |
|  4 | NULL         | 4    | 2014-01-13 12:05:33 |  1 |    4 |
|  5 | default text | 5    | 2014-01-13 12:11:31 |  1 |    4 |
+----+--------------+------+---------------------+----+------+
5 rows in set (0.00 sec)

PostgreSQL v9.1:
postgres@test(localhost) # CREATE TABLE test3 (
  c1 integer NOT NULL,
  c2 varchar(255) DEFAULT 'default text',
  c3 varchar(10) DEFAULT NULL,
  c4 timestamp NOT NULL DEFAULT now(),
c5 integer NOT NULL,
  c6 integer DEFAULT NULL,
  PRIMARY KEY (c1),
  UNIQUE (c3), CHECK (c5 > 10),
  FOREIGN KEY (c6) REFERENCES test (c1)
);
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "test3_pkey" for table "test3"
NOTICE:  CREATE TABLE / UNIQUE will create implicit index "test3_c3_key" for table "test3"
CREATE TABLE
Próba wstawienia NULL dla klucza głównego lub kolumny z constraint-em NOT NULL:
postgres@test(localhost) # insert into test3 (c1,c2,c3,c4,c5,c6) values (null,null, null, null, null, null);
ERROR:  null value in column "c1" violates not-null constraint
postgres@test(localhost) # insert into test3 (c1,c2,c3,c4,c5,c6) values (1,null, null, null, null, null);
ERROR:  null value in column "c4" violates not-null constraint
postgres@test(localhost) # insert into test3 (c1,c2,c3,c4,c5,c6) values (1,null, null, null, 1, null);
ERROR:  null value in column "c4" violates not-null constraint
PostgreSQL wspiera constraint CHECK. Dla kolumny c5 tylko wartości powyżej 10 są przymowane:
postgres@test(localhost) # insert into test3 (c1,c2,c3,c5,c6) values (1,null, null, 1, null);
ERROR:  new row for relation "test3" violates check constraint "test3_c5_check"
postgres@test(localhost) # insert into test3 (c1,c2,c3,c5,c6) values (1,null, null, 10, null);
ERROR:  new row for relation "test3" violates check constraint "test3_c5_check"
postgres@test(localhost) # insert into test3 (c1,c2,c3,c5,c6) values (1,null, null, 11, null);
INSERT 0 1
postgres@test(localhost) # insert into test3 (c1,c2,c3,c5,c6) values (2,null, null,  11, 2);
INSERT 0 1
Dla klucza głównego wartość 2 już istaniała:
postgres@test(localhost) # insert into test3 (c1,c2,c3,c5,c6) values (2,null, null,  11, 2);
ERROR:  duplicate key value violates unique constraint "test3_pkey"
DETAIL:  Key (c1)=(2) already exists.
postgres@test(localhost) # insert into test3 (c1,c2,c3,c5,c6) values (3,null, 3,  11, 2);
INSERT 0 1
Próba wstawienia istniejącej wartości do kolumny, na której nałożony został UNIQUE constraint:
postgres@test(localhost) # insert into test3 (c1,c2,c3,c5,c6) values (4,null, 3,  11, 2);
ERROR:  duplicate key value violates unique constraint "test3_c3_key"
DETAIL:  Key (c3)=(3) already exists.
Próba wstawienia wartości dla klucza obcego, która nie istnieje w kluczu głównym:
postgres@test(localhost) # insert into test3 (c1,c3,c5,c6) values (6, 5,  11, 6);
ERROR:  insert or update on table "test3" violates foreign key constraint "test3_c6_fkey"
DETAIL:  Key (c6)=(6) is not present in table "test".
postgres@test(localhost) # insert into test3 (c1,c3,c5,c6) values (6, 5,  11, 3);
INSERT 0 1
Wynik naszych insertów:
postgres@test(localhost) # select * from test3;
 c1 |      c2      | c3 |             c4             | c5 | c6 
----+--------------+----+----------------------------+----+----
  1 |              |    | 2014-01-13 12:27:04.033428 | 11 |   
  2 |              |    | 2014-01-13 12:27:30.215729 | 11 |  2
  3 |              | 3  | 2014-01-13 12:28:14.591692 | 11 |  2
  6 | default text | 5  | 2014-01-13 12:29:27.55217  | 11 |  3
(4 rows)

FOREIGN KEY

Co się stanie gdy będziemy chcieli usunąć z tabeli test wiersz? Przecież w tabeli test3 mamy klucze obce to tych wartości. Odpowiedź brzmi: NIC. Baza danych odmówi wykonania operacji ze względu na te połączenia (akurat w naszym przypadku):
mysql (root@test)> delete from test where c1 = 4;
ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails (`test`.`test3`, CONSTRAINT `test3_ibfk_1` FOREIGN KEY (`c6`) REFERENCES `test` (`c1`))

postgres@test(localhost) # delete from test where c1 = 4;
ERROR:  update or delete on table "test" violates foreign key constraint "test3_c6_fkey" on table "test3"
DETAIL:  Key (c1)=(4) is still referenced from table "test3".
Aby ułatwić nam pracę, constraint FOREIGN KEY ma kilka opcji, które teraz użyjemy. Dla zobrazowania problemu, stworzyłam 3 tabele i wprowadziłam do nich dane.
mysql (root@test)> create table t1 (c1 integer primary key, c2 varchar(10));
Query OK, 0 rows affected (0.26 sec)

mysql (root@test)> create table t2 (c1 integer primary key, c2 varchar(10));
Query OK, 0 rows affected (0.23 sec)

mysql (root@test)> create table t3 (c1 integer, c2 integer, foreign key (c1) references t1(c1) ON DELETE RESTRICT, foreign key (c2) references t2(c1) ON DELETE CASCADE);
Query OK, 0 rows affected (0.37 sec)

mysql (root@test)> select * from t1;
+----+------+
| c1 | c2   |
+----+------+
|  1 |      |
|  2 |      |
+----+------+
2 rows in set (0.00 sec)

mysql (root@test)> select * from t2;
+----+------+
| c1 | c2   |
+----+------+
|  1 | we   |
|  2 | zxcv |
|  3 | zx   |
|  4 | eee  |
+----+------+
4 rows in set (0.00 sec)

mysql (root@test)> select * from t3;
+------+------+
| c1   | c2   |
+------+------+
|    2 |    1 |
|    1 |    1 |
|    1 |    4 |
|    2 |    3 |
+------+------+
4 rows in set (0.00 sec)
To tabela t3 najbardziej nas interesuje. Posiada dwie kolumny, które są kluczami obcymy:
  • c1 do tabeli t1 (c1), z opcją ON DELETE RESTRICT
  • c2 do tabeli t2 (c1), z opcją ON DELETE CASCADE
Teraz usunę jeden wiersz z tabeli t2. Oprócz tego, że operacja na tabeli t2 została wykonana poprawnie, w tabeli t3, także zostały usunięte wiersze, gdzie klucz obcy odpowiadał usuniętemu wierszowi.
mysql (root@test)> delete from t2 where c1 = 1;
Query OK, 1 row affected (0.04 sec)

mysql (root@test)> select * from t3;
+------+------+
| c1   | c2   |
+------+------+
|    1 |    4 |
|    2 |    3 |
+------+------+
2 rows in set (0.00 sec)
Jeśli jednak tę samą operacje chce wykonać na tabeli t1, otrzymuję błąd:
mysql (root@test)> delete from t1 where c1 = 1;
ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails (`test`.`t3`, CONSTRAINT `t3_ibfk_1` FOREIGN KEY (`c1`) REFERENCES `t1` (`c1`))
Musimy zmienić nasz constraint tak aby można było usunąć wiersz z tabeli t1 i jednocześnie updatować wartość w kluczu głównym. Musimy jednak usunąć i przywrócić go z nowymi opcjami:
mysql (root@test)> alter table t3 drop FOREIGN KEY t3_ibfk_1;
Query OK, 2 rows affected (0.33 sec)
Records: 2  Duplicates: 0  Warnings: 0

mysql (root@test)> alter table t3 add CONSTRAINT t3_ibfk_1 FOREIGN KEY (`c1`) REFERENCES `t1` (`c1`) ON DELETE SET NULL;
Query OK, 2 rows affected (0.34 sec)
Records: 2  Duplicates: 0  Warnings: 0
Usuwając wiersz z tabeli t1, w t3 klucz główny, który odwoływał się do usunietego wiersza, został updatowany i teraz ma wartość NULL:
mysql (root@test)> delete from t1 where c1 = 1;
Query OK, 1 row affected (0.04 sec)

mysql (root@test)> select * from t3;
+------+------+
| c1   | c2   |
+------+------+
| NULL |    4 |
|    2 |    3 |
+------+------+
2 rows in set (0.00 sec)
Podsumowując (dokumentacja MySQL'a, dokumentacja PostgreSQL'a):
[CONSTRAINT [symbol]] FOREIGN KEY
    [index_name] (index_col_name, ...)
    REFERENCES tbl_name (index_col_name,...)
    [ON DELETE reference_option]
    [ON UPDATE reference_option]

reference_option:
    RESTRICT | CASCADE | SET NULL | NO ACTION
gdzie:
  • ON DELETE / ON UPDATE - jest możliwe aby dla każdej akcji wykonać różne operacje (np. ON DELETE CASCADE, ON UPDATE SET NULL) dla tej samej kolumny. Jeśli zdefiniowana jest tylko akacja ON DELETE, to tak naprawdę akcja ON UPDATE też jest zdefiniowana, ale z opcjami domyślnymi (RESTRICT). Aby uściślić wszystkie informację, wspomnę tylko, że operacja UPDATE w tym przypadku dotyczy samego klucza głównego, a nie innych kolumn tabeli do której się odwołuje nasz klucz obcy.
  • RESTRICT - jest domyślną opcją. Nie pozwala wykonać operacji (w tym przypadku) usunięcia, puki istnieje wiersz do którego się odnosi.
  • CASCADE - jeśli wiersz, do którego się odnosi, jest usuwany, to i ten wiersz musi być usunięty.
  • SET NULL - po usunięciu wiersza z kluczem głównym, klucz obcy jest updatowany do wartości NULL. Ważne jest aby pamiętać, że kolumna z kluczem obcym może przyjmować NULL-e
  • NO ACTION - W przeciwieństwie do opcji RESTRICT, pozwala na usunięcie wiersza z tabeli do której odnosi się klucz obcy.
  • SET DEFAULT (PostgreSQL)- to samo co SET NULL, ale wstawia wartość domyślną.
Warto jeszcze dodać, że constraint typu FOREIGN KEY ustawiamy 'łącząc' ze sobą kolumny o tych samych typach (np int(11) unsigned) gdzie ENGINE także jest ten sam (MySQL). Constraint-y zapobiegają pojawieniu się błędnych danych dlatego warto o nich pomyśleć w czasie projektowania naszej bazy danych.
Powodzenia

środa, 8 stycznia 2014

Usprawnienie i poprawki w perl-owym skrypcie do porównywania mysql-owych baz danych

Ostatnio musiałam użyć swojego perl-owego skryptu do porównania dwóch baz mysql-owych. Opisałam go na tym blogu pod tym adresem tutaj.
Baza danych jednak okazała się bardzo duża, a zwracamy wynik bardzo nieczytelny, dlatego wprowadziłam następujące zmiany:
  • porównanie opcji tabel i sprawdzenie istnienia ich w formie tabelki
  • porównanie cech kolumn i sprawdzenie istnienia ich w formie tabelki
  • poprawka błędów (sprawdzenie czy tabela istnieje, pobieranie danych dla nie numerycznych kluczy głównych)

 Przykładowy wynik porównania tabel:
Table Schema verification: START

TABLE NAME                                        |Server 01 (piwik_production_mirror)               |Server 02 (piwik_upgrade_test)                    
------------------------------------------------------------------------------------------------------------------------------------------------------
piwik_archive_blob_2014_01                        |BASE TABLE, InnoDB, Compact, utf8_general_ci      |BASE TABLE, MyISAM, Dynamic, utf8_general_ci      
piwik_archive_numeric_2014_01                     |BASE TABLE, InnoDB, Compact, utf8_general_ci      |BASE TABLE, MyISAM, Dynamic, utf8_general_ci      
piwik_archiving_process                           |DOESN'T EXIST                                     |BASE TABLE, MyISAM, Fixed, latin1_swedish_ci      
piwik_archiving_sequence                          |BASE TABLE, MyISAM, Fixed, latin1_swedish_ci      |BASE TABLE, MyISAM, Fixed, utf8_general_ci        
piwik_archive_blob_2009_01                        |BASE TABLE, MyISAM, Dynamic, utf8_general_ci      |DOESN'T EXIST                                     
 Porównanie kolumn także jest teraz przejrzystsze:
Columns Schema verification: START
TABLE NAME                    |COLUMN NAME                   |Server 01                                                   |Server 02                                                   
piwik_access                  |show_site                     |Def: tinyint(1), Default: 1, IsNull: NO                     |Def: tinyint(1), Default: 1, IsNull: NO                     
piwik_access                  |login                         |Def: varchar(100), Default: , IsNull: NO                    |Def: varchar(255), Default: , IsNull: NO                    
piwik_archive_blob            |idsite                        |Def: int(10) unsigned, Default: 0, IsNull: NO               |Def: int(10) unsigned, Default: , IsNull: YES               
piwik_archive_blob_2014_01    |idsite                        |Def: int(10) unsigned, Default: 0, IsNull: NO               |Def: int(10) unsigned, Default: , IsNull: YES               
piwik_goal                    |allow_multiple                |-                                                           |Def: tinyint(4), Default: , IsNull: NO                      
piwik_goal                    |deleted                       |Def: tinyint(4), Default: 0, IsNull: NO                     |Def: tinyint(4), Default: 0, IsNull: NO                     
piwik_goal                    |revenue                       |Def: float, Default: , IsNull: NO                           |Def: float, Default: , IsNull: NO                           
piwik_log_link_visit_action   |idaction_url                  |Def: int(10) unsigned, Default: , IsNull: NO                |Def: int(10) unsigned, Default: , IsNull: NO

Całość kodu dostępna jest na github-e pod adresem: https://github.com/eladulko/Compare-two-mysql-databases
Zachęcam Was do komitowania zmian i poprawek.

poniedziałek, 6 stycznia 2014

EXAPLAIN plan w MySQL v5.5

Pracując z bazami danych, na pewno każde z nas spotkało się z problemem długo działającego zapytania. Wtedy zastanawiamy się, co spowodowało, że one tak długo trwa i co zrobić aby działało lepiej. Pomóc nam w tym może polecenie EXPLAIN. Poniżej opisze, jak czytać informację, którą zwraca nam ta instrukcja. Jeśli jednak informacje tutaj są niewystarczająco, odsyłam do dokumentacji, która znajdziecie tutaj.

Jakie informacje możemy wyciągnąć?
  • Plan wykonania zapytania przez optymalizator
    • które tabele są wykorzystane
    • jakie index-y są wykorzystane
    • ile wierszy zostanie przeszukanych
  • W jaki sposób tabele są ze sobą łączone i w jakiej kolejności

Przykładowy wynik działania:
mysql (root@test)> explain select distinct c2, c3 from test \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 4
        Extra: Using temporary
1 row in set (0.00 sec)

Co oznaczają poszczególne kolumny?
  • id: Jest identyfikatorem polecenia SELECT. Może być wartością NULL jeśli odpowiada wynikowi działania polecenia UNION.
mysql (root@test)> explain select * from test UNION select * from test2 \G
*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: test
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 4
        Extra: 
*************************** 2. row ***************************
           id: 2
  select_type: UNION
        table: test2
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 4
        Extra: 
*************************** 3. row ***************************
           id: NULL
  select_type: UNION RESULT
        table: <union1,2>
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: NULL
        Extra: 
3 rows in set (0.00 sec)
  • select_type: typ select-u z jakim mamy do czynienia
    • SIMPLE - zapytanie SELECT, które nie jest UNION-em lub podzapytaniem
    • PRIMARY:  najbardziej zewnętrzne zapytanie
    • UNION: drugie albo ostatnie polecenie SELECT jest UNION-em
    • DEPENDENT UNION: drugie albo ostatnie polecenie SELECT jest UNION i zależy od zewnętrznego zapytania
    • UNION RESULT: wynik polecenia UNION
    • SUBQUERY: pierwsze zapytanie jest podzapytaniem
    • DEPENDENT SUBQUERY: pierwsze zapytanie jest podzapytaniem i zależy od zapytania zewnętrznego.
mysql (root@test)> explain select * from test where c1 IN (select c1 from test2) \G
*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: test
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 4
        Extra: Using where
*************************** 2. row ***************************
           id: 2
  select_type: DEPENDENT SUBQUERY
        table: test2
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 4
        Extra: Using where
2 rows in set (0.00 sec)
    • DERIVED: pochodzi z zapytania SELECT czyli po FROM mamy podzapytanie
    • UNCACHEABLE SUBQUERY: zapytanie dla którego wynik nie może być zkeszowany i za każdym razem jest wykonywane dla każdego wiersza z zewnętrznego zapytania. W porównaniu do DEPENDENT SUBQUERY, zapytanie jest wykonywane nawet dla tych samych wartości z zapytania zewnętrznego.
    • UNCACHEABLE UNION: drugi albo ostatni select w poleceniu UNION, dla którego istnieje UNCACHEABLE SUBQUERY.
  • table: Nazwa tabeli albo alias tabeli do której odnosi się opis. Może to być także <unionM,N> (odnosi się wierszy odpowiadającym UNION z id: M i N), <derivedN> (odnosi się do wierszy z id: N)
  • type: typ łączenia(Poniżesz omówię każdy z nich)
  • possible_keys: mówi, które index-y mogą być użyte do znalezienia wierszy w tabeli. Jeśli kolumna zawiera wartość NULL, nie ma index-ów, które mogły by być użyte. Ważne jest to w jakiej kolejności są łączone tabele ze sobą dlatego w praktyce niektóre z index-ów nigdy nie mogą nie być brane pod uwagę. 
  • key: identyfikuje, który z index-ów został użyty. Istnieje możliwość, że klucz (index) został wybrany mimo iż w kolumnie possible_keys go nie było. W tej sytuacji nie istnieje index przy pomocy, które można by wybrać wiersze ale wszystkie kolumny wybierane przez zapytanie są w kolumnach innego index-u:
CREATE TABLE `test` (
  `c1` int(11) NOT NULL,
  `c2` varchar(255) DEFAULT NULL,
  `c3` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`c1`),
  KEY `idx_c2` (`c2`),
  KEY `idx_c2_c3` (`c2`,`c3`),
  KEY `idx_c3_c2` (`c3`,`c2`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8; 

 CREATE TABLE `test2` (
  `c1` int(11) NOT NULL,
  `c2` varchar(255) DEFAULT NULL,
  `c3` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00',
  KEY `idx_c2` (`c2`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

mysql (root@test)> explain select * from test where c1 IN (select c1 from test2 where c2 = 'test') \G
*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: test
         type: index
possible_keys: NULL
          key: idx_c2_c3
      key_len: 772
          ref: NULL
         rows: 4
        Extra: Using where; Using index
*************************** 2. row ***************************
           id: 2
  select_type: DEPENDENT SUBQUERY
        table: test2
         type: ref
possible_keys: idx_c2
          key: idx_c2
      key_len: 768
          ref: const
         rows: 3
        Extra: Using where
2 rows in set (0.00 sec)
 Lub
mysql (root@test)> explain select * from test where c1 like 1 \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test
         type: index
possible_keys: PRIMARY
          key: idx_c2_c3
      key_len: 772
          ref: NULL
         rows: 4
        Extra: Using where; Using index
1 row in set (0.00 sec) 

  • key_len: identyfikuje długość klucza
  • ref: mówi, które kolumny lub stałe są porównywane do index-u w kolumnie key aby zwrócić wynik zapytania
  • rows: przypuszczalna liczba wierszy, które MySQL musi sprawdzić do wykonania zapytania. Dla InnoDB wartość ta estymowana i może być niedokładna
  • Extra: dodatkowa informacja jak MySQL zwraca wynik jak użycie tabeli tymczasowej (Using temporary), użycie warunku WHERE (Using where), użycie indexu dla grupowania (Using index for group-by). Więcej informacji znajdziecie w dokumentacji tutaj.

Typy złączeń (kolumna type)

Kolumna type instrukcji EXPLAIN informuje nas w jaki sposób łączymy tabele. Jest to bardzo ważna informacja dlatego przyjrzymy się im uważnie.
  • system: Tabela posiada tylko jeden wiersz
  • const: Tabela posiada co najwyżej jeden pasujący wiersz. Zazwyczaj dotyczy szukania po kluczu głównym lub innych wartości unikalnych:
mysql (root@test)> explain select * from test where c1 = 1 \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test
         type: const
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: const
         rows: 1
        Extra:
1 row in set (0.00 sec)
  • eq_ref: Dokładnie jeden wiersz jest czytany z tabeli przy kombinacji wierszy z poprzedniej tabeli. Jest to najlepszy (najbardziej optymalny) tym złączenia.
mysql (root@test)> explain select * from test a, test b where a.c1 = b.c1 AND a.c2 = 'test' \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: a
         type: ref
possible_keys: PRIMARY,idx_c2,idx_c2_c3
          key: idx_c2_c3
      key_len: 768
          ref: const
         rows: 3
        Extra: Using where; Using index
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: b
         type: eq_ref
possible_keys: PRIMARY
          key: PRIMARY
      key_len: 4
          ref: test.a.c1
         rows: 1
        Extra: 
2 rows in set (0.00 sec)
  • ref: Wszystkie wiersze z wartości index-u są czytane z tej tabeli dla wszystkich wierszy z poprzedniej tabeli. Jest to dobry typ łączenia jeśli klucz, który został użyty, dopasowuje tylko kilka wierszy.
mysql (root@test)> explain select * from test a, test2 b where a.c1 = b.c1 AND a.c2 = 'test' \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: a
         type: ref
possible_keys: PRIMARY,idx_c2,idx_c2_c3
          key: idx_c2_c3
      key_len: 768
          ref: const
         rows: 3
        Extra: Using where; Using index
*************************** 2. row ***************************
           id: 1
  select_type: SIMPLE
        table: b
         type: ALL
possible_keys: NULL
          key: NULL
      key_len: NULL
          ref: NULL
         rows: 4
        Extra: Using where; Using join buffer
2 rows in set (0.00 sec)
  • fulltext: Łączenie odbywa się przy użyciu index-u FULLTEXT.
  • ref_or_null: Ten typ łączenia jest bardzo podobny do ref z tą różnicą, że MySQL dodatkowo przeszukuje wiersze, które zawierają wartości NULL.
mysql (root@test)> explain select * from test where c2 = 'test' or c2 IS NULL \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test
         type: ref_or_null
possible_keys: idx_c2,idx_c2_c3
          key: idx_c2_c3
      key_len: 768
          ref: const
         rows: 4
        Extra: Using where; Using index
1 row in set (0.00 sec)
  • index_merge: Został użyty 'Index Merge'. Jest to metoda do otrzymania wierszy z kilku skanowań typu 'range' i połączenia ich wyniku do jednego. Więcej możecie przeczytać tutaj.
  • unique_subquery: Zastępuje ref typ łączenia przy warunkach z użyciem IN podzapytań.
mysql (root@test)> explain select * from test where c1 IN (select c1 from test2 where c2 = 'test') \G
*************************** 1. row ***************************
           id: 1
  select_type: PRIMARY
        table: test
         type: index
possible_keys: NULL
          key: idx_c2_c3
      key_len: 772
          ref: NULL
         rows: 4
        Extra: Using where; Using index
*************************** 2. row ***************************
           id: 2
  select_type: DEPENDENT SUBQUERY
        table: test2
         type: unique_subquery
possible_keys: PRIMARY,idx_c2
          key: PRIMARY
      key_len: 4
          ref: func
         rows: 1
        Extra: Using where
2 rows in set (0.01 sec)
  • index_subquery: Jest bardzo podobne do typu powyżej ale działa dla indexów, które nie są unikalne (nie klucze główne i nie z klauzurą unikalności).
  • range: Szuka wierszy, gdzie wartości są w danych przedziałach. 
mysql (root@test)> explain select * from test where c2 like 'test%' \G
*************************** 1. row ***************************
           id: 1
  select_type: SIMPLE
        table: test
         type: range
possible_keys: idx_c2,idx_c2_c3
          key: idx_c2_c3
      key_len: 768
          ref: NULL
         rows: 3
        Extra: Using where; Using index
1 row in set (0.00 sec)
  • index: Jest bardzo podobny do typu ALL z tym wyjątkiem, że to drzewo index-u jest przeszukiwane, a nie dane, aby zwrócić wynik.
  • ALL: Pełne skanowanie tabeli dla każdej kombinacji wierszy z poprzedniej tabeli. Najgorszy scenariusz przeszukiwania danych.

Instrukcja EXPLAIN ma jeszcze dwa rozszerzenia, ale które może omówimy innym razem:
  • EXPLAIN EXTENDED
  • EXPLAIN PARTITIONS

Mam nadzieję, że ta informacje pomoże wam w optymalizacji zapytań. Na pewno jest to pierszy krok aby zrozumiec jak działa optymalizator MySQL-a.
Powodzenia