第45章 SQL 与数据库基础

第四十五章 SQL 与数据库基础

在程序员的世界里,有两大终极拷问:“我的代码为什么跑不起来"和"我的数据去哪儿了”。前者靠debug,后者靠——你猜对了——数据库。而SQL,就是你和数据库之间的"翻译官"。

45.1 SQL 基础

什么是SQL?

SQL(Structured Query Language),中文名"结构化查询语言"。别看名字里带"查询"俩字,它可不只是用来查数据的——增删改查,它全包了。SQL是数据库的官方语言,就像Java是JVM上的官方语言一样(这个比喻Java味很浓吧)。

打个比方:如果数据库是一栋大楼,SQL就是进出这栋大楼的钥匙和电梯卡。你可以新建房间(CREATE)、装修房间(UPDATE)、抄家搬空(DELETE)、或者只是趴在门口看看里面住着谁(SELECT)。

SQL的"门派"之分

SQL 有一套国际标准(ANSI SQL),但各家数据库在标准之上都有自己的"方言"。常见的主流数据库产品有:

  • MySQL —— 开源免费,小巧灵活,野路子出身但粉丝众多,互联网公司的最爱
  • Oracle —— 企业级大佬,稳定可靠,你值得拥有(如果你不差钱的话)
  • PostgreSQL —— 学霸型选手,功能全面,学术圈和高端项目常客
  • SQL Server —— 微软嫡系,Windows平台上的企业级首选

它们的 SQL 大体相通,但细节上各有脾气,比如:MySQL 用反引号 `列名` 包标识符,分页写 LIMIT offset, count;PostgreSQL 用双引号包标识符,分页写 LIMIT ... OFFSET;Oracle 用双引号包标识符,分页写 OFFSET ... FETCH FIRST n ROWS ONLY(12c 之前还得靠 ROWNUM 套子查询);SQL Server 分页用 OFFSET ... FETCH,取前几行还可以用 TOP n。所以换数据库时,“能跑的 SQL” 往往需要小改。

我们这套教程的示例,主要跑在MySQL上,因为它最亲民、最常见、最重要的是——免费!

SQL的基本语法规则

先看几条最基本的书写规则:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
-- SQL语句以分号结尾(大部分数据库都这样)
SELECT * FROM users;

-- 关键字不区分大小写,但业界习惯大写
select * from users;  -- 能跑,但不推荐
SELECT * FROM users;  -- 规范写法,看着就专业

-- 字符串用单引号包起来
SELECT * FROM users WHERE name = '张三';

-- 注释用 -- 开头,和Java的 // 一样
SELECT * FROM users WHERE id = 1;  -- 查询id为1的用户

小知识:SQL 的关键字不区分大小写(select、SELECT、SeLeCt 效果完全一样),所以上面两种写法数据库都认。但数据和标识符另说:字符串 '张三' 与 '張三' 当然不同;在 PostgreSQL 里,不加引号的表名/列名会被统一转成小写,加双引号才会区分大小写。为了代码可读性,请保持"关键字大写、标识符小写"的习惯。

45.2 表操作

创建表——给数据建房子

表(Table) 是数据库中最基本的数据存储单位,你可以把它想象成一张Excel表格——有行有列,有表头有数据。

1
2
3
4
5
6
7
8
-- 创建一个用户表
CREATE TABLE users (
    id INT,                -- 用户ID,整数类型
    username VARCHAR(50),  -- 用户名,最多50个字符
    email VARCHAR(100),    -- 邮箱
    age INT,               -- 年龄
    created_at DATETIME    -- 注册时间
);

建表的时候,你需要告诉数据库:

  • 字段叫什么名字
  • 字段存什么类型的数据(数据类型)
  • 字段能不能为空(NULL)
  • 字段要不要唯一(UNIQUE)
  • 字段有没有默认值(DEFAULT)

来一个更完善的版本:

1
2
3
4
5
6
7
CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,  -- 主键:每条记录的唯一身份证
    username VARCHAR(50) NOT NULL,      -- 不能为空
    email VARCHAR(100) UNIQUE,           -- 不能重复
    age INT DEFAULT 18,                 -- 默认18岁
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP  -- 默认当前时间
);

修改表——装修时间到

表建好了,想加个字段?改个名字?完全OK!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
-- 新增一列:给用户加个"手机号"字段
ALTER TABLE users ADD COLUMN phone VARCHAR(20);

-- 修改字段类型:把手机号改成可变长度
ALTER TABLE users MODIFY COLUMN phone VARCHAR(15);

-- 删除字段:不需要手机号了
ALTER TABLE users DROP COLUMN phone;

-- 表重命名:users太土了,改成registered_users
ALTER TABLE users RENAME TO registered_users;

删除表——拆迁队来了

删表会把结构和数据一起抹掉,属于最危险的 DDL 操作,执行前先确认表名和备份。

1
2
3
4
5
-- 删除整个表,包括数据和结构都没了!
DROP TABLE users;

-- 如果表不存在会报错,加个IF EXISTS更安全
DROP TABLE IF EXISTS users;

警告:这个操作是不可逆的!删表之前请三思三思再三思,必要时先备份。删库到跑路可不是闹着玩的。

45.3 查询

终于到了SQL的精髓所在——查询(SELECT)!如果说SQL是数据库的钥匙,那SELECT就是钥匙上最亮的那颗钻石。

基础查询

SELECT 是日常使用频率最高的语句,最基本的用法就是指定要取哪些列、从哪张表取。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
-- 查询所有用户的所有信息
SELECT * FROM users;

-- 查询指定列
SELECT username, email FROM users;

-- 给列取个中文别名,看着更亲切
SELECT username AS 用户名, email AS 邮箱 FROM users;

-- 如果别名里有空格或特殊字符,MySQL 用反引号包起来
SELECT username AS `用户 名` FROM users;

⚠️ 别用单引号当别名:AS '用户名' 在 MySQL 里"碰巧能用",但那是把字符串当别名用的历史遗留行为;在 PostgreSQL、Oracle 里会直接报错。要写可移植的 SQL,别名就别加引号,或者统一用各家的标识符引号(MySQL 反引号 / PostgreSQL 双引号)。

条件查询——WHERE的艺术

WHERE 负责筛选出满足条件的行,比较运算、区间判断、模糊匹配、逻辑组合都写在它后面。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
-- 查询年龄大于18的用户
SELECT * FROM users WHERE age > 18;

-- 查询年龄在18到25之间的用户(包含两端)
SELECT * FROM users WHERE age BETWEEN 18 AND 25;

-- 查询邮箱以163结尾的用户(模糊查询)
SELECT * FROM users WHERE email LIKE '%163.com';

-- 查询男性用户(假设有gender字段)
SELECT * FROM users WHERE gender = '男';

-- 多个条件组合:男性 且 年龄大于18
SELECT * FROM users WHERE gender = '男' AND age > 18;

-- 满足任意一个条件就行
SELECT * FROM users WHERE age < 18 OR age > 60;

排序——谁站C位

ORDER BY 决定结果集的排列顺序,可以按一列排,也可以按多列依次比较。

1
2
3
4
5
6
7
8
-- 按年龄升序排序(从小到大,数字小的在前)
SELECT * FROM users ORDER BY age ASC;

-- 按年龄降序排序(从大到小)
SELECT * FROM users ORDER BY age DESC;

-- 先按年龄降序,年龄相同的话按用户名升序
SELECT * FROM users ORDER BY age DESC, username ASC;

分页查询——数据太多了怎么办

数据量大时一次性取回所有行既慢又占内存,分页只取当前页需要的那一段。

1
2
3
4
5
6
7
8
9
--  LIMIT offset, count
-- 查询第1-10条记录(第一页)
SELECT * FROM users LIMIT 0, 10;

-- 查询第11-20条记录(第二页)
SELECT * FROM users LIMIT 10, 10;

