SQL 教程

作者:Reuven M. Lerner

在我的工作中,我经常发现自己编写 CGI 程序,这些程序需要读取或写入连接到 Web 服务器的文件系统上的信息。有时,这些信息非常简单,只是一些临时性的东西,例如当我尝试调试一个特别困难的程序时积累的日志信息。

有时,正如我们在今年早些时候的一系列专栏中所看到的,我们可以使用文本文件来存储和检索结构化信息,例如多项选择测验的问答题。这些测验以简单的格式存储,每个问题单独占一行。例如,这里有一行可能来自其中一个测验

What color was George Washington's white horse? White   Black   Gray    Pink    a

虽然杂志出版的机制意味着您无法看到各种空白字符之间的区别,但上面的行被分为六个字段:问题文本、呈现给用户的四个答案以及一个字母(a、b、cd),指示四个答案中哪个是正确的。字段之间用制表符(ASCII 码 9)分隔,制表符看起来与空格字符(ASCII 码 32)相同,但在计算机看来却截然不同。

我们今年早些时候探讨的测验程序期望从包含一行或多行此类行的文件中读取数据,每行代表一个问题。包含单个问题的测验(适用于喜欢简单挑战的用户)将有一行,而包含 1000 个问题的测验将包含 1000 行。

这就引出了一个重要的可伸缩性问题,即软件即使在数据集变得非常大时也能保持效率的能力。编写能够高效处理少量数据的程序并不困难,尤其是在硬件价格持续下降而性能不断提高的情况下。然而,编写能够处理大量数据的软件要困难得多。

当处理少量数据时,ASCII 文本文件非常棒,因为它们很容易在程序中进行操作,尤其是在使用 Perl 时,Perl 在处理正则表达式方面非常强大。但是,当我们必须处理大量数据,或者当我们想要执行复杂的搜索时,我们可能会发现自己在重新发明轮子,或者在使用不再满足我们需求的工具(例如 ASCII 文本文件)。

基本 SQL

解决此问题的一个常见方法是将数据存储和检索卸载到一个名为关系数据库服务器的程序。名称中的“服务器”部分表示它期望接收来自一个或多个客户端的请求,名称中的“数据库”部分表示它代表这些客户端存储和检索信息。但是,您可能不熟悉名称中的“关系”部分,这意味着数据存储在表集中,我们可以使用 SQL(结构化查询语言)访问这些表。本月,我们将初步了解 SQL 查询,包括如何将它们集成到我们的 CGI 程序中;在接下来的几个月中,我们将更深入地探讨这个主题,为各种项目使用关系数据库服务器。

SQL 是许多企业数据库都遵守的国际标准。“SQL”中的“L”代表“语言”,但这并不意味着您可以用 SQL 编写程序。相反,SQL 是一种用于向数据库服务器制定查询的语言。SQL 命令必须被并入用真正的编程语言(如 Perl 或 C)编写的程序中。

关系数据库的工作原理与 Web 相同,都是客户端-服务器模型。Web 客户端和服务器使用 HTTP 进行通信,而数据库客户端和服务器使用 SQL 进行通信。毋庸置疑,SQL 比 HTTP 复杂得多,但正如您将看到的,它相当容易学习。SQL 可能很容易学习,但这并不意味着它很简单。相反,长期从事数据库管理和编程的人员对使用 SQL 存储和检索数据的理解比我所能想象的还要多。

理解 SQL 的关键是要认识到 SQL 数据库中的所有内容都存储在一个表中。表中的行代表表记录,而列代表字段。因此,我们可以将地址簿表示为一个表。

Name    Telephone
----    ---------
Reuven  04-824-2265
Andy    02-123-4567
Gil     04-999-8888

此表中有三个记录,每个记录由一行表示。每个记录包含两个字段,每个字段由一列表示。每个表和列都必须有一个名称,因此我们将此表称为“phone_book”表,包含两列:“name”和“telephone”。

到目前为止,这似乎并没有比我们使用文本文件所做的工作有很大的进步。当我们可以使用 ASCII 文件时,为什么还要费心使用行、列和表呢?

