《SQL 反模式》 学习笔记

第一章 引言


GoF 所著的的《设计模式》,在软件领域引入了“设计模式”(design pattern)的概念。html

然后,Andrew Koenig 在 1995 年造了 反模式(anti-pattern) (又称反面模式)这个词,灵感来自于 GoF 所著的的《设计模式》。前端

反模式指的是在实践中常常出现但又低效或是有待优化的设计模式,是用来解决问题的带有共同性的不良方法。它们已经通过研究并分类,以防止往后重蹈覆辙,并能在研发还没有投产的系统时辨认出来。mysql

因此,反模式是特殊的设计模式,而这种设计模式是欠妥的,起到了反效果。程序员

但有的时候,出于权衡考量,也会使用反模式。正则表达式

例如数据库的结构中使用的反规范化设计。sql

下面的每一章,都会列举一种特定场景下的反模式,而后再给出避免使用反模式的建议。数据库

有个别章节,我略去了反模式,直接写解决方案了。编程

第二章 乱穿马路


假设有 Product 和 Account 两个实体。设计模式

一、一对一关系

假设:Product 只有一个 Account(即 Account 也只有一个 Product)。数组

方案:只用一张表,用两个字段(Product + Account)关联便可。

如无必要,就别用多个表,这会增长复杂度(除非考虑将来的拓展性等其余状况)。

二、一对多关系

假设:Product 能够有多个 Account。

方案1:两张表,一个 Product 表,一个 ProductAccount 表,此表存 ProductId + AccountName。

ProductAccount 表称之为从属表

方案2:只用一张表,即 Product 表,而后此表有个 Account 字段,存以逗号分隔的 AccountName。

此为反模式,不推荐使用。

方案3:还有一种拓展性更好的、也是本人工做中更经常使用的作法,直接用下面 ”三、多对多关系“ 的方案 。

三、多对多关系

假设:Product 能够有多个 Account,Account 也能够有多个 Product。

方案:用三张表,一个 Product 表,一个 Account 表,一个 ProductAccount 表,此表存 ProductId + AccountId。

ProductAccount 表称之为交叉表

第三章 单纯的树


一、需求

创建一张表,存放(帖子的)评论(可嵌套回复评论)。

二、方案1:邻接表

添加 parent_id 列,指向同一张表的id。

这样的设计叫作邻接表。这多是程序员们用来存储分层结构数据中最普通的方案了。

缺点:

  • 查询一个节点的全部后代很复杂
  • 从一棵树中删除一个节点会变得比较复杂。若是须要删除一棵子树,你不得不执行屡次查询来找到全部的后代节点(其实这点跟上一个点实质同样),而后逐个从最低级别开始删除这些节点以知足外键完整性。

[拓展]

某些品牌的数据库管理系统提供扩展的 SQL 语句,来支持在邻接表中存储分层数据结构。

  • SQL-99 标准定义了递归查询的表达式规范,使用 WITH 关键字加上公共表表达式。
  • oracle 可使用层次化查询 connect by 遍历表数据。
  • postgreSQL 数据库中,咱们使用 RECURSIVE 参数配合 with 查询来实现遍历。若是安装了 tablefunc 扩展,也可使用 PG 版本的 connectby 函数。这个没有Oracle那么强大,可是能够知足基本要求。
  • mysql 暂不支持。

这里的递归查询暂时不深究,待写。

三、方案2:路径枚举

创建一个 path 字段,存路径,如1/4/6/7/

缺点:

  • 数据库不能确保路径的格式老是正确或者路径中的节点确实存在。依赖于应用程序的逻辑代码来维护路径的字符串,而且验证字符串的正确性的开销很大。
  • 不管将 VARCHAR 的长度设定为多大,依旧存在长度限制,于是并不可以支持树结构的无限扩展。

    能够用 PG 的 text 类型,最高支持存储 1G 的字符串,应该是够了。

四、方案3:嵌套集

创建 nsleftnsright 字段,存储子孙节点的相关信息,而不是节点的直接祖先.

每一个节点经过以下的方式肯定 nsleft 和nsright 的值:nsleft 的数值小于该节点全部后代的ID,同时 nsright 的值大于该节点全部后代的ID。这些数字和 comment_id 的值并无任何关联。

肯定这三个值(nsleft,comment_id,nsrigh)的简单方法是对树进行一次深度优先遍历,在逐层深刻的过程当中依次递增地分配 nsleft 的值,并在返回时依次递增地分配 nsright 的值。

