在具有 10+ 百万行的表上使用 1 个连接优化查询
Optimize query with 1 join, on tables with 10+ millions rows
我正在考虑使用 2 table 秒更快地发出请求。
我有以下 2 tables :
Table“日志”
id varchar(36) PK
date timestamp(2)
- 更多 varchar 字段和 1 个文本字段
table 具有 PHP Laravel 框架所称的与其他几个对象的“多态多对多”关系,因此存在第二个 table” logs_pivot" :
id unsigned int PK
log_id varchar(36) FOREIGN KEY (logs.id)
model_id varchar(40)
model_type varchar(50)
logs_pivot
中的每个条目在 logs
中有一个或多个条目。它们分别有 20+ 和 10+ 百万行。
我们这样查询:
select * from logs
join logs_pivot on logs.id = logs_pivot.log_id
where model_id = 'some_id' and model_type = 'My\Class'
order by date desc
limit 50;
显然我们在 model_id 和 model_type 字段上都有一个复合索引,但请求仍然很慢:每次几(几十)秒。
我们在 date
字段上也有一个索引,但是 EXPLAIN
表明这是使用的 model_id_model_type
索引。
解释语句:
+----+-------------+-------------+------------+--------+--------------------------------------------------------------------------------+-----------------------------------------------+---------+-------------------------------------------+------+----------+---------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+--------+--------------------------------------------------------------------------------+-----------------------------------------------+---------+-------------------------------------------+------+----------+---------------------------------+
| 1 | SIMPLE | logs_pivot | NULL | ref | logs_pivot_model_id_model_type_index,logs_pivot_log_id_index | logs_pivot_model_id_model_type_index | 364 | const,const | 1 | 100.00 | Using temporary; Using filesort |
| 1 | SIMPLE | logs | NULL | eq_ref | PRIMARY | PRIMARY | 146 | the_db_name.logs_pivot.log_id | 1 | 100.00 | NULL |
+----+-------------+-------------+------------+--------+--------------------------------------------------------------------------------+-----------------------------------------------+---------+-------------------------------------------+------+----------+---------------------------------+
在其他 table 中,通过在索引中包含日期字段,我能够更快地发出类似的请求。但在那种情况下,它们位于单独的 table.
中
当我们想要访问这些数据时,它们通常有一些 hours/days 旧。
我们的 InnoDB 池太小,无法在内存中保存所有数据(+所有其他 tables),因此数据很可能总是在磁盘上查询。
我们可以通过哪些方式更快地提出请求?
理想情况下仅使用另一个索引,或通过更改其完成方式。
非常感谢!
编辑 17h05 :
到目前为止,谢谢大家的回答,我会尝试像 O Jones 建议的那样,并以某种方式将日期字段包含在数据透视表 table 中,以便我可以包含在索引索引中。
编辑 14/10 10h。
解决方案:
所以我最终改变了请求的实际完成方式,通过对数据透视表 table 的 id 字段进行排序,这确实允许将其放入索引中。
此外,计算总行数的请求已更改为仅在数据透视表 table 上完成,当它未按日期过滤时。
谢谢大家!
我看到两个问题:
当 tables 相对于 RAM 大小来说很大时,UUID 是昂贵的。
LIMIT
无法得到最佳处理,因为WHERE
子句来自一个table,但是ORDER BY
列来自另一个 table。也就是说,它将执行所有 JOIN
,然后排序,最后剥离几行。
只是一个建议。使用复合索引显然是一件好事。另一个可能是按日期 pre-qualify 一个 ID,并根据你的 logs_pivot table 索引扩展你的索引 (model_id, model_type, log_id).
如果您查询数据,并且整个历史记录有 20 多万条记录,那么在您处理每个给定模型类别的限制为 50 条记录的地方,数据回溯到多远 id/type。说3个月? vs 说你的 5 年日志? (未在 post 中列出,只是一个 for-instance)。因此,如果您可以查询日期大于 3 个月前的最小日志 ID,则该 ID 可以限制您的 logs_pivot table.
发生的其他事情
类似
select
lp.*,
l.date
from
logs_pivot lp
JOIN Logs l
on lp.log_id = l.id
where
model_id = 'some_id'
and model_type = 'My\Class'
and log_id >= ( select min( id )
from logs
where date >= datesub( curdate(), interval 3 month ))
order by
l.date desc
limit
50;
因此,log_id 的 where 子句只执行一次,returns 只是 3 个月前的 ID,而不是 logs_pivot 的整个历史记录。然后你使用模型 id/type 的优化 two-part 键查询,但也跳转到其索引的末尾,ID 包含在索引键中以跳过所有历史。
您可能想要包括的另一件事是一些 pre-aggregate table 记录的数量,例如每个 month/year 每个给定模型 type/id。将其用作 pre-query 以呈现给用户,然后您可以将其用作 drill-down 以进一步获取更多详细信息。 pre-aggregate table 可以在所有历史资料上完成一次,因为它是静态的,不会改变。您唯一需要不断更新的是当前单月期间的任何内容,例如每晚。或者甚至可能更好,通过一个触发器,该触发器要么在每次添加完成时插入一条记录,要么根据 year/month 聚合更新给定 model/type 的计数。同样,只是一个建议,没有关于如何/为什么将数据呈现给 end-user.
的其他上下文
SELECT columns FROM big table ORDER BY something LIMIT small number
是一个臭名昭著的查询性能反模式。为什么?服务器对一大堆长行进行排序,然后丢弃几乎所有的行。您的 columns
之一是 LOB -- TEXT 列也无济于事。
这里有一种方法可以减少这种开销:通过查找所需的主键集来确定所需的行,然后仅获取这些行的内容。
你想要什么行?此子查询找到它们。
SELECT id
FROM logs
JOIN logs_pivot
ON logs.id = logs_pivot.log_id
WHERE logs_pivot.model_id = 'some_id'
AND logs_pivot.model_type = 'My\Class'
ORDER BY logs.date DESC
LIMIT 50
这会完成计算所需行的所有繁重工作。所以,这是您需要优化的查询。
在logs
可以通过这个索引加速
CREATE INDEX logs_date_desc ON logs (date DESC);
和 logs_pivot
上的 three-column 复合索引
CREATE INDEX logs_pivot_lookup ON logs_pivot (model_id, model_type, log_id);
这个索引可能会更好,因为优化器会在 logs_pivot
而不是 logs
上看到过滤。因此,它将首先查找 logs_pivot
。
或者也许
CREATE INDEX logs_pivot_lookup ON logs_pivot (log_id, model_id, model_type);
先试一试,看看哪个产生的结果更快。 (我不确定 JOIN 将如何使用复合索引。)(或者简单地添加两者,然后使用 EXPLAIN
来查看它使用了哪一个。)
然后,当您对子查询的性能感到满意或满意时,使用它来获取您需要的行,就像这样
SELECT *
FROM logs
WHERE id IN (
SELECT id
FROM logs
JOIN logs_pivot
ON logs.id = logs_pivot.log_id
WHERE logs_pivot.model_id = 'some_id'
AND model_type = 'My\Class'
ORDER BY logs.date DESC
LIMIT 50
)
ORDER BY date DESC
之所以有效,是因为它对较少的数据进行了排序。 logs_pivot
上的覆盖 three-column 索引也有帮助。
注意子查询和主查询都有ORDER BY子句,确保返回的详细结果集是你需要的顺序。
Edit Darnit,一直在使用 MariaDB 10+ 和 MySQL 8+,我忘记了旧的限制。试试这个。
SELECT *
FROM logs
JOIN (
SELECT id
FROM logs
JOIN logs_pivot
ON logs.id = logs_pivot.log_id
WHERE logs_pivot.model_id = 'some_id'
AND model_type = 'My\Class'
ORDER BY logs.date DESC
LIMIT 50
) id_set ON logs.id = id_set.id
ORDER BY date DESC
最后,如果您知道自己只关心比某个特定时间更新的行,则可以将类似的内容添加到您的子查询中。
AND logs.date >= NOW() - INTERVAL 5 DAY
如果您的 table 中有大量历史数据,这将有很大帮助。
我正在考虑使用 2 table 秒更快地发出请求。
我有以下 2 tables :
Table“日志”
id varchar(36) PK
date timestamp(2)
- 更多 varchar 字段和 1 个文本字段
table 具有 PHP Laravel 框架所称的与其他几个对象的“多态多对多”关系,因此存在第二个 table” logs_pivot" :
id unsigned int PK
log_id varchar(36) FOREIGN KEY (logs.id)
model_id varchar(40)
model_type varchar(50)
logs_pivot
中的每个条目在 logs
中有一个或多个条目。它们分别有 20+ 和 10+ 百万行。
我们这样查询:
select * from logs
join logs_pivot on logs.id = logs_pivot.log_id
where model_id = 'some_id' and model_type = 'My\Class'
order by date desc
limit 50;
显然我们在 model_id 和 model_type 字段上都有一个复合索引,但请求仍然很慢:每次几(几十)秒。
我们在 date
字段上也有一个索引,但是 EXPLAIN
表明这是使用的 model_id_model_type
索引。
解释语句:
+----+-------------+-------------+------------+--------+--------------------------------------------------------------------------------+-----------------------------------------------+---------+-------------------------------------------+------+----------+---------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------------+------------+--------+--------------------------------------------------------------------------------+-----------------------------------------------+---------+-------------------------------------------+------+----------+---------------------------------+
| 1 | SIMPLE | logs_pivot | NULL | ref | logs_pivot_model_id_model_type_index,logs_pivot_log_id_index | logs_pivot_model_id_model_type_index | 364 | const,const | 1 | 100.00 | Using temporary; Using filesort |
| 1 | SIMPLE | logs | NULL | eq_ref | PRIMARY | PRIMARY | 146 | the_db_name.logs_pivot.log_id | 1 | 100.00 | NULL |
+----+-------------+-------------+------------+--------+--------------------------------------------------------------------------------+-----------------------------------------------+---------+-------------------------------------------+------+----------+---------------------------------+
在其他 table 中,通过在索引中包含日期字段,我能够更快地发出类似的请求。但在那种情况下,它们位于单独的 table.
中当我们想要访问这些数据时,它们通常有一些 hours/days 旧。
我们的 InnoDB 池太小,无法在内存中保存所有数据(+所有其他 tables),因此数据很可能总是在磁盘上查询。
我们可以通过哪些方式更快地提出请求?
理想情况下仅使用另一个索引,或通过更改其完成方式。
非常感谢!
编辑 17h05 :
到目前为止,谢谢大家的回答,我会尝试像 O Jones 建议的那样,并以某种方式将日期字段包含在数据透视表 table 中,以便我可以包含在索引索引中。
编辑 14/10 10h。
解决方案:
所以我最终改变了请求的实际完成方式,通过对数据透视表 table 的 id 字段进行排序,这确实允许将其放入索引中。
此外,计算总行数的请求已更改为仅在数据透视表 table 上完成,当它未按日期过滤时。
谢谢大家!
我看到两个问题:
当 tables 相对于 RAM 大小来说很大时,UUID 是昂贵的。
LIMIT
无法得到最佳处理,因为WHERE
子句来自一个table,但是ORDER BY
列来自另一个 table。也就是说,它将执行所有JOIN
,然后排序,最后剥离几行。
只是一个建议。使用复合索引显然是一件好事。另一个可能是按日期 pre-qualify 一个 ID,并根据你的 logs_pivot table 索引扩展你的索引 (model_id, model_type, log_id).
如果您查询数据,并且整个历史记录有 20 多万条记录,那么在您处理每个给定模型类别的限制为 50 条记录的地方,数据回溯到多远 id/type。说3个月? vs 说你的 5 年日志? (未在 post 中列出,只是一个 for-instance)。因此,如果您可以查询日期大于 3 个月前的最小日志 ID,则该 ID 可以限制您的 logs_pivot table.
发生的其他事情类似
select
lp.*,
l.date
from
logs_pivot lp
JOIN Logs l
on lp.log_id = l.id
where
model_id = 'some_id'
and model_type = 'My\Class'
and log_id >= ( select min( id )
from logs
where date >= datesub( curdate(), interval 3 month ))
order by
l.date desc
limit
50;
因此,log_id 的 where 子句只执行一次,returns 只是 3 个月前的 ID,而不是 logs_pivot 的整个历史记录。然后你使用模型 id/type 的优化 two-part 键查询,但也跳转到其索引的末尾,ID 包含在索引键中以跳过所有历史。
您可能想要包括的另一件事是一些 pre-aggregate table 记录的数量,例如每个 month/year 每个给定模型 type/id。将其用作 pre-query 以呈现给用户,然后您可以将其用作 drill-down 以进一步获取更多详细信息。 pre-aggregate table 可以在所有历史资料上完成一次,因为它是静态的,不会改变。您唯一需要不断更新的是当前单月期间的任何内容,例如每晚。或者甚至可能更好,通过一个触发器,该触发器要么在每次添加完成时插入一条记录,要么根据 year/month 聚合更新给定 model/type 的计数。同样,只是一个建议,没有关于如何/为什么将数据呈现给 end-user.
的其他上下文SELECT columns FROM big table ORDER BY something LIMIT small number
是一个臭名昭著的查询性能反模式。为什么?服务器对一大堆长行进行排序,然后丢弃几乎所有的行。您的 columns
之一是 LOB -- TEXT 列也无济于事。
这里有一种方法可以减少这种开销:通过查找所需的主键集来确定所需的行,然后仅获取这些行的内容。
你想要什么行?此子查询找到它们。
SELECT id
FROM logs
JOIN logs_pivot
ON logs.id = logs_pivot.log_id
WHERE logs_pivot.model_id = 'some_id'
AND logs_pivot.model_type = 'My\Class'
ORDER BY logs.date DESC
LIMIT 50
这会完成计算所需行的所有繁重工作。所以,这是您需要优化的查询。
在logs
CREATE INDEX logs_date_desc ON logs (date DESC);
和 logs_pivot
CREATE INDEX logs_pivot_lookup ON logs_pivot (model_id, model_type, log_id);
这个索引可能会更好,因为优化器会在 logs_pivot
而不是 logs
上看到过滤。因此,它将首先查找 logs_pivot
。
或者也许
CREATE INDEX logs_pivot_lookup ON logs_pivot (log_id, model_id, model_type);
先试一试,看看哪个产生的结果更快。 (我不确定 JOIN 将如何使用复合索引。)(或者简单地添加两者,然后使用 EXPLAIN
来查看它使用了哪一个。)
然后,当您对子查询的性能感到满意或满意时,使用它来获取您需要的行,就像这样
SELECT *
FROM logs
WHERE id IN (
SELECT id
FROM logs
JOIN logs_pivot
ON logs.id = logs_pivot.log_id
WHERE logs_pivot.model_id = 'some_id'
AND model_type = 'My\Class'
ORDER BY logs.date DESC
LIMIT 50
)
ORDER BY date DESC
之所以有效,是因为它对较少的数据进行了排序。 logs_pivot
上的覆盖 three-column 索引也有帮助。
注意子查询和主查询都有ORDER BY子句,确保返回的详细结果集是你需要的顺序。
Edit Darnit,一直在使用 MariaDB 10+ 和 MySQL 8+,我忘记了旧的限制。试试这个。
SELECT *
FROM logs
JOIN (
SELECT id
FROM logs
JOIN logs_pivot
ON logs.id = logs_pivot.log_id
WHERE logs_pivot.model_id = 'some_id'
AND model_type = 'My\Class'
ORDER BY logs.date DESC
LIMIT 50
) id_set ON logs.id = id_set.id
ORDER BY date DESC
最后,如果您知道自己只关心比某个特定时间更新的行,则可以将类似的内容添加到您的子查询中。
AND logs.date >= NOW() - INTERVAL 5 DAY
如果您的 table 中有大量历史数据,这将有很大帮助。