SQL进阶教程 | 史上最易懂SQL教程 5小时零基础成长SQL大师(2)

【第五章】汇总数据

汇总统计型查询非常有用,甚至可能常常是你的主要工作内容

聚合函数

USE sql_invoicing;

SELECT 
    MAX(invoice_date) AS latest_date,  
    -- SELECT选择的不仅可以是列,也可以是数字、列间表达式、列的聚合函数
    MIN(invoice_total) lowest,
    AVG(invoice_total) average,
    SUM(invoice_total * 1.1) total,
    COUNT(*) total_records,
    COUNT(invoice_total) number_of_invoices, 
    -- 和上一个相等
    COUNT(payment_date) number_of_payments,  
    -- 【聚合函数会忽略空值】,得到的支付数少于发票数
    COUNT(DISTINCT client_id) number_of_distinct_clients
    -- DISTINCT client_id 筛掉了该列的重复值,再COUNT计数,会得到不同顾客数
FROM invoices
WHERE invoice_date > '2019-07-01'  -- 想只统计下半年的结果
  • 目标 :

SQL进阶教程 | 史上最易懂SQL教程 5小时零基础成长SQL大师(2)_第1张图片
思路:很明显要 分类子查询+聚合函数+UNION

USE sql_invoicing;

    SELECT 
        '1st_half_of_2019' AS date_range,
        SUM(invoice_total) AS total_sales,
        SUM(payment_total) AS total_payments,
        SUM(invoice_total - payment_total) AS what_we_expect
    FROM invoices
    WHERE invoice_date BETWEEN '2019-01-01' AND '2019-06-30'

UNION

    SELECT 
        '2st_half_of_2019' AS date_range,
        SUM(invoice_total) AS total_sales,
        SUM(payment_total) AS total_payments,
        SUM(invoice_total - payment_total) AS what_we_expect
    FROM invoices
    WHERE invoice_date BETWEEN '2019-07-01' AND '2019-12-31'

UNION

    SELECT 
        'Total' AS date_range,
        SUM(invoice_total) AS total_sales,
        SUM(payment_total) AS total_payments,
        SUM(invoice_total - payment_total) AS what_we_expect
    FROM invoices
    WHERE invoice_date BETWEEN '2019-01-01' AND '2019-12-31'

GROUP BY子句

  • 按一列或多列分组,注意语句的位置。
  • 案例1:按一个字段分组
    在发票记录表中按不同顾客分组统计下半年总销售额并降序排列
USE sql_invoicing;

SELECT 
    client_id,  
    SUM(invoice_total) AS total_sales
    ……

只有聚合函数是按 client_id 分组时,这里选择 client_id 列才有意义(分组统计语句里SELECT通常都是选择分组依据列+目标统计列的聚合函数,选别的列没意义)。若未分类,结果会是一条总 total_sales 和一条 client_id(该client_id无意义),即 client_id 会被压缩为只显示一条而非 SUM 广播为多条,可以理解为聚合函数比较强势吧。

……
FROM invoices
WHERE invoice_date >= '2019-07-01'  -- 筛选,过滤器
GROUP BY client_id  -- 分组
ORDER BY invoice_total DESC

若省略排序语句就会默认按分组依据排序(后面一个例子发现好像也不一定,所以最好别省略)

记住语句顺序很重要 WHERE GROUP BY ORDER BY,分组语句在排序语句之前,调换顺序会报错

  • 案例2:按多个字段分组
    算各州各城市的总销售额

如前所述,一般分组依据字段也正是 SELECT …… 里的选择字段,如下面例子里的 state 和 city

USE sql_invoicing;

SELECT 
    state,
    city,
    SUM(invoice_total) AS total_sales
FROM invoices
JOIN clients USING (client_id) 
-- 别忘了USING之后是括号,太容易忘了
GROUP BY state, city  
-- 逗号分隔就行
-- 这个例子里 GROUP BY 里去掉 state 结果一样
ORDER BY state

其实上面的例子里一个城市只能属于一个州中,所有归根结底还是算的各城市的销售额,GROUP BY …… 里去掉 state 只写 city (但 SELECT 和 ORDER BY 里保留 state)结果是完全一样的(包括结果里的 state 列),下面这个例子更能说明以多个字段为分组依据进行分组统计的意义

  • 在 payments 表中,按日期和支付方式分组统计总付款额
    每个分组显示一个日期和支付方式的独立组合,可以看到某特定日期特定支付方式的总付款额。这个例子里每一种支付方式可以在不同日子里出现,每一天也可以出现多种支付方式,这种情况,才叫真·多字段分组。不过上一个例子里那种假·多字段分组,把 state 加在分组依据里也没坏处还能落个心安,也还是加上别省比较好
USE sql_invoicing;

