Spec-Zone.ru › MySQL 8.4

10.2.1.2 Оптимизация диапазонов

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

  • Метод доступа по диапазонам для индексов с одной частью

  • Метод доступа по диапазонам для индексов с несколькими частями

  • Оптимизация диапазонов для сравнений со множественными значениями

  • Метод доступа по диапазонам с пропусканием сканирования

  • Оптимизация диапазонов для выражений конструкторов строк

  • Ограничение использования памяти для оптимизации диапазонов

Метод доступа по диапазонам для индексов с одной частью

Для индекса с одной частью интервалы значений индекса удобно представлять соответствующими условиями в WHERE запроса, обозначаемыми как условия диапазона, а не “интервалами”.

Определение условия диапазона для индекса с одной частью следующее:

  • Для индексов BTREE и HASH сравнение части ключа со значением константы является условием диапазона при использовании операторов =, <=>, IN(), IS NULL или IS NOT NULL.

  • Кроме того, для индексов BTREE сравнение части ключа со значением константы является условием диапазона при использовании операторов >, <, >=, <=, BETWEEN, != или <>, или сравнений LIKE, если аргументом к LIKE является строка-константа, которая не начинается с символа подстановки.

  • Для всех типов индексов несколько условий диапазона, объединённых с помощью OR или AND, образуют условие диапазона.

“Значение константы” в предыдущих описаниях означает одно из следующего:

  • Константа из строки запроса

  • Столбец таблицы const или system из того же соединения

  • Результат несвязанного подзапроса

  • Любое выражение, составленное целиком из подвыражений предыдущих типов

Ниже приведены примеры запросов с условиями диапазона в WHERE запроса:

SELECT * FROM t1
  WHERE key_col > 1
  AND key_col < 10;

SELECT * FROM t1
  WHERE key_col = 1
  OR key_col IN (15,18,20);

SELECT * FROM t1
  WHERE key_col LIKE 'ab%'
  OR key_col BETWEEN 'bar' AND 'foo';

Некоторые значения, не являющиеся константами, могут быть преобразованы в константы на стадии распространения констант оптимизатора.

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

Рассмотрим следующее утверждение, где key1 — это индексированный столбец, а nonkey — нет:

SELECT * FROM t1 WHERE
  (key1 < 'abc' AND (key1 LIKE 'abcde%' OR key1 LIKE '%b')) OR
  (key1 < 'bar' AND nonkey = 4) OR
  (key1 < 'uux' AND key1 > 'z');

Процесс извлечения для ключа key1 следующий:

  1. Начинаем с исходного WHERE запроса:

    (key1 < 'abc' AND (key1 LIKE 'abcde%' OR key1 LIKE '%b')) OR
    (key1 < 'bar' AND nonkey = 4) OR
    (key1 < 'uux' AND key1 > 'z')
    
  2. Удаляем nonkey = 4 и key1 LIKE '%b', так как они не могут быть использованы для сканирования по диапазону. Правильный способ их удаления — заменить их на TRUE, чтобы не пропустить ни одной совпадающей строки при сканировании по диапазону. Заменяя их на TRUE, получаем:

    (key1 < 'abc' AND (key1 LIKE 'abcde%' OR TRUE)) OR
    (key1 < 'bar' AND TRUE) OR
    (key1 < 'uux' AND key1 > 'z')
    
  3. Сжимаем условия, которые всегда истинны или ложны:

    • (key1 LIKE 'abcde%' OR TRUE) всегда истинно

    • (key1 < 'uux' AND key1 > 'z') всегда ложно

    Замена этих условий константами даёт:

    (key1 < 'abc' AND TRUE) OR (key1 < 'bar' AND TRUE) OR (FALSE)
    

    Удаление ненужных TRUE и FALSE констант даёт:

    (key1 < 'abc') OR (key1 < 'bar')
    
  4. Объединение перекрывающихся интервалов в один даёт окончательное условие, которое будет использоваться для сканирования по диапазону:

    (key1 < 'bar')
    

В общем случае (и как показано в предыдущем примере), условие, используемое для сканирования по диапазону, менее строгое, чем WHERE запрос. MySQL выполняет дополнительную проверку, чтобы отфильтровать строки, удовлетворяющие условию диапазона, но не всему WHERE запросу.

