MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

MySQL查询优化与执行计划分析

2021-12-251.1k 阅读

MySQL查询优化基础

在深入探讨MySQL查询优化与执行计划分析之前,我们先来明确一些基础概念。查询优化的核心目标是让数据库在尽可能短的时间内、使用最少的资源返回正确的查询结果。这对于处理大量数据以及高并发的应用场景至关重要。

MySQL查询优化主要从以下几个方面入手:

  1. 索引优化:索引就像是书籍的目录,能帮助MySQL快速定位到所需的数据行。合理创建和使用索引可以显著提升查询性能。例如,在一个用户表users中,包含id(主键)、nameemail等字段。如果经常根据name字段进行查询,那么在name字段上创建索引可以加快查询速度。
-- 创建普通索引
CREATE INDEX idx_name ON users(name);
  1. 查询语句优化:编写高效的查询语句是优化的关键。避免使用会导致全表扫描的语句,尽量使用覆盖索引查询等技术。比如,在查询users表中name为特定值的用户信息时,使用如下查询:
SELECT id, name, email FROM users WHERE name = 'John';

如果name字段上有索引,MySQL可以利用索引快速定位到满足条件的行,而不需要扫描整个表。

执行计划分析基础

执行计划是MySQL在执行查询语句前生成的一份详细的查询执行策略。通过分析执行计划,我们可以深入了解MySQL是如何执行查询的,从而找出性能瓶颈并进行优化。

获取执行计划的方法很简单,在查询语句前加上EXPLAIN关键字即可。例如:

EXPLAIN SELECT id, name, email FROM users WHERE name = 'John';

执行上述语句后,MySQL会返回一个结果集,包含了该查询的执行计划信息。主要字段如下:

  1. id:查询的标识符。如果是复杂查询,可能会有多个id值,表示不同的查询块执行顺序。
  2. select_type:表示查询的类型,常见的有SIMPLE(简单查询,不包含子查询或联合查询)、PRIMARY(最外层的查询)、SUBQUERY(子查询)等。
  3. table:表示当前执行计划对应的表名。
  4. partitions:如果表是分区表,这里会显示查询涉及的分区。
  5. type:非常重要的字段,描述了表的连接类型。常见类型有:
    • ALL:全表扫描,性能最差。例如查询SELECT * FROM users;就可能是这种类型。
    • index:索引扫描,遍历整个索引树。如果查询SELECT id FROM users;id是索引字段,可能是这种类型。
    • range:范围扫描,通常用于有范围条件(如BETWEEN><等)的查询。例如SELECT * FROM users WHERE id BETWEEN 1 AND 10;
    • ref:使用非唯一索引进行等值匹配。比如SELECT * FROM users WHERE name = 'John';name上有非唯一索引。
    • eq_ref:在连接查询中,使用唯一索引进行等值匹配。常见于多表连接时主键或唯一键的匹配。
    • const:常量查询,查询结果最多只有一行,通常是对主键或唯一键的等值查询。如SELECT * FROM users WHERE id = 1;
  6. possible_keys:显示可能用于查询的索引。
  7. key:实际使用的索引。如果为NULL,表示没有使用索引。
  8. key_len:表示使用的索引长度。
  9. ref:显示哪些列或常量被用于查找索引列上的值。
  10. rows:估计需要扫描的行数。这是一个估算值,不一定准确,但能反映查询的大致工作量。
  11. filtered:表示通过条件过滤后保留的行数百分比。

基于索引的查询优化

索引的类型与选择

MySQL支持多种类型的索引,除了前面提到的普通索引,还有主键索引、唯一索引、全文索引等。

  1. 主键索引:每个表只能有一个主键索引,它不仅保证了数据的唯一性,还能加快对主键字段的查询。主键索引通常是聚簇索引,即数据行的物理存储顺序与主键索引顺序一致。例如,在orders表中,order_id作为主键:
CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    order_date DATE,
    customer_id INT
);
  1. 唯一索引:确保索引列的值唯一,但可以有多个唯一索引。如果我们希望customers表中的email字段唯一,可以创建唯一索引:
CREATE UNIQUE INDEX idx_email ON customers(email);
  1. 全文索引:适用于文本类型字段的全文搜索。例如在article表的content字段上创建全文索引:
ALTER TABLE article ADD FULLTEXT(content);

全文索引在处理长文本搜索时性能要优于普通索引,因为它采用了更复杂的文本分析算法。

在选择索引类型时,要根据查询需求来决定。如果是简单的等值查询,普通索引或唯一索引可能就足够;如果是全文搜索,则需要使用全文索引。

索引的创建与维护

  1. 创建索引的时机:在设计数据库表结构时,就要考虑哪些字段可能会用于查询条件,提前为这些字段创建索引。例如,在一个电商系统中,products表经常根据category_idprice进行查询,那么可以在这两个字段上创建索引:
