written by SJTU-XHW

本部分文章是 SQL 的基础部分,纪念我丢掉的 SQL 笔记 😭

本人知识浅薄,文章难免有问题或缺漏,欢迎批评指正!

Reference: 《 SQL 必知必会 (第 5 版 )


使用前须知

本系列文章的 “章末习题” 使用的数据库和表的关系如下图所示;

这里提供创建表和数据的 SQL 文本(以 MySQL 为例):generate.sql,可以在搭建好 MySQL 环境后直接导入它,以备后面章末习题使用;

Chapter 0 数据库的基本概念

  • 数据库(从 SQL 角度看):以某种有组织的方式存储的数据集合

  • 关系型数据库 & 非关系型数据库

    • 关系型数据库:采用关系模型、遵循关系代数模式来组织数据的数据库(例如 MySQL、PostgreSQL等);关系模型类似二维表,允许表间数据相互关联,支持多表查询
    • 非关系型数据库(No SQL):不依赖关系模型的、分布式的数据库(例如 MongoDB、Redis 等);以键值来存储,结构不稳定,每个元组的字段可以不相同,可以面向高并发key-value读写 / 面向文档
    表一 关系型数据库 & 非关系型数据库比
    项目 RDB NOSQL(Not Only SQL)
    是否依赖关系模型 是,二维表容易理解 否,key-value 存储较为零散
    是否有通用的 SQL 有,使用方便 无,无法提供WHERE字段查询
    事务的一致性 强,ACID属性易于维护 弱,适宜面向网络/文档/高并发读写等场景
    拓展能力 纵向,依赖处理能力提升 横向,天然分布式
    存储数据 适宜结构化数据,例如账户、地址 适宜非结构化数据,例如文章、评论

注:一般生产环境会使用 RDB + NoSQL 组合方案,互相取长补短;

本文章仅讨论关系型数据库中的 标准 SQL

  • 数据库管理系统(DBMS):俗称数据库管理系统;数据库是通过 DBMS 创建和操纵的容器

    MySQL、mariaDB、mongoDB、SQLite 等等都是 DBMS,而它们创建的数据库才是真正的 “数据库”;

  • 表(table):某种特定类型数据的结构化清单;

    根据定义,可以理解为:在数据库(相当于文件柜)中,存放文件需要放在特定的档案中,这个“文件柜”中的“档案”就是表;

    表存储的是同一种类型的数据或清单,便于归类和查找

  • 表名:表的唯一标识符(ID),在一个数据库中唯一,但不同数据库的表之间名字可以相同;

  • 模式(schema):关于数据库和表的布局及特性的信息;

    描述数据库中特定的表,或者描述整个数据库及表的关系

    包括:储存什么数据?数据如何分解(parse)?各部分信息如何命名?……

  • 列(column):表中的一个字段,存储表的某一个部分/领域(field)的信息;

    所有表都是由一个或多个列组成

    正确地按需求对数据列分解对于分类、过滤意义重大;例如,将国家和省市信息组合在一个列中,那么仅按省市查找的难度就比 “国家、省市信息分开在两列中” 的情况更大;

  • 数据类型:每一列需要有相应的数据类型,限制该列存储的数据(以方便分类和查找);

    对表的各列设计数据类型也是重要的:正确地设计数据类型不仅可以帮助正确分类和查找,还可以优化磁盘容量;

    因为有些 DBMS 中同一个数据类型的名称不一样(数据类型兼容问题),即便名称相同,定义可能不一样,所以应该总是查阅文档、学习和应用时需要注意

  • 行:表中的一个记录(从技术上说,行才是正确的术语);

  • 主键(primary key):表中的每一行都应该有一列(或几列)可以唯一标识自己(ID)

    没有主键,更新、删除数据库表的特定行就极其困难,不能保证没有误操作

    所以规范来说,表应该总是定义主键;

    主键的要求

    1. 互异性:表的任意两行都不具有相同主键值;
    2. 非空性:每一行必须具有一个主键值(不允许为 NULL 空值);
    3. 固定性:主键列中的值不允许修改和更新;
    4. 不可重用性:如果表的一行从表中被删除,那么它的主键不能赋给以后的新行;
  • SQL:Structured Query Language,结构化查询语言(专门用来与数据库沟通的语言);

    1. 不是某个数据库厂商、某类 DBMS 特有的语言;
    2. 关键字数量极少,但功能强大;

    本文章不考虑某些 DBMS 的 SQL 扩展(某个厂商独有、独自开发的),仅讨论标准 SQL(ANSI SQL);

  • SQL 的关键字:作为 SQL 组成部分的保留字,不能用作表 / 列的名字;会在以后章节中逐渐提及;

  • SQL 语言注意事项

    1. SQL 语句的结束:以分号结尾;

    2. SQL 语句不区分大小写,但习惯上约定俗成:关键字大写、列/表名小写;

    3. 在一个 SQL 语句未结束时,可以在两个词间添加换行,不会有影响,一般是方便阅读;

    4. SQL 语言的索引从0开始,因此也是一种 0-index 语言

    5. 注释方式:行内注释 --(后面跟一个空格)、单行注释 # (某些 DBMS 不支持)、多行注释 /* [contents] */

      好习惯:和 其他所有编程语言一样,必要时多加注释;