最后结果形如:

缺点:
若是简单快速地查询是整个程序中最重要的部分,嵌套集是最佳选择——比操做单独的节点要方便快捷不少。然而,嵌套集的插入和移动节点是比较复杂的,由于须要从新分配左右值,若是你的应用程序须要频繁的插入、删除节点,那么嵌套集可能并不适合。

五、方案4:闭包表(推荐)

闭包表是解决分级存储的一个简单而优雅的解决方案,它记录了树中全部节点间的关系,而不只仅只有那些直接的父子关系。

在设计评论系统时,咱们额外建立了一张叫作 TreePaths 的表,它包含两列:ancestordescendant,每一列都是一个指向评论表的id的外键。

TreePaths 表结构以下:

六、总结

设计 查询子 查询树 插入 删除 引用完整性
邻接表 1 简单 困难 简单 简单
递归查询 1 简单 简单 简单 简单
枚举路径 1 简单 简单 简单 简单
嵌套集 1 困难 简单 困难 困难
闭包表 2 简单 简单 简单 简单



邻接表是最方便的设计,而且不少软件开发者都了解它。

若是你使用的数据库支持W ITH 或者 CONNECT BY PRIOR 的递归查询,那能使得邻接表的查询更为高效。

闭包表是最通用的设计,而且本章所描述的设计中只有它能容许一个节点属于多棵树。它要求一张额外的表来存储关系,使用空间换时间的方案减小操做过程当中由冗余的计算所形成的消耗。

我以前作过的评论功能,需求都会尽可能简化,例如弄成扁平化,只能回复评论一次,即不能评论评论的评论。若是下次鄙人真的要实现这个复杂的评论功能了,关于闭包表的具体设计及操做实现,准备回头再看原书。

第四章 须要 ID


一、什么是伪主键

在这样的表中,须要引入一个对于表的域模型无心义的新列来存储一个伪值。这一列被用做这张表的主键,从而经过它来肯定表中的一条记录。这种类型的主键列咱们一般称其为伪主键或者代理键

能够把 伪主键 理解成 伪键 或者 主键。

二、伪主键的做用

  • 确保一张表中的数据不会出现重复行;

    按照关系型数据库的定义,表里是不能够出现重复行的,可是实际中确实会出现,怎么办,引入伪键就不会重复了。

  • 在查询中引用单独的一行记录;
  • 支持外键。

三、各家数据库产品中的伪主键

伪主键直到 SQL:2003 才成为一个标准,于是每一个数据库都使用本身特有的 SQL 扩展来实现伪主键,甚至不一样数据库中对于伪主键都有不一样的名称(不一样的表述),以下表:

名称 数据库
AUTO_INCREMENT MySQL
GENERATOR Firebird, InterBase
IDENTITY DB2 Derby, Microsoft SQL Server, Sybase
ROWID SQLite
SEQUENCE DB2 Firebird, Informix, Ingres, Oracle, PostgreSQL
SERIAL MySQL, PostgreSQL

虽然各家数据库产品的伪主键叫法不一样,可是给伪主键指派的列名,确是出奇的一致,那就是 id

第五章 不用钥匙的入口


一、反模式 —— 不用外键

有时你被迫使用不支持外键约束的数据库产品(好比 MySQL 的 MyISAM 存储引擎,或者比 SQLite 3.6.19 早的版本)。

若是是这种状况,那你不得不使用别的方法来弥补。

二、推荐:使用外键

外键的好处:

  • 自动维持引用完整性(不然须要本身写监控脚本)
  • 级联更新/删除(不然须要本身写逻辑代码)

总结来看就是:避免编写没必要要的代码,节省了大量开发、调试以及维护时间

软件行业中每千行代码的平均缺陷数约为 15~50 个。在其余条件相同的状况下,越少的代码,意味着越少的缺陷。

外键的缺点:

  • 须要多一点额外的系统开销。

但这是值得的。

第六章 实体-属性-值


一、需求

表支持可变(可拓展)属性(列)。

例如:你有一个 Prodcut 表,记录了两种类型的产品:

  • 产品1:product_type = "电影",此外还有 product_name、total_duration(总时长) 属性。
  • 产品2:product_type = "图书",此外还有 product_name、total_page(总页数) 属性。

二、反模式

对于某些程序员来讲,当他们须要支持可变属性时,第一反应即是建立另外一张表,将属性当成行来存储。

