У меня очень простой запрос, который должен группировать результаты по полю из объединенной таблицы:
SELECT SQL_NO_CACHE p.name, COUNT(1) FROM ycs_sales s
INNER JOIN ycs_products p ON s.id = p.sales_id
WHERE s.dtm BETWEEN '2018-02-16 00:00:00' AND '2018-02-22 23:59:59'
GROUP BY p.name
Таблица ycs_products - фактически sales_products, перечисляет продукты в каждой продаже. Я хочу видеть долю каждого проданного продукта в течение определенного периода времени.
Текущая скорость запроса составляет 2 секунды, что слишком много для взаимодействия с пользователем. Мне нужно, чтобы этот запрос выполнялся быстро. Есть ли способ избавиться от Using temporary
без денормализации?
Порядок присоединения критически важен, в обеих таблицах много данных, и ограничение количества записей по дате является неоспоримым предварительным условием.
здесь приведен результат объяснения
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: s
type: range
possible_keys: PRIMARY,dtm
key: dtm
key_len: 6
ref: NULL
rows: 1164728
Extra: Using where; Using index; Using temporary; Using filesort
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: p
type: ref
possible_keys: sales_id
key: sales_id
key_len: 5
ref: test.s.id
rows: 1
Extra:
2 rows in set (0.00 sec)
и то же самое в json
EXPLAIN: {
"query_block": {
"select_id": 1,
"filesort": {
"sort_key": "p.'name'",
"temporary_table": {
"table": {
"table_name": "s",
"access_type": "range",
"possible_keys": ["PRIMARY", "dtm"],
"key": "dtm",
"key_length": "6",
"used_key_parts": ["dtm"],
"rows": 1164728,
"filtered": 100,
"attached_condition": "s.dtm between '2018-02-16 00:00:00' and '2018-02-22 23:59:59'",
"using_index": true
},
"table": {
"table_name": "p",
"access_type": "ref",
"possible_keys": ["sales_id"],
"key": "sales_id",
"key_length": "5",
"used_key_parts": ["sales_id"],
"ref": ["test.s.id"],
"rows": 1,
"filtered": 100
}
}
}
}
}
а также создавать таблицы, хотя я считаю это ненужным
CREATE TABLE 'ycs_sales' (
'id' int(11) NOT NULL AUTO_INCREMENT,
'dtm' datetime DEFAULT NULL,
PRIMARY KEY ('id'),
KEY 'dtm' ('dtm')
) ENGINE=InnoDB AUTO_INCREMENT=2332802 DEFAULT CHARSET=latin1
CREATE TABLE 'ycs_products' (
'id' int(11) NOT NULL AUTO_INCREMENT,
'sales_id' int(11) DEFAULT NULL,
'name' varchar(255) DEFAULT NULL,
PRIMARY KEY ('id'),
KEY 'sales_id' ('sales_id')
) ENGINE=InnoDB AUTO_INCREMENT=2332802 DEFAULT CHARSET=latin1
А также PHP-код для репликации тестовой среды
#$pdo->query("set global innodb_flush_log_at_trx_commit = 2");
$pdo->query("create table ycs_sales (id int auto_increment primary key, dtm datetime)");
$stmt = $pdo->prepare("insert into ycs_sales values (null, ?)");
foreach (range(mktime(0,0,0,2,1,2018), mktime(0,0,0,2,28,2018)) as $stamp){
$stmt->execute([date("Y-m-d", $stamp)]);
}
$max_id = $pdo->lastInsertId();
$pdo->query("alter table ycs_sales add key(dtm)");
$pdo->query("create table ycs_products (id int auto_increment primary key, sales_id int, name varchar(255))");
$stmt = $pdo->prepare("insert into ycs_products values (null, ?, ?)");
$products = ['food', 'drink', 'vape'];
foreach (range(1, $max_id) as $id){
$stmt->execute([$id, $products[rand(0,2)]]);
}
$pdo->query("alter table ycs_products add key(sales_id)");
Проблема в том, что группировка по name
заставляет вас потерять информацию sales_id
, поэтому MySQL вынужден использовать временную таблицу.
Хотя это не самый чистый из решений, и один из моих менее избранных подход, вы можете добавить новый индекс, как на name
и на столбцы sales_id
, например:
ALTER TABLE 'yourdb'.'ycs_products'
ADD INDEX 'name_sales_id_idx' ('name' ASC, 'sales_id' ASC);
и заставить запрос использовать этот индекс с force index
или use index
:
SELECT SQL_NO_CACHE p.name, COUNT(1) FROM ycs_sales s
INNER JOIN ycs_products p use index(name_sales_id_idx) ON s.id = p.sales_id
WHERE s.dtm BETWEEN '2018-02-16 00:00:00' AND '2018-02-22 23:59:59'
GROUP BY p.name;
Мое выполнение сообщило только "используя где: используя индекс" в таблице p и "используя where" в таблице s.
Во всяком случае, я настоятельно рекомендую вам задуматься о своей схеме, потому что, возможно, вы можете найти лучший дизайн для этих двух таблиц. С другой стороны, если это не критическая часть вашего приложения, вы можете иметь дело с "принудительным" индексом.
Поскольку совершенно ясно, что проблема в дизайне, я предлагаю использовать отношения как "многие ко многим". Если у вас есть возможность проверить его в тестовой среде, вот что я буду делать:
1) Создайте временную таблицу, чтобы сохранить имя и идентификатор продукта:
create temporary table tmp_prods
select min(id) id, name
from ycs_products
group by name;
2) Начиная с временной таблицы, присоединитесь к таблице продаж, чтобы создать замену для ycs_product
:
create table ycs_products_new
select * from tmp_prods;
ALTER TABLE 'poc'.'ycs_products_new'
CHANGE COLUMN 'id' 'id' INT(11) NOT NULL ,
ADD PRIMARY KEY ('id');
3) Создайте таблицу соединений:
CREATE TABLE 'prod_sale' (
'prod_id' INT(11) NOT NULL,
'sale_id' INT(11) NOT NULL,
PRIMARY KEY ('prod_id', 'sale_id'),
INDEX 'sale_fk_idx' ('sale_id' ASC),
CONSTRAINT 'prod_fk'
FOREIGN KEY ('prod_id')
REFERENCES ycs_products_new ('id')
ON DELETE NO ACTION
ON UPDATE NO ACTION,
CONSTRAINT 'sale_fk'
FOREIGN KEY ('sale_id')
REFERENCES ycs_sales ('id')
ON DELETE NO ACTION
ON UPDATE NO ACTION);
и заполнить его существующими значениями:
insert into prod_sale (prod_id, sale_id)
select tmp_prods.id, sales_id from ycs_sales s
inner join ycs_products p
on p.sales_id=s.id
inner join tmp_prods on tmp_prods.name=p.name;
Наконец, запрос соединения:
select name, count(name) from ycs_products_new p
inner join prod_sale ps on ps.prod_id=p.id
inner join ycs_sales s on s.id=ps.sale_id
WHERE s.dtm BETWEEN '2018-02-16 00:00:00' AND '2018-02-22 23:59:59'
group by p.id;
Пожалуйста, обратите внимание, что группа находится на первичном ключе, а не на имени.
Объяснить вывод:
explain select name, count(name) from ycs_products_new p inner join prod_sale ps on ps.prod_id=p.id inner join ycs_sales s on s.id=ps.sale_id WHERE s.dtm BETWEEN '2018-02-16 00:00:00' AND '2018-02-22 23:59:59' group by p.id;
+------+-------------+-------+--------+---------------------+---------+---------+-----------------+------+-------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra |
+------+-------------+-------+--------+---------------------+---------+---------+-----------------+------+-------------+
| 1 | SIMPLE | p | index | PRIMARY | PRIMARY | 4 | NULL | 3 | |
| 1 | SIMPLE | ps | ref | PRIMARY,sale_fk_idx | PRIMARY | 4 | test.p.id | 1 | Using index |
| 1 | SIMPLE | s | eq_ref | PRIMARY,dtm | PRIMARY | 4 | test.ps.sale_id | 1 | Using where |
+------+-------------+-------+--------+---------------------+---------+---------+-----------------+------+-------------+
Зачем ycs_products
id
для ycs_products
? Похоже, что sales_id
должно быть PRIMARY KEY
из этой таблицы?
Если это возможно, это устраняет проблему производительности, избавляясь от проблем, вызванных сенапе.
Если вместо этого есть несколько строк для каждого sales_id
, то изменение вторичного индекса на это поможет:
INDEX(sales_id, name)
Еще одна вещь, которую нужно проверить, - innodb_buffer_pool_size
. Это должно быть около 70% доступной оперативной памяти. Это улучшит точность данных и индексов.
Есть ли в этой неделе 1,1 миллиона строк?