Chapter 1 数据检索

1.1 SELECT 语句

  • 用途:从一个或多个表中检索信息;

  • 语法

    • 检索某一个表的 某一个列SELECT <column_name> FROM <table_name>;

    • 同时检索 多个列SELECT <col_name1, [col_name2, ...]> FORM <table_name>;

    • 检索 所有的的列SELECT * FORM <table_name>;

      谨慎使用通配符(*),除非真的需要,或者不知道需要列的列名

    • 检索表中 某列不同的值DISTINCT 关键字):SELECT DISTINCT <col_name> FROM <table_name>;

      不可以部分使用 DISTINCT 关键字,例如:SELECT DISTINCT <col_name1, col_name2> FROM <table_name>;

      因为 DISTINCT 作用于所有的列!部分使用会导致失去想要的结果(SQL 需要同时考虑其他列也有 DISTINCT,最后会出现组合的现象);(而且这么做不符合逻辑)

      DISTINCT 相反的是 ALL,一般无需指定,因为这是默认行为

    • 检索 结果限制(用来缩小范围找到想要的数据)

      1. 限制仅查询 N 行

        1
        2
        3
        SELECT <col_name>
        FROM <table_name>
        LIMIT N;
        1
        2
        3
        SELECT <col_name>
        FROM <table_name>
        WHERE ROWNUM <= 5; -- 行计数器
        1
        2
        SELECT TOP 5 <col_name>
        FROM <table_name>;
        1
        2
        3
        SELECT <col_name>
        FROM <table_name>
        FETCH FIRST 5 ROWS ONLY;

        以下仅以常见的 MySQLMariaDB为例,不再赘述

      2. 限制指定行:M ~ M+N

        SQL 语言 0-index

        1
        2
        3
        4
        5
        SELECT <col_name>
        FROM <table_name>
        LIMIT N OFFSET M; -- 即 0+M ~ 0+M+N
        -- 或者
        LIMIT M,N;
  • 习题

    1. customers 表中检索所有的 cust_id 列的数据:

      1
      2
      SELECT cust_id
      FROM customers;
    2. 已知表 OrderItems (订单表)中存在一列 prod_id(产品 ID),问所有出现在订单中的产品 ID,要求每个产品 ID 仅显示一次:

      1
      2
      SELECT DISTINCT prod_id
      FROM OrderItems;
    3. 检索 customers 表中所有的列,再检索其中列 cust_idcust_namecust_country 的第 5~9 行:

      1
      2
      3
      4
      SELECT * FROM customers;
      SELECT cust_id, cust_name, cust_country
      FROM customers
      LIMIT 5 OFFSET 5;

1.2 排序已检索数据