简单的答案是,我们可以让数据库服务器为我们完成工作——它会根据我们的指令非常快速地返回答案,而不会因数据库中的记录数量而变得迟钝。如果我们有兴趣使用上述表的文本文件版本查找 Andy 的电话号码,我们需要遍历整个文件,检查每个记录是否匹配。使用关系数据库,我们可以向数据库服务器发出 SQL 查询,仅请求那些符合我们特定条件的行。

因此,如果我们有兴趣从上表中检索 Andy 的电话号码,我们可以使用 SQL select 命令来执行此操作

select telephone from phone_book where name =
        "Andy";

上面的 SQL 语句要求数据库服务器从名为 phone_book 的表中返回 telephone 列,针对名称为 Andy 的每一行。如果只有一行与查询匹配,我们会收到数据库服务器返回的单行作为响应,但如果多行匹配,我们会收到所有这些行。如果没有行与我们的查询匹配,我们将不会收到任何行,这似乎很奇怪,直到您意识到数据库客户端程序经常迭代它们返回的结果。迭代零个值与迭代 100 个值一样容易,尽管大多数优秀的客户端程序都会检查以确保至少返回了一行。

我们可以使用以下命令将行插入到我们的表中

insert into phone_book (name,telephone) values
        ("Iris","04-999-8888");

执行上述操作后,我们的表看起来像

姓名 电话---- ---------Reuven 04-824-2265Andy 02-123-4567Gil 04-999-8888Iris 04-999-8888

我们可以通过检索所有内容来看到这一点,使用星号表示“所有列”

select * from phone_book;

如果我们想检索电话号码为 04-999-8888 的所有人的行,我们使用这一行

select name from phone_book where telephone =
        "04-999-8888";
请注意,我们不需要担心两条相同的记录,因为关系数据库严格要求每个记录在某些方面都是唯一的。两行可能仅在一个列中有所不同,但该列足以使这两行有所区别。

因此,使用 SQL 和关系数据库服务器的一个优点是提高了效率,包括我们的程序(不再需要读取文本文件的全部内容)和我们自己(因为我们不再需要编写匹配引擎和定义数据格式)。使用 SQL 和关系数据库还有其他优点;最重要的是,数据库服务器以复杂而高效的方式处理文件锁定,确保数据不会丢失,同时保持操作快速进行。

关系数据库还提供了令人惊叹的优化技术和安全级别,以及其他功能。最重要的是,SQL 是一个可移植的标准,它(在很大程度上)在许多数据库系统上的工作方式相同;也就是说,一旦您学会如何编写一些基本的 SQL 查询,您就可以将数据存储在几乎任何可用的平台上。

我的大部分 SQL 经验都来自 Solaris 系统上的 Sybase,但为了本文的目的,我决定是时候在我的 Linux 机器(运行 Red Hat 4.0,其中包含许多更新的软件包,包括 2.0.30 内核)上安装关系数据库服务器了。我决定下载 MySQL,这是一个看起来小巧但功能强大的数据库服务器,它采用 RPM(Red Hat 包管理器)格式,使我能够快速安装它。(不要将 MySQL 与 mSQL 混淆,mSQL 是另一个可用于 Linux 的关系数据库软件包。有关如何获取 MySQL 的信息,请参阅本文随附的边栏。)

从 Perl 使用 SQL

MySQL 附带一个名为 mysql 的客户端程序,这很奇怪,它允许我们直接向数据库服务器输入 SQL 查询,数据库服务器在任何时候都可能正在运行。我们使用以下命令进入数据库

[1016] ~% mysql test
Welcome to the mysql monitor. Commands end with ; or \g.
Type 'help' for help.
mysql>

正如文件系统在目录内的子目录中存储文件一样,关系数据库在整体结构中的数据库内存储表。因此,当我们进入 MySQL 时,我们需要指定我们要使用的数据库的名称。在上面的示例中,我们指定了 test 数据库,所有用户都可以访问该数据库,而无需经过输入用户名和密码的标准程序。虽然关系数据库的用户名和密码可以与用户在系统上的帐户相同,但它们不需要相同。实际上,为了系统安全,您应该使它们与您的常规系统密码不同。