CREATE INDEX idx_category_price ON products(category_id, price);
  1. 复合索引:上面创建的idx_category_price就是一个复合索引。复合索引的顺序很重要,遵循“最左前缀原则”。即复合索引在查询时,只有当查询条件从最左边的字段开始连续使用时,索引才能发挥作用。例如,对于复合索引(category_id, price),查询SELECT * FROM products WHERE category_id = 1 AND price > 100;可以使用索引,而SELECT * FROM products WHERE price > 100;则不能使用该复合索引,因为没有从最左边的category_id开始。
  2. 索引的维护:随着数据的插入、更新和删除,索引可能会变得碎片化,影响性能。可以使用OPTIMIZE TABLE语句来优化表和索引,例如:
OPTIMIZE TABLE products;

此外,定期检查哪些索引没有被使用,可以考虑删除这些无用索引,以减少索引维护的开销。

多表连接查询优化

连接类型与优化

  1. 内连接(INNER JOIN):这是最常用的连接类型,返回两个表中满足连接条件的所有行。例如,有orders表和customers表,通过customer_id进行连接查询:
SELECT * FROM orders
INNER JOIN customers ON orders.customer_id = customers.customer_id;

在执行计划中,内连接的性能取决于连接字段上是否有索引。如果连接字段上有适当的索引,MySQL可以快速定位到匹配的行,减少数据扫描量。 2. 外连接(LEFT JOIN、RIGHT JOIN): - LEFT JOIN:返回左表中的所有行以及右表中满足连接条件的行。例如,要查询所有客户及其订单信息(包括没有订单的客户):

SELECT * FROM customers
LEFT JOIN orders ON customers.customer_id = orders.customer_id;
- **RIGHT JOIN**:与LEFT JOIN相反,返回右表中的所有行以及左表中满足连接条件的行。不过,在实际应用中,LEFT JOIN更为常用,因为可以通过调整表的顺序用LEFT JOIN实现RIGHT JOIN的功能。

外连接的优化同样依赖于连接字段的索引。此外,由于外连接可能会返回大量数据,要注意避免不必要的外连接,尽量只返回需要的字段。

连接顺序优化

在多表连接查询中,连接顺序对性能有很大影响。MySQL会尝试选择最优的连接顺序,但有时需要我们手动干预。一般原则是:

  1. 小表驱动大表:即把数据量小的表放在连接的左边(如果是LEFT JOIN)。例如,有一个departments表(数据量较小)和employees表(数据量较大),通过department_id连接:
SELECT * FROM departments
LEFT JOIN employees ON departments.department_id = employees.department_id;

这样MySQL在处理连接时,会先遍历departments表,对于每一行再去employees表中匹配,相比先遍历大表可以减少很多匹配次数。 2. 使用STRAIGHT_JOIN强制连接顺序:如果MySQL选择的连接顺序不是最优的,可以使用STRAIGHT_JOIN关键字强制连接顺序。例如:

SELECT * FROM departments
STRAIGHT_JOIN employees ON departments.department_id = employees.department_id;

这里强制先处理departments表,再与employees表连接。

子查询优化

子查询的类型与问题

  1. 子查询的类型
    • 标量子查询:返回单个值的子查询,通常用于比较条件中。例如,查询高于平均订单金额的订单:
SELECT * FROM orders
WHERE order_amount > (SELECT AVG(order_amount) FROM orders);
- **列子查询**:返回一列值的子查询,常用于`IN`或`NOT IN`条件中。比如,查询属于特定客户组的客户订单:
SELECT * FROM orders
WHERE customer_id IN (SELECT customer_id FROM customers WHERE customer_group = 'VIP');
- **行子查询**:返回一行数据的子查询,用于比较一行数据。
- **表子查询**:返回一个表结果集的子查询,可用于`FROM`子句中。

2. 子查询的性能问题:子查询在某些情况下性能较差,因为MySQL需要多次执行子查询。例如上面的标量子查询,MySQL首先要执行内层的SELECT AVG(order_amount) FROM orders查询,得到平均订单金额,然后再执行外层查询,根据这个平均金额过滤订单。如果数据量较大,这种多次执行的开销会很大。

子查询优化策略

  1. 将子查询转换为连接查询:对于很多子查询,可以通过连接查询来替代,以提高性能。例如,上面的查询高于平均订单金额的订单,可以改写为连接查询:
SELECT o1.*
FROM orders o1
JOIN (SELECT AVG(order_amount) AS avg_amount FROM orders) o2
ON o1.order_amount > o2.avg_amount;

这样,MySQL可以通过一次扫描orders表来完成查询,避免了多次执行子查询。 2. 使用临时表优化子查询:在某些复杂子查询中,可以将子查询结果存储在临时表中,然后再与其他表进行操作。例如:

CREATE TEMPORARY TABLE temp_customers AS
SELECT customer_id FROM customers WHERE customer_group = 'VIP';

SELECT * FROM orders
WHERE customer_id IN (SELECT customer_id FROM temp_customers);

使用临时表可以减少子查询的重复执行次数,提高整体性能。但要注意临时表的创建和维护开销,避免滥用。

其他查询优化技巧

避免函数操作在查询条件中

在查询条件中对字段使用函数操作会导致索引失效。例如,有一个users表,包含birth_date字段,要查询1980年以后出生的用户:

-- 错误示例,函数操作导致索引失效
SELECT * FROM users WHERE YEAR(birth_date) > 1980;

