「SQL数据分析系列」3.查询入门

来源 |  Learning SQL Generate, Manipulate, and Retrieve Data, Third Edition

作者 | Alan Beaulieu

译者 | Liangchu

校对 | gongyouliu

编辑 | auroral-L

全文共12309字,预计阅读时间90分钟。

第三章  查询入门

1.  查询机制

2.  查询语句

3.  select子句

    3.1  列别名

    3.2  去除冗余

4.  from子句

    4.1  表

        4.1.1  派生(子查询生成)表

        4.1.2  临时表

        4.1.3  视图

    4.2  表连接

    4.3  定义表别名

5.  where子句

6.  group by 和 having 子句

7.  order by 子句

    7.1  升序或降序排列

    7.2  根据表达式排序

到目前为止,本书已经在前两章中介绍了一些数据库查询的例子(select语句),所以本章会介绍select语句的组成部分以及它们的作用。学习完本章内容之后,你应该就基本了解了如何检索、连接、过滤、分组和排序数据,在本书第四到第十章中会详细介绍这些内容。

1.查询机制

在分析select语句之前,让我们先了解一下MySQL服务器(或者其他任何数据库服务器)是如何执行查询的。如果你使用的是mysql命令行工具,并且已经输入了用户名和密码进行登录(如果MySQL服务器在另一台计算机上运行,还可能需要提供主机名),一旦服务器通过了用户名和密码的验证,就会为用户生成一个数据库连接。该连接由请求它的应用程序(在本例中是mysql工具)保持,直到应用程序释放连接(比如键入quit命令)或服务器关闭连接(比如服务器关闭时)才结束。MySQL服务器的每个连接都有被分配一个标识符,当你第一次登录时会显示该标识符:

Welcome to the MySQL monitor. Commands end with ; or \g. 
Your MySQL connection id is 11 
Server version: 8.0.15 MySQL Community Server - GPL 
Copyright (c) 2000, 2019, Oracle and/or its affiliates. All rights reserved. 
Oracle is a registered trademark of Oracle Corporation and/or its 
affiliates. Other names may be trademarks of their respective 
owners. 
Type 'help;' or '\h' for help. Type '\c' to clear the buffer.

在本例中,我的连接ID是11。如果出现什么问题,比如要停止运行了数小时的错误查询,那么这个信息对于数据管理员来说可能会很有用,所以你可能需要将这个ID记下来。

一旦服务器验证了输入的用户名和密码并向你发送了连接,你就可以执行查询了(以及其他SQL语句)。每次向服务器发送查询时,服务器都会在执行语句之前检查以下内容:

• 用户是否有执行该语句的权限?

• 用户是否有访问目标数据的权限?

• 撰写语句的语法是否正确?

如果你能够满足这几项要求,那么你的查询会被交给查询优化器,它的任务是确定执行查询的最有效方式。优化器会查看诸如连接from子句中指定的表的顺序以及可用的索引,然后选择一个执行方案,以供服务器执行查询。

注意

了解和影响数据库服务器如何选择执行方案是一个很有趣的话题,有许多人都对它感兴趣。对于使用MySQL的读者来说,建议阅读Baron Schwartz等人的High Performance MySQL (O’Reilly),里面介绍了如何生成索引、分析执行方案、通过查询提示(query hints)影响优化器以及优化服务器的初始参数。如果你使用的是Oracle数据库或SQL Server,也有很多有关调优的书籍可供选择。

一旦服务器执行查询结束,结果集(result set)就会被返回给调用应用程序(本例中也就是mysql工具)。我在第一章中提到过,结果集实际上就是另一个包含行和列的表。如果查询没有返回任何结果,那么mysql工具将向你显示以下示例末尾的提示消息:

mysql> SELECT first_name, last_name
 -> FROM customer
 -> WHERE last_name = 'ZIEGLER';
Empty set (0.02 sec)

如果查询返回一行或多行数据,那么mysql工具将通过添加列标题并使用-、|和+符号在列周围构建边框来进行格式化输出,如下所示:

mysql> SELECT * 
 -> FROM category; 
+-------------+-------------+---------------------+ 
| category_id | name | last_update | 
+-------------+-------------+---------------------+ 
| 1 | Action | 2006-02-15 04:46:27 | 
| 2 | Animation | 2006-02-15 04:46:27 | 
| 3 | Children | 2006-02-15 04:46:27 | 
| 4 | Classics | 2006-02-15 04:46:27 | 
| 5 | Comedy | 2006-02-15 04:46:27 | 
| 6 | Documentary | 2006-02-15 04:46:27 | 
| 7 | Drama | 2006-02-15 04:46:27 | 
| 8 | Family | 2006-02-15 04:46:27 | 
| 9 | Foreign | 2006-02-15 04:46:27 | 
| 10 | Games | 2006-02-15 04:46:27 | 
| 11 | Horror | 2006-02-15 04:46:27 | 
| 12 | Music | 2006-02-15 04:46:27 | 
| 13 | New | 2006-02-15 04:46:27 | 
| 14 | Sci-Fi | 2006-02-15 04:46:27 | 
| 15 | Sports | 2006-02-15 04:46:27 | 
| 16 | Travel | 2006-02-15 04:46:27 | 
+-------------+-------------+---------------------+ 
16 rows in set (0.02 sec)

