SQL语言是一门相对来说简单易学却又功能强大的语言,它能让你快速上手并很快就能写出比较复杂的查询语句。但是对于大多数开发者来说,使用SQL语句查询数据库的时候,如果没有一个抽象的过程和一个合理的步骤,很可能会在写一些特定的SQL查询语句来解决特定问题的时候被卡住。
这里主要讲述下SQL查询的一些基本理论,以及写查询语句的抽象思路。
SQL查询的简介
SQL语言起源于1970年E.J.Codd发表的关系数据库理论,所以可以说SQL是为关系数据库服务的。而对于SQL查询,是指从数据库中取得数据的子集。
子集指的就是从一个或多个表中提取特定的字段和字段对应的记录。
可以说,SQL中无论多复杂的查询,都可以抽象成如上面的过程。
精确查询的前置条件
想要正确取得所需要的数据子集,除了需要思路正确并将思路正确转变为对应SQL查询语句之外,还有很重要的一点是需要数据库有着良好的设计。这里的良好设计指的是数据库的设计符合业务逻辑。如果数据库设计得很烂,那数据查询起来就很困难,要达到精确查询也就变得困难起来。
举个简单的例子,A表中有a字段,B表中也有a字段,在业务处理的过程中,A表中的a字段可能和B表中的a字段发生了分歧,这时候是要取A表中的a字段的值还是取B表中的a字段的值,也就成了一个问题。
两种方式,同一种结果
在SQL中,取得相同的数据子集可以用不同的思路或不同的SQL语句,因为SQL源于关系数据库理论,而关系数据库理论又源于数学,思考如何构建查询语句时,都可以抽象为两种方法。
关系代数法
关系代数法的思路是对数据库进行分步操作,最后取得想要的结果。
SELECT NAME FROM USERS WHERE AGE > 20;
关系代数的思路描述上面语句为:对USERS表进行投影(选择列)操作,然后对结果进行筛选,只取得年龄大于20的结果。
关系演算法
相比较关系代数法而言,关系演算法更多关注的是取得数据所满足的条件。比如上面SQL用关系演算法可以被描述为:我想得到所有年龄大于20的员工的姓名,部门和年龄。
两种算法的区别
对于简单的查询语句来说,上面两种方法都不需要,因为可能用脚就可以想出来了,只是问题在于很多情况下查询语句会非常复杂,这时候就用两种算法去区分描述的优势才会表现出来。
对于关系演算法来说,更多关注的是所取出信息所满足的条件;而对于关系代数法来说,更多关注的是如何取出特定的信息。简单地说,关系演算法表示的是【what】,而关系代数法表达的是【how】。SQL语句中所体现的思路,有些时候是关系代数法,有些时候是关系演算法,还有些会是两种思路的混合。
对于某些查询情况,关系代数法可能会更简单;而对于另外一些情况,关系演算法则会显得更直接;还有一些情况则是需要混合两种思路。所以这两种思维方式在写SQL查询时都是必须要掌握的。
单表查询
单表查询是所有查询中的中间状态,即使是多个表的复杂查询,在进行各种连接后最终都能够被抽象成单表查询。所以先从单表查询开始。
选择列的子集
根据上面数据子集的说法,选择列是通过在SELECT语句后面添加所要选择的列名实现的。
SELECT NAME FROM USERS;
选择行的子集
选择行的子集,是在SQL语句的WHERE子句后面加上相应的限制条件。当WHERE子句后面的表达式为【真/true】时,也就是满足所谓的【条件】时,返回相应的行的子集。
WHERE子句后面的运算符分为两类,分别是比较运算符和逻辑运算符。
比较运算符是将两个相同类型的数据进行比较,进而返回布尔类型(boolean)的运算符。在SQL中,比较运算符一共有六种,分别是等于(=)、小于(<)、小于或等于(<=)、大于或等于(>=)以及不等于(<>)。其中,小于或等于和大于或等于可以看成是比较运算符和逻辑运算符的结合体。
SELECT NAME FROM USERS WHERE AGE >= 18;
逻辑运算符是将两个布尔类型进行连接,并返回一个新的布尔类型的运算符。在SQL中,逻辑运算符通常是将比较运算符返回的布尔类型相连接以最终确定WHERE子句后面满足条件的真假。逻辑运算符一共有三种,分别是与(AND)、或(OR)和非(NOT)。其中,非运算符可以看作是特殊的比较运算符。
SELECT NAME FROM USERS WHERE AGE > 18 AND AGE < 60;
另外,这几种运算符是有优先级的,优先级由大到小排列,是比较运算符>于逻辑运算符(AND)>或逻辑运算符(OR)。当然,也可以通过括号运算符来改变优先级,括号运算符的优先级最高。
1+1≠2的问题
假定USERS表中存在一个SEX(性别)字段,我们一般可能会认为,这个世界不是男人就是女人,因此SEX字段就只能有male和female两种值。
SELECT * FROM USERS WHERE SEX = 'male' AND SEX = 'female';
但是当我们执行上面的SQL时发现获取的并不是USERS表中的所有记录。这是因为,在实际的场景中,有一些用户在录入表单的时候可能不会填写SEX字段,因此SEX字段还会有第三个值:NULL。因此我们需要加入SQL语句中提供的NULL判断条件来获取表中的所有记录。
SELECT * FROM USERS WHERE SEX = 'male' AND SEX = 'female' AND SEX IS NULL;
当然了,实际场景中SEX(性别)一般会被设计为布尔类型,并提供默认值。
排序结果
上面的那些方法都是关于取出数据,而下面是关于将取出的子集进行排序。SQL通过ORDER BY子句来进行排序,ORDER BY子句必须是SQL查询语句的最后一个子句,也就是说,在ORDER BY子句之后,不允许再添加任何的子句了。
ORDER BY子句分为升序(ASC)和降序(DESC)。如果不指定升序或者降序,则默认为升序(由小到大),而ORDER BY是根据排序依据的数据类型来决定的排列的先后顺序的,分别有3种数据类型可以进行排序:字符、数字和时间日期。其中,字符按照字母表进行排序,数字根据数字大小排序,时间日期根据时间的先后进行排序。
SELECT * FROM USERS ORDER BY AGE DESC;
上面的语句即按AGE(年龄)对USERS表中的记录进行降序排序。
聚合函数
聚合的意思就是按照一定的条件进行分组,SQL通过GROUP BY子句来进行分组,通过分组并配合聚合函数来达到聚合统计的目的。另外还提供有HAVING子句与GROUP BY子句联合使用,作用是分组后对数据的筛选/过滤,优先级低于WHERE子句。
SELECT AREA, AVG(AGE) FROM USERS GROUP BY AREA HAVING AVG(AGE) > 18;
上面的语句即按AREA(地区)对USERS表中的记录求平均AGE(年龄),且平均年龄大于18岁,简单描述,就是求USERS表中平均年龄大于18岁的地区。
多表连接查询
在关系数据库中,一个查询往往会涉及多个表,因为很少有数据库只有一个表,而如果大多数查询只涉及到一个表的话,那么那个表也往往低于第三范式,存在大量冗余和异常。因此,连接(JOIN)就是一种把多个表连接成一个表的重要手段。
笛卡尔积
笛卡尔积在SQL中的实现方式即是交叉连接(CROSS JOIN)。所有连接方式都会先生成临时笛卡尔积表,笛卡尔积是关系代数里的一个概念,表示两个表中的每一行数据任意组合。在实际应用中,笛卡尔积本身大多没有什么实际用处,只有在两个表连接时加上限制条件,才会有实际意义。
内连接
如果分步骤理解的话,内连接可以看做是先对两个表进行了交叉连接后,再通过加上限制条件(SQL中通过关键字ON)剔除不符合条件的行的子集,得到的结果就是内连接了。
SELECT * FROM A INNER JOIN B ON A.ID = B.ID;
外连接
外连接是以一个表作为主体,通过限制条件(关键字ON)来进行连接。
左外连接(LEFT OUTER JOIN)是将左表作为主体。
SELECT * FROM A LEFT JOIN B ON A.ID = B.ID;
右外连接(RIGHT OUTER JOIN)是将右表作为主体。
SELECT * FROM A RIGHT JOIN B ON A.ID = B.ID;
全外连接(FULL OUTER JOIN)是将左表和右表每行都至少输出一次,可以看作是左外连接和右外连接的结合。
SELECT * FROM A FULL JOIN B ON A.ID = B.ID;
从上面的语句可以看出,使用外连接编写SQL时可以省略OUTER关键字。
自连接
如果说内连接是取两个表之间的交集,那么自连接也可以这么理解(如果有WHERE,没有WHERE的话则是取的两个表所有记录的完整笛卡尔积)。通常理解上是,自连接是一种特殊的内连接,不同在于内连接是使用ON关键字做限制条件,自连接则是通过WHETE子句来限制记录的获取。
SELECT * FROM A, B WHERE A.ID = B.ID;
交叉连接
交叉连接和没有WHERE条件限制的自连接获取的记录结果是一样的,它们都返回两个表所有记录的完整笛卡尔积。
SELECT * FROM A CROSS JOIN B;
多表连接查询的总结
上面说的连接都是两个表进行连接,而多个表连接实际上可以看成是对N个表进行N-1次双表连接;多个表连接查询之后也可以看作是一个整体,即看成对一个单表进行查询操作。这样分析的话,会让复杂的问题简单化,再困难的问题也能迎刃而解。
"假如人生是一场未知目的地的旅行,我们只是一味地狂奔,却忘记了旅行的意义。"