高性能 SQL 计划 Day 3

今天继续来学 SQL,主要是《MySQL必知必会》15-18章的内容。

Ch 15.联结表

1.联结

SQL 最强大的功能之一就是能在数据检索查询的执行中联结(join)表。联结是利用 SQL 的 SELECT 能执行的最重要的操作。关系数据库设计的基础是将信息分解为多个表,相同类型的数据存放在同一个表中,各个表通过某些常用的值(即关系设计中的关系(relational))互相关联。关系数据可以有效地存储和方便地处理。因此,关系数据库的可伸缩性远比非关系数据库要好。

可伸缩性指的是能够适应不断增加的工作量而不失败。设计良好的数据库或应用程序称之为可伸缩性好(scale well)。

SQL 中联结的实现依赖于主键和外键。主键为某个表的一列,其值为某条记录的唯一标识符。外键同样是某个表的一列,它包含另一个表的主键值,定义了两个表之间的关系。以下是联结的一个例子:

SELECT vend_name,prod_name,prod_price
FROM vendors,products
WHERE vendors.vend_id = products.vend_id
ORDER BY vend_name,prod_name;

其中 where 子句规定了联结条件。如果没有联结条件,返回的结果为笛卡尔积(Cartesian product),检索出的行的数目将是第一个表中的行数乘以第二个表中的行数。

上述联结称为等值联结,基于两个表之间的相等测试,也称为内部链接。以下的 SQL 语句会返回跟上述语句相同的结果:

SELECT vend_name,prod_name,prod_price
FROM vendors INNER JOIN products
ON vendors.vend_id = products.vend_id;

在使用这种语法时,联结条件用特定的ON子句而不是WHERE子句给出。传递给ON的实际条件与传递给WHERE的相同。

注意:ANSI SQL规范首选INNER JOIN语法。此外,尽管使用WHERE子句定义联结的确比较简单,但是使用明确的联结语法能够确保不会忘记联结条件,有时候这样做也能影响性能。

2.联结多个表

SQL对一条SELECT语句中可以联结的表的数目没有限制。、

SELECT vend_name,prod_name,prod_price,quantity
FROM vendors,products,ordertimes
WHERE vendors.vend_id = products.vend_id
AND ordertimes.prod_id = products.vend_id
AND order_num = 20005;

注意:MySQL 在运行时关联指定的每个表以处理联结。这种处理可能是非常耗费资源的,因此应该仔细,不要联结不必要的表。联结的表越多,性能下降越严重。

上一章使用嵌套子查询的例子同样能够使用联结实现:

SELECT cust_name,cust_contact
FROM customers,orders,ordertimes
WHERE customers.cust_id = orders.cust_id
AND ordertimes.order_num = orders.order_num
AND prod_id = 'TNT2';

为执行任一给定的SQL操作,一般存在不止一种方法。很少有绝对正确或绝对错误的方法。性能可能会受操作类型、表中数据量、是否存在索引或键以及其他一些条件的影响。

Ch 16.高级联结

1.使用表别名

前文介绍了如何使用别名引用被检索的表列,SQL 还支持给表起别名,这样一来可以缩减 SQL 语句的长度,二来可以在同一条 SQL 语句中多次使用同一张表。

SELECT cust_name, cust_contact
FROM customers AS c, orders AS o, orderitems AS oi
WHERE c.cust_id = o.cust_id
  AND oi.order_num = o.order_num
  AND prod_id = 'TNT2';

应该注意,表别名只在查询执行中使用。与列别名不一样,表别名不返回到客户机。

2.使用不同类型的联结

自联结

举一个简单的例子,如果要查询名为“DTNTR”产品的厂商所生产的其他产品,首先需要根据产品名在products表中获取厂商名,然后再利用厂商名在products表中获得其他产品。利用此前讲过的子查询可以实现:

SELECT prod_id, prod_name
FROM products
WHERE vend_id = (SELECT vend_id
                 FROM products
                 WHERE prod_id = 'DTNTR'); 

使用联结可以实现相同的功能:

SELECT p1.prod_id, p1.prod_name
FROM products AS p1, products AS p2
WHERE p1.vend_id = p2.vend_id
  AND p2.prod_id = 'DTNTR';

上述联结称为自联结。需要注意的是使用自联结时必须使用完全限定列名,否则会产生歧义。

自联结通常作为外部语句用来替代从相同表中检索数据时使用的子查询语句。虽然最终的结果是相同的,但有时候处理联结远比处理子查询快得多。

自然联结