此查询返回category表中所有行的所有三列数据。在最后一行数据之后,mysql工具会显示一条消息提示返回了多少行数据,在本例中是16行。

2.查询语句

select语句由几个组件(即子句clauses)组成。虽然在使用MySQL(select子句)时只有一个组件是必需的,但一条查询通常至少会包含六个可用子句中的两个或三个。下表(3-1)显示了不同的子句及其用处:

 

 

上表(3-1)所示的所有子句均符合ANSI规范。下面几节将深入探讨这六个主要查询子句的用法。

3.select子句

尽管select子句是select语句的第一个子句,但它却是最后被数据库服务器处理的子句之一。因为在确定最终结果集中要包含的内容之前,需要先知道最终结果集中可能包含的所有列。因此,为了充分理解select子句的作用,你需要稍微了解一下from子句。以下是一个查询:

mysql> SELECT *
 -> FROM language;
+-------------+----------+---------------------+
| language_id | name | last_update |
+-------------+----------+---------------------+
| 1 | English | 2006-02-15 05:02:19 |
| 2 | Italian | 2006-02-15 05:02:19 |
| 3 | Japanese | 2006-02-15 05:02:19 |
| 4 | Mandarin | 2006-02-15 05:02:19 |
| 5 | French | 2006-02-15 05:02:19 |
| 6 | German | 2006-02-15 05:02:19 |
+-------------+----------+---------------------+
6 rows in set (0.03 sec)

在这个查询中,from子句列出一个表(language),select子句指示language表中的所有列(由*指定)都应该被包含在结果集中。这个查询可以用中文描述如下:

显示language表中的所有行列。 

除了通过星号字符(*)指定所有列外,还可以显式使用感兴趣的列,例如:

mysql> SELECT language_id, name, last_update
 -> FROM language;
+-------------+----------+---------------------+
| language_id | name | last_update |
+-------------+----------+---------------------+
| 1 | English | 2006-02-15 05:02:19 |
| 2 | Italian | 2006-02-15 05:02:19 |
| 3 | Japanese | 2006-02-15 05:02:19 |
| 4 | Mandarin | 2006-02-15 05:02:19 |
| 5 | French | 2006-02-15 05:02:19 |
| 6 | German | 2006-02-15 05:02:19 |
+-------------+----------+---------------------+
6 rows in set (0.00 sec)

结果与第一个查询语句的输出相同,因为language表中的所有列(language_id、name和last_update)都在select子句中显式命名。你也可以选择仅包括language表中列的子集:

mysql> SELECT name
 -> FROM language;
+----------+
| name |
+----------+
| English |
| Italian |
| Japanese |
| Mandarin |
| French |
| German |
+----------+
6 rows in set (0.00 sec)

因此,select子句的作用如下:

select子句确定查询结果集中应该包括哪些可能的列。

如果你只包含from子句后面指定表中包含的列,那么就相当乏味了。所以你可以在select子句中添加以下内容:

• 字符,如数字或字符串;

• 表达式,例如transaction.amount * −1;

• 内置函数调用,例如ROUND(transaction.amount, 2);

• 用户自定义的函数调用。

下一个查询演示如何在针对employee表的单个查询中应用列、字符、表达式和内置函数调用:

mysql> SELECT language_id,
 -> 'COMMON' language_usage,
 -> language_id * 3.1415927 lang_pi_value,
 -> upper(name) language_name
 -> FROM language;
+-------------+----------------+---------------+---------------+
| language_id | language_usage | lang_pi_value | language_name |
+-------------+----------------+---------------+---------------+
| 1 | COMMON | 3.1415927 | ENGLISH |
| 2 | COMMON | 6.2831854 | ITALIAN |
| 3 | COMMON | 9.4247781 | JAPANESE |
| 4 | COMMON | 12.5663708 | MANDARIN |
| 5 | COMMON | 15.7079635 | FRENCH |
| 6 | COMMON | 18.8495562 | GERMAN |
+-------------+----------------+---------------+---------------+
6 rows in set (0.04 sec)

稍后我们将详细介绍表达式和内置函数,本例是想让你大致了解select子句中可以包含哪些内容。如果你只需要执行内置函数或计算简单表达式,可以完全跳过from子句。举个例子:

mysql> SELECT version(), 
 -> user(), 
 -> database(); 
+-----------+----------------+------------+ 
| version() | user() | database() | 
+-----------+----------------+------------+ 
| 8.0.15 | root@localhost | sakila | 
+-----------+----------------+------------+ 
1 row in set (0.00 sec)

