СУБД. Лекция 7

СУБД
Навроцкий Артем
Лекция 7

Оптимизируйте доступ к данным:

Типичные ошибки:

Декомпозиция соединения

SELECT *
FROM tag
    JOIN tag_post ON tag_post.tag_id=tag.id
    JOIN post ON tag_post.post_id=post.id
WHERE tag.tag=mysql;

SELECT * FROM tag WHERE tag = "mysql";
SELECT * FROM tag_post WHERE tag_id = 1234;
SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904);

Декомпозиция соединения

Соединение в приложении может оказаться эффективнее в следующих случаях:

Выполнение запросов

Состояние запроса

Sleep
Поток ожидает поступления нового запроса от клиента.
Query
Поток либо занят выполнением запроса, либо отправляет клиенту результаты.
Locked
Поток ожидает предоставления табличной блокировки на уровне сервера.
Analyzing и Statistics
Поток проверяет статистику, собранную подсистемой хранения, и оптимизирует запрос.
Copying to tmp table [on disk]
Поток обрабатывает запрос и копирует результаты во временную таблицу.
Sorting result
Поток занят сортировкой результирующего набора.
Sending data
Пересылает данные между различными стадиями обработки запроса или генерирует результирующий набор или возвращает результаты клиенту.

Оптимизатор запросов

SELECT SQL_NO_CACHE COUNT(*) FROM sakila.film_actor;

mysql> SHOW STATUS LIKE last_query_cost;
Variable_name Value
Last_query_cost 1040.599000

Оптимизатор запросов

Оптимизатор запросов

Изменение порядка соединения

Таблицы не обязательно соединять именно в том порядке, который указан в запросе.

Применение алгебраических правил эквивалентности

MySQL применяет алгебраические преобразования для упрощения выражений и приведения их к каноническому виду. Она умеет так-же вычислять константные выражения, исключая заведомо невыполнимые и всегда выполняющиеся условия.

(5 = 5 AND a > 5) -> a > 5
(a < b AND b = c) AND a = 5 -> b > 5 AND b = c AND a = 5

Оптимизации COUNT(), MIN() и MAX()

Наличие индексов и сведений о возможности хранения NULL-значений в столбцах часто позволяет вообще не вычислять эти выражения. Если применена такая оптимизация, то в плане, выведенном командой EXPLAIN, будет присутствовать фраза «Select tables optimized away» (некоторые таблицы исключены при оптимизации).

Оптимизатор запросов

Вычисление и свертка константных выражений

Если MySQL обнаруживает, что выражение можно свернуть в константу, то делает это на стадии оптимизации.

EXPLAIN SELECT film.film_id, film_actor.actor_id
FROM sakila.film INNER JOIN sakila.film_actor USING (film_id)
WHERE film.film_id = 1;
| id | select_type | table      | type  | key            | ref   | rows |
| 1  | SIMPLE      | film       | const | PRIMARY        | const | 1    |
| 1  | SIMPLE      | film_actor | ref   | idx_fk_film_id | const | 10   |

Покрывающие индексы

Если индекс содержит все необходимые запросу столбцы, то MySQL может воспользоваться им, вообще не читая данные таблицы.

Оптимизация подзапросов

MySQL умеет преобразовывать некоторые виды подзапросов в более эффективные эквивалентные формы, сводя их к поиску по индексу.

Оптимизатор запросов

Раннее завершение

MySQL может прекратить обработку запроса (или какой-то шаг обработки), как только поймет, что этот запрос или шаг полностью выполнен.

EXPLAIN SELECT film.film_id FROM sakila.film WHERE film_id = -1;
| id |...| Extra                                               |
| 1  |...| Impossible WHERE noticed after reading const tables |

Сравнение по списку IN()

Во многих СУБД оператор IN() – не более чем синоним нескольких условий OR, поскольку логически они эквивалентны. Но не в MySQL, здесь перечисленные в списке IN()значения сортируются, и для работы с ним применяется быстрый двоичный поиск.

Оптимизатор запросов

Распространение равенства

MySQL распознает ситуации, когда в некотором запросе два столбца должны быть равны, – например, в условии JOIN, и распространяет условие WHERE на эквивалентные.

SELECT film.film_id
FROM sakila.film
INNER JOIN sakila.film_actor USING(film_id)
WHERE film.film_id > 500;
 
...
WHERE film.film_id > 500 AND film_actor.film_id > 500

Соединения (JOIN) в MySQL

