数据库相关的知识在面试时也经常遇到,不过本人对这块内容并不是很了解,只是处于简单的增删查改的水平,姑且做一个简单的总结,以备查忘。
一、关系模型
主键
对于关系表,有个很重要的约束,就是任意两条记录不能重复。不能重复不是指两条记录不完全相同,而是指能够通过某个字段唯一区分出不同的记录,这个字段被称为主键。
主键的一个基本原则:不使用任何业务相关的字段作为主键。常见的可作为id的字段类型有:
自增整数类型:数据库会在插入数据时自动为每一条记录分配一个自增整数。
全局唯一GUID类型:使用一种全局唯一的字符串作为主键。
联合主键:关系数据库实际上还允许通过多个字段唯一标识记录。即两个或更多的字段设置为主键,这种主键被称为联合主键。
外键
如果一个关系的一个或一组属性引用(参照)了另一个关系的主键,则称这个或这组属性为外码或外键。
外键不是通过列名实现的,而是通过定义外键约束实现的:
ALTER TABLE students
ADD CONSTRAINT fk_class_id
FOREIGN KEY (class_id)
REFERENCES classes (id);
其中。外键约束的名称fk_class_id可以任意,FOREIGN KEY (class_id)指定了class_id作为外键,REFERENCES class (id)指定了这个外键将关联到classes表的id列(即classes表的主键)。
通过定义外键约束,关系数据库可以保证无法插入无效的数据。
由于外键约束会降低数据库的性能,大部分互联网应用程序为了追求速度,并不设置外键约束,而是仅靠应用程序自身来保证逻辑的正确性。
删除一个外键约束:
ALTER TABLE students
DROP FOREIGN KEY fk_class_id
索引
索引是关系数据库中对某一列或多个列的值进行预排序的数据结构。通过使用索引,可以让数据库系统不必扫描整个表,而是直接定位到符合条件的记录,这样就大大加快了查询速度。
创建索引:
ALTER TABLE students
ADD INDEX id_name_score (name, score);
索引的效率取决于索引列的值是否散列,即该列的值如果越互不相同,那么索引效率越高。
唯一索引:在设计关系数据表的时候,看上去唯一的列,例如身份证号等,因为具有业务含义,因此不宜作为主键。但是这些列根据业务要求,又具有唯一性约束,可以给该列添加一个唯一索引或唯一约束来保证该列数据的唯一性。
ALTER TABLE students
ADD UNIQUE INDEX uni_name (name);
ALTER TABLE students
ADD CONSTRAINT uni_name UNIQUE (name);
二、查询数据
基本查询
要查询数据库表的数据,我们使用如下的SQL语句:
SELECT * FROM <表名>;
SELECT 100+200;
其中,SELECT是关键字,表示将要执行一个查询。*表示所有列,FROM表示将要从哪个表查询。SELECT查询的结果也是一个二维表,它包含列名和每一行的数据。其中。FROM关键字也并不是必要的。
条件查询
条件查询语句为:
SELECT * FROM <表名> WHERE <条件表达式>
条件表达式可以用AND,OR和NOT。常见的条件表达式:
条件 | 表达式举例 | 说明 |
---|---|---|
使用=判断相等 | score =90,name ='abc' | 字符串需要用单引号括起来 |
使用>判断大于 | score >90,name >'abc' | 字符串比较根据ASCII码 |
使用>=判断大于或相等 | score >=90,name >='abc' | |
使用<判断小于 | score <90,name <'abc' | |
使用<=判断小于或相等 | score <=90,name< ='abc' | |
使用<>判断不相等 | score <>90,name <>'abc' | |
使用LIKE判断相似 | name LIKE 'ab%' | %表示任意字符 |
投影查询
如果我们只希望返回某些列的数据,而不是所有列的数据,可以用SELECT 列1, 列2,....,让结果仅包含指定列,这种操作称为投影查询。同时,还可以给每一列起个别名。
SELECT id, score points, name FROM students WHERE gender ='M'
排序
我们使用SELECT查询时,通常是按照id排序的,也是根据主键排序,如果要根据其它条件排序,可以加上ORDER BY子句:
-- 按score从低到高
SELECT id, name, gender, score FROM students ORDER BY score;
-- 按score从高到低
SELECT id, name, gender, score FROM students ORDER BY score DESC;
如果score列有相同的数据,要进一步排序,可以继续添加列名
-- 按score, gender排序
SELECT id, name, gender, score FROM students ORDER BY score DESC, gender;
默认的规则是ASC:升序,即从小到大,可以省略。如果有WHERE子句,那么ORDER BY子句要放到WHERE子句后面。
SELECT id, name, gender, score
FROM students
WHERE class_id =1
ORDER BY score DESC;
分页查询
使用SELECT查询时,如果数据集数据量很大,比如几万行数据,放在一个页面显示的话数据量很大,不如分页显示,分页查询可以用子句LIMIT
-- 查询第一页
SELECT id, name, gender, score
FROM students
ORDER BY score DESC
LIMIT 3 OFFSET 0;
-- 查询第二页
SELECT id, name, gender, score
FROM students
ORDER BY score DESC
LIMIT 3 OFFSET 3;
LIMIT总是设定为每页需要显示的数量pagesize,OFFSET为起始的索引。
聚合查询
SQL提供了专门的聚合函数来快速获得结果。以查询students表一共有多少条记录为例,可以用内置的COUNT()函数查询.
COUNT(*)表示查询所有列的行数,查询结果虽然为一个数字,但仍然是一个二维表,通常使用聚合查询时,应该为列名设置一个别名,便于处理结果.
聚合查询同样可以使用WHERE条件。
SELECT COUNT(*) boys FROM students WHERE gender ='M';
除了COUNT()函数外,还有如下聚合函数:
函数 | 说明 |
---|---|
SUM | 计算某一列的合计值,该列必须为数值类型 |
AVG | 计算某一列的平均值,该列必须为数值类型 |
MAX | 计算某一列的最大值 |
MIN | 计算某一列的最小值 |
注意,MAX()和MIN()函数并不限于数值类型。如果是字符类型,MAX()和MIN()会返回排序最后和最前的字符。此外,如果WHERE没有匹配到任何行,COUNT()会返回0,而SUNM()、AVG()、MAX()和MIN()会返回NULL。
分组
如果要统计一班的学生数量,可以用下面的语句,要继续统计二班、三班学生的数量,可以采用SQL提供的分组聚合功能:
-- 统计一班学生数量
SELECT COUNT(*) num FROM students WHERE class_id =1;
-- 按class_id分组:
SELECT class_id, COUNT(*) num FROM students GROUP BY class_id;
也可以使用多个列进行分组,例如,想统计各班的男生和女生人数:
-- 按class_id, gender分组
SELECT class_id, gender, COUNT(*) num FROM students GROUP BY class_id, gender;
多表查询
SELECT查询不但可以从一张表查询数据,还可以从多张表同时查询数据,查询多张表的语法是:SELECT * FROM <表1> <表2>,例如同时从students和classes表的“乘积”,即查询数据,可以这么写:
-- FROM students, classes:
SELECT * FROM students, classes;
这种一次查询两个表的数据,查询的结果也是一个二维表,它是students表和classes表的“乘积”,即students表的每一行与classes表的每一行两两都拼在一起返回。结果集的列数是students表和classes表的列数之和,行数是两表的行数之积。
当有两列标题重名时,可以设置别名,如下,但是使用table.alias这样的方式来音乐列和设置别名有点麻烦,SQL还允许给表设置一个别名。
-- set alias:
SELECT
students.id sid,
students.name,
students.gender,
students.score,
classes.id cid,
clssses.name cname
FROM students, classes;
-- set table alias:
SELECT
s.id sid,
s.name,
s.gender,
s.score,
c.id cid,
c.name cname
FROM students s, classes c;
连接查询
连接查询是另一种类型的多表查询。连接查询对多个表进行JOIN运算,简单地说,就是先确定一个主表作为结果集,然后,把其它表的行有选择性的“连接”在主表结果集上。
例如,我们想选出students表的所有学生信息,可以用一条简单的SELECT语句完成,但是结果集只有class_id列,没有对应班级的name列。现在问题来了,存放班级名称的name列存储在classes表中,只有根据students表的class_id,找到classes表对应的行,再取出name列,就可以获得班级名称。这时,连接查询就派上了用场,我们先用最常用的一种内连接——INNER JOIN来实现:
SELECT s.id, s.name, s.class_id, c.name class_name, s.gender, s.score
FROM students s
INNER JOIN classes c
ON s.class_id =c.id;
注意INNER JOIN查询的写法是:
先确定主表,仍然使用FROM<表1>的语法
再确定需要连接的表,使用INNER JOIN <表2>的语法
然后确定连接条件,使用ON <条件...>,这里的条件是s.class_id =c.id,表示students表的class_id列与classes表的id列相同的行需要连接
可选:加上WHERE子句、ORDER BY等子句
有内连接就有外连接,它们的区别是;
INNER JOIN 只返回同时存在与两表的行数据,由于students表的class_id包含1,2,3,classes表的id包含1,2,3,4,所以,INNER JOIN根据条件s.class_id=c.id返回的结果仅包含1,2,3
RIGHT OUTER JOIN返回右表都存在的行,如果某一行仅在右表存在,那么结果集就会以NULL填充剩下的字段
LEFT OUTER JOIN只返回左表都存在的行,如果我们给students表增加一行,并添加class_id=5,由于classes表并不存在id=5的行,所以LEFT OUTER JOIN的结果就会增加一行,对应的class_name是NULL
FULL OUTER JOIN会把两张表的所有记录全部选择出来,并且自动把对方不存在的列填充为NULL
三、修改数据
关系数据库的基本操作就是增删改查,即CRUD:Create、Retrieve、Update、Delete
插入数据
当我们向数据表插入一条新纪录时,就必须使用INSERT语句
INSERT INTO <表名> (字段1,字段2,...) VALUES (值1,值2,...);
-- 一次性添加多条新纪录
INSERT INTO students (class_id, name, gender, score) VALUES
(1, '大宝', 'M', 87),
(2, '二宝', 'M', 81);
更新数据
如果要更新数据表中的记录,就必须用UPDATE语句
UPDATE <表名> SET 字段1=值1,字段2=值2,... WHERE ...;
-- 例如,我们想更新students表id=1的记录的name和score这两个字段
UPDATE students SET name='大牛', score=66 WHERE id=1;
在UPDATE语句中,更新字段时可以使用表达式,例如。把所有80分以下的同学成绩加10分:
UPDATE students SET score=score+10 WHERE score<80;
注意的是,UPDATE语句可以没有WHERE条件,这时,整个表的所有记录都会被更新,所以,在执行UPDATE语句时要非常小心,最好先用SELECT语句来测试WHERE条件是否筛选出了期望的记录值,然后再UPDATE更新。
删除数据
如果要删除数据库表中的记录,可以用DELETE语句
DELETE FROM <表名> WHERE ...;
-- 例如,删除students表中id=1的记录
DELETE FROM students WHERE id=1;
注意,如果WHERE条件没有匹配到任何记录,DELETE语句不含报错,也不会有任何记录被删除/不带WHERE条件的DELETE语句会删除整个表的数据。
四、事务
在执行SQL语句的时候,某些业务要求,一系列操作必须全部执行,而不能执行一部分。例如,一个转账操作:
-- 从id=1的账号给id=2的账户转账100元
-- 第一步:将id=1的账户余额减去100
UPDATE accounts SET balance =balance-100 WHERE id=1;
-- 第二步:将id=2的账户余额加上100
UPDATE accounts SET balance =balance+100 WHERE id=2;
这两条SQL语句必须全部执行,或者。某些原因,如果第一条语句成功,第二条语句失败,就必须全部撤销。
这种把多条语句作为一个整体进行操作的功能,被称为数据库事务。数据库事务可以确保该事物范围内的所有操作都可以全部成功或者全部失败。如果事务失败,那么效果就和没有执行这些SQL一样,不会对数据库有任何改动。
可见,事物具有ACID这4个特性:
A:Atomic,原子性,将所有SQL作为原子工作单元执行,要么全部执行,要么全部不执行;
C:Consistent,一致性,事务完成后,所有数据的状态都是一致的,即A账户只要减去了100,B账户则必定加上了100
I:Isolation,隔离性,如果有多个事务并发执行,每个事务作出的修改必须与其它事务隔离;
D:Duration,持久性,即事务完成后,对数据库数据的修改持久化存储
对于单条SQL语句,数据库系统自动将其作为一个事务执行,这种事务被称为隐式事务。
要把多条SQL语句作为一个事务执行,使用BEGIN开启一个事务,使用COMMIT提交一个事务,这种事务被称为显式事务,例如,上面的转账操作:
BEGIN
UPDATE accounts SET balance =balance-100 WHERE id=1;
UPDATE accounts SET balance =balance+100 WHERE id=2;
COMMIT;
很显然多条SQL语句要想作为一个事务执行,就必须使用显氏事务。
COMMIT是指提交事务,即试图把事务内的所有SQL所做的修改永久保存。如果COMMIT语句执行失败了,整个事务也会失败,可以用ROLLBACK回滚事务,整个事务失败。
隔离级别:对于并发执行的事务,如果涉及到操作同一条记录的时候,可能会发生问题,因为并发操作会带来数据的不一致性,包括脏读、不可重复读、幻读等。数据库系统提供了隔离级别来让我们有针对性的选择事务的隔离级别,避免数据不一致的问题。
SQL标准定义了4种隔离级别,分别对应可能出现的数据不一致情况:
Isolation | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
Read Uncommited | Yes | Yes | Yes |
Read Commited | - | Yes | Yes |
Repeatable Read | - | - | Yes |
Serializable | - | - | - |
Read Uncommited是隔离级别最低的一种事务级别。在这种隔离级别下,一个事务会读到另一个事务更新后但未提交的数据,如果另一个事务回滚,那么当前事务读到的数据就是脏数据,这就是脏读(Dirty Read)。
在Read Commited隔离级别下,一个事务可能会遇到不可重复读(Non Repeatable Read)的问题。不可重复读是指,在一个事务内,多次读同一个数据,在这个事务还没有结束时,如果另一个事务恰好修改了这个数据,那么第一个事务,两次读取的数据就可能不一致。
在Repeatable Read隔离级别下,一个事务可能会遇到幻读(Phantom Read)的问题。幻读是指,在一个事务中,第一次查询某条记录,发现没有,但是,当试图更新这条不存在的记录时,竟然能成功,并且再次读取同一条记录,它就神奇的出现了。
Serializable是最严格的隔离级别,在这种情况下,所有事务都按照次序依次执行,因此脏读、不可重复读、幻读都不会出现。
五、实用SQL语句
插入或替换
如果我们希望插入一条新记录,但如果记录已经存在,就先删除原记录,再插入新记录。此时,可以使用REPLACE语句,这样就不必先查询,再决定是否先删除再插入:
REPLACE INTO students (id, class_id, name, gender, score) VALUES (1,1,'小米','F',99);
若id=1的记录不存在,REPLACE语句将插入新数据,否则,当前id=1的记录将被删除,然后再插入新记录。
插入或更新
如果我们希望插入一条新记录,但如果记录已经存在,就更新该记录,可以用如下语句:
INSERT INTO students (id, class_id) VALUES (1,1) ON DUPLICATE KEY UPDATE class_id=2;
若id=1的记录不存在,INSERT语句将插入新数据,否则,当前id=1的记录将被更新,更新的字段由UPDATE指定。
插入或忽略
如果我们希望插入一条新记录,但如果记录已经存在,就直接忽略,可以用如下语句:
INSERT IGNORE INTO students (id, class_id) VALUE (1,1);
快照
如果想要对一个表进行快照,即复制一份当前表的数据到一个新表:
-- 对class_id=1的记录进行快照,并存储为新表students_of_classes:
CREATE TABLE students_of_classes SELECT * FROM students WHERE class_id=1;
写入查询结果集
如果查询结果需要写入到表中,先创建一个统计成绩的表statistics,记录各班的平均成绩:
CREATE TABLE statistics (
id BIGINT NOT NULL AUTO_INCREMENT,
class_id BIGINT NOT NULL,
averge DOUBLE NOT NULL,
PRIMARY KEY (id)
);
然后用以下语句写入各班平均成绩:
INSERT INTO statistics (class_id, averge)
SELECT class_id, AVG(score) FROM students
GROUP BY class_id;