SELECT 
    date, 
    pm.name AS payment_method,
    SUM(amount) AS total_payments
FROM payments p
JOIN payment_methods pm
    ON p.payment_method = pm.payment_method_id
GROUP BY date, payment_method
-- 用的是 SELECT 里的列别名
ORDER BY date

HAVING子句

  • HAVING 和 WHERE 都是是条件筛选语句,条件的写法相通,数学、比较(包括特殊比较)、逻辑运算都可以用(如 AND、REGEXP 等等)
  • 两者本质区别:
    WHERE 是对 FROM JOIN 里原表中的列进行 事前筛选,所以WHERE可以对没选择的列进行筛选,但必须用原表列名而不能用SELECT中确定的列别名
    相反 HAVING …… 对 SELECT …… 查询后(通常是分组并聚合查询后)的结果列进行 事后筛选,若SELECT里起了别名的字段则必须用别名进行筛选,且不能对SELECT里未选择的字段进行筛选。唯一特殊情况是,当HAVING筛选的是聚合函数时,该聚合函数可以不在SELECT里显性出现,见最后补充
  • 筛选出总发票金额大于500且总发票数量大于5的顾客
USE sql_invoicing;

SELECT 
    client_id,
    SUM(invoice_total) AS total_sales,
    COUNT(*/invoice_total/invoice_date) AS number_of_invoices
FROM invoices
GROUP BY client_id
HAVING total_sales > 500 AND number_of_invoices > 5
-- 均为 SELECT 里的列别名

若写:WHERE total_sales > 500 AND number_of_invoices > 5,会报错:Error Code: 1054. Unknown column ‘total_sales’ in ‘where clause’

  • 在 sql_store 数据库(有顾客表、订单表、订单项目表等)中,找出在 ‘VA’ 州且消费总额超过100美元的顾客(这是一个面试级的问题,还很常见)
    思路:
  1. 需要的信息在顾客表、订单表、订单项目表三张表中,先将三张表合并
  2. WHERE 事前筛选 ‘VA’ 州的
  3. 按顾客分组,并选取所需的列并聚合得到每位顾客的付款总额
  4. HAVING 事后筛选超过 100美元 的
USE sql_store;

SELECT 
    c.customer_id,
    c.first_name,
    c.last_name,
    SUM(oi.quantity * oi.unit_price) AS total_sales
FROM customers c
JOIN orders o USING (customer_id)  -- 别忘了括号,特容易忘
JOIN order_items oi USING (order_id)
WHERE state = 'VA'
GROUP BY 
    c.customer_id, 
    c.first_name, 
    c.last_name
HAVING total_sales > 100
  • 学第六章第6节时发现,当 HAVING 筛选的是聚合函数时,该聚合函数可以不在SELECT里显性出现。(作为一种需要记住的特殊情况)如:下面这两种写法都能筛选出总点数大于3k的州,如果不要求显示总点数,应该用后一种
SELECT state, SUM(points)
FROM customers
GROUP BY state
HAVING SUM(points) > 3000SELECT state
FROM customers
GROUP BY state
HAVING SUM(points) > 3000

ROLLUP运算符

  • GROUP BY …… WITH ROLL UP 自动汇总型分组,若是多字段分组的话汇总也会是多层次的,注意这是MySQL扩展语法,不是SQL标准语法
  • 分组查询各客户的发票总额以及所有人的总发票额
USE sql_invoicing;

SELECT 
    client_id,
    SUM(invoice_total)
FROM invoices
GROUP BY client_id WITH ROLLUP
  • 多字段分组 例1:分组查询各州、市的总销售额(发票总额)以及州层次和全国层次的两个层次的汇总额
SELECT 
    state,
    city,
    SUM(invoice_total) AS total_sales
FROM invoices
JOIN clients USING (client_id) 
GROUP BY state, city WITH ROLLUP
  • 多字段分组 例2:分组查询特定日期特定付款方式的总支付额以及单日汇总和整体汇总
USE sql_invoicing;

SELECT 
    date, 
    pm.name AS payment_method,
    SUM(amount) AS total_payments
FROM payments p
JOIN payment_methods pm
    ON p.payment_method = pm.payment_method_id
GROUP BY date, pm.name WITH ROLLUP
  • 分组计算各个付款方式的总付款 并汇总
SELECT 
    pm.name AS payment_method,
    SUM(amount) AS total
FROM payments p
JOIN payment_methods pm
    ON p.payment_method = pm.payment_method_id
GROUP BY pm.name WITH ROLLUP

总结