这样的设计称为实体—属性—值,简称EAV。有时也称之为:开放架构、无模式或者名—值对。

例如:

  • Prodcut 表:id
  • ProdcutAttr 表:id、product_id、attr_name、attr_value

    ProdcutAttr 表数据形如:

    • (1,1, "product_type", "电影")
    • (2,1, "product_name", "阿甘正传")
    • (3,1, "total_duration", 120)
    • (4,2, "product_type", "图书")
    • (5,2, "product_name", "简爱")
    • (6,2, "total_page", 300)

三、推荐

(1)单表继承

最简单的设计是将全部相关的类型都存在一张表中,为全部类型的全部属性都保留一列。同时,使用一个属性来定义每一行表示的子类型。在这个例子中,这个属性称做issue_type

对于全部的子类型来讲,既有一些公共属性,但同时又有一些子类型特有属性。这些子类型特有属性列必须支持空值,由于根据子类型的不一样,有些属性并不须要填写,从而对于一条记录来讲,那些非空的项会变得比较零散。

例如:

  • Prodcut 表:id、product_type、product_name、total_duration、total_page

    Prodcut 表数据形如:

    • (1,"电影", "阿甘正传", 120, NULL)
    • (2,"图书", "简爱", NULL, 300)

缺点:

  • 没有任何的元信息来记录哪一个属性属于哪一个子类型

适用场景:

  • 当数据的子类型不多,以及子类型特殊属性不多
  • 使用 Active Record 模式来访问单表数据库时
(2)实体表继承

为每一个子类型建立一张独立的表。每一个表包含那些属于基类的共有属性,同时也包含子类型特殊化的属性。

例如:

  • ProdcutMovie 表:id、product_name、total_duration
  • ProdcutBook 表:id、product_name、total_page

缺点:

  • 很难将通用属性和子类特有的属性区分开来

    能够建立一个视图联合这些表,仅选择公共的列。

  • 若是将一个新的属性增长到通用属性中,必须为每一个子类表都加一遍。
(3)类表继承

此种方法模拟了继承,把表当成面向对象里的类。建立一张基类表,包含全部子类型的公共属性。对于每一个子类型,建立一个独立的表,经过外键和基类表相连。

这里须要用到数据库产品自带的表继承功能。

例如:

  • Prodcut 表:id、product_name
  • ProdcutMovie 表:id、total_duration
  • ProdcutBook 表:id、total_page
(4)半结构化数据模型

使用一个BLOB 列来存储数据,用 XML 或者 JSON 格式——同时包含了属性的名字和值。Martin Fowler 称这个模式为:序列化大对象块(Serialized BLOB)

优势:优异的扩展性

缺点:就是在这样的一个结构中,SQL 基本上没有办法获取某个指定的属性。你不能在一行blob 字段中简单地选择一个独立的属性,并对其进行限制、聚合运算、排序等其余操做。你必须获取整个blob 字段结构并经过程序去解码而且解释这些属性。

但如今的数据库,例如 PG,能够直接支持使用 JSON(B) or XML 的数据类型。因此不会存在必须整个获取再解析的麻烦了。

第七章 多态关联


一、需求

怎么声明一个指向多张表的外键?

例如,Comments 表的外键(issue_id)要引用 Bugs 表 or FeatureRequests 表。形如(这种写法是无效的):

FOREIGN KEY  (issue id)
REFERENCES Bugs (issue_id) OR FeatureRequests (issue_id)

二、反模式

有一个解决方案已经流行到足以正式命名了,那就是:多态关联。有时候也叫作杂乱关联。

例如:
除了 Comments 表 issue_id 这个外键以外,你必须再添加一列:issue_type,这个额外的列记录了当前行所引用的表名,取值范围是 "Bugs" / "FeatureRequests"。

缺点:没有任何保障数据完整性的手段来确保 Comments.issue_id 中的值在其父表中存在。

当你使用一个面向对象的框架(诸如Hibernate)时,多态关联彷佛是不可避免的。这种类型的框架经过良好的逻辑封装来减小使用多态关联的风险(即依赖上层程序代码而不是数据库的元数据)。若是你选择了一个成熟、有信誉的框架,那能够相信框架的做者已经完整地实现了相关的逻辑代码,不会形成错误。

三、推荐

(1)交叉表

把 Comments 表向下拆分,分出两个多的交叉表,即 BugsCommentsFeatureRequestsComments