由于此查询只调用了三个内置函数,没有从任何表中检索数据,因此不需要用到from子句。

3.1列别名

尽管mysql工具默认为查询返回的列生成标签,但你可能想用自己定义的标签。你可能因为表中列名含糊不清而想给它指定一个新标签,在结果集中包含根据表达式或内建函数调用产生的列的时候,你可能也想给这些列定义新的标签,这些都可以通过在select子句的每个元素后面添加列别名来实现。下面是对language表查询,与上一个查询相似,只不过为其中三列定义了列别名:

mysql> SELECT language_id, 
 -> 'COMMON' language_usage, 
 -> language_id * 3.1415927 lang_pi_value, 
 -> upper(name) language_name 
 -> FROM language; 
+-------------+----------------+---------------+---------------+ 
| language_id | language_usage | lang_pi_value | language_name | 
+-------------+----------------+---------------+---------------+ 
| 1 | COMMON | 3.1415927 | ENGLISH | 
| 2 | COMMON | 6.2831854 | ITALIAN | 
| 3 | COMMON | 9.4247781 | JAPANESE | 
| 4 | COMMON | 12.5663708 | MANDARIN | 
| 5 | COMMON | 15.7079635 | FRENCH | 
| 6 | COMMON | 18.8495562 | GERMAN | 
+-------------+----------------+---------------+---------------+ 
6 rows in set (0.04 sec)

仔细看一下select子句,可以看到如何在第二列、第三列和第四列之后添加了列别名language_usage、lang_pi_value和language_name。你会觉得使用列别名能够让输出变得更容易理解,并且如果你是从Java或Python内部发出查询(而不是通过mysql工具交互式地发出查询),那么这样做更容易编程实现。为了更清楚地指定列别名,可以选择在别名之前使用as关键字,如:

mysql> SELECT language_id,
 -> 'COMMON' AS language_usage,
 -> language_id * 3.1415927 AS lang_pi_value,
 -> upper(name) AS language_name
 -> FROM language;

很多人觉得加上as关键字(可选的)可以提高可读性,不过在本书的示例中,我并不打算使用它。


3.2去除冗余

在某些情况下,查询可能会返回重复的数据行。例如,如果要检索电影中出现的所有演员的ID,结果如下:

mysql> SELECT actor_id FROM film_actor ORDER BY actor_id;
+----------+
| actor_id |
+----------+
| 1 |
| 1 |
| 1 |
| 1 |
| 1 |
| 1 |
| 1 |
| 1 |
| 1 |
| 1 |
...
| 200 |
| 200 |
| 200 |
| 200 |
| 200 |
| 200 |
| 200 |
| 200 |
| 200 |
+----------+
5462 rows in set (0.01 sec)

因为有些演员参演了多部电影,所以你会多次看到同一个演员的ID。在这种情况下,你可能想看到不同的演员ID,而不是重复出现的演员ID,所以可以通过将关键字distinct直接添加到select关键字之后来实现,如下所示:

mysql> SELECT DISTINCT actor_id FROM film_actor ORDER BY actor_id;
+----------+
| actor_id |
+----------+
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
| 6 |
| 7 |
| 8 |
| 9 |
| 10 |
...
| 192 |
| 193 |
| 194 |
| 195 |
| 196 |
| 197 |
| 198 |
| 199 |
| 200 |
+----------+
200 rows in set (0.01 sec)

结果集现在包含200行,每个演员一行,而不是5462行。

注意

如果你只是想要一个所有演员的列表,那么可以直接查询actor表,而不是读取film_actor中的所有行并删除重复项。

如果不希望服务器删除重复的数据,或者你能确定结果集中不会有冗余数据,则可以指定all关键字,而不是distinct。但是,all关键字实际上是系统默认的,不需要显式命名,因此大多数程序员不会在查询包含all关键字。

注意

请记住,生成一组无重复结果需要对数据进行排序,这对于大型结果集来说可能非常耗时,所以不要只是为了确保没有重复数据而使用distinct。你要做的是好好了解你所使用的数据(是否可能包含重复行),这样就能知道结果集是否可能出现冗余问题,以减少distinct的非必要使用。



4.from子句

到目前为止,你看到过包含单个表的from子句查询。尽管大多数SQL书籍都将from子句定义为一个或多个表的清单,但本书对它的定义作拓展如下:

from子句定义了查询所使用的表,以及连接这些表的方式。

这个定义由两个独立却相关的概念组成,我们将在下面几节中探讨这两个概念。

4.1表

当遇到术语"表"的时候,大多数人会想到存储在数据库中的一组相关行。虽然这确实描述了一种类型的表,但我想用一种更广宽泛的方式来使用该术语,即去掉关于数据如何存储的概念,只关注一组相关的行。在这种宽泛的定义下,有下面四种类型的表:

• 永久表(使用create table语句创建的表)