-- 正确示例,提前计算好范围
SELECT * FROM users WHERE birth_date >= '1980 - 01 - 01';

如果birth_date字段上有索引,第一种写法会使MySQL无法使用索引,只能进行全表扫描,而第二种写法可以利用索引快速定位到满足条件的行。

优化LIKE查询

LIKE查询在某些情况下性能较差,特别是当LIKE条件以通配符开头时。例如:

-- 性能较差,无法使用索引
SELECT * FROM products WHERE product_name LIKE '%keyword%';

-- 可以使用索引,性能较好
SELECT * FROM products WHERE product_name LIKE 'keyword%';

如果必须要以通配符开头进行查询,可以考虑使用全文索引,全文索引支持更高效的模糊搜索。

合理使用缓存

  1. 查询缓存:MySQL提供了查询缓存功能,可以缓存查询结果。当相同的查询再次执行时,直接从缓存中返回结果,而不需要重新执行查询。可以通过配置参数query_cache_typequery_cache_size来启用和设置查询缓存。例如:
-- 启用查询缓存
SET GLOBAL query_cache_type = 1;
SET GLOBAL query_cache_size = 64 * 1024 * 1024; -- 设置缓存大小为64MB

不过,查询缓存有一些局限性,比如当表数据发生变化时,相关的缓存会被清除。所以在数据更新频繁的场景下,查询缓存可能效果不佳。 2. 应用层缓存:除了MySQL自身的查询缓存,还可以在应用层使用缓存技术,如Redis。将经常查询且不频繁变化的数据缓存到Redis中,应用程序先从Redis中获取数据,如果没有再查询MySQL,并将结果存入Redis。这样可以大大减轻MySQL的压力,提高系统整体性能。

执行计划深度分析与优化案例

复杂查询的执行计划分析

假设有一个电商数据库,包含orders(订单表)、order_items(订单项表)、products(产品表)和customers(客户表)。现在要查询每个客户的总订单金额以及购买的产品数量,并按总订单金额降序排列:

EXPLAIN
SELECT c.customer_name,
       SUM(oi.quantity * p.price) AS total_amount,
       COUNT(oi.product_id) AS product_count
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
GROUP BY c.customer_id, c.customer_name
ORDER BY total_amount DESC;

分析执行计划:

  1. id:通常为1,表示这是一个简单的查询块。
  2. select_type:为SIMPLE,因为没有子查询或联合查询。
  3. table:依次显示customersordersorder_itemsproducts,表示查询涉及的表。
  4. type:如果连接字段上有合适的索引,customers表可能是ALL(如果数据量较小)或indexorders表可能是ref(通过customer_id连接),order_items表可能是ref(通过order_id连接),products表可能是ref(通过product_id连接)。
  5. possible_keys:显示每个表可能用于连接的索引,如customers表的customer_id索引,orders表的customer_id索引等。
  6. key:实际使用的索引,若一切正常,应与possible_keys中的相关索引对应。
  7. rows:估算的扫描行数。如果某个表的rows值过大,可能表示索引使用不当或连接顺序不佳。

优化措施

  1. 索引优化:确保customers表的customer_idorders表的customer_idorder_idorder_items表的order_idproduct_idproducts表的product_id字段上都有索引。如果没有,可以创建相应的索引。
CREATE INDEX idx_customer_id ON customers(customer_id);
CREATE INDEX idx_customer_id_orders ON orders(customer_id);
CREATE INDEX idx_order_id_orders ON orders(order_id);
CREATE INDEX idx_order_id_order_items ON order_items(order_id);
CREATE INDEX idx_product_id_order_items ON order_items(product_id);
CREATE INDEX idx_product_id_products ON products(product_id);
  1. 连接顺序优化:根据“小表驱动大表”原则,检查当前的连接顺序是否最优。如果customers表数据量相对较小,可以确保它在连接的最左边。如果MySQL没有选择最优连接顺序,可以使用STRAIGHT_JOIN进行调整。
  2. 聚合优化:在进行GROUP BYORDER BY操作时,确保相关字段上有索引。例如,可以考虑在customers表的customer_idcustomer_name上创建复合索引,以优化GROUP BY操作。
CREATE INDEX idx_customer_id_name ON customers(customer_id, customer_name);

通过以上优化措施,再次分析执行计划,会发现rows值可能会降低,查询性能得到显著提升。

总结常见查询优化要点

  1. 索引方面:合理创建索引,遵循最左前缀原则,避免索引滥用和碎片化。定期维护索引,删除无用索引。
  2. 查询语句:编写高效的查询语句,避免在查询条件中使用函数操作,优化LIKE查询。对于子查询,尽量转换为连接查询或使用临时表优化。
  3. 连接查询:选择合适的连接类型,优化连接顺序,以小表驱动大表。
  4. 其他方面:合理使用缓存,无论是MySQL的查询缓存还是应用层缓存,以减轻数据库压力。同时,深入分析执行计划,根据执行计划中的信息针对性地进行优化。通过不断实践和优化,能够让MySQL数据库在处理各种查询时都保持高效的性能。