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

MySQL查询语句优化:避免使用SELECT *

2021-10-034.3k 阅读

为什么要避免使用 SELECT *

在MySQL数据库查询优化领域,“避免使用SELECT *”是一条重要的原则。虽然使用SELECT *看起来很方便,可以一次性获取表中的所有列,但实际上它存在诸多潜在问题,会对查询性能产生负面影响,同时在代码维护性等方面也带来不利。

数据传输开销增大

当使用SELECT *时,MySQL会将表中的所有列数据从存储引擎读取并通过网络传输给客户端。例如,假设我们有一个名为employees的表,结构如下:

CREATE TABLE employees (
    id INT PRIMARY KEY AUTO_INCREMENT,
    first_name VARCHAR(50),
    last_name VARCHAR(50),
    age INT,
    department VARCHAR(50),
    salary DECIMAL(10, 2),
    email VARCHAR(100),
    phone VARCHAR(20),
    address TEXT,
    hire_date DATE,
    bio TEXT,
    photo BLOB
);

如果执行SELECT * FROM employees;,像addressbiophoto这样的大字段数据也会被传输。假设photo字段存储的是员工头像图片,可能达到几MB大小。在网络环境不佳的情况下,这会显著增加数据传输时间,拖慢查询速度。

相比之下,如果我们只需要获取员工的基本信息,如姓名、年龄和部门,可以使用:

SELECT first_name, last_name, age, department FROM employees;

这样大大减少了传输的数据量,提高了查询效率。

索引利用不充分

MySQL的查询优化器依赖索引来快速定位数据。当使用SELECT *时,优化器可能无法有效地利用索引。例如,假设有一个orders表,有order_idcustomer_idorder_datetotal_amount等列,并且在customer_id列上有索引。如果我们执行查询SELECT * FROM orders WHERE customer_id = 123;,由于需要返回所有列,而索引中可能不包含所有列的数据,MySQL可能需要进行回表操作。

回表操作是指通过索引找到数据的主键,然后再根据主键去聚簇索引(通常是主键索引)中获取完整的行数据。这增加了查询的I/O操作次数,降低了查询性能。而如果我们只查询需要的列,如SELECT order_id, order_date, total_amount FROM orders WHERE customer_id = 123;,并且这些列都包含在索引中(覆盖索引),MySQL可以直接从索引中获取数据,避免回表操作,大大提高查询效率。

结果集不稳定导致维护问题

使用SELECT *会使结果集的列顺序与表定义的列顺序一致。然而,表结构可能会随着业务发展而改变。例如,在products表中,最初有product_idproduct_nameprice列,执行SELECT * FROM products;会按此顺序返回列。但如果后续添加了description列,结果集的列顺序就发生了变化。

在应用程序中,如果代码依赖于结果集的列顺序进行处理,如通过索引获取列值:

import mysql.connector

mydb = mysql.connector.connect(
  host="localhost",
  user="youruser",
  password="yourpassword",
  database="yourdatabase"
)

mycursor = mydb.cursor()
mycursor.execute("SELECT * FROM products")
result = mycursor.fetchone()
product_name = result[1]  # 依赖列顺序获取产品名称

当表结构改变后,product_name对应的索引位置可能会改变,导致程序出错。而如果明确指定列名SELECT product_id, product_name, price FROM products;,则不会受到表结构变化的影响,提高了代码的稳定性和可维护性。

隐藏的性能陷阱

在一些复杂的查询场景中,SELECT *可能会带来隐藏的性能问题。例如,在多表连接查询中:

CREATE TABLE orders (
    order_id INT PRIMARY KEY AUTO_INCREMENT,
    customer_id INT,
    order_date DATE
);

CREATE TABLE order_items (
    item_id INT PRIMARY KEY AUTO_INCREMENT,
    order_id INT,
    product_id INT,
    quantity INT
);

SELECT * FROM orders
JOIN order_items ON orders.order_id = order_items.order_id;

这个查询会返回连接后所有表的所有列。如果order_items表有很多列,并且有些列是大字段,如产品描述等,会导致大量数据传输和处理。而且,优化器在处理这种复杂连接且使用SELECT *的查询时,可能难以生成最优的执行计划,进一步影响性能。

替代 SELECT * 的方法

明确指定所需列

最直接的替代方法就是明确列出需要查询的列。例如,在customers表中,若只需要获取客户的姓名和联系方式:

CREATE TABLE customers (
    customer_id INT PRIMARY KEY AUTO_INCREMENT,
    first_name VARCHAR(50),
    last_name VARCHAR(50),
    email VARCHAR(100),
    phone VARCHAR(20),
    address TEXT
);

SELECT first_name, last_name, email, phone FROM customers;

这样不仅减少了数据传输量,还能让查询优化器更好地利用索引,提高查询性能。同时,也避免了因表结构变化导致的结果集顺序问题,增强了代码的可维护性。

使用别名提高可读性

