mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-22 19:26:41 +08:00
426 lines
26 KiB
HTML
426 lines
26 KiB
HTML
<!DOCTYPE html>
|
||
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
|
||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||
<head>
|
||
<head>
|
||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
|
||
<link rel="icon" href="/static/favicon.png">
|
||
<title>05 表结构设计:忘记范式准则.md.html</title>
|
||
<!-- Spectre.css framework -->
|
||
<link rel="stylesheet" href="/static/index.css">
|
||
<!-- theme css & js -->
|
||
<meta name="generator" content="Hexo 4.2.0">
|
||
</head>
|
||
<body>
|
||
<div class="book-container">
|
||
<div class="book-sidebar">
|
||
<div class="book-brand">
|
||
<a href="/">
|
||
<img src="/static/favicon.png">
|
||
<span>技术文章摘抄</span>
|
||
</a>
|
||
</div>
|
||
<div class="book-menu uncollapsible">
|
||
<ul class="uncollapsible">
|
||
<li><a href="/" class="current-tab">首页</a></li>
|
||
</ul>
|
||
<ul class="uncollapsible">
|
||
<li><a href="../">上一级</a></li>
|
||
</ul>
|
||
<ul class="uncollapsible">
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/00 开篇词 从业务出发,开启海量 MySQL 架构设计.md.html">00 开篇词 从业务出发,开启海量 MySQL 架构设计</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/01 数字类型:避免自增踩坑.md.html">01 数字类型:避免自增踩坑</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/02 字符串类型:不能忽略的 COLLATION.md.html">02 字符串类型:不能忽略的 COLLATION</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/03 日期类型:TIMESTAMP 可能是巨坑.md.html">03 日期类型:TIMESTAMP 可能是巨坑</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/04 非结构存储:用好 JSON 这张牌.md.html">04 非结构存储:用好 JSON 这张牌</a>
|
||
</li>
|
||
<li>
|
||
<a class="current-tab" href="/专栏/MySQL实战宝典/05 表结构设计:忘记范式准则.md.html">05 表结构设计:忘记范式准则</a>
|
||
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/06 表压缩:不仅仅是空间压缩.md.html">06 表压缩:不仅仅是空间压缩</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/07 表的访问设计:你该选择 SQL 还是 NoSQL?.md.html">07 表的访问设计:你该选择 SQL 还是 NoSQL?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/08 索引:排序的艺术.md.html">08 索引:排序的艺术</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/09 索引组织表:万物皆索引.md.html">09 索引组织表:万物皆索引</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/10 组合索引:用好,性能提升 10 倍!.md.html">10 组合索引:用好,性能提升 10 倍!</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/11 索引出错:请理解 CBO 的工作原理.md.html">11 索引出错:请理解 CBO 的工作原理</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/12 JOIN 连接:到底能不能写 JOIN?.md.html">12 JOIN 连接:到底能不能写 JOIN?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/13 子查询:放心地使用子查询功能吧!.md.html">13 子查询:放心地使用子查询功能吧!</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/14 分区表:哪些场景我不建议用分区表?.md.html">14 分区表:哪些场景我不建议用分区表?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/15 MySQL 复制:最简单也最容易配置出错.md.html">15 MySQL 复制:最简单也最容易配置出错</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/16 读写分离设计:复制延迟?其实是你用错了.md.html">16 读写分离设计:复制延迟?其实是你用错了</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/17 高可用设计:你怎么活用三大架构方案?.md.html">17 高可用设计:你怎么活用三大架构方案?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/18 金融级高可用架构:必不可少的数据核对.md.html">18 金融级高可用架构:必不可少的数据核对</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/19 高可用套件:选择这么多,你该如何选?.md.html">19 高可用套件:选择这么多,你该如何选?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/20 InnoDB Cluster:改变历史的新产品.md.html">20 InnoDB Cluster:改变历史的新产品</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/21 数据库备份:备份文件也要检查!.md.html">21 数据库备份:备份文件也要检查!</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/22 分布式数据库架构:彻底理解什么叫分布式数据库.md.html">22 分布式数据库架构:彻底理解什么叫分布式数据库</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/23 分布式数据库表结构设计:如何正确地将数据分片?.md.html">23 分布式数据库表结构设计:如何正确地将数据分片?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/24 分布式数据库索引设计:二级索引、全局索引的最佳设计实践.md.html">24 分布式数据库索引设计:二级索引、全局索引的最佳设计实践</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/25 分布式数据库架构选型:分库分表 or 中间件 ?.md.html">25 分布式数据库架构选型:分库分表 or 中间件 ?</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/26 分布式设计之禅:全链路的条带化设计.md.html">26 分布式设计之禅:全链路的条带化设计</a>
|
||
</li>
|
||
<li>
|
||
<a href="/专栏/MySQL实战宝典/27 分布式事务:我们到底要不要使用 2PC?.md.html">27 分布式事务:我们到底要不要使用 2PC?</a>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
|
||
<div class="sidebar-toggle-inner"></div>
|
||
</div>
|
||
<script>
|
||
function add_inner() {
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
inner.classList.add('show')
|
||
}
|
||
function remove_inner() {
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
inner.classList.remove('show')
|
||
}
|
||
function sidebar_toggle() {
|
||
let sidebar_toggle = document.querySelector('.sidebar-toggle')
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
let content = document.querySelector('.off-canvas-content')
|
||
if (sidebar_toggle.classList.contains('extend')) { // show
|
||
sidebar_toggle.classList.remove('extend')
|
||
sidebar.classList.remove('hide')
|
||
content.classList.remove('extend')
|
||
} else { // hide
|
||
sidebar_toggle.classList.add('extend')
|
||
sidebar.classList.add('hide')
|
||
content.classList.add('extend')
|
||
}
|
||
}
|
||
function open_sidebar() {
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
sidebar.classList.add('show')
|
||
overlay.classList.add('show')
|
||
}
|
||
function hide_canvas() {
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
sidebar.classList.remove('show')
|
||
overlay.classList.remove('show')
|
||
}
|
||
</script>
|
||
<div class="off-canvas-content">
|
||
<div class="columns">
|
||
<div class="column col-12 col-lg-12">
|
||
<div class="book-navbar">
|
||
<!-- For Responsive Layout -->
|
||
<header class="navbar">
|
||
<section class="navbar-section">
|
||
<a onclick="open_sidebar()">
|
||
<i class="icon icon-menu"></i>
|
||
</a>
|
||
</section>
|
||
</header>
|
||
</div>
|
||
<div class="book-content" style="max-width: 960px; margin: 0 auto;
|
||
overflow-x: auto;
|
||
overflow-y: hidden;">
|
||
<div class="book-post">
|
||
<p id="tip" align="center"></p>
|
||
<div><h1>05 表结构设计:忘记范式准则</h1>
|
||
<p>前面几讲我虽然带你了解了数字类型、字符串、日期类型,以及非结构化的 JSON 类型,但也只是每条记录每个字段的选择。</p>
|
||
<p>而我们在对一张表进行设计时,还要遵守一些基本的原则,比如你经常听见的“范式准则”。但范式准则过于理论,在真实业务中,你不必严格遵守三范式的要求。而且有时为了性能考虑,你还可以进行反范式的设计,比如在数据仓库领域。这一讲我就会带你了解这些内容,希望你学完这一讲之后,能从更高一层的维度来看待 MySQL 数据库的表结构设计。</p>
|
||
<h3>忘记范式准则</h3>
|
||
<p>相信你在大学学习《数据库系统概论》时,肯定学习过关系数据库的设计规范,比如第一范式、第二范式、第三范式,BC 范式等,它们是《数据库系统概论》考试中重要的考点。</p>
|
||
<p>范式设计是非常重要的理论,是通过数学集合概念来推导范式的过程,在理论上,要求表结构设计必须至少满足三范式的要求。</p>
|
||
<p>由于完全是数据推导过程,范式理论非常枯燥,<strong>但你只要记住几个要点就能抓住其中的精髓:</strong></p>
|
||
<ul>
|
||
<li>一范式要求所有属性都是不可分的基本数据项;</li>
|
||
<li>二范式解决部分依赖;</li>
|
||
<li>三范式解决传递依赖。</li>
|
||
</ul>
|
||
<p>虽然我已经提炼了范式设计的精髓,但要想真正理解范式设计,就要抛弃纯理论的范式设计准则,<strong>从业务角度出发,设计出符合范式准则要求的表结构。</strong></p>
|
||
<h3>工程上的表结构设计实战</h3>
|
||
<p>真实的业务场景是工程实现,表结构设计做好以下几点就已经足够:</p>
|
||
<ul>
|
||
<li>每张表一定要有一个主键(方法有自增主键设计、UUID 主键设计、业务自定义生成主键);</li>
|
||
<li>消除冗余数据存在的可能。</li>
|
||
</ul>
|
||
<p>我想再次强调一下,你不用过于追求所谓的数据库范式准则,甚至有些时候,我们还会进行反范式的设计。</p>
|
||
<h4>自增主键设计</h4>
|
||
<p>主键用于唯一标识一行数据,所以一张表有主键,就已经直接满足一范式的要求了。在 01 讲的整型类型中,我提及可以使用 BIGINT 的自增类型作为主键,同时由于整型的自增性,数据库插入也是顺序的,性能较好。</p>
|
||
<p>但你要注意,使用 BIGINT 的自增类型作为主键的设计仅仅适合<strong>非核心业务表</strong>,比如告警表、日志表等。<strong>真正的核心业务表,一定不要用自增键做主键</strong>,主要有 6 个原因:</p>
|
||
<ul>
|
||
<li>自增存在回溯问题;</li>
|
||
<li>自增值在服务器端产生,存在并发性能问题;</li>
|
||
<li>自增值做主键,只能在当前实例中保证唯一,<strong>不能保证全局唯一</strong>;</li>
|
||
<li>公开数据值,容易引发安全问题,例如知道地址<a href="http://www.example.com/customers/10/?fileGuid=xxQTRXtVcqtHK6j8">http://www.example.com/User/10/</a>,很容猜出 User 有 11、12 依次类推的值,容易引发数据泄露;</li>
|
||
<li>MGR(MySQL Group Replication) 可能引起的性能问题;</li>
|
||
<li>分布式架构设计问题。</li>
|
||
</ul>
|
||
<p>自增存在回溯问题,我在 01 讲中已经讲到,如果你想让核心业务表用自增作为主键,MySQL 数据库版本应该尽可能升级到 8.0 版本。</p>
|
||
<p>又因为自增值是在 MySQL 服务端产生的值,需要有一把自增的 AI 锁保护,若这时有大量的插入请求,就可能存在自增引起的性能瓶颈。比如在 MySQL 数据库中,参数 innodb_autoinc_lock_mode 用于控制自增锁持有的时间。假设有一 SQL 语句,同时插入 3 条带有自增值的记录:</p>
|
||
<pre><code>INSERT INTO ... VALUES (NULL,...),(NULL,...),(NULL,...);
|
||
</code></pre>
|
||
<p>则参数 innodb_autoinc_lock_mode 的影响如下所示:</p>
|
||
<p><img src="assets/CioPOWCZ5eqAfgY9AADxioZ-ayc577.png" alt="图片1.png" /></p>
|
||
<p>从表格中你可以看到,一条 SQL 语句插入 3 条记录,参数 innodb_autoinc_lock_mode 设置为 1,自增锁在这一条 SQL 执行完成后才释放。</p>
|
||
<p>如果参数 innodb_autoinc_lock_mode 设置为2,自增锁需要持有 3 次,每插入一条记录获取一次自增锁。</p>
|
||
<ul>
|
||
<li><strong>这样设计好处是:</strong> 当前插入不影响其他自增主键的插入,可以获得最大的自增并发插入性能。</li>
|
||
<li><strong>缺点是:</strong> 一条 SQL 插入的多条记录并不是连续的,如结果可能是 1、3、5 这样单调递增但非连续的情况。</li>
|
||
</ul>
|
||
<p><strong>所以,如果你想获得自增值的最大并发性能,把参数 innodb_autoinc_lock_mode 设置为2。</strong></p>
|
||
<p>虽然,我们可以调整参数 innodb_autoinc_lock_mode获得自增的最大性能,但是由于其还存在上述 5 个问题。因此,在互联网海量并发架构实战中,<strong>我更推荐 UUID 做主键或业务自定义生成主键。</strong></p>
|
||
<h4>UUID主键设计</h4>
|
||
<p>UUID(Universally Unique Identifier)代表全局唯一标识 ID。显然,由于全局唯一性,你可以把它用来作为数据库的主键。</p>
|
||
<p>MySQL 数据库遵循 <a href="https://tools.ietf.org/html/rfc4122?fileGuid=xxQTRXtVcqtHK6j8">DRFC 4122</a> 命名空间版本定义的 Version 1规范,可以通过函数 UUID自动生成36字节字符。如:</p>
|
||
<pre><code>mysql> SELECT UUID();
|
||
+--------------------------------------+
|
||
| UUID() |
|
||
+--------------------------------------+
|
||
| e0ea12d4-6473-11eb-943c-00155dbaa39d |
|
||
+--------------------------------------+
|
||
</code></pre>
|
||
<p>根据 Version 1的规范,MySQL中的 UUID 由以下几个部分组成:</p>
|
||
<pre><code>UUID = 时间低(4字节)- 时间中高+版本(4字节)- 时钟序列 - MAC地址
|
||
</code></pre>
|
||
<p>前 8 个字节中,60 位用于存储时间,4 位用于 UUID 的版本号,其中时间是从 1582-10-15 00:00:00.00 到现在的<strong>100ns 的计数。</strong></p>
|
||
<p>60 位的时间存储中,其存储分为:</p>
|
||
<ul>
|
||
<li>时间低位(time-low),占用 12 位;</li>
|
||
<li>时间中位(time-mid),占用 2 字节,16 位;</li>
|
||
<li>时间高位(time-high),占用 4 字节,32 位;</li>
|
||
</ul>
|
||
<p><strong>需要特别注意的是,在存储时间时,UUID 是根据时间位逆序存储,</strong> 也就是低时间低位存放在最前面,高时间位在最后,即 UUID 的前 4 个字节会随着时间的变化而不断“随机”变化,并非单调递增。而非随机值在插入时会产生离散 IO,从而产生性能瓶颈。这也是 UUID 对比自增值最大的弊端。</p>
|
||
<p>为了解决这个问题,MySQL 8.0 推出了函数 UUID_TO_BIN,它可以把 UUID 字符串:</p>
|
||
<ul>
|
||
<li>通过参数将时间高位放在最前,解决了 UUID 插入时乱序问题;</li>
|
||
<li>去掉了无用的字符串"-",精简存储空间;</li>
|
||
<li>将字符串其转换为二进制值存储,空间最终从之前的 36 个字节缩短为了 16 字节。</li>
|
||
</ul>
|
||
<p>下面我们将之前的 UUID 字符串 e0ea12d4-6473-11eb-943c-00155dbaa39d 通过函数 UUID_TO_BIN 进行转换,得到二进制值如下所示:</p>
|
||
<pre><code>SELECT UUID_TO_BIN('e0ea12d4-6473-11eb-943c-00155dbaa39d',TRUE) as UUID_BIN;
|
||
+------------------------------------+
|
||
| UUID_BIN |
|
||
+------------------------------------+
|
||
| 0x11EB6473E0EA12D4943C00155DBAA39D |
|
||
+------------------------------------+
|
||
1 row in set (0.00 sec)
|
||
</code></pre>
|
||
<p>除此之外,MySQL 8.0 也提供了函数 BIN_TO_UUID,支持将二进制值反转为 UUID 字符串。</p>
|
||
<p>当然了,MySQL 8.0版本之前没有函数 UUID_TO_BIN/BIN_TO_UUID,但是你还是可以通过用户义函数(UDF)的方式解决,如创建下面的函数:</p>
|
||
<pre><code>CREATE FUNCTION MY_UUID_TO_BIN(_uuid BINARY(36))
|
||
RETURNS BINARY(16)
|
||
LANGUAGE SQL DETERMINISTIC CONTAINS SQL SQL SECURITY INVOKER
|
||
RETURN
|
||
UNHEX(CONCAT(
|
||
SUBSTR(_uuid, 15, 4),
|
||
SUBSTR(_uuid, 10, 4),
|
||
SUBSTR(_uuid, 1, 8),
|
||
SUBSTR(_uuid, 20, 4),
|
||
SUBSTR(_uuid, 25) ));
|
||
CREATE FUNCTION MY_BIN_TO_UUID(_bin BINARY(16))
|
||
RETURNS CHAR(36)
|
||
LANGUAGE SQL DETERMINISTIC CONTAINS SQL SQL SECURITY INVOKER
|
||
RETURN
|
||
LCASE(CONCAT_WS('-',
|
||
HEX(SUBSTR(_bin, 5, 4)),
|
||
HEX(SUBSTR(_bin, 3, 2)),
|
||
HEX(SUBSTR(_bin, 1, 2)),
|
||
HEX(SUBSTR(_bin, 9, 2)),
|
||
HEX(SUBSTR(_bin, 11)) ));
|
||
</code></pre>
|
||
<p>因此,对于 04 讲创建的表 User,可以将其主键修改为 BINARY(16),用于存储排序后的 16 字节的 UUID 值。其表结构修如下:</p>
|
||
<pre><code>CREATE TABLE User (
|
||
id BINARY(16) NOT NULL,
|
||
name VARCHAR(255) NOT NULL,
|
||
sex CHAR(1) NOT NULL,
|
||
password VARCHAR(1024) NOT NULL,
|
||
money INT NOT NULL DEFAULT 0,
|
||
register_date DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
|
||
last_modify_date DATETIME(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
|
||
uuid CHAR(36) AS (BIN_TO_UUID(id)),
|
||
CONSTRAINT chk_sex CHECK (sex = 'M' OR sex = 'F'),
|
||
PRIMARY KEY(id)
|
||
);
|
||
</code></pre>
|
||
<p>现在,你可以在客户端通过以下 SQL 命令插入数据,如:</p>
|
||
<pre><code>INSERT INTO User VALUES (UUID_TO_BIN(UUID(),TRUE),......);
|
||
</code></pre>
|
||
<p>当然,很多同学也担心 UUID 的性能和存储占用的空间问题,这里我也做了相关的插入性能测试,结果如下表所示:</p>
|
||
<p><img src="assets/CioPOWCbQCOAIdiZAACr8OD6suQ547.png" alt="图片3.png" /></p>
|
||
<p>可以看到,MySQL 8.0 提供的排序 UUID 性能最好,甚至比自增ID还要好。此外,由于UUID_TO_BIN转换为的结果是16 字节,仅比自增 ID 增加 8 个字节,最后存储占用的空间也仅比自增大了 3G。</p>
|
||
<p>而且由于 UUID 能保证全局唯一,因此使用 UUID 的收益远远大于自增ID。可能你已经习惯了用自增做主键,但在海量并发的互联网业务场景下,更推荐 UUID 这样的全局唯一值做主键。</p>
|
||
<p>比如,我特别推荐游戏行业的用户表结构设计,使用 UUID 作为主键,而不是用自增 ID。因为当发生合服操作时,由于 UUID 全局唯一,用户相关数据可直接进行数据的合并,而自增 ID 却需要额外程序整合两个服务器 ID 相同的数据,这个工作是相当巨大且容易出错的。</p>
|
||
<h4>业务自定义生成主键</h4>
|
||
<p>当然了,UUID 虽好,但是在分布式数据库场景下,主键还需要加入一些额外的信息,这样才能保证后续二级索引的查询效率(具体这部分内容将在后面的分布式章节中进行介绍)。<strong>现在你只需要牢记:分布式数据库架构,仅用 UUID 做主键依然是不够的。</strong> 所以,对于分布式架构的核心业务表,我推荐类似如下的设计,比如:</p>
|
||
<pre><code>PK = 时间字段 + 随机码(可选) + 业务信息1 + 业务信息2 ......
|
||
</code></pre>
|
||
<p>电商业务中,订单表是其最为核心的表之一,你可以先打开淘宝 App,查询下自己的订单号,可以查到类似如下的订单信息:</p>
|
||
<p><img src="assets/CioPOWCTkyaAfjdyAAWAHcsp4TA536.png" alt="png" /></p>
|
||
<p>上图是我自己的淘宝订单信息(第一个订单的订单号为1550672064762308113)。</p>
|
||
<p>订单号显然是订单表的主键,但如果你以为订单号是自增整型,那就大错特错了。因为如果你仔细观察的话,可以发现图中所有订单号的后 6 位都是相同的,都为<strong>308113</strong>:</p>
|
||
<pre><code>1550672064762308113
|
||
1481195847180308113
|
||
1431156171142308113
|
||
1431146631521308113
|
||
</code></pre>
|
||
<p>所以,我认为淘宝订单号的最后 6 位是用户 ID 相关信息,前 14 位是时间相关字段,这样能保证插入的自增性,又能同时保留业务的相关信息作为后期的分布式查询。</p>
|
||
<h4>消除冗余</h4>
|
||
<p>消除冗余也是范式的要求,解决部分依赖和传递依赖,本质就是尽可能减少冗余数据。</p>
|
||
<p>所以,在进行表结构设计时,数据只需存放在一个地方,其他表要使用,通过主键关联存储即可。比如订单表中需要存放订单对应的用户信息,则保存用户 ID 即可:</p>
|
||
<pre><code>CREATE TABLE Orders (
|
||
order_id VARCHRA(20),
|
||
user_id BINARY(16),
|
||
order_date datetime,
|
||
last_modify_date datetime
|
||
...
|
||
PRIMARY KEY(order_id),
|
||
KEY(user_id,order_date)
|
||
KEY(order_date),
|
||
KEY(last_modify_date)
|
||
)
|
||
</code></pre>
|
||
<p>当然了,无论是自增主键设计、UUID主键设计、业务自定义生成主键、还是消除冗余,本质上都是遵循了范式准则。但是在一些其他业务场景下,也存在反范式设计的情况。</p>
|
||
<h4>反范式设计</h4>
|
||
<p>通常我们会在 OLAP 数据分析场景中使用反范式设计,但随着 JSON 数据类型的普及,MySQL 在线业务也可以进行反范式的设计。</p>
|
||
<p>在 04 讲中我讲了表 UserTag,就是通过 JSON 数据类型进行了反范式的设计,如果通过范式设计,则表 UserTag 应该设计为:</p>
|
||
<pre><code>CREATE TABLE UserTag (
|
||
userId BIGINT NOT NULL,
|
||
userTag INT NOT NULL,
|
||
PRIMARY KEY(userId,userTag)
|
||
);
|
||
SELECT * FROM UserTag;
|
||
+--------+---------+
|
||
| userId | userTag |
|
||
+--------+---------+
|
||
| 1 | 2 |
|
||
| 1 | 6 |
|
||
| 1 | 8 |
|
||
| 1 | 10 |
|
||
| 2 | 3 |
|
||
| 2 | 10 |
|
||
| 2 | 12 |
|
||
+--------+---------+
|
||
</code></pre>
|
||
<p>你对比后可以发现,范式设计并没有使用 JSON 数据类型来得更为有效,使用 JSON 数据类型,userID 只需保存一次,从一定程度上减少了数据的冗余:</p>
|
||
<pre><code>+--------+---------------+
|
||
| userId | userTags |
|
||
+--------+---------------+
|
||
| 1 | [2, 6, 8, 10] |
|
||
| 2 | [3, 10, 12] |
|
||
+--------+---------------
|
||
</code></pre>
|
||
<h3>总结</h3>
|
||
<p>总的来说,范式是偏数据库理论范畴的表结构设计准则,在实际的工程实践上没有必要严格遵循三范式要求,<strong>在 MySQL 海量并发的工程实践上,表结构设计应遵循这样几个规范:</strong></p>
|
||
<ul>
|
||
<li>每张表一定要有一个主键;</li>
|
||
<li>自增主键只推荐用在非核心业务表,甚至应避免使用;</li>
|
||
<li>核心业务表推荐使用 UUID 或业务自定义主键;</li>
|
||
<li>一份数据应尽可能保留一份,通过主键关联进行查询,避免冗余数据;</li>
|
||
<li>在一些场景下,可以通过 JSON 数据类型进行反范式设计,提升存储效率;</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div style="float: left">
|
||
<a href="/专栏/MySQL实战宝典/04 非结构存储:用好 JSON 这张牌.md.html">上一页</a>
|
||
</div>
|
||
<div style="float: right">
|
||
<a href="/专栏/MySQL实战宝典/06 表压缩:不仅仅是空间压缩.md.html">下一页</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
|
||
</div>
|
||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709973046bb93d60","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||
</body>
|
||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
|
||
<script>
|
||
window.dataLayer = window.dataLayer || [];
|
||
function gtag() {
|
||
dataLayer.push(arguments);
|
||
}
|
||
gtag('js', new Date());
|
||
gtag('config', 'G-NPSEEVD756');
|
||
var path = window.location.pathname
|
||
var cookie = getCookie("lastPath");
|
||
console.log(path)
|
||
if (path.replace("/", "") === "") {
|
||
if (cookie.replace("/", "") !== "") {
|
||
console.log(cookie)
|
||
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
|
||
}
|
||
} else {
|
||
setCookie("lastPath", path)
|
||
}
|
||
function setCookie(cname, cvalue) {
|
||
var d = new Date();
|
||
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
|
||
var expires = "expires=" + d.toGMTString();
|
||
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
|
||
}
|
||
function getCookie(cname) {
|
||
var name = cname + "=";
|
||
var ca = document.cookie.split(';');
|
||
for (var i = 0; i < ca.length; i++) {
|
||
var c = ca[i].trim();
|
||
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
|
||
}
|
||
return "";
|
||
}
|
||
</script>
|
||
</html>
|