数据库表主键设计原则

阅读《阿里 Java 开发手册》时,有以下 MySQL 建表规约:

表必备三字段:id, gmt_create, gmt_modified。 说明:其中id必为主键,类型为unsigned
bigint、单表时自增、步长为1。gmt_create, gmt_modified的类型均为date_time类型。

这样的要求似乎是违反数据设计的范式的,但既然手册中强调这点就搜索学习了一下这样做的理由以及相关的表设计原则。

  1. 数据库设计的三大范式
    ◆ 第一范式(1NF):强调的是列的原子性,即列不能够再分成其他几列。简而言之,第一范式就是无重复的列
    ◆ 第二范式(2NF):首先要满足它是1NF,另外还需要包含两部分内容:一是表必须有一个主键;二是没有包含在主键中的列必须完全依赖于主键,而不能只依赖于主键的一部分。简而言之,第二范式就是非主属性非部分依赖于主关键字
    ◆ 第三范式(3NF):首先是 2NF,另外非主键列必须直接依赖于主键,不能存在传递依赖。即不能存在:非主键列 A 依赖于非主键列 B,非主键列 B 依赖于主键的情况。简而言之,第三范式(3NF)要求一个数据库表中不包含已在其它表中已包含的非主关键字信息。
  2. 反范式主键的设计原则
    ◆ 主键应当是对用户没有意义的。业务上的‘主键’可以通过唯一键(Unique Key)或唯一索引(Unique Index)和其它约束条件实现
    ◆ 主键应该是单列的,以便提高连接和筛选操作的效率
    ◆ 不要更新主键。实际上,因为主键除了惟一地标识一行之外再没有其他的用途了,所以也就没有理由去对它更新。另外,主键的值通常不重用,意味着记录被删除后,该主键值不再使用
    ◆ 主键不应包含动态变化的数据,如时间戳、创建时间列、修改时间列等
    ◆ 主键应当由计算机自动生成。文章 Choose a Primary Key: Natural or Surrogate 中介绍了几种常见的实现代理主键的方式。
    1) 数据库自动生成,例如自增列或者 SEQUENCE;
    2) MAX() + 1,设定主键列为整数列,从 1 开始,每次有插入新列时,将其主键值设定为当前最大值加 1。这种方式的每次都要找到当前最大键值,会影响效率;
    3) Universally unique identifiers (UUIDs). UUIDs are 128-bit values that are created from a hash of the ID of your Ethernet card, or an equivalent software representation, and the current datetime of your computer system. The algorithm for doing this is defined by the Open Software Foundation;
    4) Globally unique identifiers (GUIDs). GUIDs are a Microsoft standard that extend UUIDs, following the same strategy if an Ethernet card exists and if not then they hash a software ID and the current datetime to produce a value that is guaranteed unique to the machine that creates it;
    5) High-low strategy. The basic idea is that your key value, often called a persistent object identifier (POID) or simply an object identified (OID), is in two logical parts: A unique HIGH value that you obtain from a defined source and an N-digit LOW value that your application assigns itself. Each time that a HIGH value is obtained the LOW value will be set to zero. For example, if the application that you’re running requests a value for HIGH it will be assigned the value 1701. Assuming that N, the number of digits for LOW, is four then all persistent object identifiers that the application assigns to objects will be combination of 17010000,17010001, 17010002, and so on until 17019999. At this point a new value for HIGH is obtained, LOW is reset to zero, and you continue again. If another application requests a value for HIGH immediately after you it will given the value of 1702, and the OIDs that will be assigned to objects that it creates will be 17020000, 17020001, and so on. As you can see, as long as HIGH is unique then all POID values will be unique.
    反范式跟范式所要求的正好相反,在反范式的设计模式,我们可以允许适当的数据的冗余,用这个冗余去取操作数据时间的缩短。本质上就是用空间来换取时间,把数据冗余在多个表中,当查询时可以减少或者是避免表之间的关联(手册中也有禁止三表以上 JOIN 的条款)。
    但需要注意的是,采用代理主键是为了避免业务逻辑变化导致主键变更,以及提高 JOIN 效率等。但在实际查询中,数据查询还是需要通过业务上的唯一键进行匹配的,而不应该将代理主键作为查询条件,尤其不能将代理主键作为查询条件输入项提供给用户。
  3. 唯一键(Unique Key)和唯一索引(Unique Index)
    从设计意图来看,如果目的是为了强调业务规则上的唯一性,则应该使用唯一键;如果只是为了提高查询效率,则应该使用唯一索引。从数据库系统的角度看,索引是存储在数据库中的物理结构;而键只是一种逻辑概念。从用户的角度看,二者的效果是一致的。如果表 B 想要使用的表 A 的列作为其外键,那么表 A 中对应的列应该是唯一键而不能是唯一索引——因为从定义上,外键也属于业务规则相关的概念,它应当关联另一个表的候选键。(By definition a foreign key must reference a candidate key of some table. It doesn’t necessarily have to be the primary key.)
    一般地,创建唯一键或主键的同时,数据库都会自动生成对应的唯一索引。主键列不允许空值,但是唯一键和唯一索引列都是允许空值存在的,另外可以删除唯一键上的索引,但是保留唯一键,只是此时唯一键只剩下了概念,并不能保证数据实际的唯一性了。
  4. 外键
    外键用来保持数据的一致性和完整性,主要目的是根据参考表来控制拥有其列作为外键的表中的数据,是两张表形成关联。同时可设定参考表和外键表之间的级联操作(ON DELETE 和 ON UPDATE)。关于外键,《阿里 Java 开发手册》不上有以下的说明可以参考。

不得使用外键与级联,一切外键概念必须在应用层解决。
说明:(概念解释)学生表中的student_id是主键,那么成绩表中的student_id则为外键。如果更新学生表中的student_id,同时触发成绩表中的student_id更新,则为级联更新。外键与级联更新适用于单机低并发,不适合分布式、高并发集群;级联更新是强阻塞,存在数据库更新风暴的风险;外键影响数据库的插入速度。

总体来说,实际应用中应当具体问题具体分析,结合范式和反范式两种设计思想。对数据一致性和完整性较高、而对查询效率要求并不严格的地方,应当更倾向于遵从范式;而类似分布式、高并发集群的场景,则应当更考虑反范式的设计方案。具体主键的选取原则与策略,请参考文章 Choose a Primary Key: Natural or Surrogate

你可能感兴趣的:(数据库开发)