我有一个非常简单的查询,必须按联接表中的字段对结果进行分组:
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)");
答案 0 :(得分:2)
摘要表。
建立并维护一个表,该表每天汇总所有销售额。它将具有date
(已规范化)和CREATE TABLE sales_summary (
dy DATE NOT NULL,
name varchar(255) NOT NULL,
daily_count SMALLINT UNSIGNED NOT NULL,
PRIMARY KEY(dy, name),
INDEX(name, dy) -- (You might need this for other queries)
) ENGINE=InnoDB;
。因此,该表应小于原始数据。
摘要表将类似于
INSERT INTO sales_summary (dy, name, one_day_count)
ON DUPLICATE KEY UPDATE
daily_count = daily_count + VALUES(one_day_count)
SELECT DATE(s.dtm) AS dy,
p.name,
COUNT(*) AS one_day_count
FROM ycs_sales s
JOIN ycs_products p ON s.id = p.sales_id
WHERE s.dtm >= CURDATE() - INTERVAL 1 DAY
AND s.dtm < CURDATE()
GROUP BY 1, 2;
每晚(午夜之后)更新将是单个查询,如下所示。可能要花2秒钟以上的时间,但是没有用户在等待它。
SELECT SQL_NO_CACHE
name,
SUM(one_day_count)
FROM sales_summary
WHERE dy >= '2018-02-16'
AND dy < '2018-02-16' + INTERVAL 7 DAY
GROUP BY name;
用户的查询将类似于:
SELECT IF(IF(EXISTS (SELECT 1 FROM `df` t3 WHERE 1 = 1), @myVar :='one',0) = 'one',1,0);
SELECT @myVar;
有关汇总表的更多讨论:http://mysql.rjweb.org/doc.php/summarytables
答案 1 :(得分:2)
请参阅您的以下评论,我认为按列s.dtm
进行过滤是不可避免的。
连接顺序至关重要,两个表中都有很多数据,按日期限制记录数是毫无疑问的前提。
您可以采取的最关键的措施是观察频繁的搜索模式。
例如,如果您对dtm的搜索标准通常是检索整天的数据(即几天的数据(例如少于15天),并且在整天的00:00:00
和23:59:59
之间, 您可以使用此信息来减轻搜索时间和插入时间的开销。
一种这样做的方法;您可以在表格中添加一个新列,其中包含截断的日期数据,还可以对该新列进行哈希索引。 (在Mysql中,没有像在Oracle中那样具有功能索引的概念。这就是为什么我们需要添加一个新列来模仿该功能的原因)。像这样:
alter table ycs_sales add dtm_truncated date;
delimiter //
create trigger dtm_truncater_insert
before insert on ycs_sales
for each row
set new.dtm_truncated = date(new.dtm);
//
delimiter //
create trigger dtm_truncater_update
before update on ycs_sales
for each row
set new.dtm_truncated = date(new.dtm);
//
create index index_ycs_sales_dtm_truncated on ycs_sales(dtm_truncated) using hash;
# execute the trigger for existing rows, bypass the safe update mode by id > -1
update ycs_sales set dtm = date(dtm) where id > -1;
然后,您可以使用dtm_truncated
命令使用IN
字段进行查询。但是,这当然有其自身的权衡,更长的范围将不起作用。 但是正如我上面粗体提到的那样,您可以做的是将新列用作函数输出,该函数为插入/更新时间内的可能搜索建立索引。
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_truncated in ( '2018-02-16', '2018-02-17', '2018-02-18', '2018-02-19', '2018-02-20', '2018-02-21', '2018-02-22')
GROUP BY p.name
另外确保您在dtm
上的密钥是BTREE密钥。 (如果它是一个哈希密钥,那么InnoDB需要遍历所有密钥。)生成BTREE语法是:
create index index_ycs_sales_dtm on ycs_sales(dtm) using btree;
最后一点:
实际上,“分区修剪”(参考:here)是在插入时对数据进行分区的概念。但是在MySql中,我不知道为什么,分区要求相关列位于主键中。我相信您不想在主键中添加dtm
列。但是,如果可以的话,还可以对数据进行分区,并消除选择时间的日期范围检查开销。
答案 2 :(得分:1)
这里并没有真正提供答案,但是我认为这里的问题的核心是确定真正放缓的地方。 我不是MySQL专家,但我会尝试运行以下查询:
SELECT SQL_NO_CACHE name, count(*) FROM (
SELECT p.name 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 name
SELECT SQL_NO_CACHE COUNT(*) FROM (
SELECT SQL_NO_CACHE name, count(*) FROM (
SELECT SQL_NO_CACHE p.name 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 name
)
SELECT SQL_NO_CACHE s.* FROM ycs_sales s
WHERE s.dtm BETWEEN '2018-02-16 00:00:00' AND '2018-02-22 23:59:59'
SELECT SQL_NO_CACHE COUNT(*) FROM ycs_sales s
WHERE s.dtm BETWEEN '2018-02-16 00:00:00' AND '2018-02-22 23:59:59'
这样做的时候,您能告诉我们每个人花了多长时间?
答案 3 :(得分:1)
我已经在同一数据集上运行了总和测试查询。这是我的结果:
您的查询将在1.4秒内执行。
用{p>在ycs_products(sales_id, name)
上添加覆盖索引后
ALTER TABLE `ycs_products`
DROP INDEX `sales_id`,
ADD INDEX `sales_id_name` (`sales_id`, `name`)
执行时间降至1.0秒。
我仍然在EXPLAIN结果中看到“使用临时;使用文件排序”。
但是现在也有了“使用索引”-这意味着,无需查找聚簇索引即可获取name
列的值。
注意:我删除了旧索引,因为它对于大多数查询来说都是多余的。
但是您可能有一些查询需要在id
之后出现sales_id
(PK)的索引。
您明确询问,如何摆脱“使用临时”。 但是,即使您找到了一种强制执行计划的方法(可以避免文件排序),您也不会赢得太多。 考虑以下查询:
SELECT SQL_NO_CACHE 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'
这需要0.855秒。 由于没有GROUP BY子句,因此不执行文件排序。 它不会返回您想要的结果- 遗憾的是:这是在不存储和维护冗余数据的情况下所能获得的最低限制。
如果您想知道引擎在哪里花费的时间最多-删除JOIN:
SELECT SQL_NO_CACHE COUNT(1) FROM ycs_sales s
WHERE s.dtm BETWEEN '2018-02-16 00:00:00' AND '2018-02-22 23:59:59'
它在0.155秒内执行。因此我们可以得出结论:JOIN是查询中最昂贵的部分。而且你无法避免。
执行时间的完整列表:
同样,在EXPLAIN结果中,“使用临时;使用文件排序”看起来很糟糕-但这不是您最大的问题。
带有innodb_buffer_pool_size = 1G
的Windows 10 + MariaDB 10.3.13
已使用以下脚本生成了测试数据(在HDD上需要1到2分钟):
drop table if exists ids;
create table ids(id mediumint unsigned auto_increment primary key);
insert into ids(id)
select null as id
from information_schema.COLUMNS c1
, information_schema.COLUMNS c2
, information_schema.COLUMNS c3
limit 2332801 -- 60*60*24*27 + 1;
drop table if exists ycs_sales;
CREATE TABLE `ycs_sales` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`dtm` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `dtm` (`dtm`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
insert into ycs_sales(id, dtm) select id, date('2018-02-01' + interval (id-1) second) from ids;
drop table if exists ycs_products;
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 DEFAULT CHARSET=latin1;
insert into ycs_products(id, sales_id, name)
select id
, id as sales_id
, case floor(rand(1)*3)
when 0 then 'food'
when 1 then 'drink'
when 2 then 'vape'
end as name
from ids;
答案 4 :(得分:1)
我有几次类似的问题。 通常,我希望通过
可以获得最佳结果CREATE INDEX s_date ON ycs_sales(dtm, id)
-- Add a covering index
CREATE INDEX p_name ON ycs_products(sales_id, name);
这应该摆脱“表非常大”的问题,因为所需的所有信息现在都包含在两个索引中。实际上,我似乎还记得,如果后者是主键,则第一个索引不需要id
。
如果这还不够,因为两个表太大,则别无选择-必须避免JOIN 。它已经尽可能快地运行了,如果还不够,那就必须走了。
我相信您可以使用几个TRIGGER
来执行此操作,以维护一个辅助的每日销售报告表(如果您从未退货,那么只需在销售中插入INSERT即可)-尝试仅使用(product_id, sales_date, sales_count)
并将其与product表联接以在输出时获取名称;但是,如果这还不够的话,请使用(product_id, product_name, sales_date, sales_count)
并定期更新product_name
以通过从主表中读取名称来保持名称同步。由于sales_date
现在是唯一的,并且您可以对其进行搜索,因此可以声明sales_date
为主键,并根据销售年份对辅助表进行分区。
(一次或两次,当无法进行分区时,但我确信很少会越过“理想的”分区边界,我手动进行分区-即sales_2012,sales_2013,sales_2014-并以编程方式建立了两者的联合或三年的时间,然后是重新分组,度假和二次总计阶段。疯狂的三月野兔,是的,但是有效)。
答案 5 :(得分:0)
为什么id
为ycs_products
?似乎sales_id
应该是该表的PRIMARY KEY
?
如果可行的话,它可以通过摆脱senape带来的问题来消除性能问题。
相反,如果每个sales_id
有多行,那么将二级索引更改为此将有所帮助:
INDEX(sales_id, name)
要检查的另一件事是innodb_buffer_pool_size
。它应该是可用 RAM的70%左右。这将提高数据和索引的可缓存性。
一周内真的有110万行吗?