• 派生表(子查询返回并保存在内存中的数据表)

• 临时表(存储在内存中的易丢失性数据)

• 虚拟表(使用create view语句创建的视图)

这几种类型的表都可以包含在查询的from子句中。现在你应该已经熟悉了在from子句中包含一个永久表,因此下面我将简要介绍如何在from子句中使用另外几种类型的表。

4.1.1派生(子查询生成)表

子查询是包含在另一个查询中的查询。子查询用圆括号括起来,可以在select语句任何部分出现。但是,在from子句中,子查询的作用是生成一个派生表,其中的from子句可以与其他表进行交互。下面是一个简单的例子:

mysql> SELECT concat(cust.last_name, ', ', cust.first_name) full_name 
 -> FROM 
 -> (SELECT first_name, last_name, email 
 -> FROM customer 
 -> WHERE first_name = 'JESSIE' 
 -> ) cust; 
+---------------+ 
| full_name | 
+---------------+ 
| BANKS, JESSIE | 
| MILAM, JESSIE | 
+---------------+ 
2 rows in set (0.00 sec)

在本例中,针对customer表的子查询返回三列,外围查询获得其中的两列。外围查询中,通过别名引用子查询,在本例中,别名是cust。cust中的数据在查询期间保存在内存中,然后被丢弃。这是一个特别简单甚至没什么用的例子,不过别担心,我会在第九章中详细介绍子查询的相关内容。

4.1.2临时表

每个关系数据库都允许定义临时表(尽管其实现并不相同),这些表看起来和永久表差不多,但是插入到临时表中的任何数据最后都不会被保存下来(通常是事务结束时或数据库会话关闭时被丢弃)。下面是一个简单的示例,演示如何临时存储姓氏以J开头的演员:

mysql> CREATE TEMPORARY TABLE actors_j 
 -> (actor_id smallint(5), 
 -> first_name varchar(45), 
 -> last_name varchar(45) 
 -> ); 
Query OK, 0 rows affected (0.00 sec) 
mysql> INSERT INTO actors_j 
 -> SELECT actor_id, first_name, last_name 
 -> FROM actor 
 -> WHERE last_name LIKE 'J%'; 
Query OK, 7 rows affected (0.03 sec) 
Records: 7 Duplicates: 0 Warnings: 0 
mysql> SELECT * FROM actors_j; 
+----------+------------+-----------+ 
| actor_id | first_name | last_name | 
+----------+------------+-----------+ 
| 119 | WARREN | JACKMAN | 
| 131 | JANE | JACKMAN | 
| 8 | MATTHEW | JOHANSSON | 
| 64 | RAY | JOHANSSON | 
| 146 | ALBERT | JOHANSSON | 
| 82 | WOODY | JOLIE | 
| 43 | KIRK | JOVOVICH | 
+----------+------------+-----------+ 
7 rows in set (0.00 sec)

这七行临时保存在内存中,并将在会话关闭后消失。

注意

大多数数据库服务器在会话结束时也会删除临时表,不过Oracle数据库是个例外,它将临时表的定义保留,供未来的会话使用。


4.1.3视图

视图是存储在数据字典中的查询,它看起来和表现得都像一个表,但实际上并没有与视图相关联的数据(所以我才叫它虚拟表)。对视图发出查询时,查询将与视图定义绑定,以创建最终要执行的查询。

下面是一个查询employee表的视图定义,其中包括四个可用列:

mysql> CREATE VIEW cust_vw AS 
 -> SELECT customer_id, first_name, last_name, active 
 -> FROM customer; 
Query OK, 0 rows affected (0.12 sec)

创建视图时,并不会产生或存任何数据:服务器只是将select语句折叠起来以备将来使用。现在视图既然已经存在,那么可以对其发出查询了,如下所示:

mysql> SELECT first_name, last_name 
 -> FROM cust_vw 
 -> WHERE active = 0; 
+------------+-----------+ 
| first_name | last_name | 
+------------+-----------+ 
| SANDRA | MARTIN | 
| JUDITH | COX | 
| SHEILA | WELLS | 
| ERICA | MATTHEWS | 
| HEIDI | LARSON | 
| PENNY | NEAL | 
| KENNETH | GOODEN | 
| HARRY | ARCE | 
| NATHAN | RUNYON | 
| THEODORE | CULP | 
| MAURICE | CRAWLEY | 
| BEN | EASTER | 
| CHRISTIAN | JUNG | 
| JIMMIE | EGGLESTON | 
| TERRANCE | ROUSH | 
+------------+-----------+ 
15 rows in set (0.00 sec)

可能出于多个理由创建视图,比如对用户隐藏列、简化复杂的数据库设计等。


4.2表连接