在明确指定列时,可以使用别名来提高查询的可读性。例如,在sales表中,有sale_idproduct_idquantity_soldtotal_amount等列。如果要查询产品的销售总量和总金额,并给列取更易懂的别名:

SELECT product_id, SUM(quantity_sold) AS total_quantity, SUM(total_amount) AS total_revenue
FROM sales
GROUP BY product_id;

这里使用AS关键字给聚合函数的结果取了别名total_quantitytotal_revenue,使查询结果更清晰易懂,无论是在开发过程中还是查看查询结果时都更加方便。

动态构建查询列

在一些动态查询场景中,如根据用户输入或业务逻辑决定查询哪些列,可以通过编程方式动态构建查询语句。例如,在Python中使用MySQL Connector库:

import mysql.connector

mydb = mysql.connector.connect(
  host="localhost",
  user="youruser",
  password="yourpassword",
  database="yourdatabase"
)

mycursor = mydb.cursor()

# 根据用户选择的列构建查询语句
selected_columns = ['first_name', 'last_name', 'email']
column_str = ', '.join(selected_columns)
query = f"SELECT {column_str} FROM customers"

mycursor.execute(query)
result = mycursor.fetchall()
for row in result:
    print(row)

这种方式灵活地根据实际需求选择查询列,既避免了SELECT *的问题,又能满足动态查询的业务场景。

对查询执行计划的影响

查看查询执行计划

MySQL提供了EXPLAIN关键字来查看查询的执行计划,这对于分析SELECT *与明确指定列查询的性能差异非常有帮助。例如,对于SELECT * FROM employees WHERE age > 30;,执行EXPLAIN SELECT * FROM employees WHERE age > 30;,会得到类似如下结果:

idselect_typetablepartitionstypepossible_keyskeykey_lenrefrowsfilteredExtra
1SIMPLEemployeesNULLALLNULLNULLNULLNULL100050.00Using where

这里typeALL,表示全表扫描,Extra中的Using where表示使用了WHERE条件进行过滤。

而对于SELECT first_name, last_name, age FROM employees WHERE age > 30;,执行EXPLAIN SELECT first_name, last_name, age FROM employees WHERE age > 30;,可能得到不同的结果。如果在age列上有索引,结果可能如下:

idselect_typetablepartitionstypepossible_keyskeykey_lenrefrowsfilteredExtra
1SIMPLEemployeesNULLrangeage_indexage_index4NULL500100.00Using index condition

这里type变为range,表示范围扫描,并且Extra中的Using index condition说明使用了索引条件,查询性能得到提升。

执行计划差异分析

从上述执行计划的差异可以看出,使用SELECT *时,MySQL可能无法充分利用索引,导致全表扫描,增加了I/O操作和查询时间。而明确指定列时,优化器可以根据索引情况选择更高效的执行计划,如范围扫描或覆盖索引扫描。

在多表连接查询中,SELECT *也可能影响优化器对连接顺序和连接类型的选择。例如,对于以下两个查询:

-- 使用SELECT *
SELECT * FROM orders
JOIN order_items ON orders.order_id = order_items.order_id
WHERE orders.customer_id = 123;

-- 明确指定列
SELECT orders.order_id, order_items.product_id, order_items.quantity
FROM orders
JOIN order_items ON orders.order_id = order_items.order_id
WHERE orders.customer_id = 123;

使用EXPLAIN查看执行计划,可能会发现SELECT *的查询执行计划中连接操作的成本更高,因为它需要处理更多的数据列,而明确指定列的查询执行计划更优化,连接操作成本相对较低。

在不同场景下的优化策略

单表查询优化

在单表查询中,除了避免SELECT *,还可以通过以下方式进一步优化。例如,对于频繁查询的条件列建立索引。假设在products表中,经常根据categoryprice进行查询:

CREATE TABLE products (
    product_id INT PRIMARY KEY AUTO_INCREMENT,
    product_name VARCHAR(50),
    category VARCHAR(50),
    price DECIMAL(10, 2),
    description TEXT
);

-- 建立联合索引
CREATE INDEX category_price_idx ON products (category, price);

然后在查询时,明确指定列并利用索引:

SELECT product_id, product_name, price
FROM products
WHERE category = 'electronics' AND price > 100;

这样可以大大提高查询性能。

多表连接查询优化

在多表连接查询中,除了避免SELECT *,合理选择连接类型也很重要。例如,对于orders表和customers表的连接查询:

CREATE TABLE orders (
    order_id INT PRIMARY KEY AUTO_INCREMENT,
    customer_id INT,
    order_date DATE
);

CREATE TABLE customers (
    customer_id INT PRIMARY KEY AUTO_INCREMENT,
    first_name VARCHAR(50),
    last_name VARCHAR(50)
);

-- 内连接
SELECT orders.order_id, customers.first_name, customers.last_name, orders.order_date
FROM orders
INNER JOIN customers ON orders.customer_id = customers.customer_id;