主要内容:SELECTORDER BY 子句;

  • 子句(clause)的定义:SQL 语句由子句构成;

    1. 有些子句是必需的,有些是可选的;
    2. 一个子句通常是由一个关键字 + 所提供的数据组成;

    举例:SELECT 语句的 FROM 子句、LIMIT 子句、ORDER BY 子句等;

  • 语法

    • 检索时 按某一列排序SELECT <col_name1[, col_name2, ...]> FROM <tb_name> ORDER BY <col_nameN>;

      必须保证 ORDER BY 子句为 SELECT 语句的最后一句,否则会出错!

      ℹ 排序依据的列可以不是选择列(即 col_nameN 可以不在 col_name1, col_name2,...中);

    • 检索时 按多个列排序SELECT <col_name1[, col_name2, ...]> FROM <tb_name> ORDER BY <col_nameN1, col_nameN2, ...>;

      含义:首先按 col_nameN1 排序,再按 col_nameN2 排序,有点像高考录取排序方法;

    • 检索时 使用已选择列的列号排序(仅仅是一种简化方法):SELECT <col_Name1[, ...]> FROM <tb_name> ORDER BY <col_num1[, ...]>;

    • 检索时 指定排序方向SELECT <col_name1[, ...]> FROM <tb_name> ORDER BY <col_nameN1[ASC/DESC][, ...]>;

      关键字 ASC(Ascending,默认升序)、DESC(Descending);

  • 习题

    1. Customers 表中检索所有的顾客名称(cust_names),并按从 Z 到 A 的顺序显示结果:

      1
      2
      3
      SELECT cust_names
      FROM Customers
      ORDER BY cust_names DESC;
    2. Orders 表中检索顾客 ID(cust_id)和订单号(order_num),并先按顾客 ID 对结果进行排序,再按订单日期(order_date)倒序排列:

      1
      2
      3
      SELECT cust_id, order_num
      FROM Orders
      ORDER BY cust_id ASC, order_date DESC;

1.3 过滤已检索数据

