Кэширование функций и подзапросов в MySQL (эмуляция)

Rin

*
Кэширование функций и подзапросов в MySQL (эмуляция)

Как известно, в настоящий момент MySQL не кэширует результат работы подзапросов и функций, честно отрабатывая их при каждом запросе. Большинство коммерческих БД это делают. И правильно -- это даёт прирост производительности.

Я попробовал сделать эксперимент с кэшированием функций в MySQL-5.0.51.

Мне нужно было увеличить скорость выполнения некоторого SELECT-запроса с коррелированным подзапросом. Причём набор входных данных, исходящий от запроса к подзапросу часто повторялся, а сам подзапрос возвращал скалярное значение. Это и послужило поводом к оптимизации через кэширование. В итоге мне удалось увеличить скорость работы запроса в 2 раза. Ниже описываю алгоритм и привожу примеры кода на PHP и SQL.

Ключевые шаги выполнения PHP-скрипта:
1) Создаём кэш-таблицу в памяти БД, если она ещё не создана.
2) Удаляем из таблицы устаревшие данные (сборщик мусора).
3) Выполняем SELECT-запрос. В том месте, где д.б. подзапрос, пишем имя функции с параметрами. Сам подзапрос помещаем в эту функцию.

Ключевые шаги выполнения SQL-функции:
1) Внутри функции делаем быстрый запрос к кэш-таблице, используя id соединения и хэш значений параметров функции.
2) Если данные есть, возвращаем результат, если нет, то:
3) Выполняем основной запрос (бывший подзапрос).
4) Сохраняем результат в кэш-таблицу, если его еще там нет.

PHP код (интерфейс к БД -- класс MysqlRapid):
PHP:
$cms->db->query('CREATE TABLE IF NOT EXISTS _func_cache
                 (
                     params_sha1   BINARY(20)       NOT NULL,
                     result        VARCHAR(1024)    NOT NULL,
                     connection_id INT(10) UNSIGNED NOT NULL,
                     PRIMARY KEY (params_sha1, connection_id)
                 ) ENGINE=MEMORY');
#сборщик мусора для кэша SQL функций: удаляем кэш для закрытых (уже отработавших) процессов
$connection_ids = $cms->db->selectCol('SHOW PROCESSLIST');
$cms->db->query('DELETE FROM _func_cache
                       WHERE connection_id NOT IN(' . $cms->db->quoteArray($connection_ids) . ')');
Пример SQL кода основного запроса:
Код:
SELECT d.*,
       CMS_PARTS_MANAGER_STAT(d.parts_manager_id) AS parts_manager_stat
  FROM table1 AS d
 WHERE ...
Пример SQL кода функции (CMS_PARTS_MANAGER_STAT.sql):
Код:
DELIMITER //

DROP FUNCTION IF EXISTS CMS_PARTS_MANAGER_STAT //

CREATE FUNCTION CMS_PARTS_MANAGER_STAT(user_id INT) RETURNS VARCHAR(255)
BEGIN
    DECLARE params_sha1 BINARY(20);
    DECLARE result VARCHAR(255);

    IF user_id IS NULL THEN RETURN NULL;
    END IF;

    SET params_sha1 := UNHEX(SHA1(CONCAT_WS('|', user_id)));

    -- быстрый запрос к кэш-таблице
    SELECT fc.result
      INTO result
      FROM _func_cache AS fc
     WHERE fc.params_sha1 = params_sha1 AND connection_id = CONNECTION_ID();

    IF result IS NOT NULL THEN RETURN result;
    END IF;

    -- бывший подзапрос
    SELECT CONCAT(COUNT(*),
                  ' / ',
                  SUM(request.parts_reply_count)
                 )
      INTO result
      FROM request
     WHERE request.parts_manager_id = user_id
           AND request.parts_status IN ('emailed', 'phoned', 'inaccessible')
           AND request.status = 'public';

    IF result IS NULL THEN RETURN result;
    END IF;

    INSERT IGNORE INTO _func_cache
                   SET params_sha1 = params_sha1,
                       result = result,
                       connection_id = CONNECTION_ID();
    RETURN result;
END;
//

DELIMITER ;
Нужно отметить, что здесь кэширование происходит в рамках соединения, а не в рамках запроса, как в настоящем (встроенном) кэшировании. Если нужно ограничиться только запросом, придётся удалить кэш для текущего соединения сразу после выполнения запроса. Еще одно (несущественное?) ограничение -- кэширование только скалярных значений.
 

Rin

*
Alexandre
Каким образом хранимые процедуры можно использовать для кэширования?
 

Alexandre

PHPПенсионер
ты сам ответил на свой вопрос...

делаем временные таблицы типа мемори
считай что это твои кеши промежуточных данных...

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

Wicked

Новичок
Rin
а во сколько раз предложенный метод быстрее нижеследующего?

0) убеждаемся, что у нас есть индекс по request.parts_manager_id
1) делаем запрос
Код:
SELECT d.*
  FROM table1 AS d
 WHERE ...
2) собираем из результата уникальные d.parts_manager_id в кучу
3) делаем запрос
Код:
    SELECT CONCAT(COUNT(*),
                  ' / ',
                  SUM(request.parts_reply_count)
                 ),
                 request. parts_manager_id 
      FROM request
      WHERE request.parts_manager_id IN (куча d.parts_manager_id через запятую)
           AND request.parts_status IN ('emailed', 'phoned', 'inaccessible')
           AND request.status = 'public'
      GROUP BY request. parts_manager_id;
4) собираем все на стороне пхп, подмешивая результаты пукта 3 в результаты пункта 1 (разумеется, за время не хуже O(max(n, m)), где n - кол-во строк результата первого запроса, m - второго).

-~{}~ 15.10.08 20:42:

В итоге мне удалось увеличить скорость работы запроса в 2 раза.
а вот это меня вообще порвало в лоскуты.
сразу видно, что ты занимаешься этим скорее из спортивно-исследовательского интереса, нежели практической пользы.

-~{}~ 15.10.08 20:51:

поправил пункты 0 и 3
 

Rin

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

Я думал над этим, в итоге получается слишком муторно.

Wicked
>сразу видно, что ты занимаешься этим скорее из спортивно-исследовательского интереса, нежели практической пользы.

Я делал замеры на реально существующем проекте.
Сейчас там эмуляция кэширования успешно работает.

>а во сколько раз предложенный метод быстрее нижеследующего?
Такой вариант я тоже рассматривал.
Возможно, это немного быстрее, но ощутимый прирост скорости это не даст.

>собираем все на стороне пхп
Тоже хороший вариант, имеющий право на существование.
Кстати, корреляция подзапроса к запросу может быть более сильной и сборка результата на стороне PHP будет более сложной.

Индекс по request.parts_manager_id естественно есть.
Ошибочки в запросе 3-го пункта исправьте.
 

Wicked

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

Ошибочки в запросе 3-го пункта исправьте.
исправил parts_manager на parts_manager_id, это все?
как ты понимаешь, возможности дебага у меня нету, а гадать я не привык.

Такой вариант я тоже рассматривал.
Возможно, это немного быстрее, но ощутимый прирост скорости это не даст.
эхо: "... а гадать я не привык ... не привык"

-~{}~ 17.10.08 12:26:

ping :)
 

Alexandre

PHPПенсионер
Rin
я делаю специальный кешировщик абстрактного типа данных : датасет
по мимо кеширования он может объединять и пересекать датасеты по некоторому ключу...
но это индивидуальное решение под hiload
 
Сверху