(2)共用的超级表

基于 Bugs 表和 FeatureRequests 表,建立共用的超级表:Issues

第八章 多列属性


一、反模式 —— 可拓展的列

例如: 有一个 Bug 表,每一个 Bug 自身可能会有多个 tag。

CREATE TABLE Bug
bug_id SERIAL PRIMARY KEY
description VARCHAR (1000)
tagl VARCHAR (20)
tag2 VARCHAR (20)
tag3 VARCHAR (20)

每次要修改 Bug 自身的最大 tag 数,会动表结构,可拓展性不好。

二、推荐

在原有 Bug 表的基础上,再建立一个 BugTag 表。包含下面几列:

  • bug_id
  • tag

第九章 元数据分裂


一、反模式

用形如 Crevenue200二、Crevenue200三、Crevenue2004 的多列,来记录销售额。

这里的问题在于部分数据存在于列名中,即混淆了元数据和数据

还有一种常见的反模式是,将数据(年份)追加在基本表名以后。

二、推荐

若是是由于同一张表数据量太多致使这种反模式,建议:

(1)水平分区(or 分片)

你仅须要定义一些规则来拆分一张逻辑表,数据库会为你管理余下的全部事情。物理上来讲,表的确是被拆分了,但你依旧能够像查询单一表那样执行SQL 查询语句。

分区在 SQL 标准中并无定义,所以每一个不一样的数据库实现这一功能的方式都是非标准的。

(2)垂直分区(or 分片)

鉴于水平分区是根据行来对表进行拆分的,垂直分区就是根据列来对表进行拆分

好比说,会在Products 表中为每一个单独的产品存储一份安装文件。这种文件一般都很大,但BLOB 类型的列能够存储庞大的二进制数据。若是你有使用通配符“*”进行查询的习惯,那么将如此大的文件存储在Products 表中,并且又不常用,很容易就会在查询时遗漏这一点,从而形成没必要要的性能问题。

正确的作法是将BLOB 列存在另外一张表中,和Products 表分离但又与其相关联。

(3)建立关联表

把列转为行。

第十章 取整错误


一、为何

关于计算机二进制浮点数表示法致使的精度丢失和取整错误,能够看我这一篇:《关于 JavaScript 的 精度丢失 与 近似舍入》,原理是同样的。

二、怎么办

解决方案:使用 SQL 中的 NUMERICDECIMAL 类型来代替 FLOAT 及与其相似的数据类型进行固定精度的小数存储。

哪怕不是存小数而是存整数,也不要用 FLOAT!一样会存在错误隐患。

第十一章 每日新花样


需求:限定列的有效值

一、反模式

一、CHECK 约束

缺点:

  • 添删有效值不方便,须要从新 drop 并 create 约束。
  • 取列的有效值的 list 很麻烦,且不可复用。

二、域

缺点:属于数据库高级操做,杀鸡焉用牛刀。不赘述了。


三、用户自定义类型(UDT)

缺点:属于数据库高级操做,杀鸡焉用牛刀。不赘述了。

二、推荐

一、使用枚举类型 ENUM

优势:

  • 添删有效值很方便
  • 可复用。能够把有效值写在应用代码中,结合 ORM,便可以 for 数据库,也能够 for 前端显示(例如 展现在 select 组件)

二、建立一个单独的表,存列的有效值,其余表使用外键引用

优势:

  • 上面 ENUM 的优势都有。
  • 更加灵活、拓展性更强。

第十二章 幽灵文件


原始图片文件能够以二进制格式存储在 BLOB 类型中,就像以前咱们存储超长字段那样。

然而,不少人选择将图片存储在文件系统中,而后在数据库里用 VARCHAR 类型来记录对应的路径。这实际上是一种反模式

具体要不要用这种反模式,见仁见智,要按照具体使用场景来判断。

如今广泛仍是流行这种反模式,例如我司,由于静态资源都是上传到 OSS 托管,有 CDN 加成。

第十三章 乱用索引


第十四章 对未知的恐惧


一、需求:如何筛选出两个列值不相等的行?

假设:咱们有 test 表:

id left right
1 111 222
2 333 333
3 444 NULL
4 NULL 555
5 NULL NULL

正确结果是 id 为 一、三、4 的行。

二、反模式

错误方法:直接使用 where "left" != "right" ,但 != 对 NULL 无效。

正以下面这个例子:

select 1 != 1; #f
select 1 != 2; #t
select 1 != NULL; #null(不是咱们想要的结果,应该返回 t)
select NULL != NULL; #null(不是咱们想要的结果,应该返回 f)

后两种状况结果为 NULL,是由于 sql 是三值逻辑而不是二值逻辑,具体能够看我以前的一篇:《SQL基础教程》+《SQL进阶教程》学习笔记,里面有详细介绍。

三、推荐

(1)将 NULL 视为特殊值

将 NULL 视为特殊值,额外用 IS ( NOT ) NULL 判断:where "left" != "right" or ( "left" is null and "right" is not null ) or ( "left" is not null and "right" is null )

这种写法很累赘。

(2)IS ( NOT ) DISTINCT FROM

直接用 IS DISTINCT FROM,即:where "left" IS DISTINCT FROM "right" ,不须要额外对 NULL 判断。


IS ( NOT ) DISTINCT FROM 的支持状况:

每一个数据库对 IS ( NOT ) DISTINCT FROM 的支持是不一样的。PostgreSQL、IBM DB2 和 Firebird 直接支持它,Oracle 和 Microsoft SQL Server 暂时还不支持。MySQL 提供了一个专有的表达式 <=>,它的工做逻辑和 IS NOT DISTINCT FROM 一致。

第十五章 模棱两可的分组


一、反模式

例如,有 test 表:

id type name join_time
1 老师 赵老师 2020-01-01
2 老师 钱老师 2020-01-02
3 同窗 张三 2020-01-03
4 同窗 李四 2020-01-04
5 同窗 王五 2020-01-05

需求:咱们须要在 老师 or 同窗 分别里找出 join_time 最先的一条记录。

执行 SELECT "type", MIN("join_time"), "name" FROM "test" GROUP BY "type"

name 列就是有歧义的列,可能包含不可预测的和不可靠的数据:

  • 在 MySQL 中,返回的值是这一组结果中的第一条记录。
  • 在 Postgres 中,会报错。

二、推荐

解决方案:无歧义地使用列。

(1)只查询功能依赖的列

最直接的解决方案就是将有歧义的列排除出查询。

执行 SELECT "type" FROM "test" GROUP BY "type"

但这知足不了咱们的需求,pass。

(2)对额外的列使用聚合函数

执行 SELECT "type", MIN("join_time"), MIN("name") FROM "test" GROUP BY "type"

若是不能保证 MIN("join_time")MIN("name") 是指向同一行,那这个写法就是错的。有风险,pass。

(3)使用关联子查询
SELECT * FROM test as t1
WHERE NOT EXISTS 
(
	SELECT * FROM test as t2
	WHERE t1."type" = t2."type" and t1.join_time > t2.join_time 
)

缺点:性能很差。


[拓展] 用关联子查询写出来的思路:

涉及 SQL 基础的全程量化和存在量化的知识点,详细可参考个人旧文:《SQL基础教程》+《SQL进阶教程》学习笔记

若是需求变成:咱们须要在 老师 or 同窗 分别里找出 join_time 最晚的一条记录,那只须要把 t1.join_time > t2.join_time 变成 t1.join_time < t2.join_time 便可:

SELECT * FROM test as t1
WHERE NOT EXISTS 
(
	SELECT * FROM test as t2
	WHERE t1."type" = t2."type" and t1.join_time > t2.join_time 
)
(4)使用衍生表 JOIN
SELECT * FROM test as t1 
INNER JOIN
(
	SELECT "type", MIN("join_time") as "join_time" FROM test  
	GROUP BY "type" 
) as t2 
ON t1.join_time = t2.join_time

缺点:性能很差。


若是需求变成:咱们须要在 老师 or 同窗 分别里找出 join_time 最晚的一条记录,那只须要把 MIN("join_time") 变成 MAX("join_time") 便可:

SELECT * FROM test as t1 
INNER JOIN
(
	SELECT "type", MAX("join_time") as "join_time" FROM test  
	GROUP BY "type" 
) as t2 
ON t1.join_time = t2.join_time
(5)直接使用 LEFT JOIN
SELECT * FROM test as t1 
LEFT JOIN test as t2
ON t1."type" = t2."type" 
AND  
(
		t1.join_time > t2.join_time 
)
WHERE t2."id" IS NULL

