Как то раз заглянул в slow-query.log на проекте и обнаружил там повторяющиеся, двадцатисекундные запросы. Как оказалось причина их оказалась в том, что разработчик не знал/подумал глянуть как запрос строится ORM-кой битрикса. Локально, пока он писал и тестировал все работало замечательно. Но вот прошло около 6 гигабайт данных в таблице - и на первый взгляд простой запрос начал “зависать” на ~10 секунд 🙁.

Предыстория

Понадобилось найти заказ по значению свойства. Был написан следующий код:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
\Bitrix\Sale\Order::getList([
    'select' => ['ID', 'PROPERTY.VALUE'],
    'filter' => [
        'PROPERTY.CODE' => 'SOME_ORDER_PROPERTY_CODE',
        'PROPERTY.VALUE' => 'my value',
    ],
    'runtime' => [
        new \Bitrix\Main\Entity\ReferenceField(
            'PROPERTY',
            '\Bitrix\sale\Internals\OrderPropsValueTable',
            ["=this.ID" => "ref.ORDER_ID"],
            ["join_type" => "left"]
        ),
    ]
]);

На первый взгляд простая таблица с данными. Есть несколько столбцов, по одному из них (строковому, что важно) используется фильтрация. Таблицы на первый взгляд тоже не очень большие:

1
2
3
4
5
6
7
+--------------------------+-----------+
| Table                    | Size (MB) |
+--------------------------+-----------+
| b_sale_order_props_value |      1339 |
| b_sale_order             |       102 |
+--------------------------+-----------+
2 rows in set (0.01 sec)

Смотрим SQL

Но вот результат выполнения запроса около 15 (пятнадцати!) секунд. Тут явно что-то не то происходит. Кажется стоило бы глянуть “под капот” ORM и оценить запрос. Печатаем запрос:

1
2
3
4
5
6
7
8
SELECT `sale_internals_order`.`ID`             AS `ID`,
       `sale_internals_order_property`.`VALUE` AS `SALE_INTERNALS_ORDER_PROPERTY_VALUE`,
       `sale_internals_order_property`.`ID`    AS `UALIAS_0`
FROM `b_sale_order` `sale_internals_order`
         LEFT JOIN `b_sale_order_props_value` `sale_internals_order_property`
            ON `sale_internals_order`.`ID` = `sale_internals_order_property`.`ORDER_ID`
WHERE UPPER(`sale_internals_order_property`.`CODE`) like upper('SOME_ORDER_PROPERTY_CODE')
  AND UPPER(`sale_internals_order_property`.`VALUE`) like upper('my value')

А вот и виновник: UPPER. Мы выкидываем возможность глянуть в индекс, что подтверждает EXPLAIN:

1
2
3
4
5
6
7
+----+-------------+-------------------------------+------------+-------+----------------------+----------------------+---------+---------------------------------+--------+----------+-------------+
| id | select_type | table                         | partitions | type  | possible_keys        | key                  | key_len | ref                             | rows   | filtered | Extra       |
+----+-------------+-------------------------------+------------+-------+----------------------+----------------------+---------+---------------------------------+--------+----------+-------------+
|  1 | SIMPLE      | sale_internals_order          | NULL       | index | PRIMARY              | IXS_ORDER_UPDATED_1C | 3       | NULL                            | 130584 |   100.00 | Using index |
|  1 | SIMPLE      | sale_internals_order_property | NULL       | ref   | IX_SOPV_ORD_PROP_UNI | IX_SOPV_ORD_PROP_UNI | 4       | lazurit.sale_internals_order.ID |     35 |   100.00 | Using where |
+----+-------------+-------------------------------+------------+-------+----------------------+----------------------+---------+---------------------------------+--------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)

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

Отступление “Зачем”

Кажется есть понимание ЗАЧЕМ так сделано: так как CMS - универсальная штука, ставящаяся на разного рода хостинги, то нет уверенности, что на очередном хостинге collations могут оказаться настроено с учетом регистра и этого не поменяют, или забудут, или оставят “так”, или еще какая-то причина, неподвластная разработчикам CMS. Но тогда уже разработчик должен “следить” за собой, и применять правильное сравнение, если он уверен в значениях для поиска. Встроенный модуль проверки сайта (проверки настроек хостинга), указываем, что нужно использовать регистронезависимое сравнение:

1
2
3
4
5
6
7
if (defined('BX_UTF') && BX_UTF === true)
{
    if ($character_set_connection != 'utf8')
        $strError = GetMessage("SC_CONNECTION_CHARSET_WRONG", array('#VAL#' => 'utf8', '#VAL1#' => $character_set_connection));
    elseif ($collation_connection != 'utf8_unicode_ci')
        $strError = GetMessage("SC_CONNECTION_COLLATION_WRONG_UTF", array('#VAL#' => $collation_connection));
}

Исправляемся