Алгоритм извлечения условия диапазона может обрабатывать вложенные конструкции AND/OR произвольной глубины, и его результат не зависит от порядка, в котором условия появляются в WHERE запросе.

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

Метод доступа к диапазону для индексов из нескольких частей

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

Например, рассмотрим индекс из нескольких частей, определенный как key1(key_part1, key_part2, key_part3), и следующий набор кортежей ключей, упорядоченных по ключу:

key_part1  key_part2  key_part3
  NULL       1          'abc'
  NULL       1          'xyz'
  NULL       2          'foo'
   1         1          'abc'
   1         1          'xyz'
   1         2          'abc'
   2         1          'aaa'

Условие key_part1 = 1 определяет этот интервал:

(1,-inf,-inf) <= (key_part1,key_part2,key_part3) < (1,+inf,+inf)

Интервал охватывает 4-й, 5-й и 6-й кортежи в предыдущем наборе данных и может использоваться методом доступа к диапазону.

В противоположность этому, условие key_part3 = 'abc' не определяет единственный интервал и не может использоваться методом доступа к диапазону.

Следующие описания более подробно показывают, как работают условия диапазона для индексов из нескольких частей.

  • Для индексов HASH каждый интервал, содержащий одинаковые значения, может быть использован. Это означает, что интервал может быть получен только для условий в следующей форме:

        key_part1 cmp const1
    AND key_part2 cmp const2
    AND ...
    AND key_partN cmp constN;
    

    Здесь const1, const2, … — константы, cmp — один из =, <=> или IS NULL операторов сравнения, и условия охватывают все части индекса. (То есть, есть N условий, по одному для каждой части индекса из N частей.) Например, следующее является условием диапазона для трёхкомпонентного индекса HASH:

    key_part1 = 1 AND key_part2 IS NULL AND key_part3 = 'foo'
    

    Для определения того, что считается константой, см. Метод доступа к диапазону для однокомпонентных индексов.

  • Для индекса BTREE интервал может быть пригоден для условий, объединённых с AND, где каждое условие сравнивает часть ключа со значением константы, используя =, <=>, IS NULL, >, <, >=, <=, !=, <>, BETWEEN или LIKE 'pattern' (где 'pattern' не начинается с подстановки). Интервал может быть использован, если возможно определить единственный кортеж ключа, содержащий все строки, которые соответствуют условию (или два интервала, если используется <> или !=).

    Оптимизатор пытается использовать дополнительные части ключа для определения интервала, до тех пор, пока оператор сравнения не является =, <=> или IS NULL. Если оператор — >, <, >=, <=, !=, <>, BETWEEN или LIKE, оптимизатор его использует, но не рассматривает больше частей ключа. Для следующего выражения оптимизатор использует = из первого сравнения. Он также использует >= из второго сравнения, но не рассматривает больше частей ключа и не использует третье сравнение для построения интервала:

    key_part1 = 'foo' AND key_part2 >= 10 AND key_part3 > 10
    

    Единственный интервал:

    ('foo',10,-inf) < (key_part1,key_part2,key_part3) < ('foo',+inf,+inf)
    

    Возможно, что созданный интервал содержит больше строк, чем исходное условие. Например, предыдущий интервал включает значение ('foo', 11, 0), которое не удовлетворяет исходному условию.

  • Если условия, охватывающие наборы строк, содержащихся в интервалах, объединены с OR, они образуют условие, охватывающее набор строк, содержащихся в объединении их интервалов. Если условия объединены с AND, они образуют условие, охватывающее набор строк, содержащихся в пересечении их интервалов. Например, для этого условия для индекса из двух частей:

    (key_part1 = 1 AND key_part2 < 2) OR (key_part1 > 5)
    

    Интервалы:

    (1,-inf) < (key_part1,key_part2) < (1,2)
    (5,-inf) < (key_part1,key_part2)
    

    В этом примере интервал на первой строке использует одну часть ключа для левой границы и две части ключа для правой границы. Интервал на второй строке использует только одну часть ключа. Столбец key_len в выводе EXPLAIN указывает максимальную длину префикса ключа, используемого.

    В некоторых случаях key_len может указывать, что была использована часть ключа, но это может быть не так, как вы ожидаете. Предположим, что key_part1 и key_part2 могут быть NULL. Тогда столбец key_len отображает две длины частей ключа для следующего условия:

    key_part1 >= 1 AND key_part2 < 2
    

    Но на самом деле условие преобразуется в это:

    key_part1 >= 1 AND key_part2 IS NOT NULL
    