与简单from子句定义的第二个不同之处在于:如果from子句中出现多个表,则还必须包括用于连接这些表的条件。这在MySQL或其他任何数据库服务器并不是强制要求的,但它是ANSI认可的连接多个表的方法,并且在各种数据库服务器中是最有可移植性的方法。在第五章和第十章中,我们将深入探讨如何连接多个表,这里为了满足你的好奇心,我就简单介绍一下:

mysql> SELECT customer.first_name, customer.last_name, 
 -> time(rental.rental_date) rental_time 
 -> FROM customer 
 -> INNER JOIN rental 
 -> ON customer.customer_id = rental.customer_id 
 -> WHERE date(rental.rental_date) = '2005-06-14'; 
+------------+-----------+-------------+ 
| first_name | last_name | rental_time | 
+------------+-----------+-------------+ 
| JEFFERY | PINSON | 22:53:33 | 
| ELMER | NOE | 22:55:13 | 
| MINNIE | ROMERO | 23:00:34 | 
| MIRIAM | MCKINNEY | 23:07:08 | 
| DANIEL | CABRAL | 23:09:38 | 
| TERRANCE | ROUSH | 23:12:46 | 
| JOYCE | EDWARDS | 23:16:26 | 
| GWENDOLYN | MAY | 23:16:27 | 
| CATHERINE | CAMPBELL | 23:17:03 | 
| MATTHEW | MAHAN | 23:25:58 | 
| HERMAN | DEVORE | 23:35:09 | 
| AMBER | DIXON | 23:42:56 | 
| TERRENCE | GUNDERSON | 23:47:35 | 
| SONIA | GREGORY | 23:50:11 | 
| CHARLES | KOWALSKI | 23:54:34 | 
| JEANETTE | GREENE | 23:54:46 | 
+------------+-----------+-------------+ 
16 rows in set (0.01 sec)

这个查询同时显示customer表(first_name,last_name)和rental表(rental_date)中的数据,因此这两个表都包含在from子句中。连接这两个表(称为join)的机制是存储在customer和rental表中的customer ID。因此,数据库服务器使用customer表中customer_id列的值来查找rental表中客户的所有租金。两个表的连接条件由from子句的on子句指定。在本例中,连接条件是ON customer.customer_id = rental.customer_id。where子句不是连接的一部分,它只用于限定结果集使得数量保持得相当小,因为rental表中有16000多行数据。关于连接多个表的详细讨论,请参阅第五章。

4.3定义表别名

当在单个查询中连接多个表时,要在select、where、group by、having和order by子句中引用列的时候指明所引用的表。在from子句之外引用表时,有两种方式:

• 使用完整的表名称,例如employee.emp_id。

• 为每个表指定一个别名,并在整个查询过程中使用该别名。

在前面的查询中,我在select和on子句中使用了完整的表名。下面是使用定义表别名的方式时,实现相同查询的语句:

SELECT c.first_name, c.last_name, 
 time(r.rental_date) rental_time 
FROM customer c 
 INNER JOIN rental r 
 ON c.customer_id = r.customer_id 
WHERE date(r.rental_date) = '2005-06-14';

如果仔细观察from子句,可以发现customer表被分配了别名c,rental表被分配了别名r,然后,在定义连接条件时,在on子句中被用到,并且在select子句中为结果集指定要包含的列的时候也用到了它们。我觉得使用别名不仅能让语句更加紧凑,还能避免混淆(只要选择了合理的别名)。此外,还可以在表别名中使用as关键字,就跟之前显式指定列别名一样,显式指定表别名:

SELECT c.first_name, c.last_name, 
 time(r.rental_date) rental_time 
FROM customer AS c 
 INNER JOIN rental AS r 
 ON c.customer_id = r.customer_id 
WHERE date(r.rental_date) = '2005-06-14';

我发现与我共事过的数据库开发人员中,大约有一半的人在列和表别名中使用as关键字,而另外一半的人不这样做。

5.where子句

在某些情况下,可能需要从表中检索所有行,特别是对于诸如language之类的小型表。但是在大多数情况下,并没有检索表中所有数据的必要,而是希望使用某种方法过滤不感兴趣的行,这就是where子句起作用的地方。

where子句用于从结果集中过滤掉不需要的行。

例如,也许你想租一部电影,但是你只对至少能租一周的G级电影感兴趣,那么可以使用以下查询:

mysql> SELECT title 
 -> FROM film 
 -> WHERE rating = 'G' AND rental_duration >= 7; 
+-------------------------+ 
| title | 
+-------------------------+ 
| BLANKET BEVERLY | 
| BORROWERS BEDAZZLED | 
| BRIDE INTRIGUE | 
| CATCH AMISTAD | 
| CITIZEN SHREK | 
| COLDBLOODED DARLING | 
| CONTROL ANTHEM | 
| CRUELTY UNFORGIVEN | 
| DARN FORRESTER | 
| DESPERATE TRAINSPOTTING | 
| DIARY PANIC | 
| DRACULA CRYSTAL | 
| EMPIRE MALKOVICH | 
| FIREHOUSE VIETNAM | 
| GILBERT PELICAN | 
| GRADUATE LORD | 
| GREASE YOUTH | 
| GUN BONNIE | 
| HOOK CHARIOTS | 
| MARRIED GO | 
| MENAGERIE RUSHMORE | 
| MUSCLE BRIGHT | 
| OPERATION OPERATION | 
| PRIMARY GLASS | 
| REBEL AIRPORT | 
| SPIKING ELEMENT | 
| TRUMAN CRAZY | 
| WAKE JAWS | 
| WAR NOTTING | 
+-------------------------+ 
29 rows in set (0.00 sec)

