连接表
尽管查询 MySQL 的速度相当快,尤其是当它与我们的 PHP 应用程序在同一服务器上时,我们还是应该尽量减少执行查询的次数,以提高应用程序的性能。到目前为止,我们只查询了一个表中的数据,但这种情况很少见。试想一下,如果你想检索有关借阅图书的信息:该表只包含 ID 和日期,因此如果你查询它,不会得到非常有意义的数据,对吗?一种方法是查询 borrowed_books
表中的数据,然后根据返回的 ID,通过筛选我们感兴趣的 ID 来查询 book
和 customer
表。但是,这种方法至少需要对 MySQL 进行三次查询,还需要在 PHP 中使用大量数组。似乎应该有更好的选择!
在 SQL 中,可以执行连接查询。连接查询是通过一个共同字段连接两个或多个表的查询,因此可以从这些表中检索数据,减少所需的查询量。当然,连接查询的性能不如普通查询好,但如果你定义了正确的键和关系,这种方法比单独查询要好得多。
为了连接表,需要使用共同字段将它们连接起来。外键在这个问题上非常有用,因为你知道两个字段是相同的。让我们来看看如何查询与借书有关的所有重要信息:
mysql> SELECT CONCAT(c.firstname, ' ', c.surname) AS name,
-> b.title,
-> b.author,
-> DATE_FORMAT(bb.start, '%d-%m-%y') AS start,
-> DATE_FORMAT(bb.end, '%d-%m-%y') AS end
-> FROM borrowed_books bb
-> LEFT JOIN customer c ON bb.customer_id = c.id
-> LEFT JOIN book b ON b.id = bb.book_id
-> WHERE bb.start >= "2015-01-01";
+------------+---------+---------------+----------+----------+
| name | title | author | start | end |
+------------+---------+---------------+----------+----------+
| Han Solo | Dracula | Bram Stoker | 10-01-15 | 13-01-15 |
| James Kirk | Dracula | Bram Stoker | 01-02-15 | 10-02-15 |
| James Kirk | 1984 | George Orwell | 12-03-15 | NULL |
+------------+---------+---------------+----------+----------+
3 rows in set (0.00 sec)
最后这个查询引入了几个新概念。特别是在连接查询中,当我们连接不同表的字段时,可能会出现两个表的字段名相同的情况,MySQL 需要我们区分它们。我们区分两个不同表的两个字段的方法是在表名前加上前缀。假设我们要区分客户的 ID 和书籍的 ID,我们应该使用 customer.id
和 book.id
。但是,如果每次都写入表名,我们的查询就会没完没了。
MySQL 具有为表添加别名的功能,只需在表的真名旁边写上 "borrowed_books"(bb)、"customer"(c)或 "book"(b)即可。一旦添加了别名,就可以用它来引用这个表,这样我们就可以写成 bb.customer_id,而不是 borrowed_books.customer_id。另外,即使字段在其他地方没有重复,也最好写出字段的表,因为连接表会让人搞不清楚每个字段来自哪里。
连接表时,需要使用 LEFT JOIN 在 FROM 子句中写入表,然后写入表名、可选别名以及连接两个表的字段。有不同的连接类型,但让我们关注对我们的目的最有用的连接类型。左连接从第一个表(定义左侧的表)中提取每一行,并在右侧表中搜索对应的字段。一旦找到,它就会将这两条记录连接起来,就像它们是一条记录一样。例如,当为每一行 borrowed_books 行连接 borrowed_books 和 customer 时,MySQL 会在 customer 中搜索与当前 customer_id 匹配的 ID,然后将该行的所有信息添加到当前的 borrowed_books 行中,就好像它们是一个大表。由于 customer_id 是外键,我们可以肯定总有一个客户与之匹配。
您可以连接多个表,MySQL 只会从左到右解决它们;也就是说,它会先将前两个表连接为一个表,然后尝试将这个表与第三个表连接,以此类推。事实上,我们在示例中就是这样做的:我们首先将 borrowed_books 与 customer 连接起来,然后将这两个表与 book 连接起来。
正如你所注意到的,字段也有别名。有时,我们所做的不仅仅是获取一个字段;例如,我们用 COUNT(*)
获取了查询匹配的行数。然而,检索该信息时列的标题也是 COUNT(*)
,这并不总是有用的。还有一些时候,我们使用了两个字段名称相冲突的表,这让一切都变得混乱。遇到这种情况时,只需像处理表名一样为字段添加别名即可;AS 是可选项,但有助于理解你正在做什么。
现在让我们来看看日期在查询中的用法。一方面,我们将首次使用 DATE_FORMAT。它接受日期/时间/日期值和格式字符串。在本例中,我们使用了 %d-%m-%y
,意思是日-月-年,但我们也可以使用 %h-%i-%s
来指定小时-分钟-秒或任何其他组合。
还要注意我们是如何在 WHERE 子句中比较日期的。如果给定两个相同类型的日期或时间值,可以像使用数字一样使用比较运算符。在本例中,我们将使用 bb.start >= "2015-01-01"
,这将为我们提供从 2015 年 1 月 1 日起的借阅图书。
关于这个复杂查询,最后需要注意的是 CONCAT
函数的使用。我们要获取的是全名,而不是返回两个字段,一个是姓名,另一个是姓氏。为此,我们将使用该函数连接字段,发送尽可能多的字符串作为函数参数,并返回连接后的字符串。正如您所看到的,您既可以发送字段,也可以发送用单引号括起来的字符串。
好了,如果你完全理解了这个查询,你应该对自己感到满意;这是我们在本章中看到的最复杂的查询。我们希望你能感受到数据库系统的强大,并从现在起,尽量在数据库端而不是 PHP 端处理数据。如果设置了正确的索引,性能会更好。