通常情况下使用联结时所设计的几张表里面应该至少有一个列出现在不止一个表中,标准的联结返回所有的数据,因此会出现多次相同的列。而自然联结排除多次出现,使每个列之返回一次。

自然联结无法由系统自动实现,只能自己编写,需要在SQL语句中明确制定要返回的列。以下是一个例子:

SELECT c.*, o.order_num, o.order_date,
       oi.prod_id, oi.quantity, OI.item_price
FROM customers AS c, orders AS o, orderitems AS oi
WHERE c.cust_id = o.cust_id
  AND oi.order_num = o.order_num
  AND prod_id = 'FB';

“事实上,迄今为止我们建立的每个内部联结都是自然联结,很可能我们永远都不会用到不是自然联结的内部联结。”

外部联结

联结的主要意义在于将不同表的行所关联起来,然而,有时候会需要包含那些没有关联行的那些行,例如以下的需求:

  • 对每个客户下了多少订单进行计数,包括那些至今尚未下订单的客户;
  • 列出所有产品以及订购数量,包括没有人订购的产品;
  • 计算平均销售规模,包括那些至今尚未下订单的客户。

这样的联结称为外部联结。

试看以下两个 SQL 语句的异同:

SELECT customers.cust_id, orders.order_num
FROM customers INNER JOIN orders
 ON customers.cust_id = orders.cust_id;
#检索所有的客户及其订单

SELECT customers.cust_id, orders.order_num
FROM customers LEFT OUTER JOIN orders
 ON customers.cust_id = orders.cust_id;
 #检索所有客户,包括那些没有订单的客户

以上这条SELECT语句使用了关键字 OUTER JOIN 来指定联结的类型(而不是在 WHERE 子句中指定)。但是,与内部联结关联两个表中的行不同的是,外部联结还包括没有关联行的行。在使用 OUTER JOIN 语法时,必须使用 RIGHT 或 LEFT 关键字指定包括其所有行的表(RIGHT 指出的是 OUTER JOIN 右边的表,而 LEFT 指出的是OUTER JOIN 左边的表)。上面的例子使用 LEFT OUTER JOIN 从 FROM 子句的左边表(customers表)中选择所有行。

注意:存在两种基本的外部联结形式:左外部联结和右外部联结。它们之间的唯一差别是所关联的表的顺序不同。换句话说,左外部联结可通过颠倒FROM或WHERE子句中表的顺序转换为右外部联结。因此,两种类型的外部联结可互换使用,而究竟使用哪一种纯粹是根据方便而定。

3.使用带聚集函数的联结

可以在内部联结、外部联结中使用聚集函数:

#内部联结中使用聚集函数
SELECT customers.cust_name,
       customers.cust_id,
       COUNT(orders.order_num) AS num_ord
FROM customers INNER JOIN orders
 ON customers.cust_id = orders.cust_id
GROUP BY customers.cust_id;

#外部联结中使用聚集函数
SELECT customers.cust_name,
       customers.cust_id,
       COUNT(orders.order_num) AS num_ord
FROM customers LEFT OUTER JOIN orders
 ON customers.cust_id = orders.cust_id
GROUP BY customers.cust_id;

4.使用联结和联结条件的注意事项

  • 注意所使用的联结类型。一般我们使用内部联结,但使用外部联结也是有效的。
  • 保证使用正确的联结条件,否则将返回不正确的数据。
  • 应该总是提供联结条件,否则会得出笛卡儿积。
  • 在一个联结中可以包含多个表,甚至对于每个联结可以采用不同的联结类型。虽然这样做是合法的,一般也很有用,但应该在一起测试它们前,分别测试每个联结。这将使故障排除更为简单。

Ch 17.组合查询

MySQL允许执行多个查询(多条SELECT语句),并将结果作为单个查询结果集返回。这些组合查询通常称为并(union)或复合查询(compoundquery)。

有两种基本情况,其中需要使用组合查询:

  • 在单个查询中从不同的表返回类似结构的数据;
  • 对单个表执行多个查询,按单个查询返回数据。

多数情况下,组合相同表的两个查询完成的工作与具有多个WHERE子句条件的单条查询完成的工作相同。换句话说,任何具有多个WHERE子句的SELECT语句都可以作为一个组合查询给出,两者的性能同时也存在差异。

组合查询的实现使用的是 UNION 关键字,其使用也非常简单,只需要在若干个 SELECT 语句中间添加 UNION 关键字即可,查询输出的结果为各个SELECT语句结果的去重并集。