本例中,where子句过滤了film表中1000行中的971行,产生了29行数据输出。where子句包含了两个过滤条件,但你可以根据需要包含任意多个条件,它们之间使用and、or和not等操作符分隔(有关where子句和过滤条件的完整讨论,请参阅第四章)。

让我们看看如果将两个条件之间的and操作符改成or会怎么样:

mysql> SELECT title 
 -> FROM film 
 -> WHERE rating = 'G' OR rental_duration >= 7; 
+---------------------------+ 
| title | 
+---------------------------+ 
| ACE GOLDFINGER | 
| ADAPTATION HOLES | 
| AFFAIR PREJUDICE | 
| AFRICAN EGG | 
| ALAMO VIDEOTAPE | 
| AMISTAD MIDSUMMER | 
| ANGELS LIFE | 
| ANNIE IDENTITY | 
|... | 
| WATERSHIP FRONTIER | 
| WEREWOLF LOLA | 
| WEST LION | 
| WESTWARD SEABISCUIT | 
| WOLVES DESIRE | 
| WON DARES | 
| WORKER TARZAN | 
| YOUNG LANGUAGE | 
+---------------------------+ 
340 rows in set (0.00 sec)

使用and操作符分隔过滤条件时,所有过滤条件的计算结果都必须为true才能包含在结果集中;但是使用or时,只要满足一个条件为true,该行就可以被包括到结果集中。因此上面结果集的大小从29行跃升到了340行。

那么,如果需要在where子句中同时使用and和or操作符又要怎么办?这种情况下就应该使用圆括号对条件进行分组。下一个查询指定在结果集中包括”只有级别为G且可租借7天或更长时间“或”级别为PG-13且租借时长不超过三天”的影片:

mysql> SELECT title, rating, rental_duration 
 -> FROM film 
 -> WHERE (rating = 'G' AND rental_duration >= 7) 
 -> OR (rating = 'PG-13' AND rental_duration < 4); 
+-------------------------+--------+-----------------+ 
| title | rating | rental_duration | 
+-------------------------+--------+-----------------+ 
| ALABAMA DEVIL | PG-13 | 3 | 
| BACKLASH UNDEFEATED | PG-13 | 3 | 
| BILKO ANONYMOUS | PG-13 | 3 | 
| BLANKET BEVERLY | G | 7 | 
| BORROWERS BEDAZZLED | G | 7 | 
| BRIDE INTRIGUE | G | 7 | 
| CASPER DRAGONFLY | PG-13 | 3 | 
| CATCH AMISTAD | G | 7 | 
| CITIZEN SHREK | G | 7 | 
| COLDBLOODED DARLING | G | 7 | 
|... | 
| TREASURE COMMAND | PG-13 | 3 | 
| TRUMAN CRAZY | G | 7 | 
| WAIT CIDER | PG-13 | 3 | 
| WAKE JAWS | G | 7 | 
| WAR NOTTING | G | 7 | 
| WORLD LEATHERNECKS | PG-13 | 3 | 
+-------------------------+--------+-----------------+ 
68 rows in set (0.00 sec)

在混合使用不同的运算符时,应该始终使用圆括号来将条件分隔成组,以便开发者、数据库服务器以及以后可能对代码进行修改的其他人都能对代码保持一致的理解。

6.group by和having子句

到目前为止,所有的查询都仅仅是对原始数据的检索操作,没有执行任何的加工。但是有时候你可能需要数据库服务器在返回结果集之前对数据进行一些修改和提炼,其中一种方式就是使用group by子句,它根据列值对数据进行分组。例如,假设你想找到所有租了40部及以上电影的顾客,那么你可以编写一个查询,指示服务器按顾客对所有租赁进行分组,计算每个顾客的租赁数量,然后只返回那些租赁数量至少为40的顾客,而不是查看rental表中所有16044行数据。在使用group by子句生成行组时,还可能需要用到having子句,该子句允许你以与where子句相同的方式过滤分组数据。

查询如下:

mysql> SELECT c.first_name, c.last_name, count(*) 
 -> FROM customer c 
 -> INNER JOIN rental r 
 -> ON c.customer_id = r.customer_id 
 -> GROUP BY c.first_name, c.last_name 
 -> HAVING count(*) >= 40; 
