﻿<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:trackback="http://madskills.com/public/xml/rss/module/trackback/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"><channel><title>C++博客-cpp-primer-随笔分类-数据库技术</title><link>http://www.cppblog.com/cpp-primer/category/8888.html</link><description /><language>zh-cn</language><lastBuildDate>Fri, 21 Nov 2008 23:12:28 GMT</lastBuildDate><pubDate>Fri, 21 Nov 2008 23:12:28 GMT</pubDate><ttl>60</ttl><item><title>Select 语句的用法</title><link>http://www.cppblog.com/cpp-primer/archive/2008/11/21/67502.html</link><dc:creator>Benson</dc:creator><author>Benson</author><pubDate>Fri, 21 Nov 2008 09:00:00 GMT</pubDate><guid>http://www.cppblog.com/cpp-primer/archive/2008/11/21/67502.html</guid><wfw:comment>http://www.cppblog.com/cpp-primer/comments/67502.html</wfw:comment><comments>http://www.cppblog.com/cpp-primer/archive/2008/11/21/67502.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/cpp-primer/comments/commentRss/67502.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/cpp-primer/services/trackbacks/67502.html</trackback:ping><description><![CDATA[<h2 class=SECT2><a name=SELECT>1.4.1. Select（选择）</a></h2>
<p><span class=ACRONYM>SQL</span> 里面最常用的命令是 SELECT 语句，用于检索数据。语法是：
<pre class=SYNOPSIS>SELECT [ ALL | DISTINCT [ ON ( <tt class=REPLACEABLE><em>expression</em></tt> [, ...] ) ] ]
* | <tt class=REPLACEABLE><em>expression</em></tt> [ AS <tt class=REPLACEABLE><em>output_name</em></tt> ] [, ...]
[ INTO [ TEMPORARY | TEMP ] [ TABLE ] <tt class=REPLACEABLE><em>new_table</em></tt> ]
[ FROM <tt class=REPLACEABLE><em>from_item</em></tt> [, ...] ]
[ WHERE <tt class=REPLACEABLE><em>condition</em></tt> ]
[ GROUP BY <tt class=REPLACEABLE><em>expression</em></tt> [, ...] ]
[ HAVING <tt class=REPLACEABLE><em>condition</em></tt> [, ...] ]
[ { UNION | INTERSECT | EXCEPT [ ALL ] } <tt class=REPLACEABLE><em>select</em></tt> ]
[ ORDER BY <tt class=REPLACEABLE><em>expression</em></tt> [ ASC | DESC | USING <tt class=REPLACEABLE><em>operator</em></tt> ] [, ...] ]
[ FOR UPDATE [ OF <tt class=REPLACEABLE><em>class_name</em></tt> [, ...] ] ]
[ LIMIT { <tt class=REPLACEABLE><em>count</em></tt> | ALL } [ { OFFSET | , } <tt class=REPLACEABLE><em>start</em></tt> ]]
</pre>
<p>&nbsp;</p>
<p>现在我们将通过不同的例子演示 SELECT 语句复杂的语法。用于这些例子的表在 <a href="http://www.pgsqldb.org/pgsqldoc-7.1c/sql.html#AEN108"><em><u><font color=#800080>供应商和部件数据库</font></u></em></a> 里定义。 </p>
<div class=SECT3>
<h3 class=SECT3><a name=AEN474>1.4.1.1. 简单的 Select</a></h3>
<p>这里是一些使用 SELECT 语句的简单例子：
<div class=EXAMPLE>
<p><strong>Example 1-4. 带有条件的简单查询</strong></p>
<p>要从表 PART 里面把字段 PRICE 大于 10 的所有记录找出来， 我们写出下面查询：
<pre class=PROGRAMLISTING>SELECT * FROM PART
WHERE PRICE &gt; 10;
</pre>
然后得到表：
<pre class=PROGRAMLISTING> PNO |  PNAME  |  PRICE
-----+---------+--------
3  |  Bolt   |   15
4  |  Cam    |   25
</pre>
<p>&nbsp;</p>
<p>在 SELECT语句里使用 "*" 将检索出表中的所有属性。 如果我们只希望从表 PART 中检索出属性 PNAME 和 PRICE， 我们使用下面的语句：
<pre class=PROGRAMLISTING>SELECT PNAME, PRICE
FROM PART
WHERE PRICE &gt; 10;
</pre>
这回我们的结果是：
<pre class=PROGRAMLISTING>                      PNAME  |  PRICE
--------+--------
Bolt   |   15
Cam    |   25
</pre>
请注意 <span class=ACRONYM>SQL</span> 的 SELECT 语句对应关系演算里面的 "projection" （映射），而不是 "selection"（选择）（参阅 <a href="http://www.pgsqldb.org/pgsqldoc-7.1c/relmodel-oper.html#AEN246"><em><u><font color=#0000ff>关系演算</font></u></em></a> 获取详细信息）。
<p>&nbsp;</p>
<p>WHERE 子句里的条件也可以用关键字 OR，AND，和 NOT 逻辑地连接起来：
<pre class=PROGRAMLISTING>SELECT PNAME, PRICE
FROM PART
WHERE PNAME = 'Bolt' AND
(PRICE = 0 OR PRICE &lt;= 15);
</pre>
这样将生成下面的结果：
<pre class=PROGRAMLISTING> PNAME  |  PRICE
--------+--------
Bolt   |   15
</pre>
<p>&nbsp;</p>
<p>目标列表和 WHERE 子句里可以使用算术操作。例如， 如果我们想知道如果我们买两个部件的话要多少钱， 我们可以用下面的查询：
<pre class=PROGRAMLISTING>SELECT PNAME, PRICE * 2 AS DOUBLE
FROM PART
WHERE PRICE * 2 &lt; 50;
</pre>
这样我们得到：
<pre class=PROGRAMLISTING> PNAME  |  DOUBLE
--------+---------
Screw  |    20
Nut    |    16
Bolt   |    30
</pre>
请注意在关键字 AS 后面的 DOUBLE 是第二个列的新名字。 这个技巧可以用于目标列表里的每个元素， 给它们赋予一个在结果列中显示的新的标题。 这个新的标题通常称为别名。这个别名不能在该查询的其他地方使用。
<p>&nbsp;</p>
</div>
<p>&nbsp;</p>
</div>
<div class=SECT3>
<h3 class=SECT3><a name=AEN493>1.4.1.2. Joins（连接）</a></h3>
<p>下面的例子显示了 <span class=ACRONYM>SQL</span> 里是如何实现<em class=FIRSTTERM>连接</em>的。 </p>
<p>要在共同的属性上连接三个表 SUPPLIER，PART 和 SELLS， 我们通常使用下面的语句：
<pre class=PROGRAMLISTING>SELECT S.SNAME, P.PNAME
FROM SUPPLIER S, PART P, SELLS SE
WHERE S.SNO = SE.SNO AND
P.PNO = SE.PNO;
</pre>
而我们得到的结果是：
<pre class=PROGRAMLISTING> SNAME | PNAME
-------+-------
Smith | Screw
Smith | Nut
Jones | Cam
Adams | Screw
Adams | Bolt
Blake | Nut
Blake | Bolt
Blake | Cam
</pre>
<p>&nbsp;</p>
<p>在 FROM 子句里，我们为每个关系使用了一个别名， 因为在这些关系间有着公共的命名属性（SNO 和 PNO）。 现在我们可以区分不同表的公共命名属性， 只需要简单的用每个关系的别名加上个点做前缀就行了。 联合是用与 <a href="http://www.pgsqldb.org/pgsqldoc-7.1c/relmodel-oper.html#AEN338"><em><u><font color=#0000ff>一个内部联接</font></u></em></a> 里显示的同样的方法计算的。首先算出笛卡儿积 SUPPLIER &#215; PART &#215; SELLS 。然后选出那些满足 WHERE 子句里给出的条件的记录 （也就是说，公共命名属性的值必须相等）。 最后我们映射出除 S.SNAME 和 P.PNAME 外的所有属性。 </p>
<p>另外一个进行连接的方法是使用下面这样的 SQL JOIN 语法：
<pre class=PROGRAMLISTING>select sname, pname from supplier
JOIN sells USING (sno)
JOIN part USING (pno);
</pre>
giving again:
<pre class=PROGRAMLISTING> sname | pname
-------+-------
Smith | Screw
Adams | Screw
Smith | Nut
Blake | Nut
Adams | Bolt
Blake | Bolt
Jones | Cam
Blake | Cam
(8 rows)
</pre>
<p>&nbsp;</p>
<p>一个用 JOIN 语法创建的连接表，是一个出现在 FROM 子句里的， 在任何 WHERE，GROUP BY 或 HAVING 子句之前的表引用列表项． 其它表引用，包括表名字或者其它 JOIN 子句，如果用逗号分隔的话， 可以包含在 FROM 子句里． 连接生成的表逻辑上和任何其它在 FROM 子句里列出的表都一样． </p>
<p>SQL JOIN 有两种主要类型，CROSS JOIN (无条件连接) 和<em class=FIRSTTERM>条件连接</em>．条件连接还可以根据声明的 <em class=FIRSTTERM>连接条件</em>(ON，USING，或 NATURAL)和它 应用的方式(INNER 或 OUTER 连接)进一步细分． </p>
<p>&nbsp;</p>
<dl>
<p><strong>连接类型</strong></p>
<dt>CROSS JOIN
<dd>
<p>{ <tt class=REPLACEABLE><em>T1</em></tt> }<strong class=COMMAND> CROSS JOIN </strong>{ <tt class=REPLACEABLE><em>T2</em></tt> }</p>
<p>一个交叉连接（cross join）接收两个分别有 N 行和 M 行 的表 T1 和 T2，然后返回一个包含交叉乘积 NxM 条记录的 连接表． 对于 T1 的每行 R1，T2 的每行 R2 都与 R1 连接生成 连接的表行 JR，JR 包含所有 R1 和 R2 的字段． CROSS JOIN 实际上就是一个 INNER JOIN ON TRUE． </p>
<dt>条件 JOIN
<dd>
<p>{ <tt class=REPLACEABLE><em>T1</em></tt> } [ NATURAL ] [ INNER | { LEFT | RIGHT | FULL } [ OUTER ] ]<strong class=COMMAND> JOIN </strong>{ <tt class=REPLACEABLE><em>T2</em></tt> } { ON <tt class=REPLACEABLE><em>search condition</em></tt> | USING ( <tt class=REPLACEABLE><em>join column list</em></tt> ) }</p>
<p>一个条件 JOIN 必须通过提供一个(并且只能有一个) NATURAL，ON，或者 USING 这样的关键字来声明它的 连接条件． ON 子句 接受一个 <tt class=REPLACEABLE><em>search condition</em></tt>， 它与一个 WHERE 子句相同．USING 子句接受一个用逗号分隔的 字段名列表，连接表中必须都有这些字段， 并且用那些字段连接这些表，生成的连接表包含每个共有字段 和两个表的所有其它字段． NATURAL 是 USING 子句的缩写，它列出两个表中所有公共 的字段名字．使用 USING 和 NATURAL 的副作用是 每个连接的字段都只有一份拷贝出现在结果表中 (与前面定义的关系演算的 JOIN 相比较)． </p>
<p>&nbsp;</p>
<dl>
<dt>
<p>[ INNER ]<strong class=COMMAND> JOIN </strong></p>
<dd>
<p>对于 T1 的每行 R1，连接成的表在 T2 里都有一行满 足与 R1 一起的连接条件． </p>
<p>对于所有 JOIN 而言，INNER 和 OUTER 都是可选的．INNER 是缺省． LEFT，RIGHT，和 FULL 只用于 OUTER JOIN． </p>
<dt>
<p>LEFT [ OUTER ]<strong class=COMMAND> JOIN </strong></p>
<dd>
<p>首先，执行一次 INNER JOIN． 然后，如果 T1 里有一行对任何 T2 的行都不满足 连接条件，那么返回一个连接行，该行的 T2 的字段 为 null． </p>
<div class=TIP>
<blockquote class=TIP>
<p><strong>小技巧: </strong>连接成的表无条件地包含 T1 里的所有行． </p>
</blockquote></div>
<dt>
<p>RIGHT [ OUTER ]<strong class=COMMAND> JOIN </strong></p>
<dd>
<p>首先，执行一次 INNER JOIN． 然后，如果 T2 里有一行对任何 T1 的行都不满足 连接条件，那么返回一个连接行，该行的 T1 的字段 为 null． </p>
<div class=TIP>
<blockquote class=TIP>
<p><strong>小技巧: </strong>连接成的表无条件地包含 T2 里的所有行． </p>
</blockquote></div>
<dt>
<p>FULL [ OUTER ]<strong class=COMMAND> JOIN </strong></p>
<dd>
<p>首先，执行一次 INNER JOIN． 然后，如果 T1 里有一行对任何 T2 的行都不满足 连接条件，那么返回一个连接行，该行的 T1 的字段 为 null． 同样，如果 T2 里有一行对任何 T1 的行都不满足 连接条件，那么返回一个连接行，该行的 T2 的字段 为 null． </p>
<div class=TIP>
<blockquote class=TIP>
<p><strong>小技巧: </strong>连接成的表无条件地拥有来自 T1 的每 一行和来自 T2 的每一行． </p>
</blockquote></div>
</dd></dl></dd></dl>
<p>所有 类型的 JOIN 都可以链接在一起或者嵌套在一起， 这时 <tt class=REPLACEABLE><em>T1</em></tt> 和 <tt class=REPLACEABLE><em>T2</em></tt> 都可以是连接生成的表．我们可以使用圆括弧控制 JOIN 的顺序，如果我们不主动控制，那么连接顺序是从左到右． </p>
</div>
<div class=SECT3>
<h3 class=SECT3><a name=AEN592>1.4.1.3. 聚集操作符</a></h3>
<p><span class=ACRONYM>SQL</span> 提供以一些聚集操作符（如， AVG，COUNT，SUM，MIN，MAX），这些聚集操作符以一个表达式为参数。 只要是满足 WHERE 子句的行，就会计算这个表达式， 然后聚集操作符对这个输入数值的集合进行计算． 通常，一个聚集对整个 SELECT 语句计算的结果是 生成一个结果．但如果在一个查询里面声明了分组， 那么数据库将对每个组进行一次独立的计算，并且 聚集结果是按照各个组出现的(见下节)．
<div class=EXAMPLE>
<p><strong>Example 1-5. 聚集</strong></p>
<p>果我们想知道表 PART 里面所有部件的平均价格，我们可以使用下面查询：
<pre class=PROGRAMLISTING>SELECT AVG(PRICE) AS AVG_PRICE
FROM PART;
</pre>
<p>&nbsp;</p>
<p>结果是：
<pre class=PROGRAMLISTING> AVG_PRICE
-----------
14.5
</pre>
<p>&nbsp;</p>
<p>如果我们想知道在表 PART 里面存储了多少部件，我们可以使用语句：
<pre class=PROGRAMLISTING>SELECT COUNT(PNO)
FROM PART;
</pre>
得到：
<pre class=PROGRAMLISTING> COUNT
-------
4
</pre>
<p>&nbsp;</p>
</div>
<p>&nbsp;</p>
</div>
<div class=SECT3>
<h3 class=SECT3><a name=AEN605>1.4.1.4. 分组聚集</a></h3>
<p><span class=ACRONYM>SQL</span> 允许我们把一个表里面的记录分成组。 然后上面描述的聚集操作符可以应用于这些组上 （也就是说，聚集操作符的值不再是对所有声明的列的值进行操作， 而是对一个组的所有值进行操作。这样聚集函数是为每个组独立地进行计算的。） </p>
<p>对记录的分组是通过关键字 <strong class=COMMAND>GROUP BY</strong> 实现的，<strong class=COMMAND>GROUP BY</strong> 后面跟着一个定义组的构成的属性列表。 如果我们使用语句 <strong class=COMMAND>GROUP BY A<sub>1</sub>, &amp;tdot;, A<sub>k</sub></strong> 我们就把关系分成了组，这样当且仅当两条记录在所有属性 A<sub>1</sub>, &amp;tdot;, A<sub>k</sub> 上达成一致，它们才是同一组的。
<div class=EXAMPLE>
<p><strong>Example 1-6. 聚集</strong></p>
<p>如果我们想知道每个供应商销售多少个部件，我们可以这样写查询：
<pre class=PROGRAMLISTING>SELECT S.SNO, S.SNAME, COUNT(SE.PNO)
FROM SUPPLIER S, SELLS SE
WHERE S.SNO = SE.SNO
GROUP BY S.SNO, S.SNAME;
</pre>
得到：
<pre class=PROGRAMLISTING> SNO | SNAME | COUNT
-----+-------+-------
1  | Smith |   2
2  | Jones |   1
3  | Adams |   2
4  | Blake |   3
</pre>
<p>&nbsp;</p>
<p>然后我们看一看发生了什么事情。首先生成表 SUPPLIER 和 SELLS 的连接：
<pre class=PROGRAMLISTING> S.SNO | S.SNAME | SE.PNO
-------+---------+--------
1   |  Smith  |   1
1   |  Smith  |   2
2   |  Jones  |   4
3   |  Adams  |   1
3   |  Adams  |   3
4   |  Blake  |   2
4   |  Blake  |   3
4   |  Blake  |   4
</pre>
<p>&nbsp;</p>
<p>然后我们把那些属性 S.SNO 和 S.SNAME 相同的记录放在组中：
<pre class=PROGRAMLISTING> S.SNO | S.SNAME | SE.PNO
-------+---------+--------
1   |  Smith  |   1
|   2
--------------------------
2   |  Jones  |   4
--------------------------
3   |  Adams  |   1
|   3
--------------------------
4   |  Blake  |   2
|   3
|   4
</pre>
<p>&nbsp;</p>
<p>在我们的例子里，我们有四个组并且现在我们可以对每个组应用聚集操作符 COUNT，生成上面给出的查询的最终结果。 </p>
</div>
<p>&nbsp;</p>
<p>请注意如果要让一个使用 GROUP BY 和聚集操作符的查询的结果有意义， 那么用于分组的属性也必须出现在目标列表中。 所有没有在 GROUP BY 子句里面出现的属性都只能通过使用聚集函数来选择。 否则就不会有唯一的数值与其它字段关联． </p>
<p>还要注意的是在聚集上聚集是没有意义的，比如，AVG(MAX(sno))， 因为 SELECT 只做一个回合的分组和聚集．你可以获得这样的结果， 方法是使用临时表或者在 FROM 子句中使用一个子 SELECT 做第一个层次的聚集． </p>
</div>
<div class=SECT3>
<h3 class=SECT3><a name=AEN629>1.4.1.5. Having</a></h3>
<p>HAVING 子句运做起来非常象 WHERE 子句， 只用于对那些满足 HAVING 子句里面给出的条件的组进行计算。 其实，WHERE 在分组和聚集之前过滤掉我们不需要的输入行， 而 HAVING 在 GROUP 之后那些不需要的组． 因此，WHERE 无法使用一个聚集函数的结果． 而另一方面，我们也没有理由写一个不涉及聚集函数的 HAVING． 如果你的条件不包含聚集，那么你也可以把它写在 WHERE 里面， 这样就可以避免对那些你准备抛弃的行进行的聚集运算．
<div class=EXAMPLE>
<p><strong>Example 1-7. Having</strong></p>
<p>如果我们想知道那些销售超过一个部件的供应商，使用下面查询：
<pre class=PROGRAMLISTING>SELECT S.SNO, S.SNAME, COUNT(SE.PNO)
FROM SUPPLIER S, SELLS SE
WHERE S.SNO = SE.SNO
GROUP BY S.SNO, S.SNAME
HAVING COUNT(SE.PNO) &gt; 1;
</pre>
and get:
<pre class=PROGRAMLISTING> SNO | SNAME | COUNT
-----+-------+-------
1  | Smith |   2
3  | Adams |   2
4  | Blake |   3
</pre>
<p>&nbsp;</p>
</div>
<p>&nbsp;</p>
</div>
<div class=SECT3>
<h3 class=SECT3><a name=AEN637>1.4.1.6. 子查询</a></h3>
<p>在 WHERE 和 HAVING 子句里，允许在任何要产生数值的地方使用子查询 （子选择）。 这种情况下，该值必须首先来自对子查询的计算。子查询的使用扩展了 <span class=ACRONYM>SQL</span> 的表达能力。
<div class=EXAMPLE>
<p><strong>Example 1-8. 子查询</strong></p>
<p>如果我们想知道所有比名为 'Screw' 的部件贵的部件，我们可以用下面的查询：
<pre class=PROGRAMLISTING>SELECT *
FROM PART
WHERE PRICE &gt; (SELECT PRICE FROM PART
WHERE PNAME='Screw');
</pre>
<p>&nbsp;</p>
<p>结果是：
<pre class=PROGRAMLISTING> PNO |  PNAME  |  PRICE
-----+---------+--------
3  |  Bolt   |   15
4  |  Cam    |   25
</pre>
<p>&nbsp;</p>
<p>当我们检查上面的查询时会发现出现了两次 SELECT 关键字。 第一个在查询的开头 - 我们将称之为外层 SELECT - 而另一个在 WHERE 子句里面，成为一个嵌入的查询 - 我们将称之为内层 SELECT。 对外层 SELECT 的每条记录都必须先计算内层 SELECT。在完成所有计算之后， 我们得知名为 'Screw' 部件的记录的价格， 然后我们就可以检查那些价格更贵的记录了。 (实际上，在本例中，内层查询只需要执行一次， 因为它不依赖于外层查询高等状态．) </p>
<p>如果我们想知道那些不销售任何部件的供应商 （比如说，我们想把这些供应商从数据库中删除），我们用：
<pre class=PROGRAMLISTING>SELECT *
FROM SUPPLIER S
WHERE NOT EXISTS
(SELECT * FROM SELLS SE
WHERE SE.SNO = S.SNO);
</pre>
<p>&nbsp;</p>
<p>在我们的例子里，结果列将是空的，因为每个供应商至少销售一个部件。 请注意我们在 WHERE 子句的内层 SELECT 里使用了来自外层 SELECT 的 S.SNO。 正如前面所说的，子查询为每个外层查询计算一次，也就是说， S.SNO 的值总是从外层 SELECT 的实际记录中取得的。 </p>
</div>
<p>&nbsp;</p>
</div>
<div class=SECT3>
<h3 class=SECT3><a name=AEN651>1.4.1.7. 在 FROM 里面的子查询</a></h3>
<p>一种有些特别的子查询的用法是把它们放在 FROM 子句里． 这个特性很有用，因为这样的子查询可以输出多列和多行， 而在表达式里使用的子查询必须生成一个结果． FROM 里的子查询还可以让我们获得多于一个回合的分组/聚集特性， 而不需要求助于临时表．
<div class=EXAMPLE>
<p><strong>Example 1-9. FROM 里面的子查询</strong></p>
<p>如果我们想知道在所有我们的供应商中的最高平均部件价格的那家， 我们不能用 MAX(AVG(PRICE))，但我们可以这么写：
<pre class=PROGRAMLISTING>SELECT MAX(subtable.avgprice)
FROM (SELECT AVG(P.PRICE) AS avgprice
FROM SUPPLIER S, PART P, SELLS SE
WHERE S.SNO = SE.SNO AND
P.PNO = SE.PNO
GROUP BY S.SNO) subtable;
</pre>
这个子查询为每个供应商返回一行（因为它的 GROUP BY） 然后我们在外层查询对所有行进行聚集．
<p>&nbsp;</p>
</div>
<p>&nbsp;</p>
</div>
<div class=SECT3>
<h3 class=SECT3><a name=AEN658>1.4.1.8. Union, Intersect, Except（联合，相交，相异）</a></h3>
<p>这些操作符分别计算两个子查询产生的元组的联合，相交和集合理论里的相异。
<div class=EXAMPLE>
<p><strong>Example 1-10. Union, Intersect, Except</strong></p>
<p>下面的例子是 UNION 的例子：
<pre class=PROGRAMLISTING>SELECT S.SNO, S.SNAME, S.CITY
FROM SUPPLIER S
WHERE S.SNAME = 'Jones'
UNION
SELECT S.SNO, S.SNAME, S.CITY
FROM SUPPLIER S
WHERE S.SNAME = 'Adams';
</pre>
产生结果：
<pre class=PROGRAMLISTING> SNO | SNAME |  CITY
-----+-------+--------
2  | Jones | Paris
3  | Adams | Vienna
</pre>
<p>&nbsp;</p>
<p>下面是相交（ INTERSECT）的例子：
<pre class=PROGRAMLISTING>SELECT S.SNO, S.SNAME, S.CITY
FROM SUPPLIER S
WHERE S.SNO &gt; 1
INTERSECT
SELECT S.SNO, S.SNAME, S.CITY
FROM SUPPLIER S
WHERE S.SNO &lt; 3;
</pre>
产生结果：
<pre class=PROGRAMLISTING> SNO | SNAME |  CITY
-----+-------+--------
2  | Jones | Paris
</pre>
两个查询都会返回的元组是那条 SNO=2 的
<p>&nbsp;</p>
<p>最后是一个 EXCEPT 的例子：
<pre class=PROGRAMLISTING>SELECT S.SNO, S.SNAME, S.CITY
FROM SUPPLIER S
WHERE S.SNO &gt; 1
EXCEPT
SELECT S.SNO, S.SNAME, S.CITY
FROM SUPPLIER S
WHERE S.SNO &gt; 3;
</pre>
结果是：
<pre class=PROGRAMLISTING> SNO | SNAME |  CITY
-----+-------+--------
2  | Jones | Paris
3  | Adams | Vienna
</pre>
</div>
</div>
<img src ="http://www.cppblog.com/cpp-primer/aggbug/67502.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/cpp-primer/" target="_blank">Benson</a> 2008-11-21 17:00 <a href="http://www.cppblog.com/cpp-primer/archive/2008/11/21/67502.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item><item><title>数据库设计三大范式应用实例剖析</title><link>http://www.cppblog.com/cpp-primer/archive/2008/11/21/67464.html</link><dc:creator>Benson</dc:creator><author>Benson</author><pubDate>Fri, 21 Nov 2008 03:09:00 GMT</pubDate><guid>http://www.cppblog.com/cpp-primer/archive/2008/11/21/67464.html</guid><wfw:comment>http://www.cppblog.com/cpp-primer/comments/67464.html</wfw:comment><comments>http://www.cppblog.com/cpp-primer/archive/2008/11/21/67464.html#Feedback</comments><slash:comments>0</slash:comments><wfw:commentRss>http://www.cppblog.com/cpp-primer/comments/commentRss/67464.html</wfw:commentRss><trackback:ping>http://www.cppblog.com/cpp-primer/services/trackbacks/67464.html</trackback:ping><description><![CDATA[数据库的设计范式是数据库设计所需要满足的规范，满足这些规范的数据库是简洁的、结构明晰的，同时，不会发生插入（insert）、删除（delete）和更新（update）操作异常。反之则是乱七八糟，不仅给数据库的编程人员制造麻烦，而且面目可憎，可能存储了大量不需要的冗余信息。<br><br>　　设计范式是不是很难懂呢？非也，大学教材上给我们一堆数学公式我们当然看不懂，也记不住。所以我们很多人就根本不按照范式来设计数据库。<br><br>　　实质上，设计范式用很形象、很简洁的话语就能说清楚，道明白。本文将对范式进行通俗地说明，并以笔者曾经设计的一个简单论坛的数据库为例来讲解怎样将这些范式应用于实际工程。<br><br>　　范式说明<br><br>　　<span style="COLOR: #ff0000"><strong>第一范式（1NF）：数据库表中的字段都是单一属性的，不可再分。</strong></span>这个单一属性由基本类型构成，包括整型、实数、字符型、逻辑型、日期型等。<br><br>　　例如，如下的数据库表是符合第一范式的：<br><br>
<table cellSpacing=0 cellPadding=2 width="90%" align=center border=1>
    <tbody>
        <tr>
            <td>字段1 </td>
            <td>字段2 </td>
            <td>字段3 </td>
            <td>字段4</td>
        </tr>
        <tr>
            <td>&nbsp;</td>
            <td>&nbsp;</td>
            <td>&nbsp;</td>
            <td>&nbsp;</td>
        </tr>
    </tbody>