-- MySQL特有的简写:只写数量,默认从0开始
SELECT * FROM users LIMIT 10;  -- 查询前10条

聚合函数——统计小能手

聚合函数就是对一组数据进行"打包处理"的函数。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
-- 统计用户总数
SELECT COUNT(*) FROM users;

-- 计算平均年龄
SELECT AVG(age) FROM users;

-- 求最大年龄和最小年龄
SELECT MAX(age), MIN(age) FROM users;

-- 计算年龄总和
SELECT SUM(age) FROM users;

分组查询——物以类聚

GROUP BY 先把行按某个字段归类,再对每一类分别做统计。

1
2
3
4
5
6
7
-- 按性别分组,统计每组的人数
SELECT gender, COUNT(*) FROM users GROUP BY gender;

-- 按城市分组,统计每个城市的用户数量,并按数量降序排列
SELECT city, COUNT(*) AS cnt FROM users 
GROUP BY city 
ORDER BY cnt DESC;

HAVING——分组后的过滤

WHERE和HAVING的区别你要搞清楚:

  • WHERE:过滤每一行数据,在分组之前起作用
  • HAVING:过滤分组后的结果,在分组之后起作用
1
2
3
4
5
-- 查询用户数量大于10的城市
SELECT city, COUNT(*) AS cnt 
FROM users 
GROUP BY city 
HAVING cnt > 10;

多表查询——表亲之间的走动

实际工作中,数据往往分散在多张表里,你需要把它们"连接"起来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
-- 假设有两张表:users(用户表)和 orders(订单表)
-- users: id, username
-- orders: id, user_id, amount

-- 内连接:只保留有关联的数据
SELECT u.username, o.amount 
FROM users u 
INNER JOIN orders o ON u.id = o.user_id;

-- 左连接:左边表的数据全部保留,右边没有匹配的显示NULL
SELECT u.username, o.amount 
FROM users u 
LEFT JOIN orders o ON u.id = o.user_id;