根据之后三篇参考文章,据说标准的 SQL 查询语句的执行顺序应该是下面这样的:

  1. FROM JOIN 选择和连接本次查询所需的表
  2. ON/USING WHERE 按条件筛选行
  3. GROUP BY 分组
  4. HAVING (事后/分组后)筛选行
  5. SELECT 筛选列
    注意1:若进行了分组,这一步常常要聚合)
    注意2:SELECT 和 HAVING 在 MySQL 里的执行顺序我还有点疑问,见后面的叙述
  6. DISTINCT 去重
  7. UNION 纵向合并
  8. ORDER BY 排序
  9. LIMIT 限制

“SELECT 是在大部分语句执行了之后才执行的,严格的说是在 FROM、WHERE 和 GROUP BY (以及 HAVING)之后执行的。理解这一点是非常重要的,这就是你不能在 WHERE 中使用在 SELECT 中设定别名的字段作为判断条件的原因。”

这个顺序可以由下面这个例子的缩进表现出来(出右往左)(注意 DISTINCT 放不进去了只有以注释的形式展示出来,另外 SELECT 还是选择放在了 HAVING 之前)

                    SELECT name, SUM(invoice_total) AS total_sales
         -- DISTINCT
                                FROM invoices JOIN clients USING (client_id) 
                            WHERE due_date < '2019-07-01'
                        GROUP BY name  
                HAVING total_sales > 150

        UNION

                    SELECT name, SUM(invoice_total) AS total_sales
         -- DISTINCT
                                FROM invoices JOIN clients USING (client_id) 
                            WHERE due_date > '2019-07-01'
                        GROUP BY name  
                HAVING total_sales > 150

    ORDER BY total_sales
LIMIT 2

关于 SELECT 的位置
1.如后面几篇参考文章所说,按标准 SQL 的执行顺序, SELECT 是在 HAVING 之后
2.但根据前面的内容,似乎在 MySQL 里,SELECT 的执行顺序是在 WHERE GROUP BY 之后,而在 HAVING 之前 —— 因而 WHERE GROUP BY 要用原列名(后来发现只有 WHERE 里必须用原列名,GROUP BY 是原列名或列别名都可用(甚至可以用1,2来指代 SELECT 中的列,不过 Mosh 不建议这样做))而 HAVING 必须用 SELECT 里的列别名(聚合函数除外)
按实践经验来看,就按 2 来记忆和理解是可行的,但之后最好还是要去看书看资料把这个执行顺序的疑惑彻底搞清楚,这个还挺重要的。

【第六章】编写复杂查询

子查询

  • 子查询: 任何一个充当另一个SQL语句的一部分的 SELECT…… 查询语句都是子查询,子查询是一个很有用的技巧。子查询的层级用括号实现。
  • 在 products 中,找到所有比生菜(id = 3)价格高的
    关键:用子查询找到生菜价格
USE sql_store;

SELECT *
FROM products
WHERE unit_price > (
    SELECT unit_price
    FROM products
    WHERE product_id = 3
)

MySQL执行时会先执行括号内的子查询(内查询),将获得的生菜价格作为结果返回给外查询
子查询不仅可用在 WHERE …… 中,也可用在 SELECT …… 或 FROM …… 等子句中,本章后面会讲

  • 在 sql_hr 库 employees 表里,选择所有工资超过平均工资的雇员
    关键:由子查询得到平均工资
USE sql_hr;

SELECT *
FROM employees
WHERE salary > (
    SELECT AVG(salary)
    FROM employees
)

IN运算符

  • 在 sql_store 库 products 表中找出那些从未被订购过的产品

思路:

  1. order_items 表里有所有产品被订购的记录,用 DISTINCT 去重,得到所有被订购过的产品列表
  2. 不在这列表里(NOT IN 的使用)的产品即为从未被订购过的产品
USE sql_store;

SELECT *
FROM products
WHERE product_id NOT IN (
    SELECT DISTINCT product_id
    FROM order_items
)

上一节是子查询返回一个值(平均工资),这一节是返回一列数据(被订购过的产品id列表),之后还会用子查询返回一个多列的表

  • 在 sql_invoicing 库 clients 表中找到那些没有过发票记录的客户
    思路:和上一个例子完全一致,在invoices里用DISTINCT找到所有有过发票记录的客户列表,再用NOT IN来筛选
USE sql_invoicing;

SELECT *
FROM clients
WHERE client_id NOT IN (
    SELECT DISTINCT client_id
    FROM invoices
)

子查询vs连接

  • 子查询(Subquery)是将一张表的查询结果作为另一张表的查询依据并层层嵌套,其实也可以先将这些表链接(Join)合并成一个包含所需全部信息的详情表再直接在详情表里筛选查询。两种方法一般是可互换的,具体用哪一种取决于 效率/性能(Performance) 和 可读性(readability),之后会学习 执行计划,到时候就知道怎样编写并更快速地执行查询,现在主要考虑可读性
  • 上节课的案例,找出从未订购(没有invoices)的顾客:
    法1. 子查询

先用子查询查出有过发票记录的顾客名单,作为筛选依据