+------------+-----------+----------+ 
| first_name | last_name | count(*) | 
+------------+-----------+----------+ 
| TAMMY | SANDERS | 41 | 
| CLARA | SHAW | 42 | 
| ELEANOR | HUNT | 46 | 
| SUE | PETERS | 40 | 
| MARCIA | DEAN | 42 | 
| WESLEY | BULL | 40 | 
| KARL | SEAL | 45 | 
+------------+-----------+----------+ 
7 rows in set (0.03 sec)

这里我只是简单介绍一下这两个子句,以便你在阅读本书后面的内容时不会感到太陌生,实际上它们比另外四个子句更高级一些。本书第八章会完整描述如何使用以及何时使用group by和having子句。

7.order by子句

一般情况下,查询返回结果集中的行没有特定的顺序。如果希望对结果集进行排序,则需要指示服务器使用order by子句对结果进行排序,该子句定义如下:

order by子句用于对结果集中原始列数据或根据列数据计算的表达式结果进行排序。

比如下面的查询,它返回2005年6月14日租赁过电影的所有客户:

mysql> SELECT c.first_name, c.last_name, 
 -> time(r.rental_date) rental_time 
 -> FROM customer c 
 -> INNER JOIN rental r 
 -> ON c.customer_id = r.customer_id 
 -> WHERE date(r.rental_date) = '2005-06-14'; 
+------------+-----------+-------------+ 
| first_name | last_name | rental_time | 
+------------+-----------+-------------+ 
| JEFFERY | PINSON | 22:53:33 | 
| ELMER | NOE | 22:55:13 | 
| MINNIE | ROMERO | 23:00:34 | 
| MIRIAM | MCKINNEY | 23:07:08 | 
| DANIEL | CABRAL | 23:09:38 | 
| TERRANCE | ROUSH | 23:12:46 | 
| JOYCE | EDWARDS | 23:16:26 | 
| GWENDOLYN | MAY | 23:16:27 | 
| CATHERINE | CAMPBELL | 23:17:03 | 
| MATTHEW | MAHAN | 23:25:58 | 
| HERMAN | DEVORE | 23:35:09 | 
| AMBER | DIXON | 23:42:56 | 
| TERRENCE | GUNDERSON | 23:47:35 | 
| SONIA | GREGORY | 23:50:11 | 
| CHARLES | KOWALSKI | 23:54:34 | 
| JEANETTE | GREENE | 23:54:46 | 
+------------+-----------+-------------+ 
16 rows in set (0.01 sec)

如果希望结果按姓氏的字母顺序排列,可以将last_name列添加到order by子句中:

mysql> SELECT c.first_name, c.last_name, 
 -> time(r.rental_date) rental_time 
 -> FROM customer c 
 -> INNER JOIN rental r 
 -> ON c.customer_id = r.customer_id 
 -> WHERE date(r.rental_date) = '2005-06-14' 
 -> ORDER BY c.last_name; 
+------------+-----------+-------------+ 
| first_name | last_name | rental_time | 
+------------+-----------+-------------+ 
| DANIEL | CABRAL | 23:09:38 | 
| CATHERINE | CAMPBELL | 23:17:03 | 
| HERMAN | DEVORE | 23:35:09 | 
| AMBER | DIXON | 23:42:56 | 
| JOYCE | EDWARDS | 23:16:26 | 
| JEANETTE | GREENE | 23:54:46 | 
| SONIA | GREGORY | 23:50:11 | 
| TERRENCE | GUNDERSON | 23:47:35 | 
| CHARLES | KOWALSKI | 23:54:34 | 
| MATTHEW | MAHAN | 23:25:58 | 
| GWENDOLYN | MAY | 23:16:27 | 
| MIRIAM | MCKINNEY | 23:07:08 | 
| ELMER | NOE | 22:55:13 | 
| JEFFERY | PINSON | 22:53:33 | 
| MINNIE | ROMERO | 23:00:34 | 
| TERRANCE | ROUSH | 23:12:46 | 
+------------+-----------+-------------+ 
16 rows in set (0.01 sec)

在大型客户列表中,通常会出现多个姓氏相同的人,所以你可能需要扩展排序条件,以包含这些人的名字。

可以通过在order by子句的last_name列之后添加first_name列来实现:

mysql> SELECT c.first_name, c.last_name, 
 -> time(r.rental_date) rental_time 
 -> FROM customer c 
 -> INNER JOIN rental r 
 -> ON c.customer_id = r.customer_id 
 -> WHERE date(r.rental_date) = '2005-06-14' 
 -> ORDER BY c.last_name, c.first_name; 
