于是又到了梧桐絮飘满校园的四月,又到了梧桐色相簿的季节,我们迎来了实习生招聘。
“范老湿从不写代码”,“范老湿从不会SQL”,说起来都是泪。去年就因为SQL挂了面了一下午的X团,然而今年仍然不会。近来又因为C能力低下挂掉了N多OJ,满屏AC的日子一去不复返。所幸现在拯救也许还不晚,那让我们来愉快的治疗它吧~
参考书籍是Ben Forta的《SQL必知必会》
一、SQL相关概念
- 数据库(database):保存有组织的数据的容器(通常是一个文件或一组文件);
- 表(table):某种特定类型数据的结构化清单;
每一个数据库中的表都有一个唯一的表名 - 模式:关于数据库和表的布局及特性的信息;
- 列(column):表中的一个字段;
所有表都是由一个或多个列组成的
每一个表列都有一个数据类型 - 行(row):表中的一个纪录;
有时称纪录(record),行才是正确的术语 - 主键(primary key):一(组)列,其值存在且唯一,故能标识表中的每一行;
主键列的值一般不允许修改与更新,且不能重用(即被删除行的值删除后不能用于以后的值)
范老湿总结:每一个database里面可能有很多table,既然是table那一定有column和row,起唯一标识作用的column被称为primary key。Primary key中的值一定符合以下性质:存在性、唯一性、不可复用性。
以及:
- 关键字:即SQL中的保留字,不能用做表或列的名字。
SQL支持以下三种注释:
#
,--
,/* */
二、SELECT
2.1 SELECT的基本使用
SELECT, like its name, 就是从表中选择数据嘛。那怎样才是SELECT的正确姿势呢?
SELECT prod_name FROM Products;
相信机智的你一眼就能看出,这是从Products这个表中选择了prod_name这个column。
从上面那条SQL语句中,可以(或者不可以)看出以下几点:
- SQL语句中,结束要加分号(半角的不用说了吧);
- SQL语句不区分大小写(看当地规范了,这个例子就是关键字大写,表名首字母大写,其它小写);
- SQL语句中,不区分空格与回车(所以上面那四个词语写每行一个也没关系,你开心就好);
- SQL语句的输出,并不一定遵循某些顺序。
那这样呢:
SELECT prod_id, prod_name, prod_price FROM Products;
当然就是从Products这个表中同时选择了prod_id, prod_name, prod_price这三列。
在语句中,列名之间以逗号分隔。
这样呢:
SELECT * FROM Products;
这样就是从Products里面选择了所有的column。
有时候,我们需要去重:
SELECT DISTINCT vend_id FROM Products;
这样,输出的vend_id就实现了去重
需要注意的是,DISTINCT
作用于所有的列,而并非跟在后面的那一列,比如:
SELECT DISTINCT vend_id, prod_price FROM Products;
我们也可以对输出的内容作出限制,比如我们想取prod_name中的TOP 5:
SELECT prod_name FROM Products LIMIT 5;
(不同SQL实现的本条语句,语法可能不同,本文以MySQL为准,下同)
如果想看后面的呢?
SELECT prod_name FROM Products LIMIT 5 OFFSET 5;
这样就返回了Products中prod_name这列从第5行起的5行数据。
2.2 排序:ORDER BY
可是,就像我们之前说的,SQL的输出默认是没有顺序的。如果你问我啊,我可以说一句“无可奉告”。但是看你们这么热情啊,一句话不说也不好。所以说啊,我们来学习一个,如何才能让SQL的输出有顺序:
SELECT prod_name FROM Products ORDER BY prod_name;
这样的话,就是按照prod_name的字母顺序升序排列了。
需要注意的是,ORDER BY
子句应被保证为SELECT语句中的最后一条子句。
不过这样只能按一个列排序啊Orz如果我们想按多个列排序呢?
SELECT prod_id, prod_price, prod_name FROM Products ORDER BY prod_price, prod_name;
可是这样写语句是不是有点太长了呢?其实这样也可以的:
SELECT prod_id, prod_price, prod_name FROM Products ORDER BY 2, 3;
就可以达到和上面语句一样的效果,其实就是从那三个查看的列里面选择了第二个和第三个吧。
上面的都是升序,那如果我们想降序排列呢?
SELECT prod_id, prod_price, prod_name FROM Products ORDER BY prod_price DESC;
要降序排列的列要后面加上DESC
,反过来说,就是DESC
只作用于它前面的那一列:
SELECT prod_id, prod_price, prod_name FROM Products ORDER BY prod_price DESC, prod_name;
这样,prod_price列就是降序的,prod_name列则呈升序排列。
2.3 数据过滤:WHERE
但是这样的话,似乎还是不够方便啊。如果我们想查看某一个列的值符合特定规律的数据呢?这样我们就用到WHERE
:
SELECT prod_name, prod_price FROM Products WHERE prod_price = 3.49;
这样就输出了所有 prod_price值为3.49的prod_name和prod_price
WHERE
字句都资瓷哪些操作符呢?
=
, <>
, !=
, <
, <=
, !<
, >
, >=
, !>
, BETWEEN
, IS NULL
SQL有个特点,就是非常直观,这些符号的含义也就不用做过多解释了。
SQL还资瓷很多花式的过滤方法!是的!就算过滤条件更复杂一些也没关系!比如像这样:
SELECT prod_id, prod_price, prod_name FROM Products WHERE vend_id = 'DLL01' AND prod_price <= 4;
既然资瓷了AND
,你问我资不资瓷OR
?当然资瓷!我就这么明确告诉你:
SELECT prod_id, prod_price, prod_name FROM Products WHERE vend_id = 'DLL01' AND vend_id = 'BRS01';
既然说到AND
和OR
,那么不可避免的就是出现逻辑运算顺序的问题了。SQL优先处理AND
运算符。在需要指定顺序时,也可以使用括号:
SELECT prod_name, prod_price FROM Products WHERE (vend_id = 'DLL01' OR vend_id = 'BRS01') AND prod_price >= 10;
以及ORDED BY
子句,一般是放在WHERE
后面。
其实OR
操作也可以这样实现
SELECT prod_name, prod_price FROM Products WHERE vend_id IN ('DLL01', 'BRS01') ORDER BY prod_name;
而且,IN
的操作一般比OR
更快哦~而且IN还可以用来实现更多更灵活的操作!
那么既然有了AND
和OR
,自然而然就有了NOT
SELECT prod_name FROM Products WHERE NOT vend_id = 'DLL01' ORDER BY prod_name;
2.4 更灵活的过滤:LIKE
这样我们就学会了过滤,但是似乎好像资瓷的过滤方式还太少啊Orz连字符串匹配都不资瓷(就你事多(误
好好好,这就说怎样才是资瓷更高级过滤条件的正确姿势:
SELECT prod_id, prod_name FROM Products WHERE prod_name LIKE 'Fish%';
SELECT prod_id, prod_name FROM Products WHERE prod_name LIKE '%bean bag%';
就是过滤出prod_name里面以Fish开头或者含有bean bag的数据。
也可以这样:
SELECT prod_id, prod_name FROM Products WHERE prod_name LIKE 'F%y';
过滤出prod_name中以F开头以y结尾的数据。
这样存在一个问题:很多DBMS会在字符串后自动添加空格,以达到和字符串类型定义时相等的长度。
以及LIKE '%'
不会匹配NULL。
当然,都说了是“更灵活的”过滤姿势,怎么能只有%
一种?
SELECT prod_id, prod_name FROM Products WHERE prod_name LIKE ‘__ inch teddy bear';
_
操作符相较于%
更加严格?感觉这么说也不好,算是用法不同吧。%
是匹配字符串,而一个_
则是匹配单个字符,不许多也不许少。
有时候我们还会遇到要匹配的字符有多种可能性的情况,除了用OR
,还有什么更优雅的方案么?
SELECT cust_contact FROM Customers WHERE cust_contact LIKE ‘[JM]%' ORDER BY cust_contact;
这样就匹配了cust_contact里面以J或M开头的数据。同样的,[]
也只是对应单个字符。
如果我们想否定,可以用NOT
,当然也可以用更优雅的^
SELECT cust_contact FROM Customers WHERE cust_contact LIKE ‘[^JM]%' ORDER BY cust_contact;
主要的通配符就是%
, _
和[]
。不幸的是通配符一般速度会比较慢,建议不要的过度使用,使用的时候也尽量放在搜索模式的后面,以减少检索时间。
2.5 计算字段与格式化输出
看来仅仅简单的把数据输出还不行啊,有时候我们还需要将数据按照一定格式输出,或者做一些数值计算。说白了就是字符串操作和数值计算嘛。比如这样:
SELECT Concat(vend_name, ' (', vend_country, ')') FROM vendors ORDER BY vend_name;
即在vend_country字段两侧加了括号。Concat
则是起连接作用的函数。
我们前面说过,有的DBMS(比如MySQL)会在字符串末尾补全空格。这时就需要用RTrim
函数解决:
SELECT Concat(RTrim(vend_name), ' (', RTrim(vend_country), ')') FROM vendors ORDER BY vend_name;
除了去除右边空格的RTrim
以外,还有去掉左边空格的LTrim
和左右空格的Trim
。
有时候我们要把新生成的部分起一个名字,也就是别名:
SELECT Concat(RTrim(vend_name), ' (', RTrim(vend_country), ')') AS vend_title FROM vendors ORDER BY vend_name;
字符串的计算就是上面那样,那么如何进行数学计算呢?
SELECT prod_id, quantity, item_price, quantity*item_price AS expanded_price FROM orderitems WHERE order_num = 20005;
这样就计算了数量和单价的乘积,并将其别名设为expanded_price。
2.6 函数运算
是的,既然资瓷一般的字符串运算,那么更应该资瓷一下更复杂的函数运算,上一节我们提到了Trim
等函数这一届里面我们简要的把函数分为文本处理函数、日子时间处理函数和数值处理函数。
首先说文本处理函数
函数 | 说明 |
---|---|
Left() |
返回串左边的字符 |
Length() |
返回串的长度 |
Locate() |
找出串的一个子串 |
Lower() |
将串转化为小写 |
LTrim() |
去掉串左边的空格 |
Right() |
返回串右边的字符 |
RTrim() |
去掉串右边的空格 |
Soundex() |
返回串的SOUNDEX值 |
SubString() |
返回子串的字符 |
Upper() |
将串转化为大写 |
其中SOUNDEX
的含义需要解释一下,简单来说就是读音相似的,比如:
SELECT cust_name, cust_contact FROM Customers WHERE SOUNDEX(cust_contact) = SOUNDEX('Michael Green')
cust_contact里面如果有'Michelle Green'这样读音相似的,就会返回。
接下来是日期时间处理函数:
函数 | 说明 |
---|---|
AddDate() |
增加一个日期 |
AddTime() |
增加一个事件 |
CurDate() |
返回当前日期 |
CurTime() |
返回当前时间 |
Date() |
返回一个日期时间的日期部分 |
DateDiff() |
计算两个日期之差 |
Date_Add() |
高度灵活的日期运算函数 |
Date_Format() |
返回一个格式化的日期或时间串 |
Day() |
返回一个日期的天数部分 |
DayOfWeek() |
返回一个日期是星期几 |
Hour() |
返回一个时间的小时部分 |
Minute() |
返回一个时间的分钟部分 |
Month() |
返回一个日期的月份部分 |
Now() |
返回当前的日期和时间 |
Second() |
返回一个时间的秒部分 |
Time() |
返回一个日期时间的时间部分 |
Year() |
返回一个日期的年份部分 |
例子:
SELECT cust_id, order_num FROM orders WHERE Date(order_date) BETWEEN '2005-09-01' AND '2005-09-30';
即匹配的日期在2005年9月1日到2005年9月30日。需要注意的是,order_date是一个datetime
类型,对它进行date
部分的匹配时要用Date
函数截取其date
部分。
最后是数值处理函数:
函数 | 说明 |
---|---|
Abs() |
返回一个数的绝对值 |
Cos() |
返回一个角度的余弦 |
Exp() |
返回一个数的指数值 |
Mod() |
返回除操作的余数 |
Pi() |
返回圆周率 |
Rand() |
返回一个随机数 |
Sin() |
返回一个角度的正弦 |
Sqrt() |
返回一个数的平方根 |
Tan() |
返回一个角度的正切 |
这个不用多说了吧
2.7 聚集函数
上面说的是对于一些数值的计算。有时候我们做数据分析时需要关注整个Dataset中某个特征的宏观特点。(说人话:比如平均值、最大值最小值等)
函数 | 说明 |
---|---|
Avg() |
返回某列的平均值 |
Count() |
返回某列的行数 |
Max() |
返回某列的最大值 |
Min() |
返回某列的最小值 |
Sum() |
返回某列值之和 |
下面分别举例说明:
SELECT AVG(prod_price) AS avg_price FROM products WHERE vend_id = 1003;
即得出vend_id为1003的产品的平均价格。
需要注意的是,Avg
函数将自动忽略值为NULL
的行。
Count
函数也是类似
SELECT COUNT(cust_email) AS num_cust FROM Customers;
也将返回cust_email不为NULL
的行数。Min
,Max
,Sum
同理。
我们前面提到过用DISTINCT
去重,这里也是一样:
SELECT AVG(DISTINCT prod_price) AS avg_price FROM products WHERE vend_id = 1003;
得到的是每个商品的单价去重之后的平均价格。
聚集函数也可以组合使用:
SELECT COUNT(*) AS num_items, MIN(prod_price) AS price_min, MAX(prod_price) AS price_max, AVG(prod_price) AS price_avg FROM products;
2.8 分组
在前面的例子中,我们知道,如果想得到某一个vend_id的num_prods数据,我们可以这样:
SELECT COUNT(*) AS num_prods FROM products WHERE vend_id = 1003;
如果我们想同时查看所有vend_id的num_prods,就需要分组处理:
SELECT vend_id, COUNT(*) AS num_prods FROM products GROUP BY vend_id;
输出为两列,一列是排序后的vend_id,一个是每个vend_id对应的num_prods数。
GROUP BY
子句中需要注意的是:
-
GROUP BY
子句可以包含任意数目的列,因而可以对分组进行嵌套。这种情况下数据将在最后指定的分组上进行汇总; -
GROUP BY
子句中的每一列都必须是检索列或有效的表达式; - 大多数SQL实现不允许
GROUP BY
列带有长度可变的数据类型; - 除聚集计算语句外,
SELECT
语句中的每一列都必须在GROUP BY
子句中给出; - 如果分组列中包含具有
NULL
值的行,则NULL
将作为一个单独的分组; -
GROUP BY
分组必须出现在WHERE
子句之后,ORDER BY
子句之前。
除了用GROUP BY
分组以外,SQL还支持过滤分组。这时候我们要用到HAVING
子句:
SELECT cust_id, COUNT(
*
) AS orders FROM Orders GROUP BY cust_id HAVING COUNT(*
) >= 2;
这样就过滤出了订单数>=2的分组。
-
HAVING
和WHERE
的区别:可以简要的理解为HAVING
在数据分组前进行过滤,WHERE
在数据分组后进行过滤。
有些时候,HAVING
和WHERE
也可以同时使用:
SELECT vend_id, COUNT(
*
) AS num_prods FROM Products WHERE prod_price >= 4 GROUP BY vend_id HAVING COUNT(*
) >= 2
WHERE
子句先是过滤出了prod_price至少为4的行,HAVING
子句又过滤出计数为2或2以上的分组。
既然分组支持过滤,那么一定支持排序:
SELECT order_num, COUNT(
*
) AS items FROM OrderItems GROUP BY order_num HAVING COUNT(*
) >= 3;
如上过滤出了订单数>=3的order_num。
SELECT order_num, COUNT(
*
) AS items FROM OrderItems GROUP BY order_num HAVING COUNT(*
) >= 3 ORDER BY items, order_num;
加上一个ORDER BY
子句,即按items、order_num从小到大排序。
到现在,我们总结一下SELECT子句使用的顺序:
子句 | 说明 | 是否必须使用 |
---|---|---|
SELECT |
要返回的列或表达式 | 是 |
FROM |
从中检索数据的表 | 仅在表中选择数据时使用 |
WHERE |
行级过滤 | 否 |
GROUP BY |
分组说明 | 仅在按组计算聚集时使用 |
HAVING |
组级过滤 | 否 |
ORDER BY |
输出排序顺序 | 否 |
2.9 子查询
SQL还允许子查询,即查询中嵌套的查询。
例如如下两个SQL查询:
SELECT order_num FROM OrderItems WHERE prod_id = 'RGAN01';
SELECT cust_id FROM Orders WHERE order_num IN (20007, 20008);
有两个表,OrderItems和Orders。OrderItems中保存的是order_num和prod_id信息,而Orders中保存的是cust_id和order_num的信息。如果我们想根据prod_id查找cust_id的相关信息,除了使用两次查找,还可以使用如下方式:
SELECT cust_id FROM Orders WHERE order IN (SELECT order_num FROM OrderItems WHERE prod_id = 'RGAN01');
除此之外,还可以作为计算字段使用子查询。
SELECT cust_name, cust_state, (SELECT COUNT(*) FROM Orders WHERE Orders.cust_id = Customers.cust_id) AS orders FROM Customers ORDER BY cust_name;