zoukankan      html  css  js  c++  java
  • SQL Antipatterns——SQL 反模式(一)

    最近设计家谱树的时候,找了相关资料,一般都是用的递归模式,其实还有一种SQL Antipatterns 的设计思路,以下会分两章来讲解SQL 反模式。

    假设你是一个著名的科技新闻网站的软件开发者。这是一个现代化的网站,读者可以发表评论,甚至互相回复,

    形成深层次的讨论线。你可以选择一个简单的解决方案来跟踪这些回复链:每个评论都引用它回复的评论。

    CREATE TABLE Comments (

      comment_id SERIAL PRIMARY KEY,

      parent_id BIGINT UNSIGNED,

      comment TEXT NOT NULL,

      FOREIGN KEY (parent_id) REFERENCES Comments(comment_id)

    );

    但是,很快就可以清楚地看到,很难在单个SQL查询中检索到一长串答复。你仅仅能得到直接的子节点或可能要加入的孙节点。

    然而这些线可以有无限的深度。你需要运行许多SQL查询才能获得给定那条线上的所有评论。

    你的另一个想法是检索所有的评论,并将它们组装到应用程序内存中的树数据结构中,使用你在学校中学到的传统树算法。

    但是网站的出版商告诉你,他们每天发表数十篇文章,每篇文章都可以有数百条评论。

    每次有人浏览该网站时,都会对数百万条评论进行排序,很显然这是不切实际的。

    必须要有更好的方法来存储这些评论线,这样你才能简单有效地检索整条讨论线。

    一、目标:存储和查询层次结构

    数据具有递归关系是很常见的。数据可以以树状或分层的方式被组织化。

    在树状数据结构中,每个条目称为节点。一个节点可能有若干个子节点和一个父节点。

    顶部节点没有父节点,称为根节点。底部的节点没有子节点,称为叶子节点。中间的节点叫做非叶节点。

    在以前的层次结构数据中,你可能需要查询单个项、集合的相关子集或整个集合。

    树型数据结构的实例包括以下:

    1、组织结构图:员工与管理者的关系是树型数据结构的典型例子。它出现在和SQL相关的无数书籍和文章中。

    在组织结构图中,每个员工有一个管理者,在树型结构中代表员工的父节点,同样管理者也是一名员工。、

    2、以线型方式组织的讨论:树型结构可用于评论链,以回应其他评论。在树中,评论节点的子节点是它的答复。

    以下我们将以线型方式组织的讨论,来举例展示反模式及其解决方案。

    二、反模式:总是依赖于父节点

    很多书籍和文章中,常见的解决方案是添加一个parent_id列。此列引用同一表中的另一个评论,

    你可以创建一个外键约束以强制执行他的关系。SQL表结构如下:

    CREATE TABLE Comments (

      comment_id SERIAL PRIMARY KEY,

      parent_id BIGINT UNSIGNED,

      bug_id BIGINT UNSIGNED NOT NULL,

      author BIGINT UNSIGNED NOT NULL,

      comment_date DATETIME NOT NULL,

      comment TEXT NOT NULL,

      FOREIGN KEY (parent_id) REFERENCES Comments(comment_id), FOREIGN KEY (bug_id) REFERENCES Bugs(bug_id),

      FOREIGN KEY (author) REFERENCES Accounts(account_id)

    );

    实体关系图如下:

    #3.1#

     

    这种设计称为邻接表。它可能是开发人员用来存储分层数据结构最常见的设计方式。

    下表是一些显示评论层次结构的示例数据,以及实例的树如下图3.2所示

    #3.2#

     

    #3.2#

     

    用邻接表查询树

    当邻接表是许多开发人员的默认选择时,它可能是反模式的,但它不能解决你需要使用树来执行的最常见任务之一:查询所有后代。

    你可以使用一个相对简单的查询,检索一个评论及其直接子节点:

    SELECT c1.*, c2.*

    FROM Comments c1 LEFT OUTER JOIN Comments c2

    ON c2.parent_id = c1.comment_id;

    但是,这只查询两层结构的树。树的一个特点是它可以扩展到任意深度,所以你需要能够查询所有后代,而不考虑层次数。

    例如,你可能需要计算评论的总数:COUNT() 或 机械组件中部件成本的总和:SUM()

    当你使用邻接表时,这种查询是很尴尬的,因为树的每个层级对应于另一个连接,而SQL查询中的连接数必须是固定的。

    以下查询检索一棵深度可达四层的树,但不能检索超过此深度的树:

    SELECT c1.*, c2.*, c3.*, c4.*

    FROM Comments c1 -- 1st level

    LEFT OUTER JOIN Comments c2

    ON c2.parent_id = c1.comment_id -- 2nd level

    LEFT OUTER JOIN Comments c3

    ON c3.parent_id = c2.comment_id -- 3rd level

    LEFT OUTER JOIN Comments c4

    ON c4.parent_id = c3.comment_id; -- 4th level

    这个查询也很尴尬,因为它通过添加更多的列,来包含更深层次的后代。这使得计算诸如count()之类的聚合函数变得困难。

    从邻接表中查询树型结构的另一种方式,是检索集合中的所有行,然后重建应用程序中的层次结构,才能把它当作树型结构使用。

    SELECT * FROM Comments WHERE bug_id = 1234;

    在进行分析之前,将大量的数据从数据库复制到应用程序,这是非常低效的。

    你可能只需要一个子树,而不是从顶部开始的整个树。你可能只需要数据的集合信息,如评论的计数COUNT()

    用邻接表维护树

    诚然,有些操作很容易用邻接表来完成。

    例如添加一个新的叶子节点:

    INSERT INTO Comments (bug_id, parent_id, author, comment) VALUES (1234, 7, 'Kukla' , 'Thanks!' );

    迁移单个节点或子树也很容易:

    UPDATE Comments SET parent_id = 3 WHERE comment_id = 6;

    但是,从树中删除节点要复杂得多。如果要删除整个子树,则必须要多个查询才能找到所有后代节点。

    然后从最底层删除后代节点,以满足外键的完整性。

    SELECT comment_id FROM Comments WHERE parent_id = 4; -- returns 5 and 6

    SELECT comment_id FROM Comments WHERE parent_id = 5; -- returns none

    SELECT comment_id FROM Comments WHERE parent_id = 6; -- returns 7

    SELECT comment_id FROM Comments WHERE parent_id = 7; -- returns none

    DELETE FROM Comments WHERE comment_id IN ( 7 );

    DELETE FROM Comments WHERE comment_id IN ( 5, 6 );

    DELETE FROM Comments WHERE comment_id = 4;

    你可以使用外键与ON DELETE CASCADE 修饰符自动匹配,只要你知道你总是想删除后代节点,而不是提升或重新定位他们。

    如果你想要删除一个非叶节点并提升它的子节点,或者将它们移动到树中的另一个位置,那么首先需要更改子节点的parent_id,然后删除所需的节点。

    SELECT parent_id FROM Comments WHERE comment_id = 6; -- returns 4

    UPDATE Comments SET parent_id = 4 WHERE parent_id = 6;

    DELETE FROM Comments WHERE comment_id = 6;

    以上是在使用邻接表设计时需要多个步骤的操作示例。这是你为了让数据库应该更简单、更高效,而必须要编写的代码。

    三、如何辨别是否使用反模式?

    如果你听到下面这样的问题,那么这是一个树的反模式正在被使用的线索:

    --我们需要在树模型中支持多少层?

    在不使用递归查询的情况下,你很难查询某个节点的所有祖先或者所有子后代节点,你可能妥协地仅支持有限深度的一棵树,

    但是自然联想到下一个问题,多深是足够深?

    --我害怕碰到管理树数据结构的代码。

    你已经采用了一种更复杂的管理层次结构解决方案,但你使用的是错误的解决方案。

    每种技术都使一些任务更容易,但通常以其他任务变得更复杂为代价。

    对于在应用中使用层次结构的方式,你可能选择了一个不是最佳选择的解决方案。

    --我需要定期运行一个脚本来清理树中孤立的行。

    你的应用程序在树中删除非叶节点时,会创建断开连接的节点。当你在数据库中存储复杂数据结构时,

    在进行任何更改后,你需要将结构保持在一个一致并且有效的状态。你可以使用后面介绍的解决方案之一,

    以及触发器和级联外键约束。来存储具有弹性的数据结构,而不是脆弱的数据结构。

    四、合理使用反模式

    邻接表设计可以很好地支持你在应用程序中需要做的工作。它的强大之处是检索指定节点的直接父节点或子节点。

    插入行也很容易。如果这是你对分层数据所需要的全部操作,那么邻接表可以很好地处理。

    一些RDBMS支持对SQL的扩展,来支持存储在邻接表格式中的层次结构。SQL99标准在普通表语法后使用with关键字来定义递归查询。

    WITH CommentTree

    (comment_id, bug_id, parent_id, author, comment, depth)

    AS (

    SELECT *, 0 AS depth FROM Comments WHERE parent_id IS NULL

    UNION ALL

    SELECT c.*, ct.depth+1 AS depth FROM CommentTree ct

    JOIN Comments c ON (ct.comment_id = c.parent_id)

    )

    SELECT * FROM CommentTree WHERE bug_id = 1234;

    MicrosoftSQLServer 2005Oracle11gIBMDB 2PostgreSQL8.4支持使用公共表表达式的递归查询,如前面所示。

    MySQLSQLiteInformix是不支持这种语法的数据库品牌的例子。

    对于仍然被广泛使用的Oracle 10g也是如此。我们可能会假设递归查询语法将能用于所有流行品牌,然后使用邻接表将不会有如此限制。

    Oracle 9i10g支持With 语句,但不是递归查询。然后,有专用的语法:START WITH CONNECT BY PRIOR。可以使用此语法执行递归查询::

    SELECT * FROM Comments

    START WITH comment_id = 9876

    CONNECT BY PRIOR parent_id = comment_id;

    本文翻译自 Bill_Karwin-SQL_Antipatterns-EN 书籍第三章

    如有转载,请标明来源:http://www.cnblogs.com/wjq310/p/8849852.html

  • 相关阅读:
    hdoj 2803 The MAX【简单规律题】
    hdoj 2579 Dating with girls(2)【三重数组标记去重】
    hdoj 1495 非常可乐【bfs隐式图】
    poj 1149 PIGS【最大流经典建图】
    poj 3281 Dining【拆点网络流】
    hdoj 3572 Task Schedule【建立超级源点超级汇点】
    hdoj 1532 Drainage Ditches【最大流模板题】
    poj 1459 Power Network【建立超级源点,超级汇点】
    hdoj 3861 The King’s Problem【强连通缩点建图&&最小路径覆盖】
    hdoj 1012 u Calculate e
  • 原文地址:https://www.cnblogs.com/wjq310/p/8849852.html
Copyright © 2011-2022 走看看