+------------+-----------+-------------+ 
| first_name | last_name | rental_time | 
+------------+-----------+-------------+ 
| DANIEL | CABRAL | 23:09:38 | 
| CATHERINE | CAMPBELL | 23:17:03 | 
| HERMAN | DEVORE | 23:35:09 | 
| AMBER | DIXON | 23:42:56 | 
| JOYCE | EDWARDS | 23:16:26 | 
| JEANETTE | GREENE | 23:54:46 | 
| SONIA | GREGORY | 23:50:11 | 
| TERRENCE | GUNDERSON | 23:47:35 | 
| CHARLES | KOWALSKI | 23:54:34 | 
| MATTHEW | MAHAN | 23:25:58 | 
| GWENDOLYN | MAY | 23:16:27 | 
| MIRIAM | MCKINNEY | 23:07:08 | 
| ELMER | NOE | 22:55:13 | 
| JEFFERY | PINSON | 22:53:33 | 
| MINNIE | ROMERO | 23:00:34 | 
| TERRANCE | ROUSH | 23:12:46 | 
+------------+-----------+-------------+ 
16 rows in set (0.01 sec)

当包含多个列时,在order by子句中各列的出现顺序决定对各列进行排序的次序。上面的例子中,如果在order by子句中切换了两列的顺序,则Amber Dixon将出现在结果集的第一行。

7.1升序或降序排序

排序时,可以通过asc和desc关键字指定升序或降序。默认情况下为升序,因此如果要使用降序排序,则需要添加desc关键字。例如,以下查询按租赁时间降序显示2005年6月14日租赁电影的所有顾客:

mysql> SELECT c.first_name, c.last_name, 
 -> time(r.rental_date) rental_time 
 -> FROM customer c 
 -> INNER JOIN rental r 
 -> ON c.customer_id = r.customer_id 
 -> WHERE date(r.rental_date) = '2005-06-14' 
 -> ORDER BY time(r.rental_date) desc; 
+------------+-----------+-------------+ 
| first_name | last_name | rental_time | 
+------------+-----------+-------------+ 
| JEANETTE | GREENE | 23:54:46 | 
| CHARLES | KOWALSKI | 23:54:34 | 
| SONIA | GREGORY | 23:50:11 | 
| TERRENCE | GUNDERSON | 23:47:35 | 
| AMBER | DIXON | 23:42:56 | 
| HERMAN | DEVORE | 23:35:09 | 
| MATTHEW | MAHAN | 23:25:58 | 
| CATHERINE | CAMPBELL | 23:17:03 | 
| GWENDOLYN | MAY | 23:16:27 | 
| JOYCE | EDWARDS | 23:16:26 | 
| TERRANCE | ROUSH | 23:12:46 | 
| DANIEL | CABRAL | 23:09:38 | 
| MIRIAM | MCKINNEY | 23:07:08 | 
| MINNIE | ROMERO | 23:00:34 | 
| ELMER | NOE | 22:55:13 | 
| JEFFERY | PINSON | 22:53:33 | 
+------------+-----------+-------------+ 
16 rows in set (0.01 sec)

降序排序通常用于排行查询,例如“show me the top five account balances”。MySQL包含一个limit子句,它允许你对数据进行排序,并只显示其中前X行。

7.2根据表达式排序

如果使用select子句中的列进行排序,可以选择按列在select子句中的位置而不是按名称引用列,这对于根据表达式进行排序(比如前一个示例)很有用。比如,order by子句使用select子句中的第三个元素指定降序排序:

mysql> SELECT c.first_name, c.last_name, 
 -> time(r.rental_date) rental_time 
 -> FROM customer c 
 -> INNER JOIN rental r 
 -> ON c.customer_id = r.customer_id 
 -> WHERE date(r.rental_date) = '2005-06-14' 
 -> ORDER BY 3 desc; 
+------------+-----------+-------------+ 
| first_name | last_name | rental_time | 
+------------+-----------+-------------+ 
| JEANETTE | GREENE | 23:54:46 | 
| CHARLES | KOWALSKI | 23:54:34 | 
| SONIA | GREGORY | 23:50:11 | 
| TERRENCE | GUNDERSON | 23:47:35 | 
| AMBER | DIXON | 23:42:56 | 
| HERMAN | DEVORE | 23:35:09 | 
| MATTHEW | MAHAN | 23:25:58 | 
| CATHERINE | CAMPBELL | 23:17:03 | 
| GWENDOLYN | MAY | 23:16:27 | 
| JOYCE | EDWARDS | 23:16:26 | 
| TERRANCE | ROUSH | 23:12:46 | 
| DANIEL | CABRAL | 23:09:38 | 
| MIRIAM | MCKINNEY | 23:07:08 | 
| MINNIE | ROMERO | 23:00:34 | 
| ELMER | NOE | 22:55:13 | 
| JEFFERY | PINSON | 22:53:33 | 
+------------+-----------+-------------+ 
16 rows in set (0.01 sec)

使用这个功能的时候还请务必小心,因为在select子句中添加列而不更改order by子句中的数字可能会导致意想不到的结果。就我个人而言,我在编写特殊查询时可能会按位置引用列,但在编写代码时,我总是按名称引用列。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

数据与智能

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值