SELECT vend_id, prod_id, prod_price
FROM products
WHERE prod_price <= 5
UNION
SELECT vend_id, prod_id, prod_price
FROM products
WHERE vend_id IN (1001,1002);

使用UNION关键字的注意事项:

  • UNION必须由两条或两条以上的SELECT语句组成,语句之间用关键字UNION分隔(因此,如果组合4条SELECT语句,将要使用3个UNION关键字)。
  • UNION中的每个查询必须包含相同的列、表达式或聚集函数(不过各个列不需要以相同的次序列出)。
  • 列数据类型必须兼容:类型不必完全相同,但必须是DBMS可以隐含地转换的类型(例如,不同的数值类型或不同的日期类型)。

非去重结果:在上述查询中,返回的结果是会被进行去重处理的。这是 MySQL 默认的行为,如果要取消去重处理,可是显示地使用 UNION ALL 关键字。前文说过,UNION和WHERE关键字执行的是相同的操作,而WHERE子句无法完成UNION ALL 的操作。

对组合查询结果进行排序:如果要对组合查询的结果进行排序,可以在最后一条SELECT语句后使用ORDER BY子句:

SELECT vend_id, prod_id, prod_price
FROM products
WHERE prod_price <= 5
UNION
SELECT vend_id, prod_id, prod_price
FROM products
WHERE vend_id IN (1001,1002)
ORDER BY vend_id, prod_price;

虽然ORDER BY子句似乎只是最后一条SELECT语句的组成部分,但实际上MySQL将用它来排序所有SELECT语句返回的所有结果。

Ch 18.全文本搜索

注意,并非所有的引擎都支持全文本搜索。MySQL最常使用的两个数据库引擎 MyISAM和InnoDB中前者支持而后者不支持。

1.理解全文本搜索

前面所介绍的LIKE关键字匹配和正则表达式匹配两种搜索机制存在以下几个重大的局限:

  • 性能:通配符和正则表达式匹配往往需要尝试匹配表中所有的记录中所有的词,如果表比较大就会非常耗时。
  • 明确控制:使用通配符和正则表达式匹配,很难(而且并不总是能)明确地控制匹配什么和不匹配什么。
  • 智能化的结果:虽然基于通配符和正则表达式的搜索提供了非常灵活的搜索,但它们都不能提供一种智能化的选择结果的方法。例如,一个特殊词的搜索将会返回包含该词的所有行,而不区分包含单个匹配的行和包含多个匹配的行(按照可能是更好的匹配来排列它们)。

以上的这些限制都可以使用全文本搜索解决。在使用全文本搜索时,MySQL不需要分别查看每个行,不需要分别分析和处理每个词。MySQL创建指定列中各词的一个索引,搜索可以针对这些词进行。这样,MySQL可以快速有效地决定哪些词匹配(哪些行包含它们),哪些词不匹配,它们匹配的频率等。

2.使用全文本搜索

要启用全文本搜索必须要定义FULLTEXT进行索引,可以在定义表时制定,也可以在导入数据完成后通过修改标的定义实现:

CREATE TABLE productnotes
(
  note_id    int           NOT NULL AUTO_INCREMENT,
  prod_id    char(10)      NOT NULL,
  note_date datetime       NOT NULL,
  note_text  text          NULL ,
  PRIMARY KEY(note_id),
  FULLTEXT(note_text)
) ENGINE=MyISAM;
#在定义表时指定FULLTEXT

在定义之后,MySQL自动维护该索引。在增加、更新或删除行时,索引随之自动更新。MySQL会动态更新索引,因此导入数据时会更新索引降低效率,因此最好是先导入数据然后修改表、定义FULLTEXT

3.进行全文本搜索

在索引之后,使用两个函数Match()和Against()执行全文本搜索,其中Match()指定被搜索的列,Against()指定要使用的搜索表达式。

SELECT note_text
FROM productnotes
WHERE Match(note_text) Against('rabbit');

注意:

  1. 传递给Match()的值必须与FULLTEXT()定义中的相同。如果指定多个列,则必须列出它们(而且次序正确)。
  2. 除非使用BINARY方式(本章中没有介绍),否则全文本搜索不区分大小写。
  3. 与通配符和正则表达式匹配不同,全文本搜索返回以文本匹配的良好程度排序的数据。两个行都包含词rabbit,但包含词rabbit作为第3个词的行的等级比作为第20个词的行高。这很重要。全文本搜索的一个重要部分就是对结果排序。具有较高等级的行先返回(因为这些行很可能是你真正想要的行)。如果指定多个搜索项,则包含多数匹配词的那些行将具有比包含较少词(或仅有一个匹配)的那些行高的等级值