一般来说,为 CGI 程序专门创建一个或多个数据库也是一个好主意,以避免让程序完全访问系统上的所有数据库。CGI 编程的性质是,用户可能能够从程序的源代码中读取用户名和密码,从而使他们能够访问给定数据库中的任何表。但是,为了节省时间和空间,我建议您阅读 MySQL 文档,其中描述了如何设置系统上各种数据库的用户权限。同时,我们将使用 test 数据库,所有用户都可以访问该数据库,用于我们的示例

要创建我们的电话目录表,我们输入

mysql> create table phone_book (name char(255),
        telephone char(255));

空白在 SQL 查询中并不重要。在上面的示例中,我在第一行末尾和第二行的 go 语句之间按了回车键。正如您可能期望的那样,go 命令告诉数据库客户端将查询发送到数据库服务器,在那里对其进行评估和执行。或者,我们可以在查询末尾使用分号,这将消除对 go 的需求。

服务器通过向我们提供一些统计信息来响应我们的查询

Query OK, 0 rows affected (0.27 sec)

换句话说,创建表花费了 0.27 秒,并且没有影响任何现有行。

您可以通过在 mysql> 提示符下键入 quit 来退出 mysql

Perl 的 MySQL 编程接口的工作方式与命令行程序非常相似,只是它使用 Perl 5 对象。基本思路很简单;我们创建一个 MySQL 对象的实例,然后使用该对象来完成登录、发送查询和解释结果的过程。

列表 1 包含一个功能程序,该程序可以查询我们的 phone_book 表并返回结果。但更重要的是,该程序是我们使用 MySQL 编写的每个程序的骨架。虽然 Sybase 和其他数据库的语法可能略有不同,但总体思路是相同的——连接到数据库服务器,选择一个数据库,发送 SQL 查询并迭代返回的任何结果。

首先,我们使用 Unix 套接字连接到数据库服务器,部分原因是 MySQL 默认启用这些套接字,这使得在像这样的短篇专栏中更容易解释。当然,您也可以连接到网络上其他地方运行的数据库服务器,就像 Web 浏览器可以连接到网络上的 Web 服务器一样。

连接到 MySQL 服务器后,我们使用 query 方法输入我们的 SQL 查询。正如连接到数据库返回数据库句柄 $dbh 一样,发送 SQL 查询返回语句句柄 $sth。正如我们需要使用 $dbh 才能发送语句一样,我们需要使用 $sth 才能检索结果。在这个特定的语句中,我们要求查看表的两个列以及表中的所有行。但是,我们可以使用 where 子句(如前所述)来限制我们的查询,这将返回表的行的子集。我们也可以请求表的列的子集,这样只会返回名称或电话号码。

结果通过迭代服务器返回的行来检索。如果没有行与我们的查询匹配,则不执行迭代;如果有 100 行与我们的查询匹配,则执行 100 次。如果我们有兴趣最大限度地提高处理 SQL 查询的程序的效率,那么构造仅返回我们最感兴趣的行的查询符合我们的利益,因为迭代大量行可能非常低效且耗时。

如果我从命令行运行列表 1 中的程序(在我的系统上命名为 sql-test.pl),我会得到

[1031] ~/Text/LJ% ./sql-test.pl
Iris    04-999-8888
Reuven  04-824-2265
Andy    02-123-4567
Gil     04-999-8888

当然,我们可以使用上面的骨架程序来插入行、创建表并执行更复杂的操作,例如将表连接在一起(这在很大程度上是 SQL 背后的魔力)以及按升序或降序排列结果。如果我们想将区号与电话号码本身放在不同的列中,我们可以进一步改进我们的搜索,例如,询问给定区号内名字为 Iris 的所有人。

从 CGI 程序中使用 MySQL

既然我们已经了解了从 Perl 中使用 MySQL 的一些基本用法,那么让我们花一些时间思考如何将 MySQL 的使用集成到 CGI 程序中。虽然对于一些小型作业来说,这似乎有点大材小用,但数据库服务器在这种任务中比我们的 CGI 程序可靠和高效得多,因此几乎总是值得使用这样的服务器,假设有一个可用的服务器。