Попробуем приме выше переписать на вариант побыстрее (добавить строгое сравнение в фильтр):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
\Bitrix\Sale\Order::getList([
    'select' => ['ID', 'PROPERTY.VALUE'],
    'filter' => [
        '=PROPERTY.CODE' => 'SOME_ORDER_PROPERTY_CODE',
        '=PROPERTY.VALUE' => 'my value',
    ],
    'runtime' => [
        new \Bitrix\Main\Entity\ReferenceField(
            'PROPERTY',
            '\Bitrix\sale\Internals\OrderPropsValueTable',
            ["=this.ID" => "ref.ORDER_ID"],
            ["join_type" => "left"]
        ),
    ]
]);

Для проверки своей теории глянем получившийся SQL:

1
2
3
4
5
6
7
8
SELECT `sale_internals_order`.`ID`             AS `ID`,
       `sale_internals_order_property`.`VALUE` AS `SALE_INTERNALS_ORDER_PROPERTY_VALUE`,
       `sale_internals_order_property`.`ID`    AS `UALIAS_0`
FROM `b_sale_order` `sale_internals_order`
         LEFT JOIN `b_sale_order_props_value` `sale_internals_order_property`
            ON `sale_internals_order`.`ID` = `sale_internals_order_property`.`ORDER_ID`
WHERE `sale_internals_order_property`.`CODE` = 'SOME_ORDER_PROPERTY_CODE'
  AND `sale_internals_order_property`.`VALUE` = 'my value';

Снова проверим EXPLAIN:

1
2
3
4
5
6
7
+----+-------------+-------------------------------+------------+--------+----------------------------------------------------+-------------------------------+---------+------------------------------------------------+------+----------+-------------+
| id | select_type | table                         | partitions | type   | possible_keys                                      | key                           | key_len | ref                                            | rows | filtered | Extra       |
+----+-------------+-------------------------------+------------+--------+----------------------------------------------------+-------------------------------+---------+------------------------------------------------+------+----------+-------------+
|  1 | SIMPLE      | sale_internals_order_property | NULL       | ref    | ix_perf_b_sale_order_props__1,IX_SOPV_ORD_PROP_UNI | ix_perf_b_sale_order_props__1 | 153     | const                                          |    1 |    10.00 | Using where |
|  1 | SIMPLE      | sale_internals_order          | NULL       | eq_ref | PRIMARY                                            | PRIMARY                       | 4       | lazurit.sale_internals_order_property.ORDER_ID |    1 |   100.00 | Using index |
+----+-------------+-------------------------------+------------+--------+----------------------------------------------------+-------------------------------+---------+------------------------------------------------+------+----------+-------------+
2 rows in set, 1 warning (0.01 sec)

А вот и что нам нужно: константный выбор из таблицы свойств, и лишь для выбранных данных присоединение таблицы с заказами. Время выполнения упало, барабанная дробь 🥁, до ~0.06 секунд.

Дополнение

Такая же история работает с фильтрацией вхождения в массив вариантов:

1
2
3
$filter = [
    '=PROPERTY.CODE' => ['first_code', 'second_code', 'third_code'],
];

Такое условие раскладывается на более грустный запрос:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
SELECT
    `sale_internals_order`.`ID` AS `ID`,
    `sale_internals_order_property`.`VALUE` AS `SALE_INTERNALS_ORDER_PROPERTY_VALUE`,
    `sale_internals_order_property`.`ID` AS `UALIAS_0`
FROM
    `b_sale_order` `sale_internals_order`
        LEFT JOIN `b_sale_order_props_value` `sale_internals_order_property` ON
            `sale_internals_order`.`ID` = `sale_internals_order_property`.`ORDER_ID`
WHERE
    (UPPER(`sale_internals_order_property`.`CODE`) like upper('first_code')
        OR UPPER(`sale_internals_order_property`.`CODE`) like upper('second_code')
        OR UPPER(`sale_internals_order_property`.`CODE`) like upper('third_code'))
  AND UPPER(`sale_internals_order_property`.`VALUE`) like upper('my value')

Хотя при использовании строгого сравнения:

1
2
3
$filter = [
    '=PROPERTY.CODE' => ['first_code', 'second_code', 'third_code'],
];

Мы получим запрос через IN (что вроде бы быстрее чем множественный OR):

1
2
3
4
5
6
7
8
SELECT 
    `sale_internals_order`.`ID` AS `ID`,
    `sale_internals_order_property`.`VALUE` AS `SALE_INTERNALS_ORDER_PROPERTY_VALUE`,
    `sale_internals_order_property`.`ID` AS `UALIAS_0`
FROM `b_sale_order` `sale_internals_order` 
LEFT JOIN `b_sale_order_props_value` `sale_internals_order_property` ON `sale_internals_order`.`ID` = `sale_internals_order_property`.`ORDER_ID`
WHERE `sale_internals_order_property`.`CODE` in ('first_code', 'second_code', 'third_code')
AND `sale_internals_order_property`.`VALUE` = 'my value'