Доработка схемы БД с учетом обратной совместимости для ускорения

Избавляемся от JOIN

Для ускорения выборки запросы с JOIN следует переписать на обычный SELECT, для чего изменить схему данных, а именно перенести часть полей из большой таблицы FieldMapping (1,5М записей) в таблицу Document.

ALTER TABLE `Document`
ADD `FM_DateCreated` datetime NOT NULL DEFAULT '0000-00-00 00:00:00',
ADD `FM_KodClient` CHAR(8) NOT NULL DEFAULT '',
ADD `FM_InECP` tinyint(1) UNSIGNED NOT NULL DEFAULT '0';

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

Приведение даты

Дата может быть в разных форматах: 2-4-2012 10:57 и 13.03.2012 16:23:11. Эти два случая покрывают все данные:

SELECT * FROM FieldMapping WHERE FieldID = 25 AND FieldValue NOT REGEXP '[0-9]*-[0-9]*-[0-9]* [0-9]*:[0-9]*'
AND FieldValue NOT REGEXP '[0-9]*\.[0-9]*\.[0-9]* [0-9]*:[0-9]*:[0-9]*'
LIMIT 10;
Empty SET (4.13 sec)

Хранимая процедура для приведения к типу DATETIME:

DROP FUNCTION IF EXISTS `PARSE_DATE`;
 
DELIMITER $$
 
CREATE FUNCTION PARSE_DATE(str VARCHAR(255))
RETURNS DATETIME
BEGIN
 
  SELECT SUBSTRING(str, 1, LOCATE(' ', str) - 1) INTO @mydate;
  SELECT SUBSTRING(str, LOCATE(' ', str) + 1, 100) INTO @mytime;
 
  IF @mydate REGEXP '[0-9]*-[0-9]*-[0-9]*' THEN
    SET @pos1 = LOCATE('-', @mydate);
    SET @pos2 = LOCATE('-', @mydate, @pos1 + 1);
    SET @DAY = SUBSTRING(@mydate, 1, @pos1 - 1);
    SET @MONTH = SUBSTRING(@mydate, @pos1 + 1, @pos2 - @pos1 - 1);
    SET @YEAR = SUBSTRING(@mydate, @pos2 + 1, 100);
  ELSE
    SET @pos1 = LOCATE('.', @mydate);
    SET @pos2 = LOCATE('.', @mydate, @pos1 + 1);
    SET @DAY = SUBSTRING(@mydate, 1, @pos1 - 1);
    SET @MONTH = SUBSTRING(@mydate, @pos1 + 1, @pos2 - @pos1 - 1);
    SET @YEAR = SUBSTRING(@mydate, @pos2 + 1, 100);
  END IF;
 
  RETURN CAST(CONCAT(@YEAR,'-', @MONTH, '-', @DAY, ' ', @mytime) AS DATETIME);
 
END$$
 
DELIMITER ;
 
-- test cases:
 
SELECT PARSE_DATE('13.03.2012 16:23:11');
SELECT PARSE_DATE('03.04.2012 9:34:59');
SELECT PARSE_DATE('09.06.2015 16:14:10');
SELECT PARSE_DATE('13-3-2012 10:56');
SELECT PARSE_DATE('2-4-2012 10:57');
SELECT PARSE_DATE('29-5-2013 17:2');

Триггеры

Триггеры поддерживают целостность данных, а за счет «денормализации» и индексации получаем выигрыш в скорости. Триггеры не сделаны для DELETE, так как этого события происходить не должно.

DROP TRIGGER IF EXISTS `date_created_insert`;
DROP TRIGGER IF EXISTS `date_created_update`;
 
DELIMITER $$
 
  CREATE TRIGGER `date_created_insert` BEFORE INSERT ON `FieldMapping`
    FOR EACH ROW BEGIN
      IF NEW.FieldID = 25 THEN
        UPDATE Document SET FM_DateCreated = PARSE_DATE(NEW.FieldValue) WHERE ID = NEW.DocumentID;
      ELSEIF NEW.FieldID = 39 THEN
        UPDATE Document SET FM_KodClient = NEW.FieldValue WHERE ID = NEW.DocumentID;
      ELSEIF NEW.FieldID = 22 THEN
        UPDATE Document SET FM_InECP = IF(LOWER(NEW.FieldValue) = 'true', 1, 0) WHERE ID = NEW.DocumentID;
      END IF;
    END;
  $$
 
  CREATE TRIGGER `date_created_update` BEFORE UPDATE ON `FieldMapping`
    FOR EACH ROW BEGIN
      IF NEW.FieldID = 25 THEN
        UPDATE Document SET FM_DateCreated = PARSE_DATE(NEW.FieldValue) WHERE ID = NEW.DocumentID;
      ELSEIF NEW.FieldID = 39 THEN
        UPDATE Document SET FM_KodClient = NEW.FieldValue WHERE ID = NEW.DocumentID;
      ELSEIF NEW.FieldID = 22 THEN
        UPDATE Document SET FM_InECP = IF(LOWER(NEW.FieldValue) = 'true', 1, 0) WHERE ID = NEW.DocumentID;
      END IF;
    END;
  $$
 
DELIMITER ;

Индексация

Типичный запрос:

SELECT SQL_CALC_FOUND_ROWS Document.ID, Document.IDEDO, Document.SigOne, Document.SigTwo, Document.KeyOne, Document.KeyTwo, Document.NameFile, Document.IsError 
FROM Document WHERE Document.FM_KodClient = '00007060' AND Document.SigOne = 1 AND Document.SigTwo = 0 
ORDER BY Document.FM_DateCreated DESC, Document.ID DESC LIMIT 0, 10

Добавлен индекс:

ALTER TABLE `Document` ADD INDEX `idx_search`(FM_KodClient, SigOne, SigTwo, FM_DateCreated, ID);
  • Без индекса: 0,11 сек
  • С индексом: 0,01 сек

Для поиска по всем полям поставлен Sphinx.