SELECT tbl1.col1, tbl2.col2
FROM tbl1 INNER JOIN tbl2 USING(col3)
WHERE tbl1.col1 IN(5,6);
outer_iter = ITERATOR OVER tbl1 WHERE col1 IN(5,6)
outer_row = outer_iter.next
WHILE outer_row
	inner_iter = ITERATOR OVER tbl2 WHERE col3 = outer_row.col3
	inner_row = inner_iter.next
	WHILE inner_row
		OUTPUT [ outer_row.col1, inner_row.col2 ]
		inner_row = inner_iter.next
	END
	outer_row = outer_iter.next
END

Соединения (JOIN) в MySQL

План выполнения

Оптимизатор сортировки

Двухпроходный (старый)

Читает указатели на строки и столбцы, упомянутые во фразе ORDER BY, сортирует их, затем проходит по отсортированному списку и снова читает исходные строки, чтобы вывести результат.

Однопроходный (новый)

Читает все необходимые запросу столбцы, сортирует строки по столбцам, упомянутым во фразе ORDER BY, проходит по отсортированному списку и выводит заданные столбцы. Следовательно, в буфер сортировки поместится меньше строк и надо будет выполнить больше циклов слияния.

Не коррелированные подзапросы

SELECT * FROM sakila.film
WHERE film_id IN (
  SELECT film_id FROM sakila.film_actor WHERE actor_id = 1
);

SELECT GROUP_CONCAT(film_id)
FROM sakila.film_actor 
WHERE actor_id = 1;
-- Result: 1,23,25,106,140,166,277,361,438,499,506,509,605,635
 
SELECT * FROM sakila.film
WHERE film_id
IN (1,23,25,106,140,166,277,361,438,499,506,509,605,635);

Коррелированные подзапросы

SELECT * FROM sakila.film
WHERE film_id IN (
  SELECT film_id FROM sakila.film_actor WHERE actor_id = 1
);

SELECT * FROM sakila.film
WHERE EXISTS (
  SELECT * FROM sakila.film_actor
  WHERE actor_id = 1 AND film_actor.film_id = film.film_id
);

Слияние индексов

SELECT film_id, actor_id FROM sakila.film_actor 
WHERE actor_id = 1 OR film_id = 1;

SELECT film_id, actor_id FROM sakila.film_actor 
WHERE actor_id = 1
UNION ALL
SELECT film_id, actor_id FROM sakila.film_actor 
WHERE film_id = 1 AND actor_id <> 1;

Непоследовательный просмотр индексов

SELECT ... FROM tbl WHERE a BETWEEN 1 AND 4 AND b BETWEEN 2 AND 3;

Непоследовательный просмотр индексов

SELECT ... FROM tbl WHERE a IN (1, 2, 3, 4) AND b BETWEEN 2 AND 3;

Непоследовательный просмотр индексов

EXPLAIN SELECT actor_id, MAX(film_id)
FROM sakila.film_actor
GROUP BY actor_id\G
********************* 1. row **************************
id: 1
select_type: SIMPLE
table: film_actor
type: range
possible_keys: NULL
key: PRIMARY
key_len: 2
Ограничения оптимизатора MySQL 241
ref: NULL
rows: 396
Extra: Using index for group-by

Непоследовательный просмотр индексов

SELECT MIN(actor_id) FROM sakila.actor 
WHERE first_name = 'PENELOPE';

SELECT actor_id FROM sakila.actor USE INDEX(PRIMARY)
WHERE first_name = 'PENELOPE' LIMIT 1;

SELECT & UPDATE

UPDATE tbl AS outer_tbl
SET cnt = (
    SELECT COUNT(*) FROM tbl AS inner_tbl
    WHERE inner_tbl.type = outer_tbl.type
);

ERROR 1093 (HY000): You can’t specify target table
‘outer_tbl’ for update in FROM clause

UPDATE tbl
INNER JOIN (
    SELECT type, COUNT(*) AS cnt
    FROM tbl
    GROUP BY type
) AS der USING (type)
SET tbl.cnt = der.cnt;

Что делает COUNT()

COUNT() ― это особая функция, которая решает две очень разные задачи: подсчитывает значения и строки.

Значение ― это выражение, отличное от NULL.

COUNT(*) просто подсчитывает количество строк в результирующем наборе.

Если вы хотите знать, сколько строк в результирующем наборе, всегда употребляйте COUNT(*).

МИФЫ:

Для таблиц типа MyISAM запросы, содержащие функцию COUNT(), выполняются очень быстро.

Оптимизация COUNT()

