SQL基础
主键
在关系数据库中,一张表中的每一行数据被称为一条记录。一条记录就是由多个字段组成的。例如,
students
表的两行记录:
id |
class_id |
name |
gender |
score |
---|---|---|---|---|
1 |
1 |
小明 |
M |
90 |
2 |
1 |
小红 |
F |
95 |
每一条记录都包含若干定义好的字段。同一个表的所有记录都有相同的字段定义。
对于关系表,有个很重要的约束,就是任意两条记录不能重复。不能重复不是指两条记录不完全相同,而是指能够通过某个字段唯一区分出不同的记录,这个字段被称为 主键 。
例如,假设我们把
name
字段作为主键,那么通过名字
小明
或
小红
就能唯一确定一条记录。但是,这么设定,就没法存储同名的同学了,因为插入相同主键的两条记录是不被允许的。
对主键的要求,最关键的一点是:记录一旦插入到表中,主键最好不要再修改,因为主键是用来唯一定位记录的,修改了主键,会造成一系列的影响。
由于主键的作用十分重要,如何选取主键会对业务开发产生重要影响。如果我们以学生的身份证号作为主键,似乎能唯一定位记录。然而,身份证号也是一种业务场景,如果身份证号升位了,或者需要变更,作为主键,不得不修改的时候,就会对业务产生严重影响。
所以,选取主键的一个基本原则是:不使用任何业务相关的字段作为主键。
因此,身份证号、手机号、邮箱地址这些看上去可以唯一的字段,均 不可 用作主键。
作为主键最好是完全业务无关的字段,我们一般把这个字段命名为
id
。常见的可作为
id
字段的类型有:
- 自增整数类型:数据库会在插入数据时自动为每一条记录分配一个自增整数,这样我们就完全不用担心主键重复,也不用自己预先生成主键;
-
全局唯一GUID类型:使用一种全局唯一的字符串作为主键,类似
8f55d96b-8acc-4636-8cb8-76bf8abc2f57
。GUID算法通过网卡MAC地址、时间戳和随机数保证任意计算机在任意时间生成的字符串都是不同的,大部分编程语言都内置了GUID算法,可以自己预算出主键。
对于大部分应用来说,通常自增类型的主键就能满足需求。我们在
students
表中定义的主键也是
BIGINT NOT NULL AUTO_INCREMENT
类型。
如果使用INT自增类型,那么当一张表的记录数超过2147483647(约21亿)时,会达到上限而出错。使用BIGINT自增类型则可以最多约922亿亿条记录。
联合主键
关系数据库实际上还允许通过多个字段唯一标识记录,即两个或更多的字段都设置为主键,这种主键被称为联合主键。
对于联合主键,允许一列有重复,只要不是所有主键列都重复即可:
id_num |
id_type |
other columns… |
---|---|---|
1 |
A |
… |
2 |
A |
… |
2 |
B |
… |
如果我们把上述表的
id_num
和
id_type
这两列作为联合主键,那么上面的3条记录都是允许的,因为没有两列主键组合起来是相同的。
没有必要的情况下,我们尽量不使用联合主键,因为它给关系表带来了复杂度的上升。
小结
主键是关系表中记录的唯一标识。主键的选取非常重要:主键不要带有业务含义,而应该使用BIGINT自增或者GUID类型。主键也不应该允许
NULL
。
可以使用多个列作为联合主键,但联合主键并不常用。
外键
当我们用主键唯一标识记录时,我们就可以在
students
表中确定任意一个学生的记录:
id |
name |
other columns… |
---|---|---|
1 |
小明 |
… |
2 |
小红 |
… |
我们还可以在
classes
表中确定任意一个班级记录:
id |
name |
other columns… |
---|---|---|
1 |
一班 |
… |
2 |
二班 |
… |
但是我们如何确定
students
表的一条记录,例如,
id=1
的小明,属于哪个班级呢?
由于一个班级可以有多个学生,在关系模型中,这两个表的关系可以称为“一对多”,即一个
classes
的记录可以对应多个
students
表的记录。
为了表达这种一对多的关系,我们需要在
students
表中加入一列
class_id
,让它的值与
classes
表的某条记录相对应:
id |
class_id |
name |
other columns… |
---|---|---|---|
1 |
1 |
小明 |
… |
2 |
1 |
小红 |
… |
5 |
2 |
小白 |
… |
这样,我们就可以根据
class_id
这个列直接定位出一个
students
表的记录应该对应到
classes
的哪条记录。
例如:
-
小明的
class_id
是1
,因此,对应的classes
表的记录是id=1
的一班; -
小红的
class_id
是1
,因此,对应的classes
表的记录是id=1
的一班; -
小白的
class_id
是2
,因此,对应的classes
表的记录是id=2
的二班。
在
students
表中,通过
class_id
的字段,可以把数据与另一张表关联起来,这种列称为
外键
。
外键并不是通过列名实现的,而是通过定义外键约束实现的:
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 classes (id)
指定了这个外键将关联到
classes
表的
id
列(即
classes
表的主键)。
通过定义外键约束,关系数据库可以保证无法插入无效的数据。即如果
classes
表不存在
id=99
的记录,
students
表就无法插入
class_id=99
的记录。
由于外键约束会降低数据库的性能,大部分互联网应用程序为了追求速度,并不设置外键约束,而是仅靠应用程序自身来保证逻辑的正确性。这种情况下,
class_id
仅仅是一个普通的列,只是它起到了外键的作用而已。
要删除一个外键约束,也是通过
ALTER TABLE
实现的:
ALTER TABLE students
DROP FOREIGN KEY fk_class_id;
注意:删除外键约束并没有删除外键这一列。删除列是通过
DROP COLUMN ...
实现的。
多对多
通过一个表的外键关联到另一个表,我们可以定义出一对多关系。有些时候,还需要定义“多对多”关系。例如,一个老师可以对应多个班级,一个班级也可以对应多个老师,因此,班级表和老师表存在多对多关系。
多对多关系实际上是通过两个一对多关系实现的,即通过一个中间表,关联两个一对多关系,就形成了多对多关系:
teachers
表:
id |
name |
---|---|
1 |
张老师 |
2 |
王老师 |
3 |
李老师 |
4 |
赵老师 |
classes
表:
id |
name |
---|---|
1 |
一班 |
2 |
二班 |
中间表
teacher_class
关联两个一对多关系:
id |
teacher_id |
class_id |
---|---|---|
1 |
1 |
1 |
2 |
1 |
2 |
3 |
2 |
1 |
4 |
2 |
2 |
5 |
3 |
1 |
6 |
4 |
2 |
通过中间表
teacher_class
可知
teachers
到
classes
的关系:
-
id=1
的张老师对应id=1,2
的一班和二班; -
id=2
的王老师对应id=1,2
的一班和二班; -
id=3
的李老师对应id=1
的一班; -
id=4
的赵老师对应id=2
的二班。
同理可知
classes
到
teachers
的关系:
-
id=1
的一班对应id=1,2,3
的张老师、王老师和李老师; -
id=2
的二班对应id=1,2,4
的张老师、王老师和赵老师;
因此,通过中间表,我们就定义了一个“多对多”关系。
一对一
一对一关系是指,一个表的记录对应到另一个表的唯一一个记录。
例如,
students
表的每个学生可以有自己的联系方式,如果把联系方式存入另一个表
contacts
,我们就可以得到一个“一对一”关系:
id |
student_id |
mobile |
---|---|---|
1 |
1 |
135xxxx6300 |
2 |
2 |
138xxxx2209 |
3 |
5 |
139xxxx8086 |
有细心的童鞋会问,既然是一对一关系,那为啥不给
students
表增加一个
mobile
列,这样就能合二为一了?
如果业务允许,完全可以把两个表合为一个表。但是,有些时候,如果某个学生没有手机号,那么,
contacts
表就不存在对应的记录。实际上,一对一关系准确地说,是
contacts
表一对一对应
students
表。
还有一些应用会把一个大表拆成两个一对一的表,目的是把经常读取和不经常读取的字段分开,以获得更高的性能。例如,把一个大的用户表分拆为用户基本信息表
user_info
和用户详细信息表
user_profiles
,大部分时候,只需要查询
user_info
表,并不需要查询
user_profiles
表,这样就提高了查询速度。
小结
关系数据库通过外键可以实现一对多、多对多和一对一的关系。外键既可以通过数据库来约束,也可以不设置约束,仅依靠应用程序的逻辑来保证。
索引
在关系数据库中,如果有上万甚至上亿条记录,在查找记录的时候,想要获得非常快的速度,就需要使用索引。
索引是关系数据库中对某一列或多个列的值进行预排序的数据结构。通过使用索引,可以让数据库系统不必扫描整个表,而是直接定位到符合条件的记录,这样就大大加快了查询速度。
例如,对于
students
表:
id |
class_id |
name |
gender |
score |
---|---|---|---|---|
1 |
1 |
小明 |
M |
90 |
2 |
1 |
小红 |
F |
95 |
3 |
1 |
小军 |
M |
88 |
如果要经常根据
score
列进行查询,就可以对
score
列创建索引:
ALTER TABLE students
ADD INDEX idx_score (score);
使用
ADD INDEX idx_score (score)
就创建了一个名称为
idx_score
,使用列
score
的索引。索引名称是任意的,索引如果有多列,可以在括号里依次写上,例如:
ALTER TABLE students
ADD INDEX idx_name_score (name, score);
索引的效率取决于索引列的值是否散列,即该列的值如果越互不相同,那么索引效率越高。反过来,如果记录的列存在大量相同的值,例如
gender
列,大约一半的记录值是
M
,另一半是
F
,因此,对该列创建索引就没有意义。
可以对一张表创建多个索引。索引的优点是提高了查询效率,缺点是在插入、更新和删除记录时,需要同时修改索引,因此,索引越多,插入、更新和删除记录的速度就越慢。
对于主键,关系数据库会自动对其创建主键索引。使用主键索引的效率是最高的,因为主键会保证绝对唯一。
唯一索引
在设计关系数据表的时候,看上去唯一的列,例如身份证号、邮箱地址等,因为他们具有业务含义,因此不宜作为主键。
但是,这些列根据业务要求,又具有唯一性约束:即不能出现两条记录存储了同一个身份证号。这个时候,就可以给该列添加一个唯一索引。例如,我们假设
students
表的
name
不能重复:
ALTER TABLE students
ADD UNIQUE INDEX uni_name (name);
通过
UNIQUE
关键字我们就添加了一个唯一索引。
也可以只对某一列添加一个唯一约束而不创建唯一索引:
ALTER TABLE students
ADD CONSTRAINT uni_name UNIQUE (name);
这种情况下,
name
列没有索引,但仍然具有唯一性保证。
无论是否创建索引,对于用户和应用程序来说,使用关系数据库不会有任何区别。这里的意思是说,当我们在数据库中查询时,如果有相应的索引可用,数据库系统就会自动使用索引来提高查询效率,如果没有索引,查询也能正常执行,只是速度会变慢。因此,索引可以在使用数据库的过程中逐步优化。
小结
通过对数据库表创建索引,可以提高查询速度。
通过创建唯一索引,可以保证某一列的值具有唯一性。
数据库索引对于用户和应用程序来说都是透明的。
基本查询
要查询数据库表的数据,我们使用如下的SQL语句:
SELECT * FROM <表名>
假设表名是
students
,要查询
students
表的所有行,我们用如下SQL语句:
SELECT * FROM students;
使用
SELECT * FROM students
时,
SELECT
是关键字,表示将要执行一个查询,
*
表示“所有列”,
FROM
表示将要从哪个表查询,本例中是
students
表。
该SQL将查询出
students
表的所有数据。注意:查询结果也是一个二维表,它包含列名和每一行的数据。
要查询
classes
表的所有行,我们用如下SQL语句:
SELECT * FROM classes;
运行上述SQL语句,观察查询结果。
SELECT
语句其实并不要求一定要有
FROM
子句。我们来试试下面的
SELECT
语句:
SELECT 100+200;
上述查询会直接计算出表达式的结果。虽然
SELECT
可以用作计算,但它并不是SQL的强项。但是,不带
FROM
子句的
SELECT
语句有一个有用的用途,就是用来判断当前到数据库的连接是否有效。许多检测工具会执行一条
SELECT 1;
来测试数据库连接。
小结
使用SELECT查询的基本语句
SELECT * FROM <表名>
可以查询一个表的所有行和所有列的数据。
SELECT查询的结果是一个二维表。
条件查询
使用
SELECT * FROM <表名>
可以查询到一张表的所有记录。但是,很多时候,我们并不希望获得所有记录,而是根据条件选择性地获取指定条件的记录,例如,查询分数在80分以上的学生记录。在一张表有数百万记录的情况下,获取所有记录不仅费时,还费内存和网络带宽。
SELECT语句可以通过
WHERE
条件来设定查询条件,查询结果是满足查询条件的记录。例如,要指定条件“分数在80分或以上的学生”,写成
WHERE
条件就是
SELECT * FROM students WHERE score >= 80
。
其中,
WHERE
关键字后面的
score >= 80
就是条件。
score
是列名,该列存储了学生的成绩,因此,
score >= 80
就筛选出了指定条件的记录:
SELECT * FROM students WHERE score >= 80;
因此,条件查询的语法就是:
SELECT * FROM <表名> WHERE <条件表达式>
条件表达式可以用
<条件1> AND <条件2>
表达满足条件1并且满足条件2。例如,符合条件“分数在80分或以上”,并且还符合条件“男生”,把这两个条件写出来:
-
条件1:根据score列的数据判断:
score >= 80
; -
条件2:根据gender列的数据判断:
gender = 'M'
,注意gender
列存储的是字符串,需要用单引号括起来。
就可以写出
WHERE
条件:
score >= 80 AND gender = 'M'
:
SELECT * FROM students WHERE score >= 80 AND gender = 'M';
第二种条件是
<条件1> OR <条件2>
,表示满足条件1或者满足条件2。例如,把上述
AND
查询的两个条件改为
OR
,查询结果就是“分数在80分或以上”或者“男生”,满足任意之一的条件即选出该记录:
SELECT * FROM students WHERE score >= 80 OR gender = 'M';
很显然
OR
条件要比
AND
条件宽松,返回的符合条件的记录也更多。
第三种条件是
NOT <条件>
,表示“不符合该条件”的记录。例如,写一个“不是2班的学生”这个条件,可以先写出“是2班的学生”:
class_id = 2
,再加上
NOT
:
NOT class_id = 2
:
SELECT * FROM students WHERE NOT class_id = 2;
上述
NOT
条件
NOT class_id = 2
其实等价于
class_id <> 2
,因此,
NOT
查询不是很常用。
要组合三个或者更多的条件,就需要用小括号
()
表示如何进行条件运算。例如,编写一个复杂的条件:分数在80以下或者90以上,并且是男生:
SELECT * FROM students WHERE (score < 80 OR score > 90) AND gender = 'M';
如果不加括号,条件运算按照
NOT
、
AND
、
OR
的优先级进行,即
NOT
优先级最高,其次是
AND
,最后是
OR
。加上括号可以改变优先级。
常用的条件表达式
条件 |
表达式举例1 |
表达式举例2 |
说明 |
---|---|---|---|
使用=判断相等 |
score = 80 |
name = ‘abc’ |
字符串需要用单引号括起来 |
使用>判断大于 |
score > 80 |
name > ‘abc’ |
字符串比较根据ASCII码,中文字符比较根据数据库设置 |
使用>=判断大于或相等 |
score >= 80 |
name >= ‘abc’ |
|
使用<判断小于 |
score < 80 |
name <= ‘abc’ |
|
使用<=判断小于或相等 |
score <= 80 |
name <= ‘abc’ |
|
使用<>判断不相等 |
score <> 80 |
name <> ‘abc’ |
|
使用LIKE判断相似 |
name LIKE ‘ab%’ |
name LIKE ‘%bc%’ |
%表示任意字符,例如’ab%’将匹配’ab’,’abc’,’abcd’ |
小结
通过
WHERE
条件查询,可以筛选出符合指定条件的记录,而不是整个表的所有记录。
投影查询
使用
SELECT * FROM <表名> WHERE <条件>
可以选出表中的若干条记录。我们注意到返回的二维表结构和原表是相同的,即结果集的所有列与原表的所有列都一一对应。
如果我们只希望返回某些列的数据,而不是所有列的数据,我们可以用
SELECT 列1, 列2, 列3 FROM ...
,让结果集仅包含指定列。这种操作称为投影查询。
例如,从
students
表中返回
id
、
score
和
name
这三列:
SELECT id, score, name FROM students;
这样返回的结果集就只包含了我们指定的列,并且,结果集的列的顺序和原表可以不一样。
使用
SELECT 列1, 列2, 列3 FROM ...
时,还可以给每一列起个别名,这样,结果集的列名就可以与原表的列名不同。它的语法是
SELECT 列1 别名1, 列2 别名2, 列3 别名3 FROM ...
。
例如,以下
SELECT
语句将列名
score
重命名为
points
,而
id
和
name
列名保持不变:
SELECT id, score points, name FROM students;
投影查询同样可以接
WHERE
条件,实现复杂的查询:
SELECT id, score points, name FROM students WHERE gender = 'M';
小结
使用
SELECT *
表示查询表的所有列,使用
SELECT 列1, 列2, 列3
则可以仅返回指定列,这种操作称为投影。
SELECT
语句可以对结果集的列进行重命名。
排序
排序
我们使用SELECT查询时,细心的读者可能注意到,查询结果集通常是按照
id
排序的,也就是根据主键排序。这也是大部分数据库的做法。如果我们要根据其他条件排序怎么办?可以加上
ORDER BY
子句。例如按照成绩从低到高进行排序:
SELECT id, name, gender, score FROM students ORDER BY score;
如果要反过来,按照成绩从高到底排序,我们可以加上
DESC
表示“倒序”:
SELECT id, name, gender, score FROM students ORDER BY score DESC;
如果
score
列有相同的数据,要进一步排序,可以继续添加列名。例如,使用
ORDER BY score DESC, gender
表示先按
score
列倒序,如果有相同分数的,再按
gender
列排序:
SELECT id, name, gender, score FROM students ORDER BY score DESC, gender;
默认的排序规则是
ASC
:“升序”,即从小到大。
ASC
可以省略,即
ORDER BY score ASC
和
ORDER BY score
效果一样。
如果有
WHERE
子句,那么
ORDER BY
子句要放到
WHERE
子句后面。例如,查询一班的学生成绩,并按照倒序排序:
SELECT id, name, gender, score
FROM students
WHERE class_id = 1
ORDER BY score DESC;
这样,结果集仅包含符合
WHERE
条件的记录,并按照
ORDER BY
的设定排序。
小结
使用
ORDER BY
可以对结果集进行排序;
可以对多列进行升序、倒序排序。
分页
使用SELECT查询时,如果结果集数据量很大,比如几万行数据,放在一个页面显示的话数据量太大,不如分页显示,每次显示100条。
要实现分页功能,实际上就是从结果集中显示第1
100条记录作为第1页,显示第101
200条记录作为第2页,以此类推。
因此,分页实际上就是从结果集中“截取”出第M~N条记录。这个查询可以通过
LIMIT <M> OFFSET <N>
子句实现。我们先把所有学生按照成绩从高到低进行排序:
SELECT id, name, gender, score FROM students ORDER BY score DESC;
现在,我们把结果集分页,每页3条记录。要获取第1页的记录,可以使用
LIMIT 3 OFFSET 0
:
SELECT id, name, gender, score
FROM students
ORDER BY score DESC
LIMIT 3 OFFSET 0;
上述查询
LIMIT 3 OFFSET 0
表示,对结果集从0号记录开始,最多取3条。注意SQL记录集的索引从0开始。
如果要查询第2页,那么我们只需要“跳过”头3条记录,也就是对结果集从3号记录开始查询,把
OFFSET
设定为3:
SELECT id, name, gender, score
FROM students
ORDER BY score DESC
LIMIT 3 OFFSET 3;
类似的,查询第3页的时候,
OFFSET
应该设定为6:
SELECT id, name, gender, score
FROM students
ORDER BY score DESC
LIMIT 3 OFFSET 6;
查询第4页的时候,
OFFSET
应该设定为9:
SELECT id, name, gender, score
FROM students
ORDER BY score DESC
LIMIT 3 OFFSET 9;
由于第4页只有1条记录,因此最终结果集按实际数量1显示。
LIMIT 3
表示的意思是“最多3条记录”。
可见,分页查询的关键在于,首先要确定每页需要显示的结果数量
pageSize
(这里是3),然后根据当前页的索引
pageIndex
(从1开始),确定
LIMIT
和
OFFSET
应该设定的值:
-
LIMIT
总是设定为pageSize
; -
OFFSET
计算公式为pageSize * (pageIndex - 1)
。
这样就能正确查询出第N页的记录集。
如果原本记录集一共就10条记录,但我们把
OFFSET
设置为20,会得到什么结果呢?
SELECT id, name, gender, score
FROM students
ORDER BY score DESC
LIMIT 3 OFFSET 20;
OFFSET
超过了查询的最大数量并不会报错,而是得到一个空的结果集。
注意
OFFSET
是可选的,如果只写
LIMIT 15
,那么相当于
LIMIT 15 OFFSET 0
。
在MySQL中,
LIMIT 15 OFFSET 30
还可以简写成
LIMIT 30, 15
。
使用
LIMIT <M> OFFSET <N>
分页时,随着
N
越来越大,查询效率也会越来越低。
小结
使用
LIMIT <M> OFFSET <N>
可以对结果集进行分页,每次查询返回结果集的一部分;
分页查询需要先确定每页的数量和当前页数,然后确定
LIMIT
和
OFFSET
的值。
聚合查询
如果我们要统计一张表的数据量,例如,想查询
students
表一共有多少条记录,难道必须用
SELECT * FROM students
查出来然后再数一数有多少行吗?
这个方法当然可以,但是比较弱智。对于统计总数、平均数这类计算,SQL提供了专门的聚合函数,使用聚合函数进行查询,就是聚合查询,它可以快速获得结果。
仍然以查询
students
表一共有多少条记录为例,我们可以使用SQL内置的
COUNT()
函数查询:
SELECT COUNT(*) FROM students;
COUNT(*)
表示查询所有列的行数,要注意聚合的计算结果虽然是一个数字,但查询的结果仍然是一个二维表,只是这个二维表只有一行一列,并且列名是
COUNT(*)
。
通常,使用聚合查询时,我们应该给列名设置一个别名,便于处理结果:
SELECT COUNT(*) num FROM students;
COUNT(*)
和
COUNT(id)
实际上是一样的效果。另外注意,聚合查询同样可以使用
WHERE
条件,因此我们可以方便地统计出有多少男生、多少女生、多少80分以上的学生等:
SELECT COUNT(*) boys FROM students WHERE gender = 'M';
除了
COUNT()
函数外,SQL还提供了如下聚合函数:
函数 |
说明 |
---|---|
SUM |
计算某一列的合计值,该列必须为数值类型 |
AVG |
计算某一列的平均值,该列必须为数值类型 |
MAX |
计算某一列的最大值 |
MIN |
计算某一列的最小值 |
注意,
MAX()
和
MIN()
函数并不限于数值类型。如果是字符类型,
MAX()
和
MIN()
会返回排序最后和排序最前的字符。
要统计男生的平均成绩,我们用下面的聚合查询:
SELECT AVG(score) average FROM students WHERE gender = 'M';