主要内容:SELECTWHERE 子句、SELECTLIKE 子句(模糊过滤);

  • 应用须知:对数据的过滤操作应该交给 SQL,而非上层的应用程序(开发语言)

    原因:

    1. 数据库 SQL 对于数据过滤优化力度远远大于你自己在外层写的,造成性能资源浪费;
    2. 降低上层应用程序的可移植性、可读性、可维护性;
  • 过滤条件:也称“搜索条件”,用于根据特定操作提取表数据的子句;

    模糊过滤条件:使用类似正则式的方式确定过滤条件(仅适用于文本-string);

  • 谓词(predicate):某些关键字不是操作符,而是作为谓词使用,例如 LIKE

  • 语法

    • 在检索时 过滤条件SELECT <col_name[, ...]> FROM <tb_name> WHERE <cond>;
    • 在检索时 使用模糊过滤条件SELECT <col_name[, ...]> FROM <tb_name> WHERE <col_str> LIKE <pattern>;
  • WHERE <cond> 子句判断操作符:小于(等于)、大于(等于)(与普通编程语言相同),等于(=,不等于(!=<>),不小于(!<),不大于(!>);

    另外:在指定两个值之间使用关键字操作符 BETWEEN...AND...,判断是否为 NULL IS NULL

    以上操作符有冗余,有些 DBMS 不支持,请自行判断;

    注意1:NULL(空值)和字段包含0、空字符串、仅含空格的情况都不一样

    注意2:只要使用了过滤条件,除了 IS NULL 操作符,其他的匹配无法选中匹配列记录为 NULL 的记录!

    注意3:等于操作符和主流编程语言不一样,只有一个等号!

  • WHERE <cond> 子句条件操作符:ANDORNOT、圆括号(改变运算顺序);

    运算顺序同主流编程语言;

  • WHERE <cond> 子句包含操作符:IN、定元组(例如:('DLL01', 'BRS01'));

    建议能用 IN,就用它:

    (例如:SELECT prod_name FROM products WHERE prod_id == 'DLL01' OR prod_id == 'BRS01'; 不如 SELECT prod_name FROM products WHERE prod_id in ('DLL01', 'BRS01');

    因为它有好处:

    1. IN 操作符可读性更强;
    2. 数据库底层优化 IN 可以使用一次 HASH 操作,效率高于 OR 连接的运算;
    3. IN 操作符可以实现 WHERE 子句的嵌套(后面讨论);
  • WHERE 子句的 pattern

    表二 SQL pattern 和 正则式对比
    SQL pattern % (NULL excluded) _ [char set] [^char set]
    Regex .? . .[char set] .[^char set]

注意1:在 LIKE 匹配时,pattern 区分大小写!

注意2:在有些数据库的数据中,对于字符串数据类型按指定位存储时,不满位数的会填充以空格,因此 pattern 可能很难匹配到;正确做法是利用 SQL 自带函数去除字符串两边空格,后面讨论;

注意3:不宜过度使用通配符、不宜将通配符作为靠前的搜索条件;

  • 习题

    1. 从Products 表中检索产品ID(prod_id)和产品名称(prod_name),只返回价格(prod_price)为 9 或更高的产品:

      1
      2
      3
      SELECT prod_id, prod_name
      FROM Products
      WHERE prod_price >= 9;
    2. OrderItems 表中检索出所有不同订单号(order_num),其中包含产品个数(quantity)在 100 个至 200 个之间:

      1
      2
      3
      4
      5
      SELECT DISTINCT order_num
      FROM OrderItems
      WHERE quantity BETWEEN 100 AND 200;
      -- 或者
      WHERE quantity >= 100 AND quantity <= 200;
    3. 返回 Products 表中所有价格在 3 美元到 6 美元之间的产品的名称(prod_name)和价格(prod_price),然后先按价格、后按 ID (prod_id)对结果都进行降序排序:

      1
      2
      3
      4
      SELECT prod_name, prod_price
      FROM Products
      WHERE prod_price BETWEEN 3 AND 6
      ORDER BY prod_name DESC, prod_id DESC;
    4. 查找所有至少订购了总量(quantity) 100 个的,ID 为 BR01BR02BR03 的订单。你需要返回OrderItems 表的订单号(order_num)、产品ID(prod_id)和数量,并按产品 ID 和数量进行升序排序:

      1
      2
      3
      4
      SELECT order_num, prod_id
      FROM OrderItems
      WHERE quantity >= 100 AND prod_id IN ('BR01', 'BR02', 'BR03')
      ORDER BY prod_id, quantity;
    5. Products 表中检索产品名称(prod_name)和描述(prod_desc),仅返回描述中未出现 toy 一词的产品。要求按产品名称对结果进行排序:

      1
      2
      3
      4
      SELECT prod_name, prod_desc
      FROM Products
      WHERE NOT (prod_desc LIKE '%toy%')
      ORDER BY prod_name;
    6. Products 表中检索产品名称(prod_name)和描述(prod_desc),仅返回在描述
      中以先后顺序同时出现 toycarrots 的产品:

      1
      2
      3
      SELECT prod_name, prod_desc
      FROM Products
      WHERE prod_desc LIKE '%toy%carrots%';

Chapter 2 字段格式化输出

表中的列称为列,而在“计算字段”(格式化字段)中称为字段(field);

格式化输出的思想也遵循本思想🔗

2.1 字段拼接

1
2
SELECT Concat(col_name1/'string1'[, ...])
FROM <tb_name>;
1
2
SELECT col_name1/'string1' + ... + col_nameN/'stringN'
FROM <tb_name>;
1
2
SELECT col_name1/'string1' || ... || col_nameN/'stringN'
FROM <tb_name>;

2.2 简单算术运算和 strip

+-*/ 和简单函数 LTRIM(str)(去左空格)、RTRIM(str)(去右空格)、TRIM(str)(去两边空格);

常见数学函数几乎都与主流编程语言相同(SIN()EXP()ABS() 等);

常量:PI()

以上描述都没有提及别名(alias,又称导出列,derived column),其实规范来说,对于所有的格式化输出(包括以后提及的),都应该设置别名,好处如下:

  1. 方便客户端引用。没有别名的格式化列无法被某些客户端使用,有些操作会变得困难;
  2. 重命名包含不合法字符的原列名;
  3. 提升可读性。在原来的列名含混、意义不清晰时扩充、具体化它;

2.3 普通函数处理

本部分内容的可移植性会受到不同 DBMS 对函数名称和定义的影响

函数名称也不区分大小写

表三 SQL 普通函数对比
函数用法 函数语法
MySQL SQL Server DB2 SQLite Oracle
字符串子串 SUBSTRING() SUBSTR()
数据类型转换 CONVERT() CAST() 因类型不同
取日期 CURDATE() GETDATE() CURRENT_DATE DATE() SYSDATE
格式化日期 YEAR()等 DATEPART(yy, DATE) 同 MySQL strftime(strf, DATE) EXTRACT(year FROM DATE)
去空格 LTRIM()、RTRIM()、TRIM()
求字符串长度 CHAR_LENGTH() LEN() STRING-LEGTH() LENGTH()
求数据所占Byte LENGTH() DATALENGTH() 同 MySQL TYPEOF()对应的Byte LENGTHB()

tips 1. SUBSTRING(str, start, end),其中 end 包含,且此处函数面向用户,是 1-index

tips 2. 还有些通用性比较好的函数:UPPER()/LOWER()(取大/小写)、SOUNDEX()(返回字符串的 SOUNDEX 值);

SOUNDEX() 适用于英文,实质是一种将单词转化为描述发音的字母数字算法,可以找出发音近似或相同的单词,在有一些误输入的查找中有用,比如顾客名 Michael 误输入为 Michelle,但二者的 SOUNDEX() 相同,因此可以匹配

2.4 聚集函数

  • 定义:对表的某个列(某一行数据类型很可能不相同)运行的函数,仅计算返回一个值;

  • AVG() 平均值函数

    1. 仅能针对单个列使用!!!如果需要多个列,请依次书写;

      仅有一种特例(实质还是一个列——导出列):列以算术运算符连接

    2. 忽略值为 NULL 的行

  • COUNT() 计数函数

    1. COUNT(*)给表中的行计数,不忽略 NULL
    2. COUNT(column)给特定列的行计数,忽略 NULL
  • MAX()/MIN() 最值函数:返回升序 / 降序排序后的第一行

  • SUM() 求和函数;

  • 注意事项

    1. 允许列结合算数运算符
    2. 聚集不同值:如果对某列要按不同值进行聚集,应该在列名前指定 DISTINCT
    3. 使用聚集函数时强烈建议取别名

2.5 数据分组

主要内容:SELECTGROUP BY 子句 和 HAVING 子句;

  • 需求:在 2.4 中的聚集函数一般都是面向整个一列,或者一列的特定部分(如果使用了 DISTINCTWHERE 约束);但没法按照分类组来统计、聚集数据(例如按照 date 时间段分组求最大值),所以引入了数据分组

  • 创建分组:SELECT <col_name-X, [AgFuctions]> FROM <tb_name> GROUP BY <col_name-X>;

    以上语句的含义指:将 col_name-XDISTINCT 一下(所以上面的列名仅推荐使用 col_name-X,即选择的列),再按 col_name-X 分组(内部完成),这时通配符 * 指代的对象会变成:当前选择列中 各自分好的组(意味着 col_name-X 只能有一个,否则不能使用通配符 *

    ℹ 你可以使用聚集函数的 * 来试一试上面的理论;

  • 分组的性质

    1. 数据列中 NULL 会被分为一组;

    2. GROUP BY 子句必须在 WHERE 子句后,在 ORDER BY 子句之后

      GROUP BY 必须在 WHERE 之后的原因:

      GROUP BY 产生内部分组,而 WHERE 只能对所有行筛选,完全没有分组的概念,所以 WHERE 不能筛选分组(否则造成意想不到的结果)!

      应该使用可以筛选分组的子句:HAVING(这个子句除了多了一个“可以筛选分组”的功能,其他使用与 WHERE 几乎一致、可以替代;但标准来说只有 GROUP BY + 需要筛选时才用到 HAVING

    3. GROUP BY 子句允许嵌套分组;

  • 过滤分组:SELECT <col_name-X [AgFunctions]> FROM <tb_name> GROUP BY <col_name-X> HAVING <cond>;

  • 排序分组:SELECT <col_name-X [AgFunctions]> FROM <tb_name> GROUP BY <col_name-X> ORDER BY <col_name-X>;

    ⚠ 虽然你可能发现 GROUP BY 本身就可以按分组后的结果排序,但这个行为不在 SQL 规范中,所以如果有需要对分组进行排序,仍需要 ORDER BY 子句,不能仅仅依赖 GROUP BY

2.6 本章习题

  1. Vendors 表中检索 vend_idvend_namevend_addressvend_city,将 vend_name 重命名为 vname,将 vend_city 重命名为 vcity,将 vend_address 重命名为 vaddress。再按供应商名称对结果进行排序。注意所有的字符串变量的存储都有定长、记录前可能有空格:

    1
    2
    3
    4
    5
    6
    SELECT vend_id,
    TRIM(vend_name) AS vname,
    TRIM(vend_city) AS vcity,
    TRIM(vend_address) AS vaddress
    FROM Vendors
    ORDER BY vname;
  2. 商店正在进行打折促销,所有产品均降价 10%. 编写 SQL 语句,从 Products 表中返回 prod_idprod_pricesale_price,其中sale_price 是一个包含促销价格的计算字段:

    1
    2
    3
    SELECT prod_id, prod_price,
    prod_price*0.9 AS sale_price
    FROM Products;
  3. 假设商店已经上线了,正在创建顾客账户。所有用户都需要登录名,默认登录名是其名称和所在城市的组合。在表 Customers 中编写 SQL 语句,返回顾客 ID(cust_id)、顾客名称(cust_name)和登录名(user_login),其中登录名全部为大写字母,并由顾客联系人的前两个字符(cust_contact)和其所在城市的前三个字符(cust_city)组成。例如,我的登录名是 BEOAKBen Forta,居住在 Oak Park):

    1
    2
    3
    4
    5
    6
    7
    8
    SELECT cust_id, cust_name,
    UPPER(
    CONCAT(
    SUBSTRING(cust_contact, 1, 2),
    SUBSTRING(cust_city, 1, 3)
    )
    ) AS user_login
    FROM Customers;
  4. 编写 SQL 语句,返回 2020 年 1 月的所有订单(Orders 表)的订单号(order_num)和订单日期(order_date),并按订单日期排序:

    1
    2
    3
    4
    SELECT order_num, order_date
    FROM Orders
    WHERE date_format(order_date, '%Y-%m') = '2020-01'
    ORDER BY order_date;
  5. 编写 SQL 语句,找出 Products 表中的 num_items 项的总个数(不包括 NULL)、值不同的个数、最小、最大、平均值,列在一个表格中:

    1
    2
    3
    4
    5
    6
    SELECT COUNT(num_items) AS total_cnt,
    COUNT(DISTINCT num_items) AS dist_cnt,
    MIN(num_items) AS min_items,
    MAX(num_items) AS max_items,
    AVG(num_items) AS avg_items
    FROM Products;
  6. 计算 OrderItems 表中各行所要花费的金钱总和(物品数量 quantity * 物品价格 item_price 再求和):

    1
    SELECT SUM(quantity * item_price) AS total_cost FROM OrderItems;
  7. 编写 SQL 语句,确定 Products 表中价格不超过 10 美元的最贵产品的价格(prod_price)。将计算所得的字段命名为 max_price

    1
    2
    3
    SELECT MAX(prod_price) AS max_price
    FROM Products
    WHERE prod_price <= 10;
  8. 编写 SQL 语句,要求统计 Products 表的供应商 ID vend_id 各自提供的商品个数(即每个供应商在 Product 中含有的数据行数),按 “ID、个数(列名 num_prods)” 顺序输出:

    1
    2
    3
    SELECT vend_id, COUNT(*) AS num_prods
    FROM Products
    GROUP BY vend_id;
  9. 编写 SQL 语句,要求找出 Orders 表中订单数不少于 2 的顾客 ID(cust_id),即每个 cust_id 在表中含有的数据行数,按 “ID、订单数” 顺序输出:

    1
    2
    3
    4
    SELECT cust_id, COUNT(*) AS num_orders
    FROM Orders
    GROUP BY cust_id
    HAVING COUNT(*) >= 2;
  10. 编写 SQL 语句,要求找出 Products 表中 “提供 2 个及以上的、价格(prod_price)不少于4 的产品” 的供应商,按 “供应商ID(vend_id)、满足条件的产品数(列名 num_prods)” 顺序输出:

    1
    2
    3
    4
    5
    SELECT vend_id, COUNT(*) AS num_prods
    FROM Products
    WHERE prod_price >= 4 -- 注意:在这里非分组筛选需要在分组前进行
    GROUP BY vend_id
    HAVING COUNT(*) >= 2;
  11. OrderItems 表包含每个订单的每个产品。编写SQL 语句,返回每个订单号(order_num)各有多少行数(order_lines),并按 order_lines 对结果进行升序排序:

    1
    2
    3
    4
    SELECT order_num, COUNT(*) AS order_lines
    FROM OrderItems
    GROUP BY order_num
    ORDER BY order_lines;
  12. 编写 SQL 语句,返回名为 cheapest_item 的字段,该字段包含每个供应商(vend_id)成本最低的产品(使用 Products 表中的 prod_price),然后从最低成本到最高成本对结果进行排序(按 “供应商 ID、cheapest_item” 输出):

    1
    2
    3
    4
    5
    # 注意:此处 prod_price 已不再是整个列,而是各分组的 prod_price,对它用聚集函数会得到各列的结果
    SELECT vend_id, MIN(prod_price) AS cheapest_item
    FROM Products
    GROUP BY vend_id
    ORDER BY cheapeset_item;
  13. 商店经营中,确定最佳顾客非常重要。有一个店主想出 2 种方法来确定他的最佳顾客:

    Ⅰ. 找出满足 “所有订购产品的数量不少于 100” 的订单,至少持有一个这样订单的顾客是 “最佳顾客”;(注:在 OrderItems 表中特定的订单号 order_num、本订单订购商品名 order_item下的商品数量 quantity,是 “这个订单中 这个产品的数量”)

    所以请编写 SQL 语句,只需找出满足上述条件的这个订单,输出订单号即可;

    1
    2
    3
    4
    SELECT order_num
    FROM OrderItems
    GROUP BY order_num
    HAVING SUM(quantity) >= 100;

    Ⅱ. 找出满足 “总价(包含不同的商品)至少为 1000” 的订单,至少持有一个这样订单的顾客是 “最佳顾客”;(注:对于一个订单中的一个商品而言,需要在 OrderItems 表中计算 item_price * quantity 来获得订单中一类商品的总价格)

    所以请编写 SQL 语句,找出满足上述条件的订单,以降序输出订单号(order_num),按 “订单号、总价(列名 total_price)” 输出;

    1
    2
    3
    4
    5
    SELECT order_num, SUM(item_price * quantity) AS total_price
    FROM OrderItems
    GROUP BY order_num
    HAVING total_price >= 1000
    ORDER BY order_num DESC;
  14. 在不运行下列语句的前提下,指出下列语句的错误:

    1
    2
    3
    4
    5
    SELECT order_num, COUNT(*) AS items
    FROM OrderItems
    GROUP BY items
    HAVING COUNT(*) >= 3
    ORDER BY items, order_num;

    Answer:GROUP BY 语句不能对聚合函数分组!从分组聚合的含义上来说,COUNT(*) 是对分组后的组进行计数,这样是循环指代,从逻辑上行不通;

    改正方案:将 GROUP BY items 分组依据改成 GROUP BY order_num 就正确了,如下:

    1
    2
    3
    4
    5
    SELECT order_num, COUNT(*) AS items
    FROM OrderItems
    GROUP BY order_num
    HAVING COUNT(*) >= 3
    ORDER BY items, order_num;

    这段语句的含义是:找出所有满足 “包含不少于 3 种商品” 的订单,并按 “订单所含商品种类数、订单号” 输出(最后按种类数,再按订单号升序排序);

EX-1 第一部分语法总结

1
2
3
4
5
6
SELECT <col_name1[, ...]> [AS new_name]
[FROM <tb_name>]
[WHERE <cond>]
[GROUP BY <col_name1>]
[HAVING <cond>]
[ORDER BY <col_name-N>];