SELECT COUNT(*) FROM world.City WHERE ID > 5;

SELECT (SELECT COUNT(*) FROM world.City) - COUNT(*)
FROM world.City WHERE ID <= 5;

SELECT COUNT(color = 'blue' OR color= 'red' OR NULL) FROM items;

SELECT COUNT(*) FROM items WHERE color = 'blue' OR color = 'red';

SELECT COUNT(*) FROM items
WHERE color IN ('blue', 'red');

Оптимизация запросов с JOIN

Оптимизация GROUP BY и DISTINCT

SELECT actor.first_name, actor.last_name, COUNT(*)
FROM sakila.film_actor
    INNER JOIN sakila.actor USING (actor_id)
GROUP BY actor.first_name, actor.last_name;

SELECT actor.first_name, actor.last_name, COUNT(*)
FROM sakila.film_actor
    INNER JOIN sakila.actor USING (actor_id)
GROUP BY film_actor.actor_id;

SELECT MIN(actor.first_name), MAX(actor.last_name), COUNT(*)
FROM sakila.film_actor
    INNER JOIN sakila.actor USING (actor_id)
GROUP BY film_actor.actor_id;

Оптимизация GROUP BY и DISTINCT

SELECT actor.first_name, actor.last_name, COUNT(*)
FROM sakila.film_actor
    INNER JOIN sakila.actor USING (actor_id)
GROUP BY actor.first_name, actor.last_name;

SELECT actor.first_name, actor.last_name, c.cnt
FROM sakila.actor
INNER JOIN (
    SELECT actor_id, COUNT(*) AS cnt
    FROM sakila.film_actor
    GROUP BY actor_id
) AS c USING (actor_id);

Оптимизация LIMIT со смещением

SELECT film_id, description 
FROM sakila.film 
ORDER BY title 
LIMIT 50, 5;

SELECT film.film_id, film.description
FROM sakila.film
INNER JOIN (
    SELECT film_id FROM sakila.film
    ORDER BY title LIMIT 50, 5
) AS lim USING(film_id);

SELECT film_id, description FROM sakila.film
WHERE position BETWEEN 50 AND 54 ORDER BY position;

SQL_CALC_FOUND_ROWS

SELECT SQL_CALC_FOUND_ROWS film_id, description 
FROM sakila.film 
ORDER BY title 
LIMIT 50, 5;

SELECT FOUND_ROWS ();

Кэш запросов

Кэш запросов

Кэш запросов

Объединенные таблицы и секционирование

Позволяют:

Объединенные таблицы

CREATE TABLE t1(a INT NOT NULL PRIMARY KEY) ENGINE=MyISAM;
CREATE TABLE t2(a INT NOT NULL PRIMARY KEY) ENGINE=MyISAM;
INSERT INTO t1(a) VALUES(1),(2);
INSERT INTO t2(a) VALUES(1),(2);
CREATE TABLE mrg(a INT NOT NULL PRIMARY KEY)
ENGINE=MERGE UNION=(t1, t2) INSERT_METHOD=LAST;
SELECT a FROM mrg;
| a |
| 1 |
| 1 |
| 2 |
| 2 |

Объединенные таблицы

Секционирование

CREATE TABLE orders_range (
    customer_surname VARCHAR(30),
    store_id INT,
    salesperson_id INT,
    order_date DATE,
    note VARCHAR(500)
) ENGINE = InnoDB
PARTITION BY RANGE (YEAR (order_date)) (
    PARTITION p_old VALUES LESS THAN(2008),
    PARTITION p_2008 VALUES LESS THAN(2009),
    PARTITION p_2009 VALUES LESS THAN(MAXVALUE)
);

Секционирование

RANGE

По диапазону значений

PARTITION BY RANGE (store_id) (
    PARTITION p0 VALUES LESS THAN (10),
    PARTITION p1 VALUES LESS THAN (20),
    PARTITION p3 VALUES LESS THAN (30)
);

Секционирование

LIST

По точному списку значений

PARTITION BY LIST(store_id) (
    PARTITION pNorth VALUES IN (3,5,6,9,17),
    PARTITION pEast VALUES IN (1,2,10,11,19,20)
);

Секционирование

HASH

По хэшу от какой-либо функции

PARTITION BY HASH(YEAR(hired))
PARTITIONS 10;

KEY

Почти то же самое что и HASH, но по ключу

PARTITION BY KEY(s1)
PARTITIONS 10;

Секционирование

Навроцкий Артем
@gent: navrotskiy@corp.mail.ru
Спасибо за внимание!