Для описания того, как выполняются оптимизации для объединения или устранения интервалов для условий диапазона для однокомпонентного индекса, см. Метод доступа к диапазону для однокомпонентных индексов. Аналогичные шаги выполняются для условий диапазона для индексов из нескольких частей.

Оптимизация диапазонов равенства для многозначных сравнений

Рассмотрим эти выражения, где col_name - индексированный столбец:

col_name IN(val1, ..., valN)
col_name = val1 OR ... OR col_name = valN

Каждое выражение истинно, если col_name равно одному из нескольких значений. Эти сравнения являются сравнениями диапазона равенства (где «диапазон» — это одно значение). Оптимизатор оценивает стоимость чтения соответствующих строк для сравнений диапазона равенства следующим образом:

  • Если существует уникальный индекс на col_name, оценка строк для каждого диапазона равна 1, так как не более одной строки может иметь данное значение.

  • В противном случае любой индекс на col_name является неуникальным, и оптимизатор может оценить количество строк для каждого диапазона, используя погружения в индекс или статистику индекса.

При использовании погружений в индекс оптимизатор производит погружение в каждом конце диапазона и использует количество строк в диапазоне как оценку. Например, выражение col_name IN (10, 20, 30) содержит три диапазона равенства, и оптимизатор производит два погружения на каждый диапазон для получения оценки строк. Каждая пара погружений даёт оценку количества строк, имеющих данное значение.

Погружения в индекс обеспечивают точные оценки строк, но по мере увеличения количества значений сравнения в выражении оптимизатор тратит больше времени на получение оценки строк. Использование статистики индекса менее точно, чем погружения в индекс, но позволяет быстрее оценивать строки для больших списков значений.

Системная переменная eq_range_index_dive_limit позволяет настроить количество значений, при которых оптимизатор переключается с одной стратегии оценки строк на другую. Чтобы разрешить использование погружений в индекс для сравнений до N диапазонов равенства, установите eq_range_index_dive_limit на N + 1. Чтобы отключить использование статистики и всегда использовать погружения в индекс независимо от N, установите eq_range_index_dive_limit на 0.

Для обновления статистики индексов таблицы для лучших оценок используйте ANALYZE TABLE.

До MySQL 8.4 нет способа пропустить использование погружений в индекс для оценки полезности индекса, кроме использования системной переменной eq_range_index_dive_limit. В MySQL 8.4 пропуск погружений в индекс возможен для запросов, удовлетворяющих всем этим условиям:

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

  • Присутствует подсказка индекса single-index FORCE INDEX. Идея в том, что если использование индекса принудительно, дополнительная нагрузка от выполнения погружений в индекс не принесёт пользы.

  • Индекс является неуникальным и не является индексом FULLTEXT.

  • Подзапрос отсутствует.

  • Нет клаузы DISTINCT, GROUP BY или ORDER BY.

Для EXPLAIN FOR CONNECTION вывод меняется следующим образом, если погружения в индекс пропускаются:

  • Для традиционного вывода значения rows и filtered равны NULL.

  • Для вывода в формате JSON rows_examined_per_scan и rows_produced_per_join не отображаются, skip_index_dive_due_to_force равно true, и вычисления стоимости не точны.

Без FOR CONNECTION вывод EXPLAIN не меняется, когда погружения в индекс пропускаются.

После выполнения запроса, для которого погружения в индекс пропускаются, соответствующая строка в таблице Информационной схемы OPTIMIZER_TRACE содержит значение index_dives_for_range_access равное skipped_due_to_force_index.

Метод доступа к диапазону Skip Scan

Рассмотрим следующий сценарий:

CREATE TABLE t1 (f1 INT NOT NULL, f2 INT NOT NULL, PRIMARY KEY(f1, f2));
INSERT INTO t1 VALUES
  (1,1), (1,2), (1,3), (1,4), (1,5),
  (2,1), (2,2), (2,3), (2,4), (2,5);
INSERT INTO t1 SELECT f1, f2 + 5 FROM t1;
INSERT INTO t1 SELECT f1, f2 + 10 FROM t1;
INSERT INTO t1 SELECT f1, f2 + 20 FROM t1;
INSERT INTO t1 SELECT f1, f2 + 40 FROM t1;
ANALYZE TABLE t1;

EXPLAIN SELECT f1, f2 FROM t1 WHERE f2 > 40;

Для выполнения этого запроса MySQL может выбрать сканирование индекса для извлечения всех строк (индекс включает все столбцы, которые нужно выбрать), а затем применить условие f2 > 40 из клаузы WHERE для получения конечного набора результатов.

Сканирование диапазона более эффективно, чем полное сканирование индекса, но в этом случае не может быть использовано, так как нет условия для f1, первого столбца индекса. Оптимизатор может выполнить несколько сканирований диапазонов, по одному для каждого значения f1, используя метод под названием Skip Scan, аналогичный Loose Index Scan (см. Раздел 10.2.1.17, «Оптимизация GROUP BY»):

  1. Пропустить между различными значениями первой части индекса, f1 (префикс индекса).

  2. Выполнить сканирование поддиапазона для каждого уникального префиксного значения для условия f2 > 40 в оставшейся части индекса.

Для набора данных, показанного ранее, алгоритм работает следующим образом:

  1. Получить первое уникальное значение первой части ключа (f1 = 1).

  2. Построить диапазон, основанный на первой и второй частях ключа (f1 = 1 AND f2 > 40).

  3. Выполнить сканирование диапазона.

  4. Получить следующее уникальное значение первой части ключа (f1 = 2).

  5. Построить диапазон, основанный на первой и второй частях ключа (f1 = 2 AND f2 > 40).

  6. Выполнить сканирование диапазона.

Использование этой стратегии уменьшает количество обращённых строк, потому что MySQL пропускает строки, которые не соответствуют каждому построенному диапазону. Этот метод доступа Skip Scan применим при следующих условиях:

  • Таблица T имеет по крайней мере один составной индекс с частями ключа в форме ([A_1, ..., A_k,] B_1, ..., B_m, C [, D_1, ..., D_n]). Части ключа A и D могут быть пустыми, но B и C должны быть непустыми.

  • Запрос ссылается только на одну таблицу.

  • Запрос не использует GROUP BY или DISTINCT.

  • Запрос ссылается только на столбцы в индексе.

  • Предикаты на A_1, ..., A_k должны быть предикатами равенства и должны быть константами. Это включает оператор IN().

  • Запрос должен быть конъюнктивным запросом; то есть, AND из OR условий: (cond1(key_part1) OR cond2(key_part1)) AND (cond1(key_part2) OR ...) AND ...

  • Должно быть условие диапазона на столбце C.

  • Условиям на столбцах D разрешено. Условия на столбцах D должны быть в союзе с условием диапазона на столбце C.

Использование Skip Scan показано в выводе EXPLAIN следующим образом:

  • Using index for skip scan в столбце Extra указывает, что используется метод доступа Loose Index Skip Scan.

  • Если индекс может быть использован для Skip Scan, он должен быть виден в столбце possible_keys.

Использование Skip Scan показано в выводе трассировки оптимизатора элементом "skip scan" следующего формата:

"skip_scan_range": {
  "type": "skip_scan",
  "index": index_used_for_skip_scan,
  "key_parts_used_for_access": [key_parts_used_for_access],
  "range": [range]
}

Вы также можете увидеть элемент "best_skip_scan_summary". Если Skip Scan выбран в качестве лучшего варианта доступа диапазона, записывается "chosen_range_access_summary". Если Skip Scan выбран в качестве наилучшего метода доступа в целом, присутствует элемент "best_access_path".

Использование Skip Scan зависит от значения флага skip_scan системной переменной optimizer_switch. По умолчанию этот флаг равен on. Чтобы его отключить, установите skip_scan на off.