USE sql_invoicing;

SELECT *
FROM clients
WHERE client_id NOT IN (
    SELECT DISTINCT client_id
    /*其实这里加不加DISTINCT对子查询返回的结果有影响
    但对最后的结果其实没有影响*/
    FROM invoices
)

法2. 链接表

用顾客表 LEFT JOIN 发票记录表,再直接在这个合并详情表中筛选出没有发票记录的顾客

USE sql_invoicing;

SELECT DISTINCT client_id, name …… 
-- 不能SELECT DISTINCT *
FROM clients
LEFT JOIN invoices USING (client_id)
-- 注意不能用内链接,否则没有发票记录的顾客(我们的目标)直接就被筛掉了
WHERE invoice_id IS NULL

就上面这个案例而言,子查询可读性更好,但有时子查询会过于复杂(嵌套层数过多),用链接表更好(下面的练习就是)。总之在选择方法时,可读性是很重要的考虑因素

  • 在 sql_store 中,选出买过生菜(id = 3)的顾客的id、姓和名
    法1. 完全子查询
USE sql_store;

SELECT customer_id, first_name, last_name
FROM customers
WHERE customer_id IN (  
    -- 子查询2:从订单表中找出哪些顾客买过生菜
    SELECT customer_id
    FROM orders
    WHERE order_id IN (  
        -- 子查询1:从订单项目表中找出哪些订单包含生菜
        SELECT DISTINCT order_id
        FROM order_items
        WHERE product_id = 3
    )
)

法2. 混合:子查询 + 表连接

USE sql_store;

SELECT customer_id, first_name, last_name
FROM customers
WHERE customer_id IN (  
    -- 子查询:哪些顾客买过生菜
    SELECT customer_id
    FROM orders
    JOIN order_items USING (order_id)  
    -- 表连接:合并订单和订单项目表得到 订单详情表
    WHERE product_id = 3
)

法3. 完全表连接

直接链接合并3张表(顾客表、订单表和订单项目表)得到 带顾客信息的订单详情表,该合并表包含我们所需的所有信息,可直接在合并表中用WHERE筛选买过生菜的顾客(注意 DISTINCT 关键字的运用)。

USE sql_store;

SELECT DISTINCT customer_id, first_name, last_name
FROM customers
LEFT JOIN orders USING (customer_id)
LEFT JOIN order_items USING (order_id)
WHERE product_id = 3

这个案例中,先将所需信息所在的几张表全部连接合并成一张大表再来查询筛选明显比层层嵌套的多重子查询更加清晰明了

ALL关键字

  • > (MAX (……))> ALL(……) 等效可互换
  • “比这里面最大的还大” = “比这里面的所有的都大”
  • sql_invoicing 库中,选出金额大于3号顾客所有发票金额(或3号顾客最大发票金额) 的发票
    法1. 用MAX关键字
USE sql_invoicing;

SELECT *
FROM invoices
WHERE invoice_total > (
    SELECT MAX(invoice_total)
    FROM invoices
    WHERE client_id = 3
)

法2. 用ALL关键字

USE sql_invoicing;

SELECT *
FROM invoices
WHERE invoice_total > ALL (
    SELECT invoice_total
    FROM invoices
    WHERE client_id = 3
)

其实就是把内层括号的MAX拿到了外层括号变成ALL:
MAX法是用MAX()返回一个顾客3的最大订单金额,再判断哪些发票的金额比这个值大;
ALL法是先返回顾客3的所有订单金额,是一列值,再用ALL()判断比所有这些金额都大的发票有哪些。
两种方法是完全等效的

ANY关键字

  • > ANY/SOME (……)> (MIN (……)) 等效
  • = ANY/SOME (……)IN (……) 等效
  • ANY (……) 与 > (MIN (……)) 等效的例子:sql_invoicing 库中,选出金额大于3号顾客任何发票金额(或最小发票金额) 的发票

USE sql_invoicing;

SELECT *
FROM invoices

WHERE invoice_total > ANY (
    SELECT invoice_total
    FROM invoices
    WHERE client_id = 3
)WHERE invoice_total > (
    SELECT MIN(invoice_total)
    FROM invoices
    WHERE client_id = 3
)
  • = ANY (……) 与 IN (……) 等效的例子:选出至少有两次发票记录的顾客
USE sql_invoicing;