解释:t1.join_time > t2.join_time 搭配 WHERE t2."id" IS NULL 是利用 LEFT JOIN 的特性,即若是找到匹配行则能够生成多行,但若找不到匹配行,则另外一边置 NUll。

缺点:性能稍好,可是较难维护。


若是需求变成:咱们须要在 老师 or 同窗 分别里找出 join_time 最晚的一条记录,那只须要把 t1.join_time > t2.join_time 变成 t1.join_time < t2.join_time 便可:

SELECT * FROM test as t1 
LEFT JOIN test as t2
ON t1."type" = t2."type" 
AND  
(
		t1.join_time < t2.join_time 
)
WHERE t2."id" IS NULL
(6)窗口函数
SELECT
	* 
FROM
	(
	SELECT
		*,
		RANK() OVER ( PARTITION BY "type" ORDER BY "join_time" ASC ) AS "rank" 
	FROM
	  test  
	) as t1
WHERE
	"t1"."rank" = 1

关于更多窗口函数的介绍,可看个人旧文:《SQL基础教程》+《SQL进阶教程》学习笔记


若是需求变成:咱们须要在 老师 or 同窗 分别里找出 join_time 最晚的一条记录,那只须要把 ASC 变成 DESC 便可:

SELECT
	* 
FROM
	(
	SELECT
		*,
		RANK() OVER ( PARTITION BY "type" ORDER BY "join_time" DESC ) AS "rank" 
	FROM
	  test  
	) as t1
WHERE
	"t1"."rank" = 1

第十六章 随机选择


相比于将整个数据集读入程序中再取出样例数据集,直接经过数据库查询拿出这些样例数据集会更好。

本章的目标就是要写出一个仅返回随机数据样本的高效 SQL 查询。

一、传统方法、random()

SELECT * FROM test ORDER BY random() limit 1

缺点:

  • 整个排序过程没法利用索引
  • 性能很差。好不容易对整个数据集完成排序,但绝大多数的结果都浪费了,由于除了返回第一行以外,其余结果都马上被丢弃了。

二、推荐方法一、从 1 到最大值之间随机选择

一种避免对全部数据进行排序的方法,就是在 1 到最大的主键值之间随机选择一个。

但要考虑 1 到最大值之间有缝隙的状况。

利用 JOIN

SELECT
	t1.* 
FROM
	test AS t1
	JOIN ( SELECT CEIL( random() * ( SELECT MAX ( "id" ) FROM test ) ) AS "id" ) AS t2
ON
	t1."id" >= t2."id"
ORDER BY
	t1."id" 
LIMIT 1

三、推荐方法二、使用偏移量选择随机行

计算总的数据行数,随机选择0 到总行数之间的一个值,而后用这个值做为位移来获取随机行。

利用 OFFSET

SELECT
	* 
FROM
	test 
	LIMIT 1 OFFSET ( 
		SELECT CEIL( 
			random() * ( SELECT COUNT ( * ) FROM test )
		) - 1 
	)

四、推荐方法三、专有解决方案

每种数据库均可能针对这个需求提供独有的解决方案:

-- Microsoft SQL Server 2005 增长了一个 TABLE-SAMPLE 子句。
-- Oracle 使用了一个相似的 SAMPLE 子句,好比返回表中1%的记录。
-- Postgres 也有相似的叫 TABLESAMPLE

可是这种采样的方法返回结果的行数很不稳定,感受仍是不推荐了。

第十七章 可怜人的搜索引擎


一、需求

全文搜索

二、反模式

使用 LIKE 或者正则表达式进行模式匹配搜索。

缺点:使用模式匹配操做符的最大缺点就在于性能问题。它们没法从传统的索引上受益,所以必须进行全表遍历。

三、推荐

解决方案:使用正确的工具。

(1)数据库扩展

每一个大品牌的数据库都有对全文搜索这个需求的解决方案。

例如,PostgreSQL 8.3 提供了一个复杂的可大量配置的方式,来将文本转化为可搜索的词聚集合,而且让这些文档可以进行模式匹配搜索。即,为了最大地提高性能,你须要将内容存两份:一份为原始文本格式,另外一份为特殊的 TSVECTOR 类型的可搜索格式。

空间换时间。

① 步骤:

建表时建立 TSVECTOR 数据类型的列。

② 步骤:

你须要确保 TSVECTOR 列的内容和你所想要搜索的列的内容同步。PostgreSQL 提供了一个内置的触发器来简化这一操做。

触发器写法略,可看原书。