4.使用查询扩展

考虑下面的情况:你想找出所有提到anvils的注释。只有一个注释包含词anvils,但你还想找出可能与你的搜索有关的所有其他行,即使它们不包含词anvils。

上述情况需要使用查询扩展来实现。在使用查询扩展时,MySQL对数据和索引进行两遍扫描来完成搜索:

  • 首先,进行一个基本的全文本搜索,找出与搜索条件匹配的所有行;
  • 其次,MySQL检查这些匹配行并选择所有有用的词(我们将会简要地解释MySQL如何断定什么有用,什么无用)。
  • 最后,MySQL再次进行全文本搜索,这次不仅使用原来的条件,而且还使用所有有用的词。

利用查询扩展,能找出可能相关的结果,即使它们并不精确包含所查找的词。

比较一下两个SQL语句:

SELECT note_text
FROM productnotes
WHERE Match(note_text) Against('anvils');
#返回一个包含 anvils 的记录

SELECT note_text
FROM productnotes
WHERE Match(note_text) Against('anvils' WITH QUERY EXPANSION);
#返回一个包含 anvils 的记录和六个与之相关的记录

查询扩展极大地增加了返回的行数,但这样做也增加了你实际上并不想要的行的数目,需要斟酌使用。

5.布尔文本搜索

利用布尔文本搜索,可以实现如下的元素匹配:

  • 包含某词匹配;
  • 包含某词不匹配;
  • 排列提示(指定某些词比其他词更重要,更重要的词等级更高);
  • 表达式分组等。

即使不定义 FULLTEXT,同样可以使用布尔文本搜索,只不过其性能很低。

#匹配包含heavy但不包含任意以rope开始的词的行
SELECT note_text
FROM productnotes
WHERE Match(note_text) Against('heavy -rope*' IN BOOLEAN MODE);

IN BOOLEAN MODE 表示使用布尔文本搜索。以下是MySQL所支持的布尔操作:

#布尔操作符  说 明
# +                 包含,词必须存在
# -                 排除,词必须不出现
# >                 包含,而且增加等级值
# <                 包含,且减少等级值
# ()                把词组成子表达式(允许这些子表达式作为一个组被包含、排除、排列等)
# ~                 取消一个词的排序值
# *                 词尾的通配符
# ""                定义一个短语(与单个词的列表不一样,它匹配整个短语以便包含或排除这个短语)

#例:匹配包含词rabbit和bait的行
SELECT note_text
FROM productnotes
WHERE Match(note_text) Against('+rabbit +bait"' IN BOOLEAN MODE);

#例:没有指定操作符,这个搜索匹配包含rabbit和bait中的至少一个词的行
SELECT note_text
FROM productnotes
WHERE Match(note_text) Against('rabbit bait' IN BOOLEAN MODE);

#例:这个搜索匹配短语rabbit bait而不是匹配两个词rabbit和bait。
SELECT note_text
FROM productnotes
WHERE Match(note_text) Against('"rabbit bait"' IN BOOLEAN MODE);

#例:匹配rabbit和carrot,增加前者的等级,降低后者的等级。
SELECT note_text
FROM productnotes
WHERE Match(note_text) Against('>rabbit 

6.全文本搜索的使用说明

  • 在索引全文本数据时,短词被忽略且从索引中排除。短词定义为那些具有3个或3个以下字符的词(如果需要,这个数目可以更改)。
  • MySQL带有一个内建的非用词(stopword)列表,这些词在索引全文本数据时总是被忽略。如果需要,可以覆盖这个列表(请参阅MySQL文档)。
  • 许多词出现的频率很高,搜索它们没有用处(返回太多的结果)。因此,MySQL规定了一条50%规则,如果一个词出现在50%以上的行中,则将它作为一个非用词忽略。50%规则不用于IN BOOLEAN MODE。
  • 如果表中的行数少于3行,则全文本搜索不返回结果(因为每个词或者不出现,或者至少出现在50%的行中)。
  • 忽略词中的单引号。例如,don't索引为dont。
  • 不具有词分隔符(包括日语和汉语)的语言不能恰当地返回全文本搜索结果。
  • 如前所述,仅在MyISAM数据库引擎中支持全文本搜索。
  • MySQL全文本搜索现在还不支持邻近操作符,不过未来的版本有支持这种操作符的计划。

未完待续···

你可能感兴趣的:(高性能 SQL 计划 Day 3)