Помимо использования системной переменной optimizer_switch для управления использованием Skip Scan оптимизатором на уровне сеанса, MySQL поддерживает подсказки оптимизатора, чтобы повлиять на оптимизатор на уровне каждого оператора. См. Раздел 10.9.3, «Подсказки оптимизатора».

END_OF_DOCUMENT_MARKER
Оптимизация диапазона для выражений конструкторов строк

Оптимизатор может применять метод доступа по диапазону к запросам такого вида:

SELECT ... FROM t1 WHERE ( col_1, col_2 ) IN (( 'a', 'b' ), ( 'c', 'd' ));

Ранее для использования сканирования по диапазону необходимо было писать запрос следующим образом:

SELECT ... FROM t1 WHERE ( col_1 = 'a' AND col_2 = 'b' )
OR ( col_1 = 'c' AND col_2 = 'd' );

Для использования сканирования по диапазону запросы должны удовлетворять этим условиям:

  • Используются только предикаты IN(), а не NOT IN().

  • В левой части предиката IN() конструктор строки содержит только ссылки на столбцы.

  • В правой части предиката IN() конструкторы строк содержат только константы выполнения, которые являются либо литералами, либо ссылками на локальные столбцы, привязанные к константам во время выполнения.

  • В правой части предиката IN() содержится более одного конструктора строки.

Дополнительную информацию об оптимизаторе и конструкторах строк см. в разделе 10.2.1.22 «Оптимизация выражений конструкторов строк».

Ограничение использования памяти для оптимизации диапазона

Для управления памятью, доступной оптимизатору диапазона, используйте системную переменную range_optimizer_max_mem_size:

  • Значение 0 означает “нет ограничений.”

  • При значении больше 0 оптимизатор отслеживает потребление памяти при рассмотрении метода доступа по диапазону. Если указанное ограничение приближается к пределу, метод доступа по диапазону отменяется, и рассматриваются другие методы, включая полное сканирование таблицы. Это может быть менее оптимальным. В этом случае появляется следующее предупреждение (где N — текущее значение range_optimizer_max_mem_size):

    Warning    3170    Memory capacity of N bytes for
                       'range_optimizer_max_mem_size' exceeded. Range
                       optimization was not done for this query.
    
  • Для операторов UPDATE и DELETE, если оптимизатор возвращается к полному сканированию таблицы, и системная переменная sql_safe_updates включена, возникает ошибка, а не предупреждение, потому что фактически не используется ключ для определения строк, которые нужно изменить. Дополнительную информацию см. в разделе о режиме безопасных обновлений (--safe-updates).

Для отдельных запросов, которые превышают доступную память для оптимизации диапазона и для которых оптимизатор переходит к менее оптимальным планам, увеличение значения range_optimizer_max_mem_size может улучшить производительность.

Для оценки необходимого объёма памяти для обработки выражения диапазона используйте следующие рекомендации:

  • Для простого запроса, такого как приведенный ниже, где существует один кандидатный ключ для метода доступа по диапазону, каждое предикат, объединённое с OR, использует приблизительно 230 байт:

    SELECT COUNT(*) FROM t
    WHERE a=1 OR a=2 OR a=3 OR .. . a=N;
    
  • Аналогично для запроса, подобного приведённому ниже, каждое предикат, объединённое с AND, использует приблизительно 125 байт:

    SELECT COUNT(*) FROM t
    WHERE a=1 AND b=1 AND c=1 ... N;
    
  • Для запроса с предикатами IN():

    SELECT COUNT(*) FROM t
    WHERE a IN (1,2, ..., M) AND b IN (1,2, ..., N);
    

    Каждое литеравльное значение в списке IN() считается предикатом, объединённым с OR. Если есть два списка IN(), количество предикатов, объединённых с OR, равно произведению количества литеравльных значений в каждом списке. Таким образом, количество предикатов, объединённых с OR в предыдущем случае, равно M × N.

© 2025 Oracle
Licensed under the GPLv2 License.
https://docs.oracle.com/cd/E17952_01/mysql-8.4-en/range-optimization.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API