关系型数据库和非关系型数据库
简单来说:
关系型:
- 以表结构管理数据,数据存在行和列中
- 通过外键建立表之间的联系
- 使用 sql 语言操作
- mysql 是常见的关系型数据库
非关系型:
- 无固定的结构,数据可以是键值对(Key-Value)、文档(JSON、BSON)等形式
- 不强调数据的关联
- 无固定操作语言,可以是 JSON 风格的查询语法(MongoDB)或简单的命令行(Redis)等等
- MongoDB 是常见的非关系型数据库
本文是关系型数据库的学习记录
表
关系型数据库通过一张张的表格管理数据,通过主键唯一标识一行数据,通过外键建立表之间的关联
主键外键
主键: 一行数据的唯一标识
外键: 通过关联另一张表或自身表的主键,建立表之间的联系
一个表可以有多个主键(当一个属性不能唯一标识数据时,可以设置多个主键)
一个表可以有多个外键
案例
通过案例能清晰理解表、主键、外键的关系
该示例图中,橘色为主键,绿色为外键,可以看到主键是唯一标识数据的
表格通过外键关联,外键可以关联自身主键,如 sup_id
Works_With 表中 员工id 和 客户id 同时是主键和外键。
常用操作
sql 语言操作指令非常语义化,刚开始不熟悉时用到再搜也不是不行,主要是要有了操作的思路就行,之后再搜命令或直接问 gpt
数据库和表操作
create database mydatabase; -- 创建数据库
show databases; -- 显示数据库
drop database sql_tutorial; -- 删除数据库
use mydatabase; -- 选中数据库,需要先选中某个数据库,再对其进行相关操作
/**
创建表,可以给属性设置条件
not null 非空
unique 唯一
default 默认值
auto_increment 自增
unsigned 无符号,一般用于限制不能是负数
check 值检查
*/
create table student( -- 创建表格
student_id int primary key auto_increment comment '学生ID', -- 主键
name varchar(20) not null comment '名称', -- 非空
major varchar(20) unique comment '学科', -- 唯一
age int default 8 comment '年龄', -- 默认值
phone varchar(20) comment '电话',
sex char(1) not null, check (sex in ('M', 'F')) comment '性别',
teacher_id int comment '老师ID',
constraint chk_age check (age >= 8), -- check 的另一种写法,年龄必须大于等于8
foreign key (teacher_id) references teacher(teacher_id) on delete set null -- teacher_id 外键,关联 teacher 表的 teacher_id
);
-- 增加表格列
alter table student add gpa decimal(3,2); -- decimal(3,2) 意思是总共三位数字,其中两位是在小数点后的,也即 `-9.99` 到 `9.99` 这个范围
-- 删除表格列
alter table student drop column gpa;
-- 若创建时没有指定主键,可这样设置
alter table student add primary key (student_id);
/**
若创建时没有设置外键,可这样设置
on delete set null:意思是当 student 关联的 teacher 删掉后,将其 teacher_id 设置成 null
on delete cascade:意思是当 student 关联的 teacher 删掉后,将此学生也删掉
*/
alter table student add foreign key(teacher_id) references teacher(teacher_id) on delete set null
show tables; -- 显示所有表
describe student -- 显示某个表详情
DROP TABLE student; -- 删除表
查
以上面的公司数据库为例
常规查询
-- 查询员工表中所有数据
select * from employee;
-- 只查询某些列
select name, salary from employee;
-- 去重
select distinct sex from employee
-- 排序,默认是升序
select * from employee order by salary asc; -- 反回员工根据工资排序,默认升序 asc 可省略
select * from employee order by salary desc; -- 降序
select * from employee order by salary, sex; -- 先根据 salary 排序,若相同,再根据 sex 排序
-- 限制数量
select * from employee limit 3; -- 返回3条员工数据
-- 筛选
select * from employee where sex = 'F'; -- 返回所有女员工
select * from employee where sex = 'F' and name = '小黄'; -- 返回性别为女,名字为小黄的员工
select * from employee where salary > 40000; -- 返回工资大于40000的员工
select * from employee where birth_date > '1970-01-01'; -- 返回出生日期在1970-01-01之后的员工
-- 注意:不等于是 <> 不是 !=
select * from employee where `name` <> '小黑'; -- 返回名字不为小黑的员工
-- in
select *
from employee
where name in('小黑', '小黄'); -- 相当于 `name` = '小黑' or `name` = '小黄',返回名字为小黑或小黄的员工
-- 条件都可以并用
select *
from employee
where sex = 'F'
order by `score`
limit 2;
聚合函数
select count(*) from employee; -- 返回员工数量
select count(sup_id) from employee; -- 返回有主管的员工的数量(sup_id有值的)
select count(*) from employee where salary > 40000; -- 返回工资大于40000的员工的数量
select avg(salary) from employee; -- 返回所有员工的平均工资
select sum(salary) from employee; -- 返回所有员工的工资总合
select max(salary) from employee; -- 返回员工表中最高的工资
select min(salary) from employee; -- 返回员工表中最低的工资
万用字符
-- “%” 代表多个字符 “_” 代表一个字符
select * from client where phone like '020%'; -- 返回电话是 020 开头的客户
select * from client where phone like '%368'; -- 返回电话是 268 结尾的客户
select * from client where phone like '%111%'; -- 返回电话中间有 111 的客户
select * from client where name like '陈%'; -- 返回姓陈的客户
select * from employee where birth_date like '_____08%'; -- 返回8月份生日的客户,前面五个下划线代表年和中划线
union
合并多个查询
select name from employee
union
select client_name from client
union
select branch_name from branch; -- 将这三个查询结果合并返回,返回员工名字、客户名字、部门名字
-- 统一属性名
select emp_id as total_id, name as total_name
from employee
union
select client_id, client_name from client; -- 将这两个查询结果合并返回,并且id统一为total_id,名字统一为total_name
-- 需要注意查询的列数目需要一致
select name, sex from employee
union
select client_name from client; -- 错误:数目不一致
join
连接多个表格
以上面的 branch
表为例,可以看到表中只有 manager_id
,若这时想查询部门经理的名字,发现是没有这一列的,这时可以将 branch
表和 employee
表链接起来,这样连接后,不就有 id 又有名字了吗,由于 manager_id
是外键,关联 employee
表的 emp_id
,连接这两个表时可以以此为关联依据
select *
from employee join branch
on emp_id = manager_id; -- 合并这两个表,并返回所有数据
-- 可以只返回某些列
select employee.emp_id, employee.name, branch.branch_name
from employee join branch
on emp_id = manager_id;
/**
以上两个语句只会查出三条,因为满足 emp_id 与 manager_id 相等的只有三条
如果需要把其他不相等的也返回,可以使用 left join 或 right join
- left join 左边的表格无论是否满足条件都返回,右边的则需要满足条件
- right join 右边的表格无论是否满足条件都返回,左边的则需要满足条件
**/
select employee.emp_id, employee.name, branch.branch_name
from employee left join branch
on emp_id = manager_id; -- 这里左边的表格是 employee
子查询
以某个查询的结果作为另一个查询的依据
/**
查询研发部门的经理名字
思路:先在 branch 表中查出研发部门的经理 id,再拿此 id 在 employee 表中查询,就能拿到 name 属性
**/
select name from employee where emp_id = (
select manager_id from branch where branch_name = '研发'
);
/** 查询销售金额超过 50000 的员工的名字 **/
select name from employee
where emp_id in(
select emp_id from work_with
where total_sales > 50000
); -- 由于销售额大于 50000 的员工可能不止一位,所以用 in
视图
简单理解为封装一个查询语句
# 创建
CREATE VIEW view_non_null_sup_employees AS
SELECT *
FROM employees
WHERE sup_id IS NOT NULL;
# 执行
SELECT * FROM view_non_null_email_employees;
储存过程
简单理解为封装一个语句,可以是增删改查,可以定义参数
# 创建
DELIMITER $$
CREATE PROCEDURE update_salary(
IN emp_id INT, -- 员工ID(输入参数)
IN increment_percent DECIMAL(5, 2) -- 增长百分比(输入参数)
)
BEGIN
-- 更新员工工资
UPDATE employees
SET salary = salary + (salary * increment_percent / 100)
WHERE id = emp_id;
-- 检查是否更新成功
IF ROW_COUNT() = 0 THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Employee not found';
END IF;
END $$
DELIMITER ;
# 执行
CALL update_salary(1, 10);
事务管理
事务处理是一种机制,用来管理必须成批执行的SQL操作,保证数据库不包含不完整的操作结果。
假设需要实现一个转账操作:
- 从 Alice 的账户扣除 200 元。
- 向 Bob 的账户增加 200 元。
- 如果任意一步失败,则回滚整个操作。
-- 开始事务
START TRANSACTION;
-- 从 Alice 的账户扣除 200 元
UPDATE accounts
SET balance = balance - 200
WHERE name = 'Alice';
-- 检查是否成功
IF ROW_COUNT() = 0 THEN
ROLLBACK;
LEAVE;
END IF;
-- 向 Bob 的账户增加 200 元
UPDATE accounts
SET balance = balance + 200
WHERE name = 'Bob';
-- 检查是否成功
IF ROW_COUNT() = 0 THEN
ROLLBACK;
LEAVE;
END IF;
-- 提交事务
COMMIT;
打游戏时,一般会有一些保留点,当失败后,可以从保留点重新开始,事务管理也类似,可以创建保留点,当某个步骤遇到错误可以回退到此安全状态
假设有一个表 orders
和一个表 order_items
:
orders
:记录订单信息。order_items
:记录订单的商品信息。
我们需要执行以下操作:
- 创建订单。
- 插入多个商品到此订单。
- 如果某个订单项插入失败,回滚到订单创建后的状态,而不是回滚整个事务。
START TRANSACTION;
-- 插入订单记录
INSERT INTO orders (order_id, order_date, customer_name)
VALUES (1, '2024-12-30', 'John Doe');
-- 创建保存点
SAVEPOINT AfterOrderCreated;
-- 插入第一个订单项
INSERT INTO order_items (order_id, product_name, quantity, price)
VALUES (1, 'Product A', 2, 20.00);
-- 检查是否出错
IF ROW_COUNT() = 0 THEN
ROLLBACK TO AfterOrderCreated;
-- 可以选择直接回滚整个事务
ROLLBACK;
LEAVE;
END IF;
-- 插入第二个订单项(故意出错,例如违反约束)
INSERT INTO order_items (order_id, product_name, quantity, price)
VALUES (1, NULL, 1, 15.00); -- NULL 不允许,导致失败
-- 检查是否出错
IF ROW_COUNT() = 0 THEN
ROLLBACK TO AfterOrderCreated;
-- 此时事务状态恢复到插入订单后的状态
END IF;
-- 插入另一个订单项(继续操作)
INSERT INTO order_items (order_id, product_name, quantity, price)
VALUES (1, 'Product B', 3, 30.00);
-- 检查是否出错
IF ROW_COUNT() = 0 THEN
ROLLBACK TO AfterOrderCreated;
ROLLBACK;
LEAVE;
END IF;
-- 提交事务
COMMIT;
游标
简单理解为循环处理数据
举例:根据以下规则调整工资:
- 如果绩效评分大于 80,加薪 10%。
- 如果评分在 60 到 80 之间,加薪 5%。
- 如果评分低于 60,不加薪。
- 记录每次加薪的日志到
salary_log
表。
# 创建
DELIMITER $$
CREATE PROCEDURE AdjustSalaries()
BEGIN
-- 声明变量
DECLARE emp_id INT;
DECLARE emp_salary DECIMAL(10, 2);
DECLARE emp_score INT;
DECLARE new_salary DECIMAL(10, 2);
DECLARE done INT DEFAULT 0;
-- 声明游标
DECLARE cursor_emp CURSOR FOR
SELECT e.id, e.salary, p.score
FROM employees e
JOIN performance p ON e.id = p.emp_id;
-- 声明结束标志处理器:当游标到达结果集末尾时,将 `done` 设置为 `1`,用来结束循环。
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
-- 打开游标
OPEN cursor_emp;
-- 游标循环
read_loop: LOOP
FETCH cursor_emp INTO emp_id, emp_salary, emp_score;
IF done = 1 THEN
LEAVE read_loop;
END IF;
-- 根据评分计算新工资
IF emp_score > 80 THEN
SET new_salary = emp_salary * 1.1;
ELSEIF emp_score >= 60 THEN
SET new_salary = emp_salary * 1.05;
ELSE
SET new_salary = emp_salary; -- 无变化
END IF;
-- 更新工资
UPDATE employees
SET salary = new_salary
WHERE id = emp_id;
-- 记录日志
INSERT INTO salary_log (emp_id, old_salary, new_salary, change_date)
VALUES (emp_id, emp_salary, new_salary, NOW());
END LOOP;
-- 关闭游标
CLOSE cursor_emp;
END $$
DELIMITER ;
# 执行
CALL AdjustSalaries();
增 改 删
以此表为例
create table student(
id int unsigned primary key auto_increment comment 'ID',
name varchar(20) comment '名称',
major varchar(20) comment '科目',
score decimal(3,1) comment '分数';
)
-- 增
insert into student values(6, '张三', '数学', '100');
-- 由于 id 是自增的,所以插入时可以不传值
insert into student (name, major, score) values ('李四', '语文', 88);
insert into student (name, major, score) values ('王五', null, 99);
-- 改
update student
set major = '英语口语'
where major = '英语'; -- 将 英语 的改成 英语口语
update student
set major = '生物'
where student_id = 3; -- 将 student_id 为 3 的学生的科目改成 生物
update student
set major = '生化'
where major = '生物' or major = '化学'; -- 将 生物 和 化学 改成 生化
-- 可改多个属性
update student
set name = '小灰', major = '物理'
where student_id = 3; -- 将 student_id 为 3 的学生的名字改成小灰,科目改成物理
-- 可以不限制条件
update student
set major = '数理化'; -- 将所有科目改成数理化
-- 删除
delete from student
where student_id = 4; -- 删除 student_id 为 4 的学生
delete from student
where score <= 60; -- 删除分数小于等于 60 的学生
参考
《sql必知必会》