③ 步骤:

你也应该同时在 TSVECTOR 列上建立一个反向索引(GIN)

写法略,可看原书。

④ 步骤:

在作完这一切以后,就能够在全文索引的帮助下使用PostgreSQL 的文本搜索操做符@@来高效地执行搜索查询。

写法略,可看原书。

(2)本身实现 反向索引

太复杂,略。

(3)第三方搜索引擎

你没必要使用 SQL 来解决全部问题。

两个产品:Sphinx SearchApache Lucene

使用略,可看原书。

第十八章 意大利面条式查询


一、反模式

一条精心设计的复杂 SQL 查询,相比于那些直接简单的查询来讲,不得不使用不少的JOIN、关联子查询和其余让 SQL 引擎难以优化和快速执行的操做符。而程序员直觉地认为越少的SQL 执行次数性能越好。

二、推荐

目标:减小 SQL 查询数量

解决方案:

  • 分而治之,一步一个脚印
  • 你能够将几个查询的结果进行 UNION 操做,从而最终获得一个结果集

好处:

  • 性能更好
  • 便于开发、维护

第十九章 隐式的列


一、反模式

我所遇到的程序员使用SQL通配符时问得最多的问题是:“有没有选择除了几个我不想要的列以外全部列的方法?

答案是“没有”。

其实我仍是但愿数据库厂商能加上,如今网上有不少 hack 的方法,需求毕竟是在的。诶。

二、推荐

解决方案:明确列出列名,而不是使用通配符或者隐式列的列表。

第二十章 明文密码


能够参考我以前的文章:《数据库里帐号的密码,须要怎样安全的存放?—— 密码哈希(Password Hash)》

第二十一章 SQL 注入


一、需求

防止 SQL 注入

二、反模式

(1)转义

好比,在PHP 的 PDO 扩展中,可使用一个 quote()函数来定义一个包含引号的字符串或者还原一个字符串中的引号字符。

三、推荐

解决方案:不信任任何人。

(1)check 数据
  • 过滤输入内容.好比在 PHP 中,可使用filter 扩展

    Node.js 的 joi 库。

  • 正则表达式来匹配安全的子串

    用上一条的过滤库也能够实现。

  • 用类型转换函数
(2)参数化动态内容

你应该使用查询参数将其和 SQL 表达式分离。

没有哪一种 SQL 注入的攻击可以改变一个参数化了的查询的语法结构。

缺点:

① 会影响优化器的效果,最终影响性能

好比说,假设在 Accounts 表中有一个 is_active 列。这一列中99%的记录都是真实值。对 is_active = false 的查询会得益于这一列上的索引,但对于 is_active = true 的查询却会在读取索引的过程当中浪费不少时间。然而,若是你用了一个参数 is_active = ? 来构造这个表达式,优化器不知道在预处理这条语句的时候你最终会传入哪一个值,所以颇有可能就选择了错误的优化方案。

要规避这样的问题,直接将变量内容插入到SQL 语句中会是更好的方法,不要去理会查询参数。一旦你决定这么作了,就必定要当心地引用字符串。

能够结合下面的 ”(3)将用户与代码隔离“ 一块儿使用。


② 这还不是一个通用的解决方案,由于查询参数总被视为是一个字面值

例如:

  • 多个值的列表不能够当成单一参数: 例如 in(x,x,x)

    解决方案:使用了一些 PHP 内置的数组函数来生成一个占位符数组。

  • 表名、列名、SQL 关键字 没法做为参数。

    解决方案:能够用存储过程;或者经过事先在应用逻辑代码里,先用字符串拼接的方式生成好 sql 代码。


③ 很差调试

这意味着,若是你获取到一个预先准备好的SQL 查询语句,它里面是不会包含任何实际的参数值的。当你调试或者记录查询时,很方便就能看到带有参数值的SQL 语句,但这些值永远不会以可读的SQL 形式整合到查询中去。

解决方案:调试动态化SQL 语句的最好方法,就是将准备阶段的带有占位符的查询语句和执行阶段传入的参数都记录下来。(本身动手,丰衣足食。)

(3)数据访问框架

你可能看过数据访问框架的拥护者声称他们的库可以抵御全部SQL 注入的攻击。对于全部容许你使用字符串方式传入SQL 语句的框架来讲,这都是扯淡。

