У меня есть запрос SQL, который считает количество результатов для сложного запроса. Фактический запрос Select очень быстр при ограничении 20 результатами, но версия количества занимает приблизительно 4,5 секунды на моих текущих таблицах после большой оптимизации.
Если я удаляю два соединения и где пункты на тегах сайта и тегах галереи, запрос работает в 1,5 секунды. Если я создаю 3 отдельных запроса - один для выбора сайтов платы, один для выбора имен и один для сплачивания всего - я могу свалить запрос к.6 секундам, который все еще не достаточно хорош. Это также вынудило бы меня использовать хранимую процедуру, так как я должен буду сделать в общей сложности 4 запроса в, в спящем режиме.
Для запроса, "как", вот некоторая информация:
Handler_read_key 1746669
Handler_read_next 1546324
Таблица галереи имеет 40 000 строк
Таблица сайта имеет 900 строк
Таблица имени имеет 800 строк
Таблица тега имеет 3 560 строк
Я довольно плохо знаком с MySQL и настройкой, и у меня есть индексы на:
Я надеюсь получать этот запрос к 0,1 миллисекундам.
SELECT count(distinct gallery.id)
from gallery gallery
inner join
site site
on gallery.site_id = site.id
inner join
site_to_tag p2t
on site.id = p2t.site_id
inner join
tag site_tag
on p2t.tag_id = site_tag.id
inner join
gallery_to_name g2mn
on gallery.id = g2mn.gallery_id
inner join
name name
on g2mn.name_id = name.id
inner join
gallery_to_tag g2t
on gallery.id = g2t.gallery_id
inner join
tag tag
on g2t.tag_id = tag.id
where
gallery.published = true and (
name.value LIKE 'sometext%' or
tag.term = 'sometext' or
site.`name` like 'sometext%' or
site_tag.term = 'sometext'
)
Объясните данные:
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+----+-------------+--------------+--------+-------------------------------------------------------------------+--------------------+---------+-------------------------------------------+------+------------------------------------+
| 1 | SIMPLE | site | index | PRIMARY,nameIndex | nameIndex | 258 | NULL | 950 | Using index; Using temporary |
| 1 | SIMPLE | gallery | ref | PRIMARY,publishedIndex,FKF44C775296EECE37,publishedSiteIdIndex | FKF44C775296EECE37 | 9 | production.site.id | 20 | Using where |
| 1 | SIMPLE | g2mn | ref | PRIMARY,FK3EFFD7F8AFAD7A5E,FK3EFFD7F832C04188 | FK3EFFD7F8AFAD7A5E | 8 | production.gallery.id | 1 | Using index; Distinct |
| 1 | SIMPLE | name | eq_ref | PRIMARY,valueIndex | PRIMARY | 8 | production.g2mn.name_id | 1 | Distinct |
| 1 | SIMPLE | g2t | ref | PRIMARY,FK3DDB4D63AFAD7A5E,FK3DDB4D63E210FBA6 | FK3DDB4D63AFAD7A5E | 8 | production.g2mn.gallery_id | 2 | Using where; Using index; Distinct |
| 1 | SIMPLE | tag | eq_ref | PRIMARY,termIndex | PRIMARY | 8 | production.g2t.tag_id | 1 | Distinct |
| 1 | SIMPLE | p2t | ref | PRIMARY,FK29424AB796EECE37,FK29424AB7E210FBA6 | PRIMARY | 8 | production.gallery.site_id | 3 | Using where; Using index; Distinct |
| 1 | SIMPLE | site_tag | eq_ref | PRIMARY,termIndex | PRIMARY | 8 | production.p2t.tag_id | 1 | Using where; Distinct |
+----+-------------+--------------+--------+-------------------------------------------------------------------+--------------------+---------+-------------------------------------------+------+------------------------------------+
Отдельные скорости количества:
[SQL] select count(*) from gallery;
Affected rows: 0
Time: 0.014ms
Results: 40385
[SQL]
select count(*) from gallery_to_name;
Affected rows: 0
Time: 0.012ms
Results: 35615
[SQL]
select count(*) from gallery_to_tag;
Affected rows: 0
Time: 0.055ms
Results: 165104
[SQL]
select count(*) from tag;
Affected rows: 0
Time: 0.002ms
Results: 3560
[SQL]
select count(*) from site;
Affected rows: 0
Time: 0.001ms
Results: 901
[SQL]
select count(*) from site_to_tag;
Affected rows: 0
Time: 0.003ms
Results: 7026
Я включил свою тестовую схему и сценарий для создания тестовых данных в конце этого поста. Я использовал параметр SQL_NO_CACHE
, чтобы MySQL не кэшировал результаты запросов - это только для тестирования и в конечном итоге должно быть удалено.
Это идея, аналогичная предложенной Донни, но я немного привел ее в порядок. Если я правильно понял объединения, нет необходимости повторять все объединения при каждом выборе, поскольку каждое фактически независимо от других. Исходный пункт WHERE
оговаривает, что gallery.published
должен быть истинным, а затем следует ряд из 4 условий, соединенных OR
. Таким образом, каждый запрос может выполняться отдельно. Вот четыре соединения:
gallery <--> gallery_to_name <--> name
gallery <--> gallery_to_tag <--> tag
gallery <--> site
gallery <--> site <--> site_to_tag <--> tag
Поскольку галерея
содержит site_id
, в этом случае нет необходимости в промежуточном соединении через таблицу site
. Таким образом, последнее соединение может быть сокращено до следующего:
gallery <--> site_to_tag <--> tag
Выполнение каждого SELECT
отдельно и использование UNION
для объединения результатов выполняется очень быстро. Результаты здесь предполагают структуры таблиц и индексы , показанные в конце этого сообщения:
SELECT SQL_NO_CACHE COUNT(id) AS matches FROM (
(SELECT g.id
FROM gallery AS g
INNER JOIN site AS s ON s.id = g.site_id
WHERE g.published = TRUE AND s.name LIKE '3GRD%')
UNION
(SELECT g.id
FROM gallery AS g
INNER JOIN gallery_to_name AS g2n ON g2n.gallery_id = g.id
INNER JOIN name AS n ON n.id = g2n.name_id
WHERE g.published = TRUE AND n.value LIKE '3GRD%')
UNION
(SELECT g.id
FROM gallery AS g
INNER JOIN gallery_to_tag AS g2t ON g2t.gallery_id = g.id
INNER JOIN tag AS gt ON gt.id = g2t.tag_id
WHERE g.published = TRUE AND gt.term = '3GRD')
UNION
(SELECT g.id
FROM gallery AS g
INNER JOIN site_to_tag AS s2t ON s2t.site_id = g.site_id
INNER JOIN tag AS st ON st.id = s2t.tag_id
WHERE g.published = TRUE AND st.term = '3GRD')
) AS totals;
+---------+
| matches |
+---------+
| 99 |
+---------+
1 row in set (0.00 sec)
Скорость действительно зависит от критериев поиска. В следующем примере для каждой таблицы используется различное значение поиска, и оператору LIKE нужно проделать немного больше работы, поскольку теперь существует больше потенциальных совпадений для каждой:
SELECT SQL_NO_CACHE COUNT(id) AS matches FROM (
(SELECT g.id
FROM gallery AS g
INNER JOIN site AS s ON s.id = g.site_id
WHERE g.published = TRUE AND s.name LIKE '3H%')
UNION
(SELECT g.id
FROM gallery AS g
INNER JOIN gallery_to_name AS g2n ON g2n.gallery_id = g.id
INNER JOIN name AS n ON n.id = g2n.name_id
WHERE g.published = TRUE AND n.value LIKE '3G%')
UNION
(SELECT g.id
FROM gallery AS g
INNER JOIN gallery_to_tag AS g2t ON g2t.gallery_id = g.id
INNER JOIN tag AS gt ON gt.id = g2t.tag_id
WHERE g.published = TRUE AND gt.term = '3IDP')
UNION
(SELECT g.id
FROM gallery AS g
INNER JOIN site_to_tag AS s2t ON s2t.site_id = g.site_id
INNER JOIN tag AS st ON st.id = s2t.tag_id
WHERE g.published = TRUE AND st.term = '3OJX')
) AS totals;
+---------+
| matches |
+---------+
| 12505 |
+---------+
1 row in set (0.24 sec)
Эти результаты выгодно отличаются от результатов запроса, который использует несколько объединений:
SELECT SQL_NO_CACHE COUNT(DISTINCT g.id) AS matches
FROM gallery AS g
INNER JOIN gallery_to_name AS g2n ON g2n.gallery_id = g.id
INNER JOIN name AS n ON n.id = g2n.name_id
INNER JOIN gallery_to_tag AS g2t ON g2t.gallery_id = g.id
INNER JOIN tag AS gt ON gt.id = g2t.tag_id
INNER JOIN site AS s ON s.id = g.site_id
INNER JOIN site_to_tag AS s2t ON s2t.site_id = s.id
INNER JOIN tag AS st ON st.id = s2t.tag_id
WHERE g.published = TRUE AND (
gt.term = '3GRD' OR
st.term = '3GRD' OR
n.value LIKE '3GRD%' OR
s.name LIKE '3GRD%');
+---------+
| matches |
+---------+
| 99 |
+---------+
1 row in set (2.62 sec)
SELECT SQL_NO_CACHE COUNT(DISTINCT g.id) AS matches
FROM gallery AS g
INNER JOIN gallery_to_name AS g2n ON g2n.gallery_id = g.id
INNER JOIN name AS n ON n.id = g2n.name_id
INNER JOIN gallery_to_tag AS g2t ON g2t.gallery_id = g.id
INNER JOIN tag AS gt ON gt.id = g2t.tag_id
INNER JOIN site AS s ON s.id = g.site_id
INNER JOIN site_to_tag AS s2t ON s2t.site_id = s.id
INNER JOIN tag AS st ON st.id = s2t.tag_id
WHERE g.published = TRUE AND (
gt.term = '3IDP' OR
st.term = '3OJX' OR
n.value LIKE '3G%' OR
s.name LIKE '3H%');
+---------+
| matches |
+---------+
| 12505 |
+---------+
1 row in set (3.17 sec)
SCHEMA
Важны индексы столбцов id плюс site.name
, name.value
и tag.term
:
DROP SCHEMA IF EXISTS `egervari`;
CREATE SCHEMA IF NOT EXISTS `egervari`;
USE `egervari`;
-- -----------------------------------------------------
-- Table `site`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `site` ;
CREATE TABLE IF NOT EXISTS `site` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT ,
`name` VARCHAR(255) NOT NULL ,
INDEX `name` (`name` ASC) ,
PRIMARY KEY (`id`) )
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `gallery`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `gallery` ;
CREATE TABLE IF NOT EXISTS `gallery` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT ,
`site_id` INT UNSIGNED NOT NULL ,
`published` TINYINT(1) NOT NULL DEFAULT 0 ,
PRIMARY KEY (`id`) ,
INDEX `fk_gallery_site` (`site_id` ASC) ,
CONSTRAINT `fk_gallery_site`
FOREIGN KEY (`site_id` )
REFERENCES `site` (`id` )
ON DELETE CASCADE
ON UPDATE CASCADE)
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `name`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `name` ;
CREATE TABLE IF NOT EXISTS `name` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT ,
`value` VARCHAR(255) NOT NULL ,
INDEX `value` (`value` ASC) ,
PRIMARY KEY (`id`) )
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `tag`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `tag` ;
CREATE TABLE IF NOT EXISTS `tag` (
`id` INT UNSIGNED NOT NULL AUTO_INCREMENT ,
`term` VARCHAR(255) NOT NULL ,
INDEX `term` (`term` ASC) ,
PRIMARY KEY (`id`) )
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `gallery_to_name`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `gallery_to_name` ;
CREATE TABLE IF NOT EXISTS `gallery_to_name` (
`gallery_id` INT UNSIGNED NOT NULL ,
`name_id` INT UNSIGNED NOT NULL ,
PRIMARY KEY (`gallery_id`, `name_id`) ,
INDEX `fk_gallery_to_name_gallery` (`gallery_id` ASC) ,
INDEX `fk_gallery_to_name_name` (`name_id` ASC) ,
CONSTRAINT `fk_gallery_to_name_gallery`
FOREIGN KEY (`gallery_id` )
REFERENCES `gallery` (`id` )
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT `fk_gallery_to_name_name`
FOREIGN KEY (`name_id` )
REFERENCES `name` (`id` )
ON DELETE CASCADE
ON UPDATE CASCADE)
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `gallery_to_tag`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `gallery_to_tag` ;
CREATE TABLE IF NOT EXISTS `gallery_to_tag` (
`gallery_id` INT UNSIGNED NOT NULL ,
`tag_id` INT UNSIGNED NOT NULL ,
PRIMARY KEY (`gallery_id`, `tag_id`) ,
INDEX `fk_gallery_to_tag_gallery` (`gallery_id` ASC) ,
INDEX `fk_gallery_to_tag_tag` (`tag_id` ASC) ,
CONSTRAINT `fk_gallery_to_tag_gallery`
FOREIGN KEY (`gallery_id` )
REFERENCES `gallery` (`id` )
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT `fk_gallery_to_tag_tag`
FOREIGN KEY (`tag_id` )
REFERENCES `tag` (`id` )
ON DELETE CASCADE
ON UPDATE CASCADE)
ENGINE = InnoDB;
-- -----------------------------------------------------
-- Table `site_to_tag`
-- -----------------------------------------------------
DROP TABLE IF EXISTS `site_to_tag` ;
CREATE TABLE IF NOT EXISTS `site_to_tag` (
`site_id` INT UNSIGNED NOT NULL ,
`tag_id` INT UNSIGNED NOT NULL ,
PRIMARY KEY (`site_id`, `tag_id`) ,
INDEX `fk_site_to_tag_site` (`site_id` ASC) ,
INDEX `fk_site_to_tag_tag` (`tag_id` ASC) ,
CONSTRAINT `fk_site_to_tag_site`
FOREIGN KEY (`site_id` )
REFERENCES `site` (`id` )
ON DELETE CASCADE
ON UPDATE CASCADE,
CONSTRAINT `fk_site_to_tag_tag`
FOREIGN KEY (`tag_id` )
REFERENCES `tag` (`id` )
ON DELETE CASCADE
ON UPDATE CASCADE)
ENGINE = InnoDB;
] ТЕСТОВЫЕ ДАННЫЕ
Заполняются сайт
900 строками, тег
3560 строками, имя
800 строками и галерея
40 000 строк и вставляет записи в таблицы ссылок:
DELIMITER //
DROP PROCEDURE IF EXISTS populate//
CREATE PROCEDURE populate()
BEGIN
DECLARE i INT DEFAULT 0;
WHILE i < 900 DO
INSERT INTO site (name) VALUES (CONV(i + 1 * 10000, 20, 36));
SET i = i + 1;
END WHILE;
SET i = 0;
WHILE i < 3560 DO
INSERT INTO tag (term) VALUES (CONV(i + 1 * 10000, 20, 36));
INSERT INTO site_to_tag (site_id, tag_id) VALUES ( (i MOD 900) + 1, i + 1 );
SET i = i + 1;
END WHILE;
SET i = 0;
WHILE i < 800 DO
INSERT INTO name (value) VALUES (CONV(i + 1 * 10000, 20, 36));
SET i = i + 1;
END WHILE;
SET i = 0;
WHILE i < 40000 DO
INSERT INTO gallery (site_id, published) VALUES ( (i MOD 900) + 1, i MOD 2 );
INSERT INTO gallery_to_name (gallery_id, name_id) VALUES ( i + 1, (i MOD 800) + 1 );
INSERT INTO gallery_to_tag (gallery_id, tag_id) VALUES ( i + 1, (i MOD 3560) + 1 );
SET i = i + 1;
END WHILE;
END;
//
DELIMITER ;
CALL populate();
Похоже, что ваше предложение WHERE
может быть нарушителем, особенно следующее:
lower(name2_.value) like ?
Согласно документации MySQL :
Значение по умолчанию набор символов и сопоставление - latin1 и latin1_swedish_ci, поэтому небинарные сравнения строк по умолчанию нечувствительны к регистру.
Возможно, вам не понадобится функция LOWER () в предложении WHERE. Функции в левой части сравнения предотвращают использование индексов.
Как выглядят ваши значения LIKE
? Если вы используете подстановочный знак слева от значения, это предотвращает использование индексов.
Попробуйте заменить ваши операторы OR
на UNION
.
Попробуйте выполнить запрос без DISTINCT
, чтобы увидеть, насколько это влияет на ваш запрос.
Счетчики часто бывают медленными, так как они требуют выборки всех данных, возвращаемых курсором, чтобы выяснить, сколько строк будет фактически извлечено.
Сколько времени нужно для подсчета каждой отдельной таблицы? Сложите общее время - если оно больше 0,1 миллисекунды, я не думаю, что вы сможете заставить запрос выполняться так быстро, как вам хотелось бы. Что касается способов его ускорения, вы можете попробовать вставить некоторые из критериев предложения WHERE в подвыборку, как в
select
count(distinct this_.id) as y0_
from
(select * from gallery where published=?) this_
inner join
site site3_
on this_.site_id=site3_.id
inner join
site_to_tag list7_
on site3_.id=list7_.site_id
inner join
tag sitetag4_
on list7_.tag_id=sitetag4_.id
inner join
gallery_to_name names9_
on this_.id=names9_.gallery_id
inner join
name name2_
on names9_.name_id=name2_.id
inner join
gallery_to_tag list11_
on this_.id=list11_.gallery_id
inner join
tag tag1_
on list11_.tag_id=tag1_.id
where lower(name2_.value) like ? or
tag1_.term=? or
lower(site3_.name) like ? or
lower(this_.description) like ? or
sitetag4_.term=?
. Сколько полей находится в каждой из этих таблиц? Можете ли вы использовать подвыборки, чтобы сократить объем данных, которые база данных должна объединить, или вам действительно нужны все столбцы?
Наличие трех предикатов LIKE замедлит работу, как и использование функции LOWER в предложении WHERE. Если вам нужно выполнять сравнение без учета регистра, может быть лучше иметь два поля, одно в «нормальном» (как введено) регистре и одно, хранящееся в нижнем (или ВЕРХНЕМ) регистре, для выполнения нечувствительного поиска. Вы можете использовать триггер, чтобы синхронизировать нижний / верхний вариант с версией «нормального» регистра.
Надеюсь, это поможет.
РЕДАКТИРОВАТЬ:
Глядя на вывод EXPLAIN PLAN, не видно, что поля, используемые в вашем предложении WHERE, проиндексированы - или, по крайней мере, кажется, что индексы не используются. Это может быть побочным продуктом всех предикатов ИЛИ в WHERE. Если эти поля не проиндексированы, вы можете попробовать их проиндексировать.
ИЛИ
снижает производительность запроса даже при хороших индексах. Чем больше таблицы, тем хуже.
Это ужасно некрасиво, но, вероятно, будет быстрее (очевидно, за счет удобочитаемости). Если бы MySQL поддерживал только CTE, это было бы намного проще.
Вы также можете написать короткий пакет и выбрать общую часть повторяющегося запроса во временную таблицу, а затем выполнить все действия в соответствии с временной таблицей. Возможно, вам придется индексировать временную таблицу, а может и нет, чтобы это сработало, на самом деле это зависит от количества строк.
(Обратите внимание, что union
уже выполняет отдельный
, поэтому нет необходимости делать это снова, подсчет
и принудительно выполнять другой вид)
select
count(id)
from (
SELECT gallery.id
from gallery gallery
inner join
site site
on gallery.site_id = site.id
inner join
site_to_tag p2t
on site.id = p2t.site_id
inner join
tag site_tag
on p2t.tag_id = site_tag.id
inner join
gallery_to_name g2mn
on gallery.id = g2mn.gallery_id
inner join
name name
on g2mn.name_id = name.id
inner join
gallery_to_tag g2t
on gallery.id = g2t.gallery_id
inner join
tag tag
on g2t.tag_id = tag.id
where
gallery.published = true and name.value like 'sometext%'
UNION
SELECT gallery.id
from gallery gallery
inner join
site site
on gallery.site_id = site.id
inner join
site_to_tag p2t
on site.id = p2t.site_id
inner join
tag site_tag
on p2t.tag_id = site_tag.id
inner join
gallery_to_name g2mn
on gallery.id = g2mn.gallery_id
inner join
name name
on g2mn.name_id = name.id
inner join
gallery_to_tag g2t
on gallery.id = g2t.gallery_id
inner join
tag tag
on g2t.tag_id = tag.id
where
gallery.published = true and tag.term = 'sometext'
UNION
SELECT gallery.id
from gallery gallery
inner join
site site
on gallery.site_id = site.id
inner join
site_to_tag p2t
on site.id = p2t.site_id
inner join
tag site_tag
on p2t.tag_id = site_tag.id
inner join
gallery_to_name g2mn
on gallery.id = g2mn.gallery_id
inner join
name name
on g2mn.name_id = name.id
inner join
gallery_to_tag g2t
on gallery.id = g2t.gallery_id
inner join
tag tag
on g2t.tag_id = tag.id
where
gallery.published = true and site.`name` like 'sometext%'
UNION
SELECT gallery.id
from gallery gallery
inner join
site site
on gallery.site_id = site.id
inner join
site_to_tag p2t
on site.id = p2t.site_id
inner join
tag site_tag
on p2t.tag_id = site_tag.id
inner join
gallery_to_name g2mn
on gallery.id = g2mn.gallery_id
inner join
name name
on g2mn.name_id = name.id
inner join
gallery_to_tag g2t
on gallery.id = g2t.gallery_id
inner join
tag tag
on g2t.tag_id = tag.id
where
gallery.published = true and site_tag.term = 'sometext'
) as x
Признаюсь, я не нашел времени, чтобы полностью разобраться в ваших таблицах и запросах. Однако, учитывая требуемое время ответа и очевидную сложность текущих предложений, я бы сказал, что это одна из тех ситуаций, когда (вместо того, чтобы просить SQL подсчитать все записи, которые я хочу подсчитать), я Я буду вести отдельную таблицу всегда актуальных подсчетов и всегда обновлять любые соответствующие подсчеты с помощью инициированного кода при добавлении / изменении / удалении любой записи.
Например, представьте себе файл транзакции с миллионом строк, и мне нужна сумма поля 2. Я могу попросить базу данных выполнить SUM () для поля, или я могу сохранить отдельную сумму для поля 2 в таблице где-нибудь, что корректируется каждый раз, когда запись добавляется, удаляется или редактируется поле 2. Это избыточно, но очень быстро, когда я хочу узнать общую сумму. И я всегда могу SUM (), если я хочу проверить свою отдельную вычисленную сумму.
Хм ... просто просматриваю ваше сообщение в течение двух минут, так что мой ответ может быть не идеальным ... но вы не думали о введении индексной таблицы, которая ссылается на другие объекты?
как
CREATE TABLE `references`
`text` VARC>HAR(...) NOT NULL,
`name` VARCHAR(255) NOT NULL,
`reference_type` WHATEVER, // enum or what suits your needs
`reference_id` INTEGER NOT NULL
);
Тогда просто запросите эту таблицу:
SELECT COUNT(*) FROM references WHERE sometext LIKE ...;
Придется обрабатывать случаи с 'sometext%', хотя ...
Кроме того, действительно ли важно количество галерей, или ваш запрос предназначен только для проверить, существует ли хоть один?