</table>
<br>　　而这样的数据库表是不符合第一范式的：<br><br>
<table cellSpacing=0 cellPadding=2 width="90%" align=center border=1>
    <tbody>
        <tr>
            <td>字段1 </td>
            <td>字段2 </td>
            <td colSpan=2>字段3 </td>
            <td>字段4</td>
        </tr>
        <tr>
            <td>&nbsp;</td>
            <td>&nbsp;</td>
            <td>字段3.1</td>
            <td>字段3.2 </td>
            <td>&nbsp;</td>
        </tr>
    </tbody>
</table>
<p><br>　　很显然，在当前的任何关系数据库管理系统（DBMS）中，傻瓜也不可能做出不符合第一范式的数据库，因为这些DBMS不允许你把数据库表的一列再分成二列或多列。因此，你想在现有的DBMS中设计出不符合第一范式的数据库都是不可能的。<br><br>　　<span style="COLOR: #ff0000"><strong>第二范式（2NF）：数据库表中不存在非关键字段对任一候选关键字段的部分函数依赖（部分函数依赖指的是存在组合关键字中的某些字段决定非关键字段的情况），也即所有非关键字段都完全依赖于任意一组候选关键字。</strong></span> <br>　假定选课关系表为SelectCourse(学号, 姓名, 年龄, 课程名称, 成绩, 学分)，关键字为组合关键字(学号, 课程名称)，因为存在如下决定关系：<br><br>　　(学号, 课程名称) &#8594; (姓名, 年龄, 成绩, 学分)<br><br>　　这个数据库表不满足第二范式，因为存在如下决定关系：<br><br>　　(课程名称) &#8594; (学分)<br><br>　　(学号) &#8594; (姓名, 年龄)<br><br>　　即存在组合关键字中的字段决定非关键字的情况。<br><br>　　由于不符合2NF，这个选课关系表会存在如下问题：<br><br>　　(1) 数据冗余：<br><br>　　同一门课程由n个学生选修，"学分"就重复n-1次；同一个学生选修了m门课程，姓名和年龄就重复了m-1次。<br><br>　　(2) 更新异常：<br><br>　　若调整了某门课程的学分，数据表中所有行的"学分"值都要更新，否则会出现同一门课程学分不同的情况。<br><br>　　(3) 插入异常：<br><br>　　假设要开设一门新的课程，暂时还没有人选修。这样，由于还没有"学号"关键字，课程名称和学分也无法记录入数据库。<br><br>　　(4) 删除异常：<br><br>　　假设一批学生已经完成课程的选修，这些选修记录就应该从数据库表中删除。但是，与此同时，课程名称和学分信息也被删除了。很显然，这也会导致插入异常。 <br><br>　　把选课关系表SelectCourse改为如下三个表：<br><br>　　学生：Student(学号, 姓名, 年龄)；<br><br>　　课程：Course(课程名称, 学分)；<br><br>　　选课关系：SelectCourse(学号, 课程名称, 成绩)。<br><br>　　这样的数据库表是符合第二范式的，消除了数据冗余、更新异常、插入异常和删除异常。<br><br>　　另外，所有单关键字的数据库表都符合第二范式，因为不可能存在组合关键字。<br><br>　　<span style="COLOR: #ff0000"><strong>第三范式（3NF）：在第二范式的基础上，数据表中如果不存在非关键字段对任一候选关键字段的传递函数依赖则符合第三范式。所谓传递函数依赖，指的是如果存在"A &#8594; B &#8594; C"的决定关系，则C传递函数依赖于A。</strong></span>因此，满足第三范式的数据库表应该不存在如下依赖关系：<br><br>　　关键字段 &#8594; 非关键字段x &#8594; 非关键字段y<br><br>假定学生关系表为Student(学号, 姓名, 年龄, 所在学院, 学院地点, 学院电话)，关键字为单一关键字"学号"，因为存在如下决定关系：</p>
<p>　　(学号) &#8594; (姓名, 年龄, 所在学院, 学院地点, 学院电话)</p>
<p>　　这个数据库是符合2NF的，但是不符合3NF，因为存在如下决定关系：</p>
<p>　　(学号) &#8594; (所在学院) &#8594; (学院地点, 学院电话)</p>
<p>　　即存在非关键字段"学院地点"、"学院电话"对关键字段"学号"的传递函数依赖。</p>
<p>　　它也会存在数据冗余、更新异常、插入异常和删除异常的情况，读者可自行分析得知。</p>
<p>　　把学生关系表分为如下两个表：</p>
<p>　　学生：(学号, 姓名, 年龄, 所在学院)；</p>
<p>　　学院：(学院, 地点, 电话)。<br><br>　　这样的数据库表是符合第三范式的，消除了数据冗余、更新异常、插入异常和删除异常。<br><br>　　鲍依斯-科得范式（BCNF）：在第三范式的基础上，数据库表中如果不存在任何字段对任一候选关键字段的传递函数依赖则符合第三范式。</p>
<p>　假设仓库管理关系表为StorehouseManage(仓库ID, 存储物品ID, 管理员ID, 数量)，且有一个管理员只在一个仓库工作；一个仓库可以存储多种物品。这个数据库表中存在如下决定关系：<br><br>　　(仓库ID, 存储物品ID) &#8594;(管理员ID, 数量)<br><br>　　(管理员ID, 存储物品ID) &#8594; (仓库ID, 数量)<br><br>　　所以，(仓库ID, 存储物品ID)和(管理员ID, 存储物品ID)都是StorehouseManage的候选关键字，表中的唯一非关键字段为数量，它是符合第三范式的。但是，由于存在如下决定关系：<br><br>　　(仓库ID) &#8594; (管理员ID)<br><br>　　(管理员ID) &#8594; (仓库ID)<br><br>　　即存在关键字段决定关键字段的情况，所以其不符合BCNF范式。它会出现如下异常情况：<br><br>　　(1) 删除异常：<br><br>　　当仓库被清空后，所有"存储物品ID"和"数量"信息被删除的同时，"仓库ID"和"管理员ID"信息也被删除了。<br><br>　　(2) 插入异常：<br><br>　　当仓库没有存储任何物品时，无法给仓库分配管理员。<br><br>　　(3) 更新异常：<br><br>　　如果仓库换了管理员，则表中所有行的管理员ID都要修改。<br><br>　　把仓库管理关系表分解为二个关系表：<br><br>　　仓库管理：StorehouseManage(仓库ID, 管理员ID)；<br><br>　　仓库：Storehouse(仓库ID, 存储物品ID, 数量)。<br><br>　　这样的数据库表是符合BCNF范式的，消除了删除异常、插入异常和更新异常。 </p>
<p>&nbsp;</p>
<p>范式应用<br><br>　　我们来逐步搞定一个论坛的数据库，有如下信息：<br><br>　　（1） 用户：用户名，email，主页，电话，联系地址<br><br>　　（2） 帖子：发帖标题，发帖内容，回复标题，回复内容 <br><br>　　第一次我们将数据库设计为仅仅存在表：<br>　　
<table cellSpacing=0 cellPadding=2 width="90%" align=center border=1>
    <tbody>
        <tr>
            <td>用户名 </td>
            <td>email </td>
            <td>主页</td>
            <td>电话</td>
            <td>联系地址</td>
            <td>发帖标题</td>
            <td>发帖内容</td>
            <td>回复标题</td>
            <td>回复内容</td>
        </tr>
    </tbody>
