一、数据库基础操作
MySQL 中对数据库的核心操作包括查看、创建、选中和删除,这是后续所有数据表、数据操作的前提。
1. 查看数据库
查看当前 MySQL 服务器中所有已创建的数据库:
show databases;
2. 创建数据库
创建一个新的数据库,推荐指定完整字符集并添加「不存在则创建」的判断,避免重复创建报错:
create database if not exists [数据库名] charset UTF8mb4;
关键说明:
- 优先使用
UTF8mb4字符集,它是完整的 UTF-8 实现,支持存储 emoji 表情,而 MySQL 自带的UTF8是简化版,仅支持部分 UTF-8 字符。 - 若数据库名、表名或列名与 MySQL 关键字冲突,可使用反引号
`包裹(例如create database if not exists `database`;)。 - 省略
if not exists时,若数据库已存在,执行语句会直接报错。
3. 选中数据库
MySQL 服务器中可存在多个数据库,所有数据表操作都需要先选中目标数据库,明确操作上下文:
use 数据库名;
4. 删除数据库
删除指定数据库(危险操作,删除后数据库内所有数据将永久丢失,无法恢复):
drop database 数据库名;
二、MySQL 核心数据类型
MySQL 提供了多种数据类型,用于定义数据表的列结构,核心分为数字、字符串、日期三大类,二进制、枚举/集合为辅。
1. 数字数据类型
用于存储整数、小数等数值类型数据,常用类型及占用空间如下:
| 数据类型 | 占用空间 | 核心用途 |
|---|---|---|
| BIT(M) | M bit(M 省略为 1) | 存储布尔值、二进制标记(0/1) |
| BOOL | 1 byte | 等价于 TINYINT(1),存储真(1)/假(0) |
| TINYINT | 1 byte | 存储小范围整数(如性别、状态码) |
| SMALLINT | 2 byte | 存储较小范围整数(如年级、班级号) |
| INT | 4 byte | 存储常规整数(如学号、ID),最大可存约 21 亿 |
| BIGINT | 8 byte | 存储大范围整数(如手机号、超大序号) |
| FLOAT | 4 byte | 存储单精度浮点数(存在精度损失) |
| DOUBLE | 8 byte | 存储双精度浮点数(精度高于 FLOAT,仍有微小损失) |
| DECIMAL(M,D) | 动态分配 | 存储精确小数(无精度损失,如金额、价格) |
关键说明:
DECIMAL(M,D)中,M 是总位数(整数部分 + 小数部分),最大 65;D 是小数点后位数,最大 30,默认 M=10、D=0(即无小数的精确整数)。- 浮点数(FLOAT/DOUBLE)运算效率高,适合无需精确的场景(如身高、体重);DECIMAL 运算速度慢、占用空间大,适合对精度要求高的场景(如金融数据)。
2. 字符串类型
用于存储文本类数据,常用类型及长度限制如下:
| 数据类型 | 占用说明与长度限制 | 核心用途 |
|---|---|---|
| CHAR(M) | 0 ~ 255 个字符(M 省略时默认 1),固定长度 | 存储长度固定的字符串(如身份证号、手机号) |
| VARCHAR(M) | M 为最大字符长度,实际占用空间等于真实字符长度 + 1/2 字节(长度标记) | 存储长度不固定的字符串(如姓名、地址) |
| TINYTEXT | 最大支持 255 个字节 | 存储极短文本(如备注、标签) |
| TEXT | 最大支持 65535 个字节(约 64KB) | 存储常规文本(如文章内容、简介) |
| MEDIUMTEXT | 最大支持 (2^{24}-1) 个字节(约 16MB) | 存储较长文本(如长文、附件描述) |
| LONGTEXT | 最大支持 (2^{32}-1) 个字节(约 4GB) | 存储超大文本(如日志、大型文档) |
关键说明:
- 字符与字节的对应关系由字符集决定:
- ASCII、GBK:英文字符占 1 字节,中文字符占 2 字节。
- UTF-8/UTF8mb4:英文字符占 1 字节,中文字符占 3 字节,emoji 占 4 字节。
- CHAR(M) 无论实际存储多少字符,都占用 M 对应的字节空间,查询效率高;VARCHAR(M) 按需占用空间,节省存储,查询效率略低于 CHAR(M)。
3. 二进制数据
用于存储图片、视频、音频等二进制文件,但实际开发中不推荐直接存储:
- 数据库存储二进制文件会导致数据库体积急剧增大,影响查询、备份效率。
- 最佳实践:将二进制文件以文件形式存储在服务器硬盘/对象存储(如阿里云 OSS)中,仅将文件的访问路径存储在数据库中。
4. 枚举和集合
- 枚举(ENUM):用于存储「二选一」或「多选一」的数据(如性别:男/女,状态:启用/禁用),取值范围固定。
- 集合(SET):用于存储「多选多」的数据(如爱好:读书/运动/音乐),取值范围固定。
- 核心优势:限制取值范围,保证数据一致性,查询效率高于手动存储字符串(如「读书,运动」)。
5. 日期类型
用于存储日期、时间相关数据,最常用的是 DATETIME,补充常用日期类型如下:
| 类型 | 占用空间 | 格式 | 核心用途 |
|---|---|---|---|
DATE | 3 Bytes | YYYY-MM-DD | 存储仅包含日期的数据(如生日、入职日期) |
TIME | 3 Bytes | HH:mm:ss | 存储仅包含时间的数据(如打卡时间、时长) |
DATETIME | 8 Bytes | YYYY-MM-DD hh:mm:ss | 存储日期+时间(如订单创建时间、日志时间) |
TIMESTAMP | 4 Bytes | YYYY-MM-DD hh:mm:ss | 存储日期+时间,支持自动更新(如最后修改时间),范围小于 DATETIME |
三、数据表核心操作
数据表是数据库存储数据的核心载体,所有数据都以行、列形式存储在数据表中,核心操作包括查看、创建、修改、删除。
1. 查看数据库的所有表
选中数据库后,查看该数据库内所有已创建的数据表:
show tables;
2. 创建数据表
创建一个新的数据表,定义列名、数据类型及可选约束、注释、存储引擎等:
create [temporary] table [if not exists] 表名(
列名 类型 [comment "列描述"],
列名 类型 [comment "列描述"]
) [engine 存储引擎] [character set 字符集] [collate 排序规则];
关键说明:
- SQL 命名推荐使用「蛇形命名法」(小写字母 + 下划线),例如
student_id、user_name,区分Java的驼峰命名法。 temporary关键字表示创建临时表,临时表仅存在于当前会话,会话关闭后自动删除,不存储在硬盘中。- 常用存储引擎为
InnoDB(默认,支持事务、外键、行级锁),旧版 MySQL 中的MyISAM已逐步被淘汰。
3. 查看表结构
查看指定数据表的列结构、数据类型、约束等详细信息:
desc 表名;
查询结果字段说明:
+-------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id | int | YES | | NULL | |
| name | varchar(20) | YES | | NULL | |
+-------+-------------+------+-----+---------+-------+
Field:列名。Type:列的数据类型。Null:是否允许存储 NULL 值(YES 允许,NO 不允许)。Key:索引类型(PRI 主键、UNI 唯一索引、MUL 普通索引)。Default:列的默认值。Extra:扩展信息(如 auto_increment 自增)。
4. 修改表
使用 alter table 语句修改已创建的数据表结构,包括添加、修改、删除列等操作。
4.1 添加列
在指定列之后添加新列(若省略 after 列名,默认添加到表的最后一列):
alter table 表名 add 列名 类型 [after 参考列名];
# 示例:在 name 列后添加 age 列,类型为 int
alter table student add age int after name;
4.2 修改列类型
修改指定列的数据类型(注意数据兼容性,避免数据丢失,如将 BIGINT 改为 INT 可能丢失超大数值):
alter table 表名 modify 列名 新类型;
# 示例:将 age 列类型改为 BIGINT
alter table student modify age bigint;
4.3 修改列名
修改指定列的名称,同时可修改其数据类型:
alter table 表名 rename column 旧列名 to 新列名;
# 示例:将 age 列名改为 AGE
alter table student rename column age to AGE;
4.4 删除列
删除数据表中的指定列(危险操作,列中所有数据将永久丢失):
alter table 表名 drop 列名;
# 示例:删除 AGE 列
alter table student drop AGE;
5. 删除表
删除指定数据表(危险操作,表中所有数据、结构将永久丢失,无法恢复):
drop table [if exists] 表名;
# 示例:删除 student 表(不存在则不报错)
drop table if exists student;
四、数据增删查改(CRUD)
CRUD 是数据库操作的核心,对应数据的创建(Create)、读取(Read)、更新(Update)、删除(Delete),也是日常开发中使用频率最高的操作。
增(Insert)
向数据表中插入一条或多条数据,支持全列插入和指定列插入。
1. 普通全列插入
按数据表的列顺序插入所有列的数据,列数必须与数据表列数一致:
insert into 表名 values (列值1, 列值2, ...);
# 示例:向 student 表插入全列数据(id, name, class_id)
insert into student values (1, '张三', 1);
2. 指定列插入
仅向指定列插入数据,未指定的列将使用默认值(或 NULL,若允许),灵活性更高:
insert into 表名 (列名1, 列名2, ...) values (列值1, 列值2, ...);
# 示例:向 student 表插入 name 和 class_id 列,id 列使用自增默认值
insert into student (name, class_id) values ('李四', 1);
3. 批量插入
一次性插入多条数据,效率高于多次执行单条插入语句:
insert into 表名 (列名1, 列名2, ...)
values (列值1, 列值2, ...), (列值1, 列值2, ...), ...;
# 示例:向 student 表批量插入 3 条数据
insert into student (name, class_id)
values ('王五', 2), ('赵六', 2), ('钱七', 1);
查(Select)
从数据表中查询指定数据,是 CRUD 中最复杂、使用最灵活的操作,支持多条件筛选、排序、分页等。
1. 全列查询
查询数据表中所有列、所有行的数据(不推荐在生产环境使用,数据量较大时会占用大量硬盘 I/O 和网络带宽):
select * from 表名;
# 示例:查询 student 表所有数据
select * from student;
2. 指定列查询
仅查询需要的列,减少数据传输,提高查询效率,查询结果为临时表:
select 列名1, 列名2, ... from 表名;
# 示例:查询 student 表的 name 和 class_id 列
select name, class_id from student;
3. 查询时带表达式
查询时可对列值进行算术运算、拼接等表达式操作,生成临时列数据:
# 示例1:查询考试成绩,语文成绩加 10 分
select name, chinese + 10 from exam;
# 示例2:查询考试总成绩(语文+数学+英语)
select name, chinese + math + english from exam;
4. 查询时带别名
为查询结果的列(或表达式列)指定别名,提高结果可读性,别名仅对当前查询结果有效:
select 列名1 [as] 别名1, 表达式 [as] 别名2, ... from 表名;
# 示例:查询总成绩,并指定别名为“总成绩”(as 可省略)
select name, chinese + math + english as "总成绩" from exam;
select name, chinese + math + english "总成绩" from exam;
5. 查询结果去重
使用 distinct 关键字去除查询结果中的重复行,仅保留唯一行数据:
select distinct 列名1, 列名2, ... from 表名;
# 示例:查询 student 表中不重复的 class_id
select distinct class_id from student;
6. 条件查询
使用 where 子句添加查询条件,仅返回满足条件的行数据,条件运算符支持比较、范围、模糊匹配等。
核心条件运算符
| 运算符 | 含义说明 |
|---|---|
>、>=、<、<= | 比较运算符:大于、大于等于、小于、小于等于 |
= | 等值匹配:常规值等值判断,NULL=NULL 结果为 NULL(未知值无法对等) |
<=> | 安全等值匹配:支持 NULL 等值判断,NULL<=>NULL 结果为 TRUE(1) |
!=(兼容 <>) | 不等于(<> 为 SQL 标准语法,推荐优先使用) |
value BETWEEN a0 AND a1 | 范围匹配:匹配闭区间 [a0, a1] 内的值,NOT BETWEEN 为反向匹配 |
value IN (option1, ...) | 列表匹配:value 存在于列表中返回 TRUE,NOT IN 为反向匹配 |
列名 IS NULL | NULL 判断:判断列值是否为 NULL |
列名 IS NOT NULL | NULL 反向判断:判断列值是否不为 NULL |
LIKE | 模糊匹配:% 匹配任意多个字符(含 0 个);_ 匹配任意单个字符,NOT LIKE 为反向匹配 |
AND | 逻辑与:多个条件同时满足时返回 TRUE |
OR | 逻辑或:多个条件满足其一即返回 TRUE |
NOT | 逻辑非:对单个条件结果取反 |
注意:NULL 与任何值进行比较(除 <=>、IS NULL),结果均为 FALSE。
6.1 基本条件查询
# 示例1:查询英语成绩大于 60 分的学生
select name, english from exam where english > 60;
# 示例2:查询语文成绩大于英语成绩的学生
select name, chinese, english from exam where chinese > english;
# 示例3:查询总成绩大于 200 分的学生
select name from exam where (chinese + math + english) > 200;
6.2 逻辑组合查询
AND 优先级高于 OR,可使用括号 () 改变优先级:
# 示例1:查询语文和英语成绩都大于 60 分的学生(同时满足)
select name, english, chinese from exam where english > 60 and chinese > 60;
# 示例2:查询语文或英语成绩大于 60 分的学生(满足其一)
select name, english, chinese from exam where english > 60 or chinese > 60;
6.3 范围查询
# 示例1:查询英语成绩在 60 ~ 80 分之间的学生(闭区间)
select name, english from exam where english between 60 and 80;
# 示例2:查询英语成绩为 77、88、99 分的学生
select name, english from exam where english in (77, 88, 99);
6.4 模糊查询
使用 % 和 _ 作为通配符,查询效率较低,尽量避免在大数据量表中使用:
# 示例1:查询所有以“孙”开头的学生姓名(后面可跟任意字符)
select name from exam where name like "孙%";
# 示例2:查询以“孙”开头、后面仅有 1 个字符的学生姓名
select name from exam where name like "孙_";
# 示例3:查询以“孙”开头、后面仅有 2 个字符的学生姓名
select name from exam where name like "孙__";
6.5 NULL 查询
# 示例:查询语文成绩为 NULL 的学生(两种方式)
select * from exam where chinese is null;
select * from exam where chinese <=> NULL;
7. 排序查询
使用 order by 子句对查询结果进行排序,默认升序(ASC),可指定降序(DESC)。
select 列名 from 表名 order by 列名1 [desc/asc], 列名2 [desc/asc], ...;
示例:
# 示例1:按语文成绩降序排序,语文成绩相同则按数学成绩升序排序
select * from exam order by chinese desc, math;
# 示例2:按总成绩降序排序,使用别名简化表达式
select name, chinese + math + english as total from exam order by total desc;
# 示例3:查询非 NULL 的语文成绩,按降序排序
select name, chinese from exam where chinese is not null order by chinese desc;
关键说明:
- 排序优先级:先按列名1排序,列名1值相同则按列名2排序,以此类推。
- NULL 值默认视为最小值,升序时排在最前面,降序时排在最后面。
- 排序结果为临时表,不修改原数据表的数据顺序。
8. 分页操作
使用 limit 子句获取指定范围的数据,避免一次性查询大量数据,提高查询效率,常用于分页展示。
# 格式1:limit 偏移量, 每页条数(偏移量从 0 开始)
select 列名 from 表名 limit M, N;
# 格式2:limit 每页条数 offset 偏移量(与格式1等价,可读性更高)
select 列名 from 表名 limit N offset M;
示例:
# 示例1:查询前 10 条数据(偏移量 0,取 10 条)
select * from exam limit 0, 10;
select * from exam limit 10 offset 0;
# 示例2:查询第 11 ~ 20 条数据(偏移量 10,取 10 条)
select * from exam limit 10, 10;
关键说明:偏移量超出数据总条数时,不会报错,仅返回空结果集。
改(Update)
修改数据表中满足条件的行数据,不指定 where 条件时,将修改表中所有行(极度危险,务必谨慎)。
update 表名 set 列名1 = 新值1, 列名2 = 新值2, ... [where 条件] [order by 列名] [limit N];
示例:
# 示例1:将 id=1 的学生姓名改为“张三丰”(指定条件,安全)
update student set name = '张三丰' where id = 1;
# 示例2:将所有班级为 1 的学生,class_id 改为 3(批量修改,带条件)
update student set class_id = 3 where class_id = 1;
删(Delete)
删除数据表中满足条件的行数据,不指定 where 条件时,将删除表中所有行(极度危险,数据无法恢复)。
delete from 表名 [where 条件] [order by 列名] [limit N];
示例:
# 示例1:删除 id=10 的学生数据(指定条件,安全)
delete from student where id = 10;
# 示例2:删除班级为 2 的前 5 条学生数据(批量删除,带条件和限制)
delete from student where class_id = 2 limit 5;
补充操作
1. 截断表
快速删除表中所有数据,效率高于 delete from 表名(不记录日志,无法回滚),且会重置自增主键:
truncate table 表名;
2. 插入查询结果
将一个查询的结果集插入到另一个数据表中,要求查询结果的列数、数据类型与目标表匹配:
insert into 目标表名 [(列名1, 列名2, ...)] select 列名1, 列名2, ... from 源表名 [where 条件];
聚合查询
聚合查询是对查询结果集进行行与行之间的运算,依赖聚合函数,常用于统计分析(如计数、求和、求平均值)。
核心聚合函数
| 聚合函数 | 含义说明 |
|---|---|
COUNT(列名/*) | 统计非 NULL 记录行数,COUNT(*) 统计所有行数(包括 NULL),推荐使用 |
SUM(列名) | 计算数值类型列的总和,自动忽略 NULL 值 |
AVG(列名) | 计算数值类型列的平均值,自动忽略 NULL 值,平均值 = 总和 / 非空值行数 |
MAX(列名) | 筛选列的最大值,支持数值、字符串、日期类型,自动忽略 NULL 值 |
MIN(列名) | 筛选列的最小值,支持数值、字符串、日期类型,自动忽略 NULL 值 |
示例:
# 示例1:统计 student 表总记录数
select count(*) from student;
# 示例2:统计 exam 表语文成绩的总和、平均值、最高分、最低分
select sum(chinese), avg(chinese), max(chinese), min(chinese) from exam;
# 示例3:统计班级 1 的学生人数
select count(*) from student where class_id = 1;
分组查询
使用 group by 子句将数据表按指定列分组(列值相同的行分为一组),常用于分组统计,分组后仅能查询分组列和聚合函数结果。
select 分组列, 聚合函数 from 表名 [where 条件] group by 分组列 [having 聚合条件];
关键说明:
where:分组前筛选,过滤不满足条件的行,不支持聚合函数。having:分组后筛选,过滤不满足聚合条件的组,支持聚合函数。
示例:
# 示例1:按职位分组,统计每个职位的平均薪资
select role, avg(salary) from emp group by role;
# 示例2:先过滤掉“博哥”,再按职位分组统计平均薪资(where + group by)
select role, avg(salary) from emp where name != '博哥' group by role;
# 示例3:按职位分组统计平均薪资,仅返回平均薪资小于 15000 的组(group by + having)
select role, avg(salary) from emp group by role having avg(salary) < 15000;
内置函数
MySQL 提供了大量内置函数,用于数据处理、格式转换等,可直接在 select 语句中调用。
1. 时间函数
常用日期时间处理函数,用于获取当前时间、日期运算等:
# 示例1:获取当前日期、当前时间、当前日期时间
select curdate(), curtime(), now();
# 示例2:给当前日期加 7 天
select adddate(now(), interval 7 day);
# 示例3:计算两个日期的天数差
select datediff('2026-12-31', now());
2. 字符串函数
常用字符串处理函数,用于拼接、截取、转换大小写等:
# 示例1:拼接字符串(带分隔符)
select concat_ws('-', '姓名', '张三', '班级', '1班');
# 示例2:将字符串转换为大写
select upper('hello mysql');
# 示例3:截取字符串(从第 1 位开始,截取 3 个字符)
select substr('hello mysql', 1, 3);
# 示例4:去除字符串两侧的空白符
select trim(' hello mysql ');
3. 数学函数
常用数值处理函数,用于取整、绝对值、随机数等:
# 示例1:获取绝对值
select abs(-100);
# 示例2:向上取整、向下取整
select ceil(3.14), floor(3.99);
# 示例3:生成 0~1 之间的随机数
select rand();
# 示例4:四舍五入保留 2 位小数
select round(3.14159, 2);
4. 其他实用函数
# 示例1:查询 MySQL 版本、当前数据库、当前登录用户
select version(), database(), user();
# 示例2:替换 NULL 值(若 name 为 NULL,返回“无名氏”)
select ifnull(name, '无名氏') from student;
# 示例3:对字符串进行 MD5 加密
select md5('123456');
五、数据库约束
数据库约束是对数据表列数据的限制条件,用于保证数据的完整性、一致性和准确性,创建表时指定,核心约束类型如下:
| 约束类型 | 说明 |
|---|---|
NOT NULL | 非空约束:指定列不能存储 NULL 值,必须填写数据 |
DEFAULT | 默认约束:列未赋值时,自动使用默认值 |
UNIQUE | 唯一约束:指定列的每行数据必须唯一,允许 NULL(多个 NULL 不视为重复) |
PRIMARY KEY | 主键约束:NOT NULL + UNIQUE,唯一标识一行数据,一个表仅能有一个主键 |
FOREIGN KEY | 外键约束:定义两个表之间的关联关系,保证数据的引用完整性 |
CHECK | 自定义约束:限制列值的范围(MySQL 支持语法,但不强制执行,仅做校验提示) |
示例代码:
# 1. 非空约束
create table student(id int not null, name varchar(20) not null);
insert into student values(null, '张三'); -- 报错:id 不允许为 NULL
# 2. 默认约束
create table student(id int, name varchar(20) default '无名氏');
insert into student (id) values (1); -- name 自动填充为“无名氏”
# 3. 唯一约束
create table student(id int, name varchar(20) unique);
insert into student values (1, '张三'), (2, '张三'); -- 报错:name 重复
# 4. 主键约束(单字段主键 + 自增)
create table student(id int primary key auto_increment, name varchar(20));
insert into student (name) values ('张三'), ('李四'); -- id 自动自增为 1、2
# 5. 联合主键(多字段组合唯一)
create table student(name varchar(20), class varchar(20), primary key(name, class));
# 6. 外键约束(学生表关联班级表)
-- 父表:班级表
create table class(class_id int primary key auto_increment, class_name varchar(10));
-- 子表:学生表(class_id 关联班级表的 class_id)
create table student(
student_id int primary key auto_increment,
student_name varchar(10),
class_id int,
foreign key(class_id) references class(class_id)
);
# 7. CHECK 约束(自定义年龄、性别范围)
create table student(
id int,
name varchar(20),
age int,
gender varchar(1),
check(age > 18),
check(gender in ('男', '女'))
);
关键说明:
- 自增主键(
auto_increment)仅支持整数类型,删除数据后,自增序列不会重置,下一个自增值将延续最大ID。 - 外键约束中,父表(被关联表)的关联列必须是主键或唯一索引,子表的外键列数据必须存在于父表中,否则无法插入。
- 外键会约束父表的删除/修改操作(若父表数据被子表引用,无法直接删除),如需解除约束,可添加
on delete cascade(级联删除)或on update cascade(级联更新)。
六、数据库设计
数据库设计是构建高效、可维护数据库的核心,核心遵循三大范式,同时结合业务场景灵活调整。
三大范式
范式是数据库表结构的设计规范,目的是减少数据冗余,保证数据一致性,越高的范式冗余度越低,查询复杂度越高。
1. 第一范式(1NF)
核心要求:数据表的每一列都是不可分割的原子数据项,不能是集合、数组、对象等复合数据。
- 符合 1NF:姓名(张三)、年龄(18)、地址(上海市浦东新区)。
- 不符合 1NF:姓名(张三/李四)、地址(上海市,浦东新区,XX街道)(可分割为多个原子项)。
2. 第二范式(2NF)
核心要求:在满足 1NF 的基础上,不存在非关键字段对候选键的部分函数依赖(仅针对复合主键)。
- 关键概念:
- 复合主键:由多个列组成的主键(如学号+课程号)。
- 候选键:主键的组成列(如复合主键中的学号、课程号)。
- 非关键字段:除候选键外的其他列(如成绩、课程名称)。
- 部分函数依赖:仅通过候选键的一部分即可确定非关键字段(如通过课程号即可确定课程名称,无需学号)。
- 核心解决:拆分表,将部分依赖的列提取到新表中,消除部分函数依赖。
3. 第三范式(3NF)
核心要求:在满足 2NF 的基础上,不存在非关键字段对候选键的传递函数依赖。
- 传递函数依赖:非关键字段 A 依赖于非关键字段 B,B 又依赖于候选键(如:ID → 学院 → 学院电话,学院电话传递依赖于 ID)。
- 核心解决:拆分表,将传递依赖的列提取到新表中,通过外键关联,消灭传递依赖。
示例:拆分存在传递依赖的学生表
| 原表(存在传递依赖) | 拆分后学生表 | 拆分后学院表 |
|---|---|---|
| ID、学号、姓名、学院、学院电话 | ID、学号、姓名、学院ID | 学院ID、学院、学院电话、学院地址 |
数据库设计过程
- 抽象实体类:从业务逻辑中提取核心实体(如学生、班级、课程),实体对应数据表,实体属性对应数据表列。
- 确定实体关系:梳理实体之间的关系(一对一、一对多、多对多),绘制 E-R 图。
- 一对一:如学生与身份证(一个学生对应一个身份证,一个身份证对应一个学生)。
- 一对多:如班级与学生(一个班级对应多个学生,一个学生对应一个班级)。
- 多对多:如学生与课程(一个学生对应多个课程,一个课程对应多个学生),需引入中间关联表。
- 生成 SQL 语句:根据 E-R 图,结合三大范式,创建数据表,添加约束、索引等。
- 优化调整:根据业务场景调整表结构,适当增加冗余字段(牺牲范式)提高查询效率,平衡冗余与性能。
七、联合查询(多表查询)
联合查询是同时查询多个数据表的数据,核心前提是多表之间存在关联关系(如外键、相同含义的列),核心分为笛卡尔积、内连接、外连接、自连接、子查询、合并查询。
1. 笛卡尔积
将两个或多个表的所有行进行排列组合,生成一张临时表(列数=各表列数之和,行数=各表行数之积),其中包含大量无效数据,需通过连接条件筛选有效数据。
# 示例:查询 student 表和 class 表的笛卡尔积
select * from student, class;
# 筛选有效数据(通过 class_id 关联,消除无效数据)
select * from student, class where student.class_id = class.class_id;
2. 内连接
仅返回多表中满足连接条件的公共数据(即交集),是最常用的联合查询方式,两种语法格式等价。
# 格式1:传统语法(基于笛卡尔积筛选)
select 列名 from 表1, 表2 where 连接条件 [and 其他条件];
# 格式2:JOIN 语法(推荐,可读性更高)
select 列名 from 表1 [inner] join 表2 on 连接条件 [where 其他条件];
示例:
# 查询学生姓名及其对应的班级名称(内连接)
select student.name, class.class_name
from student join class
on student.class_id = class.class_id;
3. 外连接
返回满足连接条件的公共数据,同时返回其中一张表的所有数据(另一张表无匹配数据时填充 NULL),分为左外连接和右外连接,MySQL 不支持全外连接。
3.1 左外连接(LEFT JOIN)
返回左表的所有数据,右表仅返回满足连接条件的数据,无匹配数据时右表列填充 NULL。
select 列名 from 左表 left [outer] join 右表 on 连接条件;
3.2 右外连接(RIGHT JOIN)
返回右表的所有数据,左表仅返回满足连接条件的数据,无匹配数据时左表列填充 NULL。
select 列名 from 左表 right [outer] join 右表 on 连接条件;
示例:
# 左外连接:查询所有学生,即使无对应班级(班级列填充 NULL)
select student.name, class.class_name
from student left join class
on student.class_id = class.class_id;
# 右外连接:查询所有班级,即使无对应学生(学生列填充 NULL)
select student.name, class.class_name
from student right join class
on student.class_id = class.class_id;
4. 自连接
将一张表视为两张或多张表,进行自身连接查询,用于查询表内行与行之间的关系(如层级数据、对比数据),必须为表指定别名区分。
select 列名 from 表名 别名1 join 表名 别名2 on 连接条件;
示例:
# 查询 Java 成绩比 C 成绩高的学生
select s1.student_id, s1.name, s1.java_score, s2.c_score
from student s1 join score s2
on s1.student_id = s2.student_id
where s1.java_score > s2.c_score;
5. 子查询
将一个查询结果集作为另一个查询的条件、数据源或字段,分为标量子查询(返回单个值)、列子查询(返回一列值)、表子查询(返回一张表)。
# 示例1:标量子查询(查询与张三同一个班级的学生)
select name from student
where class_id = (select class_id from student where name = '张三')
and name != '张三';
# 示例2:列子查询(使用 IN 关键字,查询多个班级的学生)
select name from student
where class_id in (select class_id from class where class_name like 'java%');
6. 合并查询
将多个 select 语句的查询结果集合并为一个结果集,要求各结果集的列数、数据类型一致,分为 UNION(去重)和 UNION ALL(不去重,效率更高)。
select 列名 from 表名1 [where 条件]
union [all]
select 列名 from 表名2 [where 条件];
示例:
# 合并查询 id=1 的学生和所有学生数据(不去重)
select * from student where id = 1
union all
select * from student;
八、索引
索引是 MySQL 中的特殊数据结构(基于 B+ 树),用于加快查询速度,相当于图书的目录,但会占用额外存储空间,且增删改操作时需要维护索引,带来额外开销。
1. 索引的数据结构
MySQL 索引的底层实现是 B+ 树,它是一种优化后的 N 叉搜索树,相比二叉搜索树、AVL 树、哈希表,具有明显优势:
- 高度更低:N 叉结构减少了树的高度,查询时访问硬盘的次数更少(硬盘 I/O 是数据库查询的瓶颈)。
- 查询稳定:所有数据行都存储在叶子结点,所有查询最终都落到叶子结点,访问硬盘次数一致。
- 支持范围查询:叶子结点以链表形式连接,便于快速遍历范围数据(如
between、order by)。 - 空间利用率高:非叶子结点仅存储索引列(如主键 ID),不存储完整数据行,占用空间小,一个页(16KB)可存储更多索引项。
2. 索引的分类
一个数据表可以有多个索引,核心分类如下:
- 主键索引:创建表时由
PRIMARY KEY约束自动创建,唯一标识一行数据,一个表仅能有一个主键索引,不允许 NULL。 - 唯一索引:由
UNIQUE约束自动创建,保证列值唯一,允许 NULL(多个 NULL 不视为重复)。 - 普通索引:手动创建的索引,无唯一性约束,仅用于加快查询速度。
- 复合索引:基于多个列创建的索引,遵循「最左前缀原则」(查询时需匹配索引的最左列,否则索引失效)。
- 聚集索引:InnoDB 引擎中,主键索引即为聚集索引,叶子结点存储完整数据行。
- 非聚集索引:InnoDB 引擎中,非主键索引(如普通索引、唯一索引)即为非聚集索引,叶子结点存储主键 ID,查询时需要回表(通过主键 ID 查找聚集索引获取完整数据)。
- 索引覆盖:查询的列全部包含在索引中,无需回表,直接从索引中获取数据,查询效率最高。
3. 索引的使用
3.1 建立索引
(1)主键索引
# 方式1:创建表时指定
create table student(id int primary key, name varchar(20));
# 方式2:修改表时添加
alter table student add primary key (id);
# 添加自增主键索引
alter table student modify id int auto_increment;
(2)唯一索引
# 方式1:创建表时指定
create table student(id int, name varchar(20) unique);
# 方式2:修改表时添加
alter table student add unique (name);
(3)普通索引
# 方式1:创建表时指定
create table student(id int, name varchar(20), index idx_name (name));
# 方式2:修改表时添加
alter table student add index idx_name (name);
# 方式3:使用 create index 语句
create index idx_name on student(name);
(4)复合索引
# 方式1:创建表时指定
create table student(id int, name varchar(20), class_id int, index idx_name_class (name, class_id));
# 方式2:修改表时添加
alter table student add index idx_name_class (name, class_id);
# 方式3:使用 create index 语句
create index idx_name_class on student(name, class_id);
3.2 查看索引
# 三种方式等价,查看指定表的所有索引
show keys from 表名;
show index from 表名;
desc 表名;
3.3 删除索引
# (1)删除主键索引
alter table 表名 drop primary key;
# (2)删除唯一索引/普通索引/复合索引
alter table 表名 drop index 索引名;
# 示例:删除 idx_name 索引
alter table student drop index idx_name;
3.4 索引失效场景
索引并非万能,以下场景会导致索引失效,触发全表扫描(EXPLAIN 语句可查看索引使用情况,type 列显示 ALL 表示全表扫描):
- 查询未使用索引列,或查询条件中使用了函数/表达式(如
where id+1=10)。 - 索引列值的区分度极低(如性别列:男/女,索引优化效果差,MySQL 会直接放弃使用索引)。
LIKE模糊查询时,通配符%放在开头(如where name like '%张三')。- 查询条件包含
OR,且一侧列无索引(如where id=1 or name='张三',name 无索引)。 - 使用
!=、<>、NOT等运算符(对索引列操作)。 - 复合索引未遵循最左前缀原则(如复合索引
(a,b,c),查询where b=1 and c=2)。 - 多表联合查询时,表的字符集、排序规则不一致。
九、事务
事务是一组不可分割的 SQL 操作集合,要么全部执行成功(提交),要么全部执行失败(回滚),用于保证数据的完整性和一致性,典型场景如转账、订单创建。
1. 事务的四大特性(ACID)
- 原子性(Atomicity):事务是一个整体,所有 SQL 操作要么全部完成,要么全部回滚,不存在部分执行。
- 一致性(Consistency):事务执行前后,数据的完整性、准确性保持一致(如转账前后,双方总金额不变)。
- 持久性(Durability):事务提交后,对数据的修改永久生效,即使数据库崩溃,数据也不会丢失(写入硬盘)。
- 隔离性(Isolation):多个事务并发执行时,事务之间相互隔离,互不干扰,避免出现脏读、不可重复读、幻读问题。
并发事务的三大问题
- 脏读:事务 A 读取了事务 B 尚未提交的修改数据,事务 B 后续回滚,事务 A 读取的是无效数据。
- 不可重复读:事务 A 多次读取同一数据,事务 B 在期间修改并提交了该数据,导致事务 A 多次读取结果不一致(针对单行数据)。
- 幻读:事务 A 多次执行同一查询,事务 B 在期间插入/删除了数据并提交,导致事务 A 多次查询的结果集不一致(针对结果集)。
2. 事务操作
# 1. 开启事务(默认自动提交关闭,后续 SQL 加入事务)
start transaction;
# 2. 执行一系列 SQL 操作(增删改等)
update account set balance = balance - 100 where id = 1;
update account set balance = balance + 100 where id = 2;
# 3. 提交事务(所有操作生效,永久写入硬盘)
commit;
# 4. 回滚事务(放弃所有操作,恢复到事务开启前状态)
rollback;
# 5. 设置保存点(可回滚到指定保存点,无需回滚整个事务)
savepoint sp1;
# 6. 回滚到保存点
rollback to sp1;
3. 事务隔离级别
MySQL 提供了 4 种事务隔离级别,用于平衡隔离性和并发性,默认隔离级别为 REPEATABLE READ(可重复读)。
| 隔离级别 | 隔离性 | 并发性 | 解决的问题 | 存在的问题 |
|---|---|---|---|---|
READ UNCOMMITTED | 最低 | 最高 | 无 | 脏读、不可重复读、幻读 |
READ COMMITTED | 较低 | 较高 | 脏读 | 不可重复读、幻读 |
REPEATABLE READ(默认) | 较高 | 较低 | 脏读、不可重复读 | 部分幻读(InnoDB 已解决) |
SERIALIZABLE | 最高 | 最低 | 脏读、不可重复读、幻读 | 并发性能极差,仅适用于单事务场景 |
隔离级别操作
# 1. 查询当前隔离级别
select @@global.transaction_isolation; -- 全局隔离级别
select @@session.transaction_isolation; -- 当前会话隔离级别
# 2. 设置隔离级别(临时生效,服务器重启后失效)
set global transaction isolation level 隔离级别; -- 全局
set session transaction isolation level 隔离级别; -- 当前会话
# 示例:设置当前会话隔离级别为 READ COMMITTED
set session transaction isolation level read committed;
十、视图
视图是一张虚拟表,不存储实际数据,仅存储查询语句,查询视图时,会执行其对应的查询语句,返回临时结果集,用于简化复杂查询、控制数据访问权限。
1. 创建视图
create view 视图名 [(列名1, 列名2, ...)] as 查询语句;
# 示例:创建视图,查询学生姓名及其对应的班级名称
create view v_student_class as
select student.name, class.class_name
from student join class
on student.class_id = class.class_id;
2. 使用视图
视图的使用方式与普通数据表一致,可直接用于查询、筛选等操作:
# 示例:查询视图 v_student_class 中的数据
select * from v_student_class where class_name = 'java100';
3. 修改视图
# 方式1:创建或替换视图(视图存在则修改,不存在则创建)
create or replace view 视图名 as 新查询语句;
# 方式2:使用 alter 语句修改
alter view 视图名 as 新查询语句;
注意:并非所有视图都可修改,当视图的查询语句包含聚合函数、group by、distinct 等时,视图无法修改(无法与原数据表一一对应)。
4. 删除视图
drop view [if exists] 视图名;
# 示例:删除视图 v_student_class
drop view if exists v_student_class;
十一、JDBC 编程
JDBC(Java Database Connectivity)是 Java 操作数据库的标准接口,用于实现 Java 程序与 MySQL 等数据库的交互,实际开发中更多使用 MyBatis、JPA 等框架封装 JDBC,简化操作。
前置准备
- 导入 MySQL 驱动包(如
mysql-connector-java-8.0.30.jar)。 - 确保 MySQL 服务已启动,数据库、数据表已创建。
1. 增删改操作(executeUpdate)
核心步骤:创建数据源 → 建立连接 → 构造 SQL 语句 → 执行 SQL → 释放资源。
import com.mysql.cj.jdbc.MysqlDataSource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.Scanner;
public class JdbcInsertDemo {
public static void main(String[] args) throws SQLException {
// 1. 创建数据源(配置数据库连接信息)
DataSource dataSource = new MysqlDataSource();
((MysqlDataSource) dataSource).setURL("jdbc:mysql://127.0.0.1:3306/meat" +
"?characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true");
((MysqlDataSource) dataSource).setUser("root"); // 数据库用户名
((MysqlDataSource) dataSource).setPassword("1234"); // 数据库密码
// 2. 建立与数据库的连接
Connection connection = dataSource.getConnection();
// 3. 构造 SQL 语句(使用 ? 占位符,防止 SQL 注入)
Scanner sc = new Scanner(System.in);
System.out.print("学号:");
int id = sc.nextInt();
System.out.print("姓名:");
String name = sc.next();
System.out.print("班级:");
int classId = sc.nextInt();
String sql = "insert into student values(?, ?, ?)";
PreparedStatement statement = connection.prepareStatement(sql);
// 替换占位符(索引从 1 开始)
statement.setInt(1, id);
statement.setString(2, name);
statement.setInt(3, classId);
// 4. 执行 SQL 语句(增删改使用 executeUpdate,返回受影响的行数)
int affectedRows = statement.executeUpdate();
System.out.println("受影响的行数:" + affectedRows);
// 5. 释放资源(逆序释放,避免资源泄漏)
statement.close();
connection.close();
sc.close();
}
}
2. 查询操作(executeQuery)
核心步骤与增删改一致,差异在于执行 SQL 后返回 ResultSet 结果集,需遍历结果集获取数据。
import com.mysql.cj.jdbc.MysqlDataSource;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
public class JdbcSelectDemo {
public static void main(String[] args) throws SQLException {
// 1. 创建数据源
DataSource dataSource = new MysqlDataSource();
((MysqlDataSource) dataSource).setUrl("jdbc:mysql://127.0.0.1:3306/meat?" +
"characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true");
((MysqlDataSource) dataSource).setUser("root");
((MysqlDataSource) dataSource).setPassword("1234");
// 2. 建立连接
Connection connection = dataSource.getConnection();
// 3. 构造 SQL 语句
String sql = "select * from student";
PreparedStatement statement = connection.prepareStatement(sql);
// 4. 执行 SQL 语句(查询使用 executeQuery,返回结果集)
ResultSet resultSet = statement.executeQuery();
// 5. 遍历结果集,获取数据
while (resultSet.next()) { // next() 移动到下一行,无数据返回 false
int id = resultSet.getInt("id");
String name = resultSet.getString("name");
int classId = resultSet.getInt("class_id");
System.out.println(id + " " + name + " " + classId);
}
// 6. 释放资源(新增 ResultSet,需先释放)
resultSet.close();
statement.close();
connection.close();
}
}