-- 右连接:右边表的数据全部保留
SELECT u.username, o.amount 
FROM users u 
RIGHT JOIN orders o ON u.id = o.user_id;
erDiagram
    USERS ||--o{ ORDERS : "一个用户可以有多个订单"
    USERS {
        int id PK "主键"
        string username "用户名"
    }
    ORDERS {
        int id PK "主键"
        int user_id FK "外键,指向 users.id"
        decimal amount "订单金额"
    }

图示说明:用户表(users)和订单表(orders)通过 orders.user_id → users.id 关联。一个用户可以没有订单,也可以有多个订单;反过来,一个订单只属于一个用户——这就是数据库设计中最常见的"一对多"关系。上面 INNER JOIN、LEFT JOIN、RIGHT JOIN 三种写法的差别,正是在于"没有匹配数据时,要不要保留某一边的行"。

子查询——查询里面套查询

子查询就是让一个 SELECT 的结果作为另一个 SELECT 的输入条件。

1
2
3
4
5
6
7
-- 查询比平均年龄大的用户
SELECT * FROM users 
WHERE age > (SELECT AVG(age) FROM users);

-- 查询订单金额大于100的用户
SELECT * FROM users 
WHERE id IN (SELECT user_id FROM orders WHERE amount > 100);

45.4 函数

SQL不仅能做数据筛选和计算,还内置了大量函数来处理数据。下面介绍几个最常用的。

字符串函数

字符串函数用于拼接、求长度、截取、大小写转换和去除空白。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
-- 拼接字符串
SELECT CONCAT(username, '-', email) FROM users;

-- 计算字符串长度
SELECT username, LENGTH(username) FROM users;

-- 注意 LENGTH 数的是"字节数",CHAR_LENGTH 数的才是"字符数"
-- 在 utf8mb4 下,一个汉字占 3 个字节,所以 LENGTH('张三') = 6,CHAR_LENGTH('张三') = 2
SELECT LENGTH('张三'), CHAR_LENGTH('张三');

-- 截取字符串(从第1个字符开始,截取5个)
SELECT SUBSTRING(username, 1, 5) FROM users;

-- 转大写/小写
SELECT UPPER(username), LOWER(email) FROM users;

-- 去除首尾空格
SELECT TRIM('  hello  ');

数值函数

数值函数负责四舍五入、取整、求绝对值和生成随机数这类计算。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
-- 四舍五入
SELECT ROUND(3.14159, 2);  -- 结果:3.14

-- 向上取整
SELECT CEIL(3.14);  -- 结果:4

-- 向下取整
SELECT FLOOR(3.14);  -- 结果:3

-- 绝对值
SELECT ABS(-100);  -- 结果:100

-- 随机数(0-1之间)
SELECT RAND();

日期函数

日期函数用来获取当前时间、提取日期的某个部分,以及做日期加减与格式化。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
-- 获取当前日期时间
SELECT NOW();

-- 获取当前日期
SELECT CURDATE();

-- 获取当前时间
SELECT CURTIME();

-- 提取日期的某个部分
SELECT YEAR(NOW());   -- 年
SELECT MONTH(NOW());  -- 月
SELECT DAY(NOW());    -- 日

-- 日期加减
SELECT DATE_ADD(NOW(), INTERVAL 1 DAY);      -- 明天
SELECT DATE_SUB(NOW(), INTERVAL 1 MONTH);    -- 上个月今天

-- 日期格式化
SELECT DATE_FORMAT(NOW(), '%Y年%m月%d日 %H:%i:%s');

条件函数

条件函数让 SQL 也能写分支逻辑,根据条件返回不同的值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
-- IF函数:类似Java的三元运算符
SELECT username, IF(age >= 18, '成年人', '未成年人') AS 身份 FROM users;

-- IFNULL函数:如果值为NULL就替换成指定值
SELECT IFNULL(email, '未填写') FROM users;

-- CASE WHEN:多条件判断
SELECT 
    username,
    CASE 
        WHEN age < 18 THEN '未成年'
        WHEN age < 30 THEN '青年'
        WHEN age < 60 THEN '中年'
        ELSE '老年'
    END AS 年龄段
FROM users;

45.5 索引

什么是索引?

索引(Index) 是数据库为了加快查询速度而建立的数据结构。你可以把它想象成一本书的目录——没有目录你要翻完整本书才能找到想看的内容,有了目录直接翻到对应页码。

索引的工作原理

当你对一个字段建立索引后,数据库会为这个字段的值单独维护一个数据结构(通常是B+树),这个结构里的值是排序好的,并且每个值都指向对应的数据行。

查询的时候,数据库先在索引树里快速定位,然后直接跳转到目标数据行——不用一行一行去扫描了。

创建索引

创建索引有两条路:单独用 CREATE INDEX 补建,或者在建表时一并声明。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
-- 直接创建索引
CREATE INDEX idx_username ON users(username);

-- 给username和email建立联合索引
CREATE INDEX idx_user_info ON users(username, email);

-- 给email字段加唯一索引(既约束了唯一性,又提升了查询速度)
CREATE UNIQUE INDEX idx_email ON users(email);

-- 建表的时候直接指定索引
CREATE TABLE users (
    id INT PRIMARY KEY,
    username VARCHAR(50),
    INDEX idx_username (username)  -- 在建表时创建索引
);

删除索引

索引可以随时删除,但删掉之后原本依赖它的查询会退化成全表扫描。

1
DROP INDEX idx_username ON users;

索引的代价

天下没有免费的午餐,索引虽好,但也有代价:

  1. 占用空间:索引本身要占用磁盘空间
  2. 拖慢写入:INSERT、UPDATE、DELETE时要同时维护索引
  3. 过度索引:字段太少、数据量太小的时候,索引反而是累赘

最佳实践:只为查询频繁、数据量大、区分度高的字段建立索引。

何时使用索引?

要不要建索引,取决于查询能不能真正用上它——EXPLAIN 一看便知。

1
2
3
4
5
6
7
8
-- 适合建索引的情况
SELECT * FROM users WHERE username = '张三';        -- 等值查询
SELECT * FROM users WHERE age > 18;                -- 范围查询
SELECT * FROM users WHERE username LIKE '张%';     -- 前缀模糊查询

-- 不适合建索引的情况
SELECT * FROM users WHERE email LIKE '%163.com';   -- 后缀模糊查询(用不上索引)
SELECT * FROM users WHERE gender = '男';           -- 区分度太低的字段

45.6 事务

什么是事务?

事务(Transaction) 是数据库管理系统执行过程中的一个逻辑单位,由一系列操作组成。一个事务内的所有操作要么全部成功,要么全部失败——没有中间状态。

打个比方:你去银行转账,从A账户转100块到B账户。这个过程实际上是两步:

  1. A账户减100
  2. B账户加100

如果第一步成功了但第二步失败了怎么办?A账户平白无故少了100块,这肯定不行!这时候就需要事务来保证:要么两步都成功,要么两步都回滚(撤销)。

事务的四大特性(ACID)

  • Atomicity(原子性):事务是最小执行单位,不可分割。要么全做,要么全不做。
  • Consistency(一致性):事务执行前后,数据库状态必须保持一致。
  • Isolation(隔离性):并发执行的事务相互隔离,不能互相干扰。
  • Durability(持久性):事务一旦提交,对数据库的改变就是永久性的。

事务的基本操作

事务用 START TRANSACTION(或 BEGIN)开启,最后用 COMMIT 提交或 ROLLBACK 撤销。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
-- 开启事务
START TRANSACTION;
-- 或者
BEGIN;

-- 执行操作
UPDATE accounts SET balance = balance - 100 WHERE id = 1;  -- A账户减100
UPDATE accounts SET balance = balance + 100 WHERE id = 2;  -- B账户加100

-- 提交事务(所有操作正式生效)
COMMIT;

-- 回滚事务(撤销所有操作)
ROLLBACK;

并发问题与隔离级别

当多个事务同时操作同一批数据时,会产生各种问题:

问题说明
脏读读取到了其他事务未提交的数据
不可重复读同一个事务中,两次读取同一行数据结果不同
幻读同一个事务中,两次查询结果集不一致(多了或少了行)

为了解决这些问题,SQL标准定义了4个隔离级别:

1
2
3
4
5
6
7
8
-- 查看当前隔离级别
SELECT @@transaction_isolation;

-- 老版本 MySQL(8.0 之前)里这个变量叫 @@tx_isolation,
-- 从 8.0 起已改名并移除了旧名字,写老名字会直接报错

-- 设置隔离级别
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
隔离级别脏读不可重复读幻读
READ UNCOMMITTED可能可能可能
READ COMMITTED不可能可能可能
REPEATABLE READ(MySQL默认)不可能不可能可能
SERIALIZABLE不可能不可能不可能

MySQL的InnoDB引擎在REPEATABLE READ级别下,通过MVCC(多版本并发控制)基本解决了幻读问题,所以通常不用把隔离级别调到最高的SERIALIZABLE。

实战例子

下面把前面学的语法串起来,模拟一次完整的转账事务。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
-- 模拟转账操作
START TRANSACTION;

UPDATE account SET balance = balance - 1000 WHERE id = 1 AND balance >= 1000;

-- 检查影响行数,如果为0说明余额不足
-- 这里用一个存储过程或者应用逻辑来检查
UPDATE account SET balance = balance + 1000 WHERE id = 2;

-- 两条UPDATE都成功了,提交
COMMIT;
-- 如果任何一步失败,执行ROLLBACK

45.7 JDBC编程步骤

终于到了Java和数据库"牵手"的时候了!JDBC(Java Database Connectivity) 是Java提供的数据库连接技术,是Java程序操作数据库的标准API。

JDBC工作原理

JDBC采用驱动管理器 + 数据库驱动的模式。应用程序只需要调用JDBC API,而具体的数据库操作由对应的数据库驱动来完成。

Java程序 → JDBC API → JDBC驱动管理器 → 数据库驱动 → 数据库

JDBC 编程六步曲

第一步:加载驱动

JDBC 4.0 以后驱动可以自动发现,但在显式指定驱动、或使用连接池时仍会看到手动加载的写法。

1
2
3
4
5
// 方式一:手动加载驱动类(老派写法)
Class.forName("com.mysql.cj.jdbc.Driver");

// 方式二:从JDBC 4.0开始,MySQL驱动支持自动加载
// 只需要把驱动jar包放到classpath里就行

第二步:建立连接

连接由 DriverManager(或连接池的 DataSource)根据 URL、用户名、密码创建。

1
2
3
4
5
6
7
// 数据库连接地址
String url = "jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Shanghai";
String username = "root";
String password = "123456";

// 建立连接
Connection conn = DriverManager.getConnection(url, username, password);

第三步:创建语句对象

Statement 用来执行拼接好的 SQL,PreparedStatement 则先用 ? 占位再逐个填值。

1
2
3
4
5
6
7
// 创建普通SQL语句对象
Statement stmt = conn.createStatement();

// 创建预编译SQL语句对象(推荐,防止SQL注入)
String sql = "SELECT * FROM users WHERE id = ?";
PreparedStatement pstmt = conn.prepareStatement(sql);
pstmt.setInt(1, 1);  // 第一个问号的值设为1

第四步:执行SQL

查询用 executeQuery() 拿回 ResultSet,增删改用 executeUpdate() 拿到影响行数。

1
2
3
4
5
// 执行查询,返回ResultSet
ResultSet rs = pstmt.executeQuery();

// 执行增删改,返回影响的行数
int rows = stmt.executeUpdate("INSERT INTO users (username) VALUES ('张三')");

第五步:处理结果

ResultSet 是一张游标式的结果表,next() 每调用一次就往下移一行,直到返回 false。

1
2
3
4
5
6
7
// 遍历ResultSet
while (rs.next()) {
    int id = rs.getInt("id");           // 按列名获取
    String name = rs.getString(2);      // 按列索引获取(从1开始)
    
    System.out.println("用户ID: " + id + ", 用户名: " + name);
}

第六步:关闭资源

资源要按"后创建的先关闭"的顺序释放,否则连接池很快就会被耗尽。

1
2
3
4
// 按照后创建先关闭的原则,依次关闭
if (rs != null) rs.close();
if (stmt != null) stmt.close();
if (conn != null) conn.close();

完整示例

下面这个例子把六步完整地走了一遍,可以直接作为入门模板。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

public class JdbcDemo {
    
    public static void main(String[] args) {
        // 数据库配置
        String url = "jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Shanghai";
        String user = "root";
        String password = "123456";
        
        Connection conn = null;
        PreparedStatement pstmt = null;
        ResultSet rs = null;
        
        try {
            // 第一步:加载驱动(MySQL 8.x)
            Class.forName("com.mysql.cj.jdbc.Driver");
            
            // 第二步:建立连接
            conn = DriverManager.getConnection(url, user, password);
            System.out.println("数据库连接成功!");
            
            // 第三步:创建预编译SQL语句
            String sql = "SELECT * FROM users WHERE age > ?";
            pstmt = conn.prepareStatement(sql);
            pstmt.setInt(1, 18);  // 查询18岁以上的用户
            
            // 第四步:执行查询
            rs = pstmt.executeQuery();
            
            // 第五步:处理结果
            System.out.println("查询结果:");
            while (rs.next()) {
                int id = rs.getInt("id");
                String username = rs.getString("username");
                String email = rs.getString("email");
                int age = rs.getInt("age");
                System.out.printf("ID: %d, 用户名: %s, 邮箱: %s, 年龄: %d%n", 
                                  id, username, email, age);
            }
            
        } catch (ClassNotFoundException e) {
            System.out.println("驱动类找不到!" + e.getMessage());
        } catch (SQLException e) {
            System.out.println("数据库操作出错!" + e.getMessage());
        } finally {
            // 第六步:关闭资源(重要!防止连接泄漏)
            try {
                if (rs != null) rs.close();
                if (pstmt != null) pstmt.close();
                if (conn != null) conn.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    }
}

使用try-with-resources简化关闭操作

从Java 7开始,你可以用try-with-resources来自动关闭资源:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
String url = "jdbc:mysql://localhost:3306/mydb?useSSL=false&serverTimezone=Asia/Shanghai";
String sql = "SELECT * FROM users WHERE age > ?";

try (Connection conn = DriverManager.getConnection(url, "root", "123456");
     PreparedStatement pstmt = conn.prepareStatement(sql)) {
    
    pstmt.setInt(1, 18);
    
    try (ResultSet rs = pstmt.executeQuery()) {
        while (rs.next()) {
            System.out.println(rs.getString("username"));
        }
    }
    
} catch (SQLException e) {
    e.printStackTrace();
}
// 无需手动关闭,资源会自动释放

增删改操作示例

增删改共用同一套写法,区别只在 SQL 和参数绑定上。下面三个方法假定你已经有一个返回 Connection 的 getConnection() 辅助方法(见上一节的完整示例)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// 插入数据
public int insertUser(String username, String email, int age) {
    String sql = "INSERT INTO users (username, email, age) VALUES (?, ?, ?)";
    
    try (Connection conn = getConnection();
         PreparedStatement pstmt = conn.prepareStatement(sql)) {
        
        pstmt.setString(1, username);
        pstmt.setString(2, email);
        pstmt.setInt(3, age);
        
        return pstmt.executeUpdate();  // 返回影响的行数
        
    } catch (SQLException e) {
        e.printStackTrace();
        return 0;
    }
}

// 更新数据
public int updateUserAge(int id, int newAge) {
    String sql = "UPDATE users SET age = ? WHERE id = ?";
    
    try (Connection conn = getConnection();
         PreparedStatement pstmt = conn.prepareStatement(sql)) {
        
        pstmt.setInt(1, newAge);
        pstmt.setInt(2, id);
        
        return pstmt.executeUpdate();
        
    } catch (SQLException e) {
        e.printStackTrace();
        return 0;
    }
}

// 删除数据
public int deleteUser(int id) {
    String sql = "DELETE FROM users WHERE id = ?";
    
    try (Connection conn = getConnection();
         PreparedStatement pstmt = conn.prepareStatement(sql)) {
        
        pstmt.setInt(1, id);
        return pstmt.executeUpdate();
        
    } catch (SQLException e) {
        e.printStackTrace();
        return 0;
    }
}

本章小结

本章我们学习了SQL与数据库的基础知识,主要包括:

  1. SQL基础:了解了SQL是什么、它的门派分支(MySQL、Oracle等),以及基本的语法规则。
  2. 表操作:掌握了CREATE TABLE创建表、ALTER TABLE修改表、DROP TABLE删除表的用法,以及主键、唯一约束、默认值等概念。
  3. 查询:深入学习了SELECT的各种用法,包括条件查询、排序、分页、聚合函数、分组查询、多表连接和子查询。
  4. 函数:了解了字符串函数、数值函数、日期函数和条件函数的使用。
  5. 索引:理解了索引的工作原理、创建与删除方法,以及索引的适用场景和注意事项。
  6. 事务:掌握了事务的ACID特性、开启/提交/回滚操作,以及并发问题和隔离级别。
  7. JDBC编程:学习了Java通过JDBC操作数据库的完整步骤,包括加载驱动、建立连接、创建语句、执行SQL、处理结果和关闭资源。

数据库是现代软件开发不可或缺的一部分。无论是Web应用、企业系统还是移动后台,都离不开数据的存储与管理。学好SQL和JDBC,就等于掌握了与数据打交道的基本功。

记住:程序员的三大浪漫——操作系统、编译原理、数据库。你已经解锁了第三项,接下来继续加油吧!

最后修改 September 19, 2026: 更新 (3489033b1)