通过使用数据库服务器,我们可以确保我们的数据存储比文本文件更可靠。作为额外的奖励,信息可以使用 SQL 获取,SQL 比文本文件更高效和灵活。

我们如何从我们的 CGI 程序中使用数据库服务器?简单的答案是,它实际上与从非 CGI 程序连接到数据库服务器没有什么不同。我们仍然创建 Mysql 对象,使用其方法发送 SQL 查询并检索结果。不同之处在于我们能够根据 HTML 表单中发送给我们的输入来修改我们的查询,以及必须使用公认的内容类型(通常是 HTML)将我们的输出发送到用户的浏览器。这样一个程序,我称之为 cgi-sql-test.pl,如列表 2所示。

虽然 cgi-sql-test.pl 比它所基于的程序更长,但它并没有复杂多少。

首先,我们启动 Perl 的 CGI 模块,您可以从综合 Perl 存档网络 (CPAN) 获取该模块,网址为 https://perldotcom.perl5.cn/CPAN。在创建 CGI 实例后,我们向用户的浏览器发送一个 HTTP Content-type 标头,指示我们将返回 text/html 类型的结果,即 HTML 格式的文本。

在初始化 CGI 环境之后,我们继续执行我们在程序的非 CGI 版本中所做的事情,即连接到数据库,发送我们的查询并检索结果。

这是最大的不同之处。我们不是将结果打印到标准输出,而是以 HTML 格式将它们发送到用户的浏览器,以便我们可以使用各种漂亮的 HTML 格式化技术来显示结果。

在这个特定的示例中,我决定将电话列表的结果放在一个 HTML 表格中,这很有吸引力,并且易于理解结果。<tr> 标签引入表格行,而 <td> 标签引入行内的列。由于通过 while 循环的每次迭代都代表数据库中的一个新记录,我们可以在每个循环的顶部启动一个新的 HTML 行,并在每个循环的底部结束它。

我们将在接下来的几期中继续探讨 SQL 和 CGI 之间的交互,但在我结束本月专栏之前,我想至少展示一个示例,说明我们如何根据用户的输入修改 SQL 查询。为了简单起见,我们修改我们的程序,使其仅要求数据库服务器返回 name 列与我们在查询字符串中输入的内容匹配的那些行。因此,如果我们有兴趣查找 Gil 的电话号码,我们可以转到

/cgi-bin/cgi-sql-test.pl?Gil

如果我们有兴趣查找 Andy 的电话号码,我们可以转到

/cgi-bin/cgi-sql-test.pl?Andy
这将仅生成该列表。

但是,如果有人在没有在查询字符串中输入名称的情况下调用我们的程序会发生什么?嗯,我们的程序巧妙地注意到了这一点,并生成了一个非常小的 HTML 页面作为响应。这个小的 HTML 页面要求用户输入要搜索的名称,然后使用 <isindex> 标签在 HTML 页面中创建一个文本字段。

<isindex> 标签通常已不再受欢迎,因为 HTML 表单更灵活和有用。当用户在 <isindex> 字段中输入信息并按回车键时,<isindex> 标签出现的 URL 将重新加载——用户的输入作为查询字符串的一部分附加在后面。

因此,如果我们的程序在查询字符串中没有收到任何输入,它将生成一个包含 <isindex> 的页面。用户在该文本字段中输入的任何内容都会导致我们的程序重新加载,这次查询字符串中有一个值。该值被我们的程序拾取并传递给 MySQL,MySQL 在 HTML 表格中返回结果。

关于 SQL 和 CGI 程序集成的基本介绍到此结束。正如您可能想象的那样,SQL 数据库比我们本月看到的程序和数据库强大得多。在接下来的几个月中,我们将花更多时间研究我们可以使用 MySQL(以及一般的关系数据库服务器)的不同方式,以使网站更具趣味性、效率和实用性。

Reuven M. Lerner 是以色列海法的一位互联网和 Web 顾问,自 1993 年初以来一直使用 Web。在业余时间,他做饭、阅读并为社区的教育项目做志愿者。您可以通过 reuven@netvision.net.il 与他联系。

加载 Disqus 评论