SELECT *
FROM clients
WHERE client_id IN (  -- 或 = ANY ( 
    -- 子查询:有2次以上发票记录的顾客
    SELECT client_id
    FROM invoices
    GROUP BY client_id
    HAVING COUNT(*) >= 2
)

相关子查询

  • 之前都是非关联主/子(外/内)查询,比如子查询先查出整体的某平均值或满足某些条件的一列id,作为主查询的筛选依据,这种子查询与主查询无关,会先一次性得出查询结果再返回给主查询供其使用。
  • 而下面这种相关联子查询例子里,子查询要查询的是某员工所在办公室的平均值,子查询是依赖主查询的,注意这种关联查询是在主查询的每一行/每一条记录层面上依次进行的,这一点可以为我们写关联子查询提供线索(注意表别名的使用),另外也正因为这一点,相关子查询会比非关联查询执行起来慢一些。
  • 选出 sql_hr.employees 里那些工资超过他所在办公室平均工资(而不是整体平均工资)的员工
    关键:如何查询目前主查询员工的所在办公室的平均工资而不是整体的平均工资?
    思路:给主查询 employees表 设置别名 e,这样在子查询查询平均工资时加上 WHERE office_id = e.office_id 筛选条件即可相关联地查询到目前员工所在地办公室的平均工资
USE sql_hr;

SELECT *
FROM employees e  -- 关键 1
WHERE salary > (
    SELECT AVG(salary)
    FROM employees
    WHERE office_id = e.office_id  -- 关键 2
    -- 【子查询表字段不用加前缀,主查询表的字段要加前缀,以此区分】
)

相关子查询很慢,但很强大,也有很多实际运用

  • 在 sql_invoicing 库 invoices 表中,找出高于每位顾客平均发票金额的发票
USE sql_invoicing;

SELECT *
FROM invoices i
WHERE  invoice_total > (
    -- 子查询:目前客户的平均发票额
    SELECT AVG(invoice_total)
    FROM invoices
    WHERE client_id = i.client_id
)

EXISTS运算符

  • IN + 子查询 等效于 EXIST + 相关子查询,如果前者子查询的结果集过大占用内存,用后者逐条验证更有效率。另外 EXIST() 本质上是根据是否为空返回 TRUE 和 FALSE,所以也可以加 NOT 取反。
  • 找出有过发票记录的客户,第4节学过用子查询或表连接来实现
    法1. 子查询
USE sql_invoicing;

SELECT *
FROM clients
WHERE client_id IN (
    SELECT DISTINCT client_id
    FROM invoices
)

法2. 链接表

USE sql_invoicing;

SELECT DISTINCT client_id, name …… 
FROM clients
JOIN invoices USING (client_id)
-- 内链接,只留下有过发票记录的客户

第3种方法是用EXISTS运算符实现

USE sql_invoicing;

SELECT *
FROM clients c
WHERE EXISTS (
    SELECT */client_id  
    /* 就这个子查询的目的来说,SELECT的选择不影响结果,
    因为EXISTS()函数只根据是否为空返回 TRUE 和 FALSE */
    FROM invoices
    WHERE client_id = c.client_id
)

这还是个相关子查询,因为在其中引用了主查询的 clients 表。这同样是按照主查询的记录一条条验证执行的。具体说来,对于 clients 表(设置别名为 c)里的每一个顾客,子查询在 invoices 表查找这个人的发票记录( 即 client_id = c.client_id 的发票记录),有就返回相关记录否者返回空,然后 EXISTS() 根据是否为空得到 TRUE 和 FALSE(表示此人有无发票记录),然后主查询凭此确定是否保留此条记录。

对比一下,法1是用子查询返回一个有发票记录的顾客id列表,如(1,3,8 ……),然后用IN运算符来判断,如果子查询表太大,可能返回一个上百万千万甚至上亿的id列表,这个id列表就会很占内存非常影响性能,对于这种子查询会返回一个很大的结果集的情况,用这里的EXIST+相关子查询逐条筛选会更有效率

另外,因为 SELECT() 返回的是 TRUE/FALSE,所以自然也可以加上NOT取反,见下面的练习

  • 在sql_store中,找出从来没有被订购过的产品。
USE sql_store;

SELECT *
FROM products 
WHERE product_id NOT IN (
    SELECT product_id 
    -- 加不加DISTINCT对最终结果无影响
    FROM order_items
)

SELECT *
FROM products p
WHERE NOT EXISTS (
    SELECT *
    FROM order_items
    WHERE product_id = p.product_id
)

对于亚马逊这样的大电商来说,如果用IN+子查询法,子查询可能会返回一个百万量级的产品id列表,这种情况还是用EXIST+相关子查询逐条验证法更有效率

SELECT子句的子查询

不仅 WHERE 筛选条件里可以用子查询,SELECT 选择子句和 FROM 来源表子句也能用子查询,这节课讲 SELECT 子句里的子查询

简单讲就是,SELECT选择语句是用来确定查询结果选择包含哪些字段,每个字段都可以是一个表达式,而每个字段表达式里的元素除了可以是原始的列,具体的数值,也同样可以是其它各种花里胡哨的子查询的结果

任何子查询都是简单查询的嵌套,没什么新东西,只是多了一个层级而已,由内向外地一层层梳理就很清楚

要特别注意记住以子查询方式实现在SELECT中使用同级列别名的方法

  • 得到一个有如下列的表格:invoice_id, invoice_total, avarege(总平均发票额), difference(前两个值的差)
USE sql_invoicing;

SELECT 
    invoice_id,
    invoice_total,
    (SELECT AVG(invoice_total) FROM invoices) AS invoice_average,
    /*不能直接用聚合函数,因为“比较强势”,会压缩聚合结果为一条
    用括号+子查询(SELECT AVG(invoice_total) FROM invoices) 
    将其作为一个数值结果 152.388235 加入主查询语句*/
    invoice_total - (SELECT invoice_average) AS difference
    /*SELECT表达式里要用原列名,不能直接用别名invoice_average
    要用列别名的话用子查询(SELECT 同级的列别名)即可
    说真的,感觉这个子查询有点难以理解,但记住会用就行*/
FROM invoices
  • 得到一个有如下列的表格:client_id, name, total_sales(各个客户的发票总额), average(总平均发票额), difference(前两个值的差)
USE sql_invoicing;

SELECT 
    client_id,
    name,
    (SELECT SUM(invoice_total) FROM invoices WHERE client_id = c.client_id) AS total_sales,
    -- 要得到【相关】客户的发票总额,要用相关子查询 WHERE client_id = c.client_id
    (SELECT AVG(invoice_total) FROM invoices) AS average,
    (SELECT total_sales - average) AS difference   
    /* 如前所述,引用同级的列别名,要加括号和 SELECT,
    和前两行子查询的区别是,引用同级的列别名不需要说明来源,
    所以没有 FROM …… */
FROM clients c

注意第四个客户的 total_sales 和 difference 都是空值 null

FROM子句的子查询

  • 子查询的结果同样可以充当一个“虚拟表”作为FROM语句中的来源表,即将筛选查询结果作为来源再进行进一步的筛选查询。但注意只有在子查询不太复杂时进行这样的嵌套,否则最好用后面讲的视图先把子查询结果储存起来再使用。
  • 将上一节练习里的查询结果当作来源表,查询其中 total_sales 非空的记录
USE sql_invoicing;

SELECT * 
FROM (
    SELECT 
        client_id,
        name,
        (SELECT SUM(invoice_total) FROM invoices WHERE client_id = c.client_id) AS total_sales,
        (SELECT AVG(invoice_total) FROM invoices) AS average,
        (SELECT total_sales - average) AS difference   
    FROM clients c
) AS sales_summury
/* 在FROM中使用子查询,即使用 “派生表” 时,
必须给派生表取个别名(不管用不用),这是硬性要求,不写会报错:
Error Code: 1248. Every derived table(派生表、导出表)
must have its own alias */
WHERE total_sales IS NOT NULL

复杂的子查询再嵌套进 FROM 里会让整个查询看起来过于复杂,上面这个最好是将子查询结果储存为叫 sales_summury 的视图,然后再直接使用该视图作为来源表,之后会讲。

【第七章】MySQL的基本函数

内置的用来处理数值、文本、日期等的函数

数值函数

  • 主要介绍最常用的几个数值函数:ROUND、TRUNCATE、CEILING、FLOOR、ABS、RAND
  • 查看MySQL全部数值函数可谷歌 ‘mysql numeric function’,第一个就是官方文档。
SELECT ROUND(5.7365, 2)  -- 四舍五入
SELECT TRUNCATE(5.7365, 2)  -- 截断
SELECT CEILING(5.2)  -- 天花板函数,大于等于此数的最小整数
SELECT FLOOR(5.6)  -- 地板函数,小于等于此数的最大整数
SELECT ABS(-5.2)  -- 绝对值
SELECT RAND()  -- 随机函数,0到1的随机值

字符串函数

  • 依然介绍最常用的字符串函数:
  1. LENGTH, UPPER, LOWER
  2. TRIM, LTRIM, RTRIM
  3. LEFT, RIGHT, SUBSTRING
  4. LOCATE, REPLACE, 【CONCAT】
  • 查看全部搜索关键词 ‘mysql string functions’
  • 长度、转大小写:
SELECT LENGTH('sky')  -- 字符串字符个数/长度(LENGTH)
SELECT UPPER('sky')  -- 转大写
SELECT LOWER('Sky')  -- 转小写
  • 用户输入时时常多打空格,下面三个函数用于处理/修剪(trim)字符串前后的空格,L、R 表示 LEFT、RIGHT:
SELECT LTRIM('  Sky')
SELECT RTRIM('Sky  ')
SELECT TRIM(' Sky ')
  • 切片:
-- 取左边,取右边,取中间
SELECT LEFT('Kindergarden', 4)  -- 取左边(LEFT)4个字符
SELECT RIGHT('Kindergarden', 6)  -- 取右边(RIGHT)6个字符
SELECT SUBSTRING('Kindergarden', 7, 6)  
-- 取中间从第7个开始的长度为6的子串(SUBSTRING)
-- 注意是从第1个(而非第0个)开始计数的
-- 省略第3参数(子串长度)则一直截取到最后
  • 定位:
SELECT LOCATE('gar', 'Kindergarden')  -- 定位(LOCATE)首次出现的位置
-- 没有的话返回0(其他编程语言大多返回-1,可能因为索引是从0开始的)
-- 这个定位/查找函数依然是不区分大小写的
  • 替换:
SELECT REPLACE('Kindergarten', 'garten', 'garden')
  • 连接:
USE sql_store;

SELECT CONCAT(first_name, ' ', last_name) AS full_name
-- concatenate v. 连接
FROM customers

MySQL中的日期函数

  • 本节学基本的处理时间日期的函数,下节课学日期时间的格式化
  • 当前时间
SELECT NOW()  -- 2020-09-12 08:50:46
SELECT CURDATE()  -- current date, 2020-09-12
SELECT CURTIME()  -- current time, 08:50:46
  • 以上函数将返回时间日期对象
  • 提取时间日期对象中的元素:
SELECT YEAR(NOW())  -- 2020

还有MONTH, DAY, HOUR, MINUTE, SECOND。

  • 以上函数均返回整数,还有另外两个返回字符串的:
SELECT DAYNAME(NOW())  -- Saturday
SELECT MONTHNAME(NOW())  -- September

标准SQL语句有一个类似的函数 EXTRACT(),若需要在不同DBMS中录入代码,最好用EXTRACT():

SELECT EXTRACT(YEAR FROM NOW())

当然第一参数也可以是MONTH, DAY, HOUR ……
总之就是:EXTRACT(单位 FROM 日期时间对象)

  • 返回【今年】的订单
    用时间日期函数而非手动输入年份,代码更可靠,不会随着时间的改变而失效
USE sql_store;

SELECT * 
FROM orders
WHERE YEAR(order_date) = YEAR(now())

格式化日期和时间

  • DATE_FORMAT(date, format) 将 date 根据 format 字符串进行格式化。
  • TIME_FORMAT(time, format) 类似于 DATE_FORMAT 函数,但这里 format 字符串只能包含用于小时,分钟,秒和微秒的格式说明符。其他说明符产生一个 NULL 值或0。
  • 很多像这种完全不需要记也不可能记得完,重要的是知道有这么个可以实现这个功能的函数,具体的格式说明符(Specifiers)可以需要的时候去查,至少有两种方法:
  1. 直接谷歌关键词 如 mysql date format functions, 其实是在官方文档的 12.7 Date and Time Functions 小结里,有两个函数的说明和 specifiers 表
  2. 用软件里的帮助功能,如 workbench 里的 HELP INDEX 打开官方文档查询或者右侧栏的 automatic comtext help (其是也是查官方文档,不过是自动的)
SELECT DATE_FORMAT(NOW(), '%M %d, %Y')  -- September 12, 2020
-- 格式说明符里,大小写是不同的,这是目前SQL里第一次出现大小写不同的情况
SELECT TIME_FORMAT(NOW(), '%H:%i %p')  -- 11:07 AM

计算日期和时间

  • 有时需要对日期事件对象进行运算,如增加一天或算两个时间的差值之类,介绍一些最有用的日期时间计算函数:
  • 增加或减少一定的天数、月数、年数、小时数等等
SELECT DATE_ADD(NOW(), INTERVAL -1 DAY)
SELECT DATE_SUB(NOW(), INTERVAL 1 YEAR)
  • 但其实不用函数,直接加减更简洁:
NOW() - INTERVAL 1 DAY
NOW() - INTERVAL 1 YEAR 
  • 计算日期差异
SELECT DATEDIFF('2019-01-01 09:00', '2019-01-05')  -- -4
-- 会忽略时间部分,只算日期差异

借助 TIME_TO_SEC 函数计算时间差异

TIME_TO_SEC:计算从 00:00 到某时间经历的秒数

​```sql
SELECT TIME_TO_SEC('09:00')  -- 32400
SELECT TIME_TO_SEC('09:00') - TIME_TO_SEC('09:02')  -- -120

IFNULL和COALESCE函数

  • 之前讲了基本的处理数值、文本、日期时间的函数,再介绍几个其它的有用的MySQL函数
  • 两个用来替换空值的函数:IFNULL, COALESCE. 后者更灵活
  • 将 orders 里 shipper.id 中的空值替换为 ‘Not Assigned’(未分配)
USE sql_store;

SELECT 
    order_id,
    IFNULL(shipper_id, 'Not Assigned') AS shipper
    /* If expr1 is not NULL, IFNULL() returns expr1; 
    otherwise it returns expr2. */
FROM orders
  • 将 orders 里 shipper.id 中的空值替换为 comments,若 comments 也为空则替换为 ‘Not Assigned’(未分配)
USE sql_store;

SELECT 
    order_id,
    COALESCE(shipper_id, comments, 'Not Assigned') AS shipper
    /* Returns the first non-NULL value in the list, 
    or NULL if there are no non-NULLvalues. */
FROM orders

COALESCE 函数是返回一系列值中的首个非空值,更灵活
(coalesce vi. 合并;结合;联合)

  • 返回一个有如下两列的查询结果:
  1. customer (顾客的全名)
  2. phone (没有的话,显示’Unknown’)
USE sql_store;

SELECT 
    CONCAT(first_name, ' ', last_name) AS customer,
    IFNULL/COALESCE(phone, 'Unknown') AS phone   
FROM customers

IF函数

  • 根据是否满足条件返回不同的值:
  • IF(条件表达式, 返回值1, 返回值2) 返回值可以是任何东西,数值 文本 日期时间 空值null 均可
  • 将订单表中订单按是否是今年的订单分类为active(活跃)和archived(存档),之前讲过用UNION法,即用两次查询分别得到今年的和今年以前的订单,添加上分类列再用UNION合并,这里直接在SELECT里运用IF函数可以更容易地得到相同的结果
USE sql_store;

SELECT 
    *,
    IF(YEAR(order_date) = YEAR(NOW()),
       'Active',
       'Archived') AS category
FROM orders
  • 得到包含如下字段的表:
  1. product_id
  2. name (产品名称)
  3. orders (该产品出现在订单中的次数)
  4. frequency (根据是否多于一次而分类为’Once’或’Many times’)
USE sql_store;

SELECT 
    product_id,
    name,
    COUNT(*) AS orders,
    IF(COUNT(*) = 1, 'Once', 'Many times') AS frequency
    /* 因为之后的内连接筛选掉了无订单的商品,
    所以这里不变考虑次数为0的情况 */
FROM products
JOIN order_items USING(product_id)
GROUP BY product_id

另外,发现如果想用同级列别名orders怎么都不行:

若写成 IF(orders = 1, ‘Once’, ‘Many times’) AS frequency
会报错:Error Code: 1054. Unknown column ‘orders’ in ‘field list’

若写成 IF((SELECT orders) = 1, ‘Once’, ‘Many times’) AS frequency
会报错:Error Code: 1247. Reference ‘orders’ not supported (reference to group function)

CASE运算符

  • 当分类多余两种时,可以用IF嵌套,也可以用CASE语句,后者可读性更好
CASE 
    WHEN …… THEN ……
    WHEN …… THEN ……
    WHEN …… THEN ……
    ……
    [ELSE ……]ELSE子句是可选的)
END
  • 不是将订单分两类,而是分为三类:今年的是 ‘Active’, 去年的是 ‘Last Year’, 比去年更早的是 ‘Achived’:
USE sql_store;

SELECT
    order_id,
    CASE
        WHEN YEAR(order_date) = YEAR(NOW()) THEN 'Active'
        WHEN YEAR(order_date) = YEAR(NOW()) - 1 THEN 'Last Year'
        WHEN YEAR(order_date) < YEAR(NOW()) - 1 THEN 'Achived'
        ELSE 'Future'  
    END AS 'category'
FROM orders

ELSE ‘Future’ 是可选的,实验发现若分类不完整,比如只写了今年和去年的两个分类条件,则不在这两个分类的记录的 category 字段会是 null.

  • 得到包含如下字段的表:customer, points, category(根据积分 <2k、2k~3k(包含两端)、>3k 分为青铜、白银和黄金用户)
    之前也是用过 UNION 法,分别查询增加分类字段再合并,很麻烦。
USE sql_store;

SELECT
    CONCAT(first_name, ' ', last_name) AS customer,
    points,
    CASE
        WHEN points < 2000 THEN 'Bronze'
        WHEN points BETWEEN 2000 AND 3000 THEN 'Silver'
        WHEN points > 3000 THEN 'Gold'
        -- ELSE null
    END AS category
FROM customers
ORDER BY points DESC

其实也可以用IF嵌套,甚至代码还少些,但感觉没有CASE语句结构清晰、可读性好

SELECT
    CONCAT(first_name, ' ', last_name) AS customer,
    points,
    IF(points < 2000, 'Bronze', 
        IF(points BETWEEN 2000 AND 3000, 'Silver', 
        -- 第二层的条件表达式也可以简化为 <= 3000
            IF(points > 3000, 'Gold', null))) AS category
FROM customers
ORDER BY points DESC

你可能感兴趣的:(mysql)