没有任何框架能强制你写出安全的 SQL 代码。一个框架可能会提供一系列简单的函数来帮助你,但很容易就能绕开这些函数,而后使用一般的修改字符串的办法来编写不安全的SQL语句。

就是你得保证本身写的是符合框架规范的写法,否则人为因素仍是会致使出错。

(4)存储过程

(5)将用户与代码隔离

将请求的参数做为索引值去查找预先定义好的值,而后用这些预先定义好的值来组织 SQL 查询语句。

例如把请求的参数通过 if else ,来分配进预先定义好的 SQL 查询语句。

(6)找个可靠的人来帮你审查代码

找到瑕疵的最好方法就是再找一双眼睛一块儿来盯着看。

有条件能够结对编程。

第二十二章 伪键洁癖


一、反模式

不能忍受主键中间出现不连续的缺位。

重用主键并非一个好主意,由于断档每每是因为一些合理的删除或者回滚数据所形成的。

二、推荐

(1)克服内心障碍

它们不必定非得是连续值才能用来标记行。

将伪键当作行的惟一性标识,但它们不是行号。别把主键值和行号混为一谈。


问:怎么抵挡一个但愿清理数据库中伪键断档的老板的请求?
答:这是一个沟通方面的问题,而不是技术问题

(2)使用GUID

GUID (Globally Unique Identifier 全局惟一标识符)是一个128 位的伪随机数(一般使用32 个十六进制字符表示)。

GUID 也称 UUID (Universally unique identifier 通用惟一标识符)。

GUID 相比传统的伪键生成方法来讲,至少有以下两个优点:

  • 能够在多个数据库服务器上并发地生成伪键,而不用担忧生成一样的值。
  • 没有人会再抱怨有断档——他们会忙于抱怨输入32 个十六进制字符作主键。

第二十三章 非礼勿视


一、反模式 —— 忽略错误处理

“我不会让错误处理弄乱了个人代码结构的。”

致使问题:

  • 代码健壮性很差
  • 出现错误很差回溯
  • 用户体验差(用户看不见代码,他们只能看见输出。当一个致命错误没有被处理时,用户就只能看到一
    个白屏,或者是一个不完整的异常信息。)

二、推荐 —— 优雅地从错误中恢复

一些计算机科学家推测在一个稳固的程序中,至少有50%的代码是用来进行错误处理的

全部喜欢跳舞的人都知道,跳错舞步是不可避免的。优雅的秘诀就是弄明白怎么挽回。给本身一个了解错误产生缘由的机会,而后就能够快速响应,在任何人注意到你出丑以前,神不知鬼不觉地回到应有的节奏上。

第二十四章 外交豁免权


一、反模式

技术债务(technical debt),是程序设计及软件工程中的一个比喻。指开发人员为了加速软件开发,在应该采用最佳方案时进行了妥协,改用了短时间内能加速软件开发的方案,从而在将来给本身带来的额外开发负担。这种技术上的选择,就像一笔债务同样,虽然眼前看起来能够获得好处,但必须在将来偿还。软件工程师必须付出额外的时间和精力持续修复以前的妥协所形成的问题及反作用,或是进行重构,把架构改善为最佳实现方式。

二、推荐

  • 画实体关系图(ER 图),更复杂一点的ER 图包含了列、主键、索引和其余数据库对象。

    还有些工具可以经过SQL 脚本或者运行中的数据库直接经过反向工程获得 ER 图。

  • 写文档
  • 源代码管理
  • 测试

[拓展] 版本管理 之 管理数据库:

版本管理工具管理了代码,但并无管理数据库。Ruby on Rails 提供了一种技术叫作“迁移”,用来将版本控制应用到数据库实例的升级管理上。

大多数其余的网站开发框架,包括PHP 的Doctrine、Python 的Django 以及微软的 ASP.NET,都支持相似于Rails 的“迁移”这样的特性。

我目前 Node.js 用的 sequelize 就包含了这种数据库的迁移脚本。

缺点:但它们还不是完美的,只能处理一些简单类型的结构变动。并且从根本上说,它们在原有版本控制服务以外又创建了一个版本系统。

第二十五章 魔豆


好词好句


所谓专家,就是在一个很小的领域里把全部错误都犯过了的人。 —— 尼尔斯·玻尔

规范仅仅在它有帮助时才是好的。

Mitch Ratcliffe 说:“计算机是人类历史中最容易让你犯更多错误的发明……除了手枪和龙舌兰以外。”

相关文章
相关标签/搜索