个人主页:小韩学长yyds-CSDN博客
⛺️ 欢迎关注:点赞 留言 收藏
箴言:拥有耐心才是生活的关键
目录
引言
MySQL 查询基础架构
Server 层
存储引擎层
查询详细过程实战
连接数据库
查询缓存机制(MySQL 8.0 前)
SQL 解析与执行
不同情况下的查询技巧
单表查询
多表查询
嵌套查询与子查询
复杂条件组合查询
查询性能优化策略
索引优化
查询优化技巧
数据库配置与其他策略
总结与展望
在 Java 开发的广阔领域中,与数据库的交互是后端开发的核心任务之一,而 MySQL 作为最常用的关系型数据库,其数据查询操作更是重中之重。对于 Java 程序员而言,熟练掌握 MySQL 数据查询,就如同剑客精通剑术,是在编程江湖中立足的必备技能。无论是小型项目还是大型企业级应用,从简单的数据检索到复杂的多表关联查询,MySQL 数据查询贯穿始终,它直接影响着系统的数据获取效率和用户体验。良好的查询设计可以使系统快速响应用户请求,而糟糕的查询则可能导致系统性能瓶颈,甚至影响业务的正常运转。接下来,就让我们深入探索 MySQL 数据查询的世界,从基础到进阶,全面掌握这一关键技能。
在深入学习 MySQL 数据查询之前,了解其查询基础架构是十分必要的,这有助于我们理解查询语句在数据库中的执行流程,从而更好地进行查询优化。MySQL 的架构主要分为两层:Server 层和存储引擎层 ,每一层都有其独特的功能和作用。
Server 层是 MySQL 的核心服务层,负责处理 SQL 语句层面的各种操作,包括连接管理、查询缓存(MySQL 8.0 之后已移除)、SQL 解析、查询优化以及执行等。
- 连接处理器:负责管理客户端与 MySQL 服务器的连接。当客户端发起连接请求时,连接处理器首先进行 TCP 握手,建立连接。然后,它会对客户端提供的用户名和密码进行身份认证,如果认证失败,将拒绝连接;若认证成功,则从权限表中获取该用户的权限信息。此后,该连接的权限判断都基于此时获取的权限。连接建立后,如果客户端长时间没有活动,连接器会根据wait_timeout参数(默认为 8 小时)自动断开连接 。在实际应用中,为了减少连接建立的开销,通常会使用长连接,但要注意长连接可能导致内存占用过高的问题,可以通过定期断开长连接或使用mysql_reset_connection(MySQL 5.7 及以上版本)来解决。
- 查询缓存(MySQL 8.0 之后已移除):在 MySQL 8.0 之前,查询缓存用于缓存 SELECT 语句及其结果。当接收到查询请求时,MySQL 会先检查查询缓存,看是否存在相同的查询语句。如果存在,则直接返回缓存的结果,而无需执行后续的解析、优化和执行步骤,从而大大提高查询效率。然而,查询缓存的命中率受多种因素影响,例如查询语句的微小差异(包括空格、注释、大小写等)都会导致缓存无法命中,而且当表数据发生变化(如 INSERT、UPDATE、DELETE 等操作)时,该表相关的所有查询缓存都会被清空。由于维护查询缓存的成本较高,且在实际应用中效果并不理想,MySQL 8.0 将其彻底移除。
- 解析器:解析器负责对客户端传来的 SQL 语句进行语法解析和词法解析。语法解析检查 SQL 语句的语法是否正确,比如括号是否匹配、引号是否闭合等;词法解析则将 SQL 语句中的关键词、表名、字段名等拆分成一个个节点,最终生成一颗解析树。例如,对于 SQL 语句SELECT name, age FROM users WHERE age > 18;,解析器会识别出SELECT、FROM、WHERE等关键词,以及name、age、users等表名和字段名,并构建出相应的解析树。此外,解析器还会进行预处理器操作,进一步检查解析树的语义是否正确,如表和字段是否存在等。
- 查询优化器:查询优化器的作用是根据解析器生成的解析树,生成多种可能的执行计划,并选择其中成本最小的执行计划。在选择执行计划时,查询优化器会考虑多种因素,比如表中有多个索引时,决定使用哪个索引;在多表关联查询时,确定各个表的连接顺序。例如,对于查询语句SELECT * FROM orders o JOIN customers c ON o.customer_id = c.id WHERE o.order_date > '2023-01-01';,查询优化器需要决定是先扫描orders表还是customers表,以及使用何种索引来加快连接和筛选操作。查询优化器采用基于开销(cost)的优化方式,通过评估不同执行计划的 CPU 成本和 I/O 成本,选择成本最低的执行计划,但有时这个 “最优” 计划不一定是绝对最优的,还需要结合实际情况和 SQL 语句的书写合理性来综合考虑。
- 执行器:执行器负责按照查询优化器生成的执行计划,调用存储引擎提供的接口来执行 SQL 语句。在执行之前,执行器会先检查用户对相关表是否具有执行查询的权限。如果有权限,则开始执行查询,并将结果返回给客户端;如果没有权限,则返回权限不足的错误信息。在执行过程中,执行器会与存储引擎频繁交互,获取数据并进行处理 。
存储引擎层负责数据的存储和读取,它是 MySQL 的插件式组件,不同的存储引擎具有不同的数据存储和查询方式,但都向 Server 层提供统一的接口。常见的存储引擎有 InnoDB、MyISAM、Memory 等,它们各自有其特点和适用场景。
- InnoDB:是 MySQL 5.5 及之后的默认存储引擎,具有事务支持、行级锁、外键约束等特性。它支持 ACID 事务,能保证数据的完整性和一致性,适用于对数据一致性要求较高的场景,如银行转账、电商订单处理等。InnoDB 使用行级锁,大大提高了并发访问性能,减少了锁冲突。同时,它支持外键约束,可以确保数据的参照完整性。在存储结构上,InnoDB 将数据和索引存储在同一个文件(.ibd文件)中,采用聚簇索引,主键索引和数据存储在一起,这使得基于主键的查询效率非常高。
- MyISAM:曾经是 MySQL 的默认存储引擎,它不支持事务和外键,采用表级锁。MyISAM 的优势在于结构简单,访问速度快,适用于读密集型的场景,如数据仓库、日志存储系统等。它支持全文索引,对于需要进行全文搜索的应用比较友好。在存储结构上,MyISAM 将数据和索引分别存储在不同的文件中(.MYD文件存储数据,.MYI文件存储索引) 。不过,由于表级锁的限制,MyISAM 在高并发写操作时性能较差。
- Memory:将数据存储在内存中,因此读写速度非常快,但数据不具有持久性,服务器重启后数据会丢失。Memory 存储引擎适用于临时数据处理、缓存数据等场景,如存储临时表或会话数据。它采用表级锁,并且默认使用哈希索引,这使得等值查询效率很高,但不支持范围查询。
当执行器执行查询时,会调用存储引擎的接口来获取数据。存储引擎根据自身的存储结构和算法,从磁盘或内存中读取数据,并返回给执行器。例如,对于 InnoDB 存储引擎,执行器调用其接口后,InnoDB 会根据查询条件在聚簇索引或二级索引中查找数据,如果是基于主键的查询,直接在聚簇索引中查找;如果是基于非主键索引的查询,先在二级索引中找到对应的主键值,然后再通过主键值在聚簇索引中查找完整的数据行,这个过程称为 “回表” 。而 MyISAM 存储引擎则会在.MYD文件中读取数据,在.MYI文件中查找索引。
在 Java 中,使用 JDBC(Java Database Connectivity)来连接 MySQL 数据库是最常见的方式。首先,需要在项目中添加 MySQL JDBC 驱动的依赖,比如使用 Maven 项目,可以在pom.xml文件中添加如下依赖:
mysql
mysql-connector-java
8.0.33
在代码中,通过以下步骤建立连接:
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class DatabaseConnection {
public static Connection getConnection() {
String url = "jdbc:mysql://localhost:3306/your_database_name";
String username = "your_username";
String password = "your_password";
Connection connection = null;
try {
// 加载MySQL驱动
Class.forName("com.mysql.cj.jdbc.Driver");
// 建立连接
connection = DriverManager.getConnection(url, username, password);
} catch (SQLException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return connection;
}
}
这里的DriverManager.getConnection(url, username, password)方法负责与 MySQL 服务器进行 TCP 握手,建立连接,并进行身份验证。如果连接成功,会返回一个Connection对象,后续的查询操作都将基于这个连接进行 。如果使用连接池技术,如 HikariCP,配置和使用会更加复杂一些,但能显著提高连接管理的效率和性能。例如,使用 HikariCP 连接池:
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.SQLException;
public class HikariCPConnection {
private static HikariDataSource dataSource;
static {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/your_database_name");
config.setUsername("your_username");
config.setPassword("your_password");
// 其他配置参数,如最大连接数、最小空闲连接数等
config.setMaximumPoolSize(10);
config.setMinimumIdle(5);
dataSource = new HikariDataSource(config);
}
public static Connection getConnection() {
try {
return dataSource.getConnection();
} catch (SQLException e) {
e.printStackTrace();
return null;
}
}
}
连接池会预先创建一定数量的连接,当应用程序需要连接时,直接从连接池中获取,而不是每次都重新建立连接,大大减少了连接建立的开销,提高了系统的响应速度 。
在 MySQL 8.0 之前,查询缓存是一个重要的性能优化机制。当执行一个查询时,MySQL 会先计算查询语句的哈希值,然后用这个哈希值在查询缓存中查找是否有对应的结果。如果找到,则直接返回缓存的结果,跳过解析、优化和执行步骤。例如:
SELECT name, age FROM users WHERE age > 18;
当第一次执行这个查询时,如果查询缓存未命中,MySQL 会执行完整的查询流程,然后将查询结果和查询语句以键值对的形式缓存起来,其中查询语句是键,查询结果是值 。下次再执行相同的查询时,就能命中缓存,快速返回结果。然而,查询缓存有其局限性。首先,它对查询语句的匹配非常严格,即使两个查询语句只是空格或注释不同,也被视为不同的查询,无法命中缓存。其次,当表数据发生变化时,如执行INSERT、UPDATE、DELETE操作,该表相关的所有查询缓存都会被清空,这在数据更新频繁的场景下,会导致查询缓存的频繁失效,反而降低性能。例如,在一个电商系统中,商品表的数据经常更新,如果对商品查询使用查询缓存,每次商品信息更新后,缓存就会失效,无法发挥缓存的优势。由于这些问题,MySQL 8.0 将查询缓存彻底移除。在实际开发中,如果需要缓存查询结果,可以考虑使用 Redis 等外部缓存工具,它们提供了更灵活和高效的缓存管理机制。例如,在 Java 中使用 Redis 缓存查询结果:
import redis.clients.jedis.Jedis;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class RedisCachedQuery {
private static final Jedis jedis = new Jedis("localhost", 6379);
public static String getUsersFromCacheOrDB() {
String cacheKey = "users_above_18";
String result = jedis.get(cacheKey);
if (result != null) {
return result;
}
// 从数据库查询
Connection connection = DatabaseConnection.getConnection();
try {
PreparedStatement statement = connection.prepareStatement("SELECT name, age FROM users WHERE age > 18");
ResultSet resultSet = statement.executeQuery();
StringBuilder data = new StringBuilder();
while (resultSet.next()) {
String name = resultSet.getString("name");
int age = resultSet.getInt("age");
data.append(name).append(",").append(age).append("\n");
}
result = data.toString();
// 将结果存入Redis缓存
jedis.set(cacheKey, result);
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
return result;
}
}
通过这种方式,将查询结果缓存到 Redis 中,利用 Redis 的高性能和灵活的缓存策略,提高查询性能 。
当查询缓存未命中(或在 MySQL 8.0 及之后没有查询缓存时),MySQL 会对 SQL 语句进行解析和执行。以查询语句SELECT name, age FROM users WHERE age > 18;为例,解析器首先进行词法解析,将 SQL 语句拆分成一个个词法单元,如SELECT、name、age、FROM、users、WHERE、age、>、18等,并识别出它们的类型,比如SELECT是关键字,name和age是字段名,users是表名等。然后进行语法解析,检查这些词法单元组成的语句是否符合 MySQL 的语法规则,生成一棵解析树。接下来,查询优化器会根据解析树生成执行计划。在这个查询中,如果users表的age字段上有索引,查询优化器会考虑使用该索引来加快查询速度。它会评估不同执行计划的成本,比如使用全表扫描和使用索引扫描的成本,选择成本最低的执行计划 。假设选择了使用索引扫描的执行计划,执行器会根据这个计划调用存储引擎(如 InnoDB)的接口来执行查询。执行器先检查用户对users表是否有查询权限,如果有权限,则开始执行。它会从索引中读取满足age > 18条件的记录的主键值,然后通过主键值在聚簇索引中查找对应的完整数据行,获取name和age字段的值,并将结果返回给客户端 。在 Java 中,通过 JDBC 执行上述查询的代码如下:
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class UserQuery {
public static void main(String[] args) {
Connection connection = DatabaseConnection.getConnection();
try {
PreparedStatement statement = connection.prepareStatement("SELECT name, age FROM users WHERE age > 18");
ResultSet resultSet = statement.executeQuery();
while (resultSet.next()) {
String name = resultSet.getString("name");
int age = resultSet.getInt("age");
System.out.println("Name: " + name + ", Age: " + age);
}
} catch (SQLException e) {
e.printStackTrace();
} finally {
if (connection != null) {
try {
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
}
这段代码通过PreparedStatement对象执行 SQL 查询,并处理结果集,将符合条件的用户信息打印出来 。
SELECT * FROM users;
查询特定列,列出需要的列名。比如只查询用户的name和age:
SELECT name, age FROM users;
SELECT * FROM users WHERE id = 1;
SELECT * FROM users WHERE age > 20;
SELECT * FROM users WHERE age > 20 AND email LIKE '%@gmail.com';
查询年龄大于 20 岁或者邮箱以@yahoo.com结尾的用户:
SELECT * FROM users WHERE age > 20 OR email LIKE '%@yahoo.com';
查询年龄不大于 20 岁的用户:
SELECT * FROM users WHERE NOT age > 20;
SELECT * FROM users WHERE name LIKE '张%';
查询名字是三个字符且第二个字符为 “小” 的用户:
SELECT * FROM users WHERE name LIKE '_小_';
SELECT * FROM users WHERE id IN (1, 3, 5);
查询id不在 1、3、5 中的用户:
SELECT * FROM users WHERE id NOT IN (1, 3, 5);
SELECT * FROM users ORDER BY age ASC;
按照年龄降序,若年龄相同则按照id升序查询用户:
SELECT * FROM users ORDER BY age DESC, id ASC;
SELECT * FROM users LIMIT 5;
查询第 6 到第 10 个用户(偏移量从 0 开始):
SELECT * FROM users LIMIT 5 OFFSET 5;
SELECT e.employee_id, e.employee_name, d.department_name
FROM employees e, departments d
WHERE e.department_id = d.department_id;
SELECT e.employee_name, s.salary_grade
FROM employees e, salaries s
WHERE e.salary BETWEEN s.min_salary AND s.max_salary;
SELECT e.employee_id, e.employee_name, d.department_name
FROM employees e
JOIN departments d ON e.department_id = d.department_id;
SELECT e.employee_name AS employee, m.employee_name AS manager
FROM employees e
JOIN employees m ON e.manager_id = m.employee_id;
SELECT e.employee_id, e.employee_name, d.department_name
FROM employees e
LEFT JOIN departments d ON e.department_id = d.department_id;
SELECT e.employee_id, e.employee_name, d.department_name
FROM employees e
RIGHT JOIN departments d ON e.department_id = d.department_id;
(SELECT e.employee_id, e.employee_name, d.department_name
FROM employees e
LEFT JOIN departments d ON e.department_id = d.department_id)
UNION
(SELECT e.employee_id, e.employee_name, d.department_name
FROM employees e
RIGHT JOIN departments d ON e.department_id = d.department_id);
SELECT c.customer_name, sub.order_count
FROM customers c
JOIN (
SELECT customer_id, COUNT(*) AS order_count
FROM orders
GROUP BY customer_id
) sub ON c.customer_id = sub.customer_id;
SELECT *
FROM orders
WHERE order_amount > (
SELECT AVG(order_amount)
FROM orders
);
SELECT employee_name, salary, (
SELECT AVG(salary)
FROM employees
WHERE department_id = e.department_id
) AS department_avg_salary
FROM employees e;
SELECT *
FROM employees
WHERE age > 25
AND (work_experience > 3 OR position = '经理');
SELECT *
FROM employees
WHERE department_id IN (
SELECT department_id
FROM departments
WHERE department_name IN ('销售部', '市场部')
);
查询不在 “销售部” 和 “市场部” 的员工:
SELECT *
FROM employees
WHERE department_id NOT IN (
SELECT department_id
FROM departments
WHERE department_name IN ('销售部', '市场部')
);
CREATE UNIQUE INDEX idx_users_email ON users (email);
这样,当插入新用户时,如果email已存在,数据库会立即报错,保证了数据的完整性。在查询时,基于email的查询可以快速定位到唯一的记录。唯一索引的优点是能有效保证数据的唯一性,减少数据重复的风险;缺点是插入和更新操作时,如果违反唯一性约束,会导致操作失败,需要额外的错误处理。
CREATE INDEX idx_orders_user_date ON orders (user_id, order_date);
复合索引遵循最左前缀原则,即查询条件必须从索引的最左前列开始,并且不跳过索引中的列,才能有效利用索引。例如,SELECT * FROM orders WHERE user_id = 123 AND order_date >= '2023-01-01';这样的查询可以利用上述复合索引,但SELECT * FROM orders WHERE order_date >= '2023-01-01';(跳过了user_id列)则无法利用该索引。复合索引的优点是能有效加速多列条件的查询;缺点是创建和维护成本较高,且如果使用不当(如不遵循最左前缀原则),索引可能失效。
CREATE INDEX idx_products_name_price ON products (product_name, price, product_id);
这里将product_id也包含在索引中,是因为在 InnoDB 存储引擎中,二级索引的叶子节点存储的是主键值,查询结果需要返回product_id,所以将其包含在索引中,实现覆盖索引。覆盖索引的优点是减少了磁盘 I/O 操作,提高查询速度;缺点是会增加索引的大小,占用更多磁盘空间。
CREATE INDEX idx_customers_id ON customers (customer_id);
这样,查询SELECT * FROM customers WHERE customer_id = 123;就可以利用索引快速定位到记录,而不是扫描整个表。另外,避免在索引列上使用函数或表达式,因为这会导致索引失效,引发全表扫描。例如,SELECT * FROM orders WHERE YEAR(order_date) = 2023;(YEAR函数导致索引失效)应改为SELECT * FROM orders WHERE order_date >= '2023-01-01' AND order_date < '2024-01-01';。
SELECT * FROM users LIMIT 10 OFFSET 10;
但是,当OFFSET值很大时,这种方式性能会下降,因为 MySQL 需要先扫描前面的OFFSET条记录,然后再返回后面的LIMIT条记录。对于这种情况,可以使用书签记录上次查询的位置,然后基于书签进行查询,提高性能。例如,假设users表按id升序排列,上次查询到的最大id是 100,查询下一页:
SELECT * FROM users WHERE id > 100 LIMIT 10;
EXPLAIN SELECT * FROM products WHERE category = 'electronics' AND price > 100;
EXPLAIN的输出结果包含多个字段,如id(查询的标识符)、select_type(查询类型)、table(查询涉及的表)、type(连接类型,如ALL表示全表扫描,index表示索引扫描,range表示范围扫描等)、possible_keys(可能使用的索引)、key(实际使用的索引)、key_len(索引长度)、ref(与索引比较的列)、rows(估计需要扫描的行数)、Extra(额外信息,如Using where表示使用了WHERE条件过滤,Using index表示使用了覆盖索引等)。通过分析这些字段,可以判断查询是否使用了合适的索引,是否存在全表扫描等问题,并进行相应的优化。
[mysqld]
innodb_buffer_pool_size = 2G
一般来说,对于专用的 MySQL 服务器,可以将innodb_buffer_pool_size设置为物理内存的 50% - 80%,具体数值需要根据服务器内存大小、数据量和并发访问情况等因素进行调整。
CREATE INDEX idx_orders_customer_id ON orders (customer_id);
CREATE INDEX idx_customers_customer_id ON customers (customer_id);
此外,尽量减少 JOIN 的表数量,避免不必要的复杂 JOIN 操作,因为每增加一个 JOIN 的表,查询的复杂度和执行时间都会增加。在进行 JOIN 时,合理选择 JOIN 类型(如内连接、左连接、右连接等),根据业务需求确保获取正确的数据。
INSERT INTO orders_archive
SELECT * FROM orders WHERE order_date < CURDATE() - INTERVAL 1 YEAR;
DELETE FROM orders WHERE order_date < CURDATE() - INTERVAL 1 YEAR;
对于大数据表,可以使用分区技术,将表按照某个规则(如时间、地理位置等)划分为多个分区,查询时只扫描相关分区,减少数据扫描范围。例如,按月份对orders表进行分区:
CREATE TABLE orders (
order_id INT,
order_date DATE,
amount DECIMAL(10, 2)
)
PARTITION BY RANGE (YEAR(order_date) * 100 + MONTH(order_date)) (
PARTITION p0 VALUES LESS THAN (202301),
PARTITION p1 VALUES LESS THAN (202302),
PARTITION p2 VALUES LESS THAN (202303),
-- 以此类推
);
这样,当查询某个月的订单时,只需要扫描对应的分区,而不是整个表。
[mysqld]
log-bin=mysql-bin
server-id=1
在从库配置文件中设置:
[mysqld]
server-id=2
然后在从库上执行相关命令,配置主从复制关系,如CHANGE MASTER TO命令指定主库的地址、用户名、密码和日志文件等信息。通过这种方式,读请求可以被分发到多个从库上,提高系统的并发处理能力。
MySQL 数据查询是 Java 开发中与数据库交互的核心技能,从基础的连接数据库、查询缓存(MySQL 8.0 前)、SQL 解析与执行,到不同情况下的单表查询、多表查询、嵌套查询和复杂条件组合查询,再到查询性能优化的索引优化、查询优化技巧、数据库配置与其他策略,每一个环节都至关重要 。通过合理运用这些知识和技巧,可以提高数据查询的效率,提升系统性能,为用户提供更优质的服务。
在实际开发中,数据查询的场景复杂多变,需要我们不断学习和实践,根据具体的业务需求和数据特点,灵活选择合适的查询方式和优化策略。同时,随着技术的不断发展,MySQL 数据库也在持续更新和演进,新的特性和功能不断涌现,我们要保持学习的热情,关注数据库技术的最新动态,不断提升自己在 MySQL 数据查询方面的能力 。相信通过持续的学习和实践,每一位 Java 程序员都能在 MySQL 数据查询的领域中得心应手,为开发出高效、稳定的应用系统贡献自己的力量。
结语
如果此文对你有帮助的话,欢迎关注、点赞、⭐收藏、✍️评论,支持一下博主~