这里使用内连接只返回满足连接条件的行。如果使用外连接,如左连接LEFT JOIN,可能会返回更多的数据,影响性能。同时,确保连接条件列上有索引,如在orders表的customer_id列和customers表的customer_id列上建立索引,可以提高连接效率。

子查询优化

子查询在MySQL中也很常见,但使用不当会影响性能。例如,对于以下子查询:

SELECT product_id, product_name
FROM products
WHERE price > (SELECT AVG(price) FROM products);

这个子查询先计算出产品的平均价格,然后再筛选出价格高于平均价格的产品。可以将其改写为连接查询,性能可能会更好:

SELECT p1.product_id, p1.product_name
FROM products p1
JOIN (SELECT AVG(price) AS avg_price FROM products) p2
ON p1.price > p2.avg_price;

在子查询优化中,同样要避免在子查询中使用SELECT *,明确指定所需列,以减少数据处理量,提高查询性能。

数据库架构设计与查询优化的结合

合理的表设计

在数据库架构设计阶段,合理的表设计对于查询优化至关重要。例如,避免在表中创建过多不必要的列。如果某些数据不经常查询,或者可以通过其他数据计算得出,就不应该将其存储在表中。假设在employees表中,员工的工作年限可以通过hire_date和当前日期计算得出,就没必要单独创建一个years_of_service列。

同时,要根据查询需求设计表结构。如果经常需要按部门查询员工信息,可以在employees表中增加department列,并在该列上建立索引,方便查询优化。

分区表的应用

对于数据量较大的表,可以考虑使用分区表。例如,在sales表中,如果数据按月份增长很快,可以按月份对表进行分区:

CREATE TABLE sales (
    sale_id INT PRIMARY KEY AUTO_INCREMENT,
    product_id INT,
    quantity INT,
    sale_date DATE
)
PARTITION BY RANGE (YEAR(sale_date) * 100 + MONTH(sale_date)) (
    PARTITION p0 VALUES LESS THAN (202301),
    PARTITION p1 VALUES LESS THAN (202302),
    PARTITION p2 VALUES LESS THAN (202303),
    -- 以此类推
);

这样在查询特定月份的销售数据时,如SELECT * FROM sales WHERE sale_date BETWEEN '2023 - 02 - 01' AND '2023 - 02 - 28';,MySQL可以直接定位到对应的分区,而不需要扫描整个表,大大提高查询性能。不过,在使用分区表时,同样要避免使用SELECT *,明确指定所需列,减少数据传输和处理开销。

索引设计与查询需求匹配

索引是查询优化的重要手段,但过多或不合理的索引也会影响性能。索引设计要与查询需求紧密匹配。例如,如果经常执行类似SELECT product_id, product_name FROM products WHERE category = 'clothes' ORDER BY price DESC;的查询,就需要在categoryprice列上建立联合索引CREATE INDEX category_price_idx ON products (category, price DESC);,这样查询优化器可以利用索引快速定位数据并按指定顺序排序,提高查询效率。同时,要定期评估索引的使用情况,删除不再使用的索引,以减少索引维护开销。

性能监控与调优工具

MySQL自带工具

MySQL提供了一些自带的性能监控与调优工具。例如,SHOW STATUS命令可以查看服务器的状态信息,包括查询次数、连接数等。通过执行SHOW STATUS LIKE 'Questions';可以查看服务器执行的查询次数。SHOW VARIABLES命令可以查看服务器的配置变量,如SHOW VARIABLES LIKE 'innodb_buffer_pool_size';可以查看InnoDB存储引擎的缓冲池大小。

EXPLAIN命令前面已经提到,用于查看查询执行计划。通过分析执行计划,可以了解查询是否有效地利用了索引,是否存在全表扫描等问题,从而针对性地进行优化。

第三方工具

除了MySQL自带工具,还有一些第三方工具可用于性能监控与调优。例如,pt - query - digest是Percona Toolkit中的一个工具,可以分析MySQL查询日志,找出慢查询,并提供优化建议。假设我们有一个查询日志文件query.log,可以通过pt - query - digest query.log命令进行分析,它会输出查询的执行次数、平均执行时间、查询语句等信息,并指出可能的优化方向,如是否缺少索引等。

MySQL Enterprise Monitor是MySQL官方提供的企业级监控工具,它可以实时监控数据库的性能指标,如CPU使用率、内存使用率、查询响应时间等。通过图形化界面,可以直观地了解数据库的运行状态,及时发现性能问题并进行调优。

在实际应用中,结合使用MySQL自带工具和第三方工具,可以更全面地监控和优化数据库性能,确保在避免使用SELECT *的基础上,进一步提升查询效率。

在数据库查询优化的旅程中,避免使用SELECT *只是众多优化原则中的一项,但它却是一个容易被忽视却又对性能有显著影响的关键环节。通过明确指定所需列、合理设计数据库架构、结合性能监控与调优工具等一系列措施,我们能够打造出高性能、稳定可靠的MySQL数据库应用。