</table>
<br>　　这个数据库表符合第一范式，但是没有任何一组候选关键字能决定数据库表的整行，唯一的关键字段用户名也不能完全决定整个元组。我们需要增加"发帖ID"、"回复ID"字段，即将表修改为：<br><br>
<table cellSpacing=0 cellPadding=2 width="90%" align=center border=1>
    <tbody>
        <tr>
            <td>用户名</td>
            <td>email</td>
            <td>主页</td>
            <td>电话</td>
            <td>联系地址</td>
            <td>发帖ID</td>
            <td>发帖标题</td>
            <td>发帖内容</td>
            <td>回复ID</td>
            <td>回复标题</td>
            <td>回复内容</td>
        </tr>
    </tbody>
</table>
<br>　　这样数据表中的关键字(用户名，发帖ID，回复ID)能决定整行：<br><br>　　(用户名,发帖ID,回复ID) &#8594; (email,主页,电话,联系地址,发帖标题,发帖内容,回复标题,回复内容)<br><br>　　但是，这样的设计不符合第二范式，因为存在如下决定关系：<br><br>　　(用户名) &#8594; (email,主页,电话,联系地址)<br><br>　　(发帖ID) &#8594; (发帖标题,发帖内容)<br><br>　　(回复ID) &#8594; (回复标题,回复内容)<br><br>　　即非关键字段部分函数依赖于候选关键字段，很明显，这个设计会导致大量的数据冗余和操作异常。 <br><br>　　我们将数据库表分解为（带下划线的为关键字）：<br><br>　　（1） 用户信息：用户名，email，主页，电话，联系地址<br><br>　　（2） 帖子信息：发帖ID，标题，内容<br><br>　　（3） 回复信息：回复ID，标题，内容<br><br>　　（4） 发贴：用户名，发帖ID<br><br>　　（5） 回复：发帖ID，回复ID<br><br>　　这样的设计是满足第1、2、3范式和BCNF范式要求的，但是这样的设计是不是最好的呢？<br><br>　　不一定。<br><br>　　观察可知，第4项"发帖"中的"用户名"和"发帖ID"之间是1：N的关系，因此我们可以把"发帖"合并到第2项的"帖子信息"中；第5项"回复"中的"发帖ID"和"回复ID"之间也是1：N的关系，因此我们可以把"回复"合并到第3项的"回复信息"中。这样可以一定量地减少数据冗余，新的设计为：<br><br>　　（1） 用户信息：用户名，email，主页，电话，联系地址<br><br>　　（2） 帖子信息：用户名，发帖ID，标题，内容<br><br>　　（3） 回复信息：发帖ID，回复ID，标题，内容<br><br>　　数据库表1显然满足所有范式的要求；<br><br>　　数据库表2中存在非关键字段"标题"、"内容"对关键字段"发帖ID"的部分函数依赖，即不满足第二范式的要求，但是这一设计并不会导致数据冗余和操作异常；<br><br>　　数据库表3中也存在非关键字段"标题"、"内容"对关键字段"回复ID"的部分函数依赖，也不满足第二范式的要求，但是与数据库表2相似，这一设计也不会导致数据冗余和操作异常。<br><br>　　由此可以看出，并不一定要强行满足范式的要求，对于1：N关系，当1的一边合并到N的那边后，N的那边就不再满足第二范式了，但是这种设计反而比较好！<br><br>　　对于M：N的关系，不能将M一边或N一边合并到另一边去，这样会导致不符合范式要求，同时导致操作异常和数据冗余。 <br>对于1：1的关系，我们可以将左边的1或者右边的1合并到另一边去，设计导致不符合范式要求，但是并不会导致操作异常和数据冗余。<br><br>　　结论<br><br>　　满足范式要求的数据库设计是结构清晰的，同时可避免数据冗余和操作异常。这并意味着不符合范式要求的设计一定是错误的，在数据库表中存在1：1或1：N关系这种较特殊的情况下，合并导致的不符合范式要求反而是合理的。<br><br>　　在我们设计数据库的时候，一定要时刻考虑范式的要求。</p>
<img src ="http://www.cppblog.com/cpp-primer/aggbug/67464.html" width = "1" height = "1" /><br><br><div align=right><a style="text-decoration:none;" href="http://www.cppblog.com/cpp-primer/" target="_blank">Benson</a> 2008-11-21 11:09 <a href="http://www.cppblog.com/cpp-primer/archive/2008/11/21/67464.html#Feedback" target="_blank" style="text-decoration:none;">发表评论</a></div>]]></description></item></channel></rss>