{
    "componentChunkName": "component---src-templates-blog-blog-detail-tsx",
    "path": "/blog/choice-between-tidb-and-mysql",
    "result": {"pageContext":{"blog":{"id":"Blogs_490","title":"一名开发者眼中的 TiDB 与 MySQL 的选择丨TiDB Community","tags":["MySQL","TiDB"],"category":{"name":"案例实践"},"summary":"随着 MySQL 8.0 的发布和即将到来的 5.7 版本的停止支持，许多 MySQL 用户正面临升级和转型的抉择。本文为 TiDB 社区用户撰写，以一名开发者的视角，深入探讨和比较了 TiDB 和 MySQL 的差异。","body":"> 本文作者：大数据模型。对制造业、银行业、通讯业了解多一点，关心专注国产数据库技术布道以及数据资产建设的应用实践。\n\n## 导读\n\n随着 MySQL 8.0 的发布和即将到来的 5.7 版本的停止支持，许多 MySQL 用户正面临升级和转型的抉择。本文为 TiDB 社区用户撰写，以一名开发者的视角，深入探讨和比较了 TiDB 和 MySQL 的差异。希望通过本文，能为读者在架构选型方面提供一些帮助和指导。\n\nTiDB 在墨天轮国产数据库排行榜中长年位列前茅，社区活跃度高且人气旺盛。那么 TiDB 使用场景相似产品中，有哪些比较优秀呢？我认为其中一个是 MySQL——毕竟在中国，MySQL 早已深入人心，并且工程师们能够轻松地运用它。\n\n## TiDB 与 MySQL 的对比\n\n有些人直接将 TiDB 称为\"大号的 MySQL\"，但实际情况并非如此。为了使工程师们能够像使用 MySQL 一样使用 TiDB，TiDB 在接口层进行了大量的改进。它在语法、表名、引用甚至元数据等方面尽量与 MySQL 保持一致，但是实际执行的每个语句背后都有不同的数据流程和服务流向。因此，尽管在表面上它们相似，但其背后的数据处理和服务机制是不同的。  \n\n**类型方面**，MySQL 是纯粹单机式数据库，TiDB 则是分布式数据库。TiDB 能够方便自由地增加节点来扩展存算能力，而 MySQL 则需要通过定向策略，如中间件路由或读写分离等方式来增加节点以提升性能，这使得 MySQL 的扩展性相对受限且相对僵化。\n\n**引擎方面**，MySQL 拥有多个引擎选项，如 MyISAM、InnoDB、Memory 等，并且可以通过插件支持更多的引擎，如 RocksDB 和 HandlerSocket 等。而 TiDB 虽然只有两个引擎选项，但却能够应对各种应用场景的需求。\n\n**架构方面** ， MySQL 是偏紧密耦合，分为接口层、服务层、存储层三个层次。接口层负责请求处理、授权认证和安全性，服务层负责查询解析、分析、优化、缓存和系统内置函数，存储层负责数据的存储和处理，所有这些组件都在一个服务进程中统一运行。**而 TiDB 采用松散耦合的架构，将数据库的关键组件进行抽象，并根据其分布式特性划分为计算层、存储层和协调层**。\n\n- TiDB 计算层类似 MySQL 的接口层，负责接收 SQL 请求，处理 SQL 相关的逻辑，并通过协调层找到存储层数据的位置。它与存储层进行交互以获取数据，并最终返回结果。\n- TiDB 的存储层负责数据的存储，其存储容量没有上限。通常情况下，存储层会为同一份数据维护 3 个副本；以满足高并发需求。协调层会对存储层中的数据进行负载均衡的处理。\n- TiDB 的协调层是集群的管理模块，其主要工作包括三个方面：管理集群的元信息、调度和负载均衡存储层的数据，以及分配全局唯一且递增的事务 ID。\n\n**数据处理技术上**，MySQL 是 **B+树**的组织存储结构，B+树适合读多写少，如果写多了，写的影响动作主要是插入、删除，会导致全局的平衡树下面的页频繁分裂或者合并，直接影响性能，影响读放大。TiDB 是 **LSM** 树的组织存储结构，擅长写多读少，如果读多了，在内存扫描不到数据，就会去硬盘里面去寻找无序的 sst 文件，所以数据越多越大就会读放大。\n\n**处理存储上**，MySQL 类似微内核，微内核架构由核心服务和插件模块组成，核心服务负责请求后处理机制流程并进行优化，插件模块主要用来放置 置处理存储的引擎，引擎决定性能上限， 微内核的插件式对开发者友好，可以自由扩展，所以 MySQL 派生了 infobright、MyRocks 等第三方相关引擎，TiDB 的核心服务分散在 TiDB 模块和 PD 模块里面，两者协同工作构成请求解析、处理、优化及其它服务功能 ， TiKV 模块和 TiFlash 模块则是引擎。无论是顺序读写还是随机读写，核心服务协同背端的引擎工作串成整个数据全链条过程，MySQL 是在单机单进程的内部去完成这个过程的，而 TiDB 是分布式多进程完成这个过程的。\n\n**产品方面**，MySQL 默认使用 InnoDB 引擎，擅长处理 OLTP 的业务场景。同时，MySQL 还支持插件组装各种引擎，使其成为一个通用型的数据库产品，适用于各种业务场景。而 TiDB 默认采用悲观事务的方式，同样专注于 OLTP，也是一个通用型的数据库产品。然而，这两者之间存在一些差异。由于 MySQL 是单机型结构，如果需要进行扩展，只能通过数据库中间件路由的方式进行划分。而如果数据已满，就需要停机或停服，重新进行数据的分割。\n\nTiDB 具有对业务的无侵入性，且扩展非常简单。在其发展至今，安装和维护方面已经非常成熟。通过 TiUP 工具，可以轻松进行分布式集群的组装和维护操作，并且支持在线升级和无缝迁移。这使得使用 TiDB 的过程更加便捷和高效，使用户能够更好地管理和运维他们的分布式数据库系统。\n\n综上所述，TiDB 与 MySQL 属于不同类型的数据产品，并不能直接进行对比。然而，从数据库的特性和市场趋势的角度来看，它们可以有一些维度上的对比指标。事实上，TiDB 致力于向 MySQL 学习，并且还聘请了 InnoDB 的核心开发工程师，致力于调整 TiDB 的底盘，使其在内部和外部都更像 MySQL。\n\n## 同类竞争产品\n\nTiDB 是一款**分布式数据库产品**，它以分布式为标识并能基于线下安装，在国内外都有类似的产品。那么 TiDB 与其他产品有什么不同？参照数据库处理的流程，我将从任务开始到任务结束来详述。  \n\n1. 用户发起请求：数据库客户端向指定的数据库集群发起请求。\n\n2. 目标数据库响应：数据库集群的指定节点响应用户的请求。\n\n3. 两者建立会话：数据库集群其中一个节点与客户端产生会话。\n\n4. 对象请求解析 ：数据库对接收到的请求进行语法检查、对象解析，并将其转换为对应的关系代数结构，然后进行计划任务优化。\n\n5. 调度并且执行：寻找最合适的副本，根据优先级进行，是内存、缓存、数据快照、存储等等。\n\n6. 监测任务状态：数据库监测执行中任务的状态。\n\n7. 返回数据结果：数据库服务端将执行结果返回给数据库客户端。\n\n上述环节中，最关键的是第 2 步、第 4 步和第 5 步。\n\n第 2 步是 哪一个节点响应数据库客户的请求，分布式数据库有两种系统架构，一种是中心化架构【master\\slave】，一种是去中心化架构。中心化架构的负色职责分清，负责干活、负责指挥、负责接待用户，而去中心化架构则是每个节点角色平等，对待客户的请求，其中的一个节点会瞬间切换成负责接待，剩余的节点根据情况转化执行。\n\nTiDB 在这里采用中心化的架构，节点角色之间的职责更加清晰，分工更加明确。\n\n第 4 步和第 5 步是数据计算和数据存储的关键步骤，TiDB 在这里做了深度的松散解耦，数据计算用 TiDB，数据存储用 TiKV，两者是真正意义上的存算分离，要增加存储容量，可以增加没有 CPU 的硬盘服务器，要增加计算能力，可以增加没有硬盘的服务器。关于分布式的功能和作用则集中在一个 PD 的模块上。\n\n采用集中式的分布式架构的产品则采用了去中心架构，而且计算和存储高度耦合，又称为单机式的分布式架构。\n\nTiDB 比起同类产品在架构上更加高度松散耦合，与云计算技术更加紧密协作，珠联璧合。\n\n## TiDB vs MySQL\n\n如果 TiDB 要做大做强，必须要撼动广大开发人员的工作使用习惯。大部分开发人员已经十分熟悉并广泛使用 MySQL，无论是在 TP 应用还是 AP 应用中。不论性能如何，他们首先会选择 MySQL 来开发业务代码。这也意味着 MySQL 经常被用作 HTAP 数据库。接下来，我将使用 CH-benchmark 来对 TiDB 6.0 和 MySQL 8.0 进行一项测试。\n\n**TPC-CH 由未经修改的 TPC-C 模型和事务、以及 TPC-H 查询的改编版本构成，TPC-CH 保持所有 TPC-C 实体和关系完全不变**，并集成了 TPC-H 模型中的 SUPPLIER、REGION 和 NATION 表。这些表在 TPC-H 查询中频繁使用，并允许以非侵入的方式集成到 TPC-C 模型中。SUPPLIER 包含固定数量（10,000 条）的条目。因此，STOCK 中的一条记录可以通过 STOCK.S I ID × STOCK.S W ID mod 10, 000 = SUPPLIER.SU SUPPKEY 与其唯一的供应商（SUPPLIER 表中对应记录）关联起来。TPC-C 中的原始 CUSTOMER 表不包含引用自 NATION 表的外键。我们并没有改变原始模型，从而保持了与现有 TPC-C 的兼容性，所以外键是从字段 C STATE 的第一个字符开始计算的。TPC-C 规定第一个字符可以有 62 个不同的值（即大写字母、小写字母、数字），因此我们选择了 62 个国家来填充 NATION。根据 TPC-H 规范，主键 N NATIONKEY 是一个标识符。它的值被规定，从而使得与这些值相关联的 ASCII 值是一个字母或数字,即 N NATIONKEY ∈ [48, 57]∪[65, 90]∪[97. 122]。因此，不需要额外的计算来跳过 ASCII 码中数字、大写字母和小写字母之间的间隔。不支持从字符转换到 ASCII 码的数据库系统可能会偏离 TPC-H 模式，使用单个字符作为 NATION 的主键。REGION 包含国家的五个地区。新表之间的关系使用简单的外键字段来建模：NATION.N REGIONKEY 和 SUPPLIER.SU NATIONKEY。\n\n**在 CH-Benchmark 中结合了 TPC-C 和 TPC-H 两种基准**，它把原来 TPC-C 中的 9 个表和 TPC-H 中的 8 个表修改合并成了 12 个表，并将两者的伸缩模型也统一起来（Scaling TPC-H by the same factors of TPC-C）。\n\n### 测试环境\n\n#### 硬件配置\n\n| 操作系统  | CentOS Linux release 7.6.1810  |\n|:----------|:----------|\n| CPU    | 8 核 Intel(R) Xeon(R)    |\n| 内存  | 16    |\n\n#### 测试配置\n\n| 软件版本  | IP  | 作用  |\n|:----------|:----------|:----------|\n| MySQL 8.0    | 192.168.1.X    | MySQL 单机    |\n| TiDB 6.0    | 192.168.1.X    | TiDB 单机    |\n| CH-benchmark    | 192.168.2.X    | HTAP 测试工具，生成数据    |\n| TiUP Bench    | 192.168.2.X    | HTAP 测试工具，进行测试    |\n\n## 生成数据\n\n在我的实验中，我使用了 TiDB Bench 对数据进行了压测，生成这些数据的工具是 CH-benchmark。\n\n**安装 CH-benchmark**\n\n```\nhttps://github.com/DASLab-IDA/CH-benchmark\n-rwxr-xr-x. 1 root root 1007440 Mar 29 16:13 chBenchmark\n-rw-r--r--. 1 root root   12745 Mar  3  2022 chBenchmark.cpp\n-rw-r--r--. 1 root root  194096 Mar 29 16:13 chBenchmark.o\n-rw-r--r--. 1 root root     561 Mar  3  2022 LICENSE\n-rw-r--r--. 1 root root    1167 Mar  3  2022 Makefile\n-rw-r--r--. 1 root root    2650 Mar  3  2022 README.md\ndrwxr-xr-x. 3 root root    4096 Mar 29 16:13 src\n[root@hdp1 CH-benchmark-main]# make\n\n运行make之后会就对当天的文件进行编译，生成chBenchmark 执行命令\n\nchBenchmark命令如下\nCreate initial database as CSV files:\n    chBenchmark\n    -csv\n    -wh <WAREHOUSE_COUNT>\n    -pa <INITIAL_DB_GEN_PATH>\n\n    example: chBenchmark -csv -wh 50 -pa /path/to/any/directory\n    \n生成数据如下,生成一个warehouse的数据\n\nchBenchmark -csv -wh 1 -pa  /tmp/chBenchmark1\n```\n\n### 建表语句  \n\n```\nCREATE TABLE `customer` (\n  `c_id` int NOT NULL,\n  `c_d_id` int NOT NULL,\n  `c_w_id` int NOT NULL,\n  `c_first` varchar(16) DEFAULT NULL,\n  `c_middle` char(2) DEFAULT NULL,\n  `c_last` varchar(16) DEFAULT NULL,\n  `c_street_1` varchar(20) DEFAULT NULL,\n  `c_street_2` varchar(20) DEFAULT NULL,\n  `c_city` varchar(20) DEFAULT NULL,\n  `c_state` char(2) DEFAULT NULL,\n  `c_zip` char(9) DEFAULT NULL,\n  `c_phone` char(16) DEFAULT NULL,\n  `c_since` datetime DEFAULT NULL,\n  `c_credit` char(2) DEFAULT NULL,\n  `c_credit_lim` decimal(12,2) DEFAULT NULL,\n  `c_discount` decimal(4,4) DEFAULT NULL,\n  `c_balance` decimal(12,2) DEFAULT NULL,\n  `c_ytd_payment` decimal(12,2) DEFAULT NULL,\n  `c_payment_cnt` int DEFAULT NULL,\n  `c_delivery_cnt` int DEFAULT NULL,\n  `c_data` varchar(500) DEFAULT NULL,\n  PRIMARY KEY (`c_w_id`,`c_d_id`,`c_id`),\n  KEY `idx_customer` (`c_w_id`,`c_d_id`,`c_last`,`c_first`)\n);\n\n\nCREATE TABLE `district` (\n  `d_id` int NOT NULL,\n  `d_w_id` int NOT NULL,\n  `d_name` varchar(10) DEFAULT NULL,\n  `d_street_1` varchar(20) DEFAULT NULL,\n  `d_street_2` varchar(20) DEFAULT NULL,\n  `d_city` varchar(20) DEFAULT NULL,\n  `d_state` char(2) DEFAULT NULL,\n  `d_zip` char(9) DEFAULT NULL,\n  `d_tax` decimal(4,4) DEFAULT NULL,\n  `d_ytd` decimal(12,2) DEFAULT NULL,\n  `d_next_o_id` int DEFAULT NULL,\n  PRIMARY KEY (`d_w_id`,`d_id`)\n);\n\n\nCREATE TABLE `history` (\n  `h_c_id` int NOT NULL,\n  `h_c_d_id` int NOT NULL,\n  `h_c_w_id` int NOT NULL,\n  `h_d_id` int NOT NULL,\n  `h_w_id` int NOT NULL,\n  `h_date` datetime DEFAULT NULL,\n  `h_amount` decimal(6,2) DEFAULT NULL,\n  `h_data` varchar(24) DEFAULT NULL,\n  KEY `idx_h_w_id` (`h_w_id`),\n  KEY `idx_h_c_w_id` (`h_c_w_id`)\n);\n\n\nCREATE TABLE `item` (\n  `i_id` int NOT NULL,\n  `i_im_id` int DEFAULT NULL,\n  `i_name` varchar(24) DEFAULT NULL,\n  `i_price` decimal(5,2) DEFAULT NULL,\n  `i_data` varchar(50) DEFAULT NULL,\n  PRIMARY KEY (`i_id`)\n);\n\nCREATE TABLE `nation` (\n  `n_nationkey` tinyint NOT NULL,\n  `n_name` char(25) NOT NULL,\n  `n_regionkey` tinyint NOT NULL,\n  `n_comment` char(152) NOT NULL,\n  PRIMARY KEY (`n_nationkey`)\n);\n\nCREATE TABLE `new_order` (\n  `no_o_id` int NOT NULL,\n  `no_d_id` int NOT NULL,\n  `no_w_id` int NOT NULL,\n  PRIMARY KEY (`no_w_id`,`no_d_id`,`no_o_id`)\n);\n\n\nCREATE TABLE `orderline` (\n  `ol_o_id` int NOT NULL,\n  `ol_d_id` tinyint NOT NULL,\n  `ol_w_id` int NOT NULL,\n  `ol_number` tinyint NOT NULL,\n  `ol_i_id` int DEFAULT NULL,\n  `ol_supply_w_id` int DEFAULT NULL,\n  `ol_delivery_d` date DEFAULT NULL,\n  `ol_quantity` smallint DEFAULT NULL,\n  `ol_amount` decimal(6,2) DEFAULT NULL,\n  `ol_dist_info` char(24) DEFAULT NULL,\n  PRIMARY KEY (`ol_w_id`,`ol_d_id`,`ol_o_id`,`ol_number`),\n  KEY `fk_orderline_order` (`ol_w_id`,`ol_d_id`,`ol_o_id`),\n  KEY `fk_orderline_stock` (`ol_supply_w_id`,`ol_i_id`)\n);\n\n\nCREATE TABLE `orders` (\n  `o_id` int NOT NULL,\n  `o_d_id` int NOT NULL,\n  `o_w_id` int NOT NULL,\n  `o_c_id` int DEFAULT NULL,\n  `o_entry_d` datetime DEFAULT NULL,\n  `o_carrier_id` int DEFAULT NULL,\n  `o_ol_cnt` int DEFAULT NULL,\n  `o_all_local` int DEFAULT NULL,\n  PRIMARY KEY (`o_w_id`,`o_d_id`,`o_id`),\n  KEY `idx_order` (`o_w_id`,`o_d_id`,`o_c_id`,`o_id`)\n);\n\n\n CREATE TABLE `region` (\n  `r_regionkey` tinyint NOT NULL,\n  `r_name` char(55) NOT NULL,\n  `r_comment` char(152) NOT NULL,\n  PRIMARY KEY (`r_regionkey`)\n);\n\n\nCREATE TABLE `stock` (\n  `s_i_id` int NOT NULL,\n  `s_w_id` int NOT NULL,\n  `s_quantity` int DEFAULT NULL,\n  `s_dist_01` char(24) DEFAULT NULL,\n  `s_dist_02` char(24) DEFAULT NULL,\n  `s_dist_03` char(24) DEFAULT NULL,\n  `s_dist_04` char(24) DEFAULT NULL,\n  `s_dist_05` char(24) DEFAULT NULL,\n  `s_dist_06` char(24) DEFAULT NULL,\n  `s_dist_07` char(24) DEFAULT NULL,\n  `s_dist_08` char(24) DEFAULT NULL,\n  `s_dist_09` char(24) DEFAULT NULL,\n  `s_dist_10` char(24) DEFAULT NULL,\n  `s_ytd` int DEFAULT NULL,\n  `s_order_cnt` int DEFAULT NULL,\n  `s_remote_cnt` int DEFAULT NULL,\n  `s_data` varchar(50) DEFAULT NULL,\n  PRIMARY KEY (`s_w_id`,`s_i_id`)\n) ;\n\n\nCREATE TABLE `supplier` (\n  `s_suppkey` smallint NOT NULL,\n  `s_name` char(25) NOT NULL,\n  `s_address` char(40) NOT NULL,\n  `s_nationkey` tinyint NOT NULL,\n  `s_phone` char(15) NOT NULL,\n  `s_acctbal` decimal(12,2) NOT NULL,\n  `s_comment` char(101) NOT NULL,\n  PRIMARY KEY (`s_suppkey`)\n);\n\n\nCREATE TABLE `warehouse` (\n  `w_id` int NOT NULL,\n  `w_name` varchar(10) DEFAULT NULL,\n  `w_street_1` varchar(20) DEFAULT NULL,\n  `w_street_2` varchar(20) DEFAULT NULL,\n  `w_city` varchar(20) DEFAULT NULL,\n  `w_state` char(2) DEFAULT NULL,\n  `w_zip` char(9) DEFAULT NULL,\n  `w_tax` decimal(4,4) DEFAULT NULL,\n  `w_ytd` decimal(12,2) DEFAULT NULL,\n  PRIMARY KEY (`w_id`)\n);\n```\n\n### 导入数据  \n\n```\n导入数据前，注意要对tidb运行以下命令\n\nALTER DATABASE tpcch SET tiflash replica 1;\n\nmysql> SELECT * FROM information_schema.tiflash_replica WHERE TABLE_SCHEMA = 'tpcch';\n+--------------+------------+----------+---------------+-----------------+-----------+----------+\n| TABLE_SCHEMA | TABLE_NAME | TABLE_ID | REPLICA_COUNT | LOCATION_LABELS | AVAILABLE | PROGRESS |\n+--------------+------------+----------+---------------+-----------------+-----------+----------+\n| tpcch        | customer   |       90 |             1 |                 |         0 |        0 |\n| tpcch        | district   |       93 |             1 |                 |         0 |        0 |\n| tpcch        | history    |       96 |             1 |                 |         0 |        0 |\n| tpcch        | item       |       99 |             1 |                 |         0 |        0 |\n| tpcch        | nation     |      102 |             1 |                 |         0 |        0 |\n| tpcch        | new_order  |      105 |             1 |                 |         0 |        0 |\n| tpcch        | neworder   |      108 |             1 |                 |         0 |        0 |\n| tpcch        | order      |      111 |             1 |                 |         0 |        0 |\n| tpcch        | order_line |      113 |             1 |                 |         0 |        0 |\n| tpcch        | orderline  |      115 |             1 |                 |         0 |        0 |\n| tpcch        | orders     |      117 |             1 |                 |         0 |        0 |\n| tpcch        | region     |      119 |             1 |                 |         0 |        0 |\n| tpcch        | stock      |      121 |             1 |                 |         0 |        0 |\n| tpcch        | warehouse  |      125 |             1 |                 |         0 |        0 |\n| tpcch        | supplier   |      128 |             1 |                 |         0 |        0 |\n+--------------+------------+----------+---------------+-----------------+-----------+----------+\n15 rows in set (0.00 sec)\n\n\n\nLOAD DATA local INFILE   '/tmp/chBenchmark1/CUSTOMER.tbl' INTO TABLE tpcch.customer   FIELDS TERMINATED BY '|';     \nLOAD DATA local INFILE   '/tmp/chBenchmark1/DISTRICT.tbl' INTO TABLE tpcch.district   FIELDS TERMINATED BY '|';     \nLOAD DATA local INFILE   '/tmp/chBenchmark1/HISTORY.tbl' INTO TABLE tpcch.history   FIELDS TERMINATED BY '|';     \nLOAD DATA local INFILE   '/tmp/chBenchmark1/ITEM.tbl' INTO TABLE tpcch.item   FIELDS TERMINATED BY '|';     \nLOAD DATA local INFILE   '/tmp/chBenchmark1/NATION.tbl' INTO TABLE tpcch.nation   FIELDS TERMINATED BY '|';     \nLOAD DATA local INFILE   '/tmp/chBenchmark1/NEWORDER.tbl' INTO TABLE tpcch.new_order   FIELDS TERMINATED BY '|';     \nLOAD DATA local INFILE   '/tmp/chBenchmark1/ORDER.tbl' INTO TABLE tpcch.orders   FIELDS TERMINATED BY '|';     \nLOAD DATA local INFILE   '/tmp/chBenchmark1/ORDERLINE.tbl' INTO TABLE tpcch.orderline   FIELDS TERMINATED BY '|';     \nLOAD DATA local INFILE   '/tmp/chBenchmark1/REGION.tbl' INTO TABLE tpcch.region   FIELDS TERMINATED BY '|';     \nLOAD DATA local INFILE   '/tmp/chBenchmark1/STOCK.tbl' INTO TABLE tpcch.stock   FIELDS TERMINATED BY '|';     \nLOAD DATA local INFILE   '/tmp/chBenchmark1/SUPPLIER.tbl' INTO TABLE tpcch.supplier   FIELDS TERMINATED BY '|';     \nLOAD DATA local INFILE   '/tmp/chBenchmark1/WAREHOUSE.tbl' INTO TABLE tpcch.warehouse   FIELDS TERMINATED BY '|';    \n```\n\n### 运行压测命令  \n\n192.168.2.x上面安装tiup bench  \n\n```\ntiup install bench\n持续对TIDB的数据库tpcch发起50个TP并发量，并进行一次AP的21个语句查询\ntiup bench ch --host 192.168.1.x  -Uhenley  -pxxxxxx -P4000 --warehouses 1 run -D tpcch -T 50 -t 1 --time 1m\n\n持续对mysql的数据库tpcch发起50个TP并发量，并进行一次AP的21个语句查询\ntiup bench ch --host 192.168.1.x  -Uhenley  -pxxxxxx -P3306 --warehouses 1 run -D tpcch -T 50 -t 1 --time 1m\n```\n\n### 测试摘要  \n\n```\ntiup bench ch --host 192.168.1.x  -Uhenley  -pxxxxxx -P3306 --warehouses 1 run -D tpcch -T 50 -t 1 --time 1m\n\n\ntpmC: 4168.9, tpmTotal: 9231.7, efficiency: 32417.2%\n[Summary] Q1     - Count: 1, Sum(ms): 67.7, Avg(ms): 67.7\n[Summary] Q10    - Count: 1, Sum(ms): 30.6, Avg(ms): 30.6\n[Summary] Q11    - Count: 1, Sum(ms): 558.8, Avg(ms): 558.6\n[Summary] Q12    - Count: 1, Sum(ms): 19.8, Avg(ms): 19.8\n[Summary] Q13    - Count: 1, Sum(ms): 163.9, Avg(ms): 163.9\n[Summary] Q14    - Count: 1, Sum(ms): 14.5, Avg(ms): 14.5\n[Summary] Q15_ERR - Count: 1, Sum(ms): 277.5, Avg(ms): 277.5\n[Summary] Q2     - Count: 1, Sum(ms): 1360.0, Avg(ms): 1359.5\n[Summary] Q3     - Count: 1, Sum(ms): 90.3, Avg(ms): 90.3\n[Summary] Q4     - Count: 1, Sum(ms): 116.4, Avg(ms): 116.4\n[Summary] Q5     - Count: 1, Sum(ms): 204.9, Avg(ms): 204.8\n[Summary] Q6     - Count: 1, Sum(ms): 18.9, Avg(ms): 18.9\n[Summary] Q7     - Count: 1, Sum(ms): 21.2, Avg(ms): 21.3\n[Summary] Q8     - Count: 1, Sum(ms): 195.2, Avg(ms): 195.1\n[Summary] Q9     - Count: 1, Sum(ms): 265.0, Avg(ms): 265.0\nQphH: 62.5\n\n\ntiup bench ch --host 192.168.2.xx  -Uhenley  -pP@xxx -P4000 --warehouses 1 run -D tpcch -T 50 -t 1 --time 1m\ntpmC: 1805.5, tpmTotal: 4092.0, efficiency: 14039.7%\n[Summary] Q1     - Count: 1, Sum(ms): 145.6, Avg(ms): 145.6\n[Summary] Q10    - Count: 1, Sum(ms): 275.2, Avg(ms): 275.1\n[Summary] Q11    - Count: 1, Sum(ms): 330.5, Avg(ms): 330.4\n[Summary] Q12    - Count: 1, Sum(ms): 124.3, Avg(ms): 124.4\n[Summary] Q13    - Count: 1, Sum(ms): 98.4, Avg(ms): 98.4\n[Summary] Q14    - Count: 1, Sum(ms): 275.1, Avg(ms): 275.1\n[Summary] Q15_ERR - Count: 1, Sum(ms): 1.1, Avg(ms): 1.1\n[Summary] Q2     - Count: 1, Sum(ms): 469.7, Avg(ms): 469.6\n[Summary] Q3     - Count: 1, Sum(ms): 283.8, Avg(ms): 283.8\n[Summary] Q4     - Count: 1, Sum(ms): 481.1, Avg(ms): 481.2\n[Summary] Q5     - Count: 1, Sum(ms): 256.7, Avg(ms): 256.7\n[Summary] Q6     - Count: 1, Sum(ms): 98.1, Avg(ms): 98.1\n[Summary] Q7     - Count: 1, Sum(ms): 192.4, Avg(ms): 192.3\n[Summary] Q8     - Count: 1, Sum(ms): 143.1, Avg(ms): 143.1\n[Summary] Q9     - Count: 1, Sum(ms): 667.7, Avg(ms): 667.7\nQphH: 62.4\n```\n\n### 测试总结  \n\n保留对 MySQL 8.0 和 TiDB 6.0 的内部参数不变， 单纯从单机 load data 数据插入、 tpmC 性能、以及 tpc-h 的性能数据表面来看，MySQL 8.0 要比 TiDB 6.0 要好。然而，实际情况并非如此，因为 TiDB 还有很大的调优空间。正如前面提到的，它们是两个不同的产品线，但这里证明了 TiDB 的友好性。它是十分兼容 MySQL 的，如果你从单机版的 TiDB 开始，随着业务的扩大，你可以自由、轻松地进行扩展。\n\n## 我对 TiDB 的展望\n\n软件开发的角度，TiDB 的解耦是完整的，如今 TiDB 已经发展到了 7.0 版本。我对 TiDB 未来的期待有三个方面：\n- TiDB 模块源代码，可以做为分布式计算基础参考，派生更多的可能性，类似 presto 的路线延伸；\n- TiKV 模块源代码，可以作为分布式存储参考，以后的发展方向可能是文件数据存储；\n- PD 模块源代码的技术路径发展是轻量级的元数据存储的管理，三者兼进，TiDB 将能够最大化地帮助用户降低存储成本，提升计算弹性，通过分布式实现元数据最优存储，灵活、可靠，在更多场景得到应用。","date":"2023-05-19","author":"大数据模型","fillInMethod":"writeDirectly","customUrl":"choice-between-tidb-and-mysql","file":null,"relatedBlogs":[{"relatedBlog":{"body":"> CRM 并不是简单的销售和客户服务的效率工具。本质上，CRM 是以平台化思维实现业务管理和数据的打通，为 360 度客户旅程提供数字化支撑。\n\nIDC 发布的《IDC 中国企业级应用管理 SaaS 市场，2021H2 》报告显示：2021 年中国企业级应用管理  SaaS 市场规模达 67.8 亿美金，其中 CRM 市场份额为 32%，未来五年将以 23.1% 的 CAGR 快速增长。  \n\n虽然受到疫情影响，但随着企业数字化转型的不断深化，更多的企业需要用数字化手段、标准化流程来提高效率和节约成本。从中国企业级应用管理 SaaS 市场来看，平台化、数据化、生态化成为重要的发展趋势。CRM 作为云计算时代典型的数据密集型应用，提升海量数据的处理能力和时效性成为 CRM 服务商提升客户体验、构建核心竞争力的关键所在。\n\n## CRM 的数据架构面临重重挑战\n\n某中国企业级 CRM 头部服务商，其 CRM 产品支持从营销、销售到服务的全流程自动化业务场景，帮助企业转型为真正“以客户为中心”的数字化运营组织，实现业绩的可持续增长。随着产品能力的迭代加速和业务的规模化增长，该服务商实现了从中小企业到大中型企业的覆盖，为了应对不同行业用户的个性化需求推出了 PaaS 平台，基于平台的定制能力服务垂直行业的特定需求；同时，该服务商发力 C 端市场，陆续推出 SCRM 等产品连接海量 C 端用户，打造客户旅程的一体化闭环。  \n\n同时服务几万家不同规模的企业，打通每家企业各个业务系统的数据，为 360 度客户旅程提供数字化支撑，这些都对 CRM 服务商底层的数据平台提出了非常严苛的要求。该 CRM 服务商的数据平台架构采用多形态混合式数据存储模式来满足多租户架构下可定制的业务需求。下图所示的多租户共享数据库，考虑到大表性能、租户数据量，采用了元数据、租户分库、租户分表的整体方案，共享库按照租户和实体做水平扩展的分库分表设计。  \n\n![多租户共享数据库的分库分表设计.png](https://img1.www.pingcap.com/prod/_f7c203daeb.png)\n<center>多租户共享数据库的分库分表设计</center>\n\n与此同时，为了满足多租户定制化需求，实体主数据表需要预留大量的扩展列，呈现出数据库表多，列多，索引多等特点，也给 CRM 的数据架构带来了重重挑战：\n\n### 大数据量下的分库分表无以为继\n\n随着业务高速增长，CRM 用户数接近 20 万，租户的增长一方面带来海量数据高并发访问的挑战，另一方面使得单租户单实体库数据量快速增长，单表已经超过千万级别。单体 MySQL 数据库无法水平弹性扩展，配置也已经到极限，出现性能下降的问题。此外，MySQL 的分库分表方案，业务拆分难度大，需要进行代码侧的改动进行适配，维护工作也变得非常繁琐。\n\n### 2B 和 2C 客户的体验大打折扣\n\nCRM 业务具有灵活、多变的的特征，经常遇上索引缺失/索引选取错误或者复杂模糊查询等情况，MySQL 性能完全不可接受，慢查询增多导致客户体验差。在批量操作场景，单个任务可能将系统卡死，大量慢查询堆积，严重的时候甚至会造成 OOM，查询被拉黑名单。\n\n### 业务创新的灵活度受到制约\n\n中国的企业级客户由于行业特点和业务环境的变化，通常对 CRM 有定制化和多样化需求，导致数据库需要预留很多的扩展列，经常需要在数据表中增加字段或者增加索引。MySQL 行宽只有 64K，也不支持 Online DDL，使得业务创新的灵活度受到极大制约。\n\n## CRM 真实场景下 MySQL 和 TiDB 的比拼\n\n通过与 CRM 服务商技术部门的探讨，由于 SaaS 业务的稳定性、性能都对最终客户至关重要，组织了一次全面的 PoC 验证工作，覆盖了产品的性能，兼顾数据迁移、稳定性、安全性、运维监控等能力。  \n\n从业务场景的实测结果可以看到：TiDB 并发处理和查询性能提升明显，千万级以上性能提升 70% 左右，单条、跑批都得到了大幅的性能提升（80% 在毫秒级），原先过长时间查询无返回结果的任务，在 TiDB 中得到好转；各类痛点查询，如权限查询、模糊查询、分页查询、无索引查询、聚合查询、联表查询等性能提升几倍至几百倍不等；TiDB 在添加字段，修改字段，增加索引等 DDL 上优势明显。此外，TiDB 提供高效的同步工具 DM，从 RDS-MySQL 同步到 TiDB 延迟控制在毫秒级别。\n\n## 两套数据架构的对比\n\n某 CRM 服务商原有的数据架构中，OLTP 业务通过 Mycat 连接多套 MySQL 数据库处理和存储应用的数据，所有 MySQL 通过 DataPipline 同步到 Greenplum 数仓，由 Greenplum 提供 OLAP 查询服务。引入 TiDB 分布式数据库替换原有架构之后，所有的 OLTP 业务和 OLAP 查询访问一套 TiDB 集群即可，根据业务的实际需求，灵活增加 TiKV 和 TiFlash 节点即可。  \n\n![某 CRM 服务商数据平台架构.png](https://img1.www.pingcap.com/prod/CRM_e1779da679.png)\n<center>某 CRM 服务商数据平台架构</center>\n\n基于 TiDB 的新架构在高可用、性能、定制化需求、客户体验、开发和运维等多个维度体现出优势：\n![TiDB 优势体现.png](https://img1.www.pingcap.com/prod/Ti_DB_9095466037.png)\n\n## “两升一降”助力 SaaS 服务商构建核心竞争力\n\n### 提升海量数据的处理能力和时效性\n\nTiDB 分布式数据库按需弹性扩展能力使得 CRM 服务商可以轻松应对租户、2B 与 2C 业务海量数据的增长。对于 CRM 类 SaaS 服务商来说，TiDB 分布式架构对库、表无数量限制，一键实现节点扩缩容，使得使得数据架构可以动态、平滑地适配业务的增长。TiDB 原生分布式特点在高并发查询、缺失索引、模糊查询等方面性能表现优异，无需担心租户增加带来高并发问题以及租户单实体数据量大等问题；TiDB HTAP 能力使得海量数据场景下的各类查询秒级得到反馈，极大提升了 CRM 的用户使用体验。\n\n### 提升 SaaS 服务商的业务灵活性\n\n在 SaaS 的基础上集成 PaaS 能力已经成为中国企业级 CRM 市场发展的重要趋势，为了满足企业用户的高度定制化和多样化需求，PaaS 平台要求数据平台具备高度灵活的可定制能力，例如增加字段完成功能迭代等。TiDB 支持 Online DDL，可实现在线添加字段，修改字段对业务无影响，加快了业务创新的速度，进一步增强了 CRM 等 SaaS 服务商在同行业中的竞争力。\n\n### 多管齐下，降低成本\n\nTiDB 一个数据栈支持混合负载，并提供实时的数据洞察，精简了 CRM 服务商数据平台的架构。从单体数据库集群、ETL 工具、数据仓库的多套系统组合到一个 TiDB 集群，大幅节省了数据在各个技术栈之间同步的时间成本以及多个数据副本的存储成本。TiDB 无需分库分表，提高了产品研发的敏捷度，减轻了各类应用的适配难度，研发人员可以把更多精力放在功能迭代和业务创新上。此外，TiDB 还提供了整套的数据同步及数据库管理、监控工具，进一步降低了后期的运维管理成本。  \n\n采用一个简单、强大、一栈式的数据服务平台成为越来越多 SaaS 企业的选择，这个平台既可以支撑海量在线交易，又可以提供实时分析能力，以极少的 DBA 和资源投入，就可以从容应对不确定的环境、多变的业务挑战。  \n\n![TiDB应对挑战.jpeg](https://img1.www.pingcap.com/prod/Ti_DB_a8a3f2964f.jpeg)\n","author":"PingCAP","category":4,"customUrl":"htap-database-practice-in-a-chinese-saas-provider","fillInMethod":"writeDirectly","id":435,"summary":"CRM 并不是简单的销售和客户服务的效率工具。本质上，CRM 是以平台化思维实现业务管理和数据的打通，为 360 度客户旅程提供数字化支撑。","tags":["HTAP"],"title":"MySQL or TiDB？HTAP 数据库在中国 SaaS 行业头部服务商的应用实践"}},{"relatedBlog":{"body":"## 导读\n\n云盛海宏的零售系统是支持全渠道、全品类运动鞋服的零售服务平台，为全球 8000+ 多家线下门店提供零售服务支持。发展至今，云海零售系统的数据库经历了从 MySQL 到 Oracle 再到全面 TiDB 的架构演进。\n\n**本文由 InfoQ 主编赵钰莹撰写，与云盛海宏首席架构师洪亮共同探讨了云海零售系统整体架构从 MySQL 到原生分布式变迁的思路和收获，以及数据库设计在零售业业务发展中的重要性**。\n\n目前，国内某知名运动品牌在全球经营着 12 家鞋服运动品牌，在全国有近万家线下门店，耐克、阿迪达斯、彪马、匡威等品牌门店绝大部分都是其代理经营，注册会员达 6000 多万，这些业务由旗下科技公司云盛海宏全面支撑。过去十年间，云海零售系统是支撑全渠道、全品类运动鞋服的零售服务平台，支撑了 8000+ 线下门店的零售。\n\n这样一家零售领域的老牌企业是如何一步步从 MySQL 转向原生分布式数据库的？整体的架构变迁思路是怎样的？实践过后又是如何从成本视角评价 Oracle 和国产分布式数据库的......近期，InfoQ 有幸采访到了云盛海宏首席架构师洪亮，就上述问题逐一进行了探讨。\n\n## 背景介绍  \n\n在介绍云盛海宏的数据库架构设计之前，我们先了解下其整体的业务背景。云盛海宏的核心业务是零售系统，包括库存、终端零售以及用于集团内部的财务辅助系统三大模块。\n\n自 2013 年起，云盛海宏就开始搭建整个数据库架构，中间因为业务的不断发展经历了多轮迭代。2016 年之前，云盛海宏基本还处于传统零售时代，内部各大区自建设信息化系统，维护自己的数据库架构，每天向总部上传业务数据，数据库采用集中式单库，这种方式的优点是架构简单，缺点则随着业务发展越来越明显，比如没有办法及时查看地区汇总数据，也无法跨大区查看全国的实时库存等。\n\n为了解决这些问题，云盛海宏在 2016 年上线了全新的架构——云海零售系统，开启了数字化零售时代的架构演进之路。\n\n## 从 MySQL 到 Oracle 再到全面 TiDB 的架构演进\n\n发展至今，云海零售系统主要经历了三个阶段的演进。\n\n**阶段一：应用微服务化，实现数据共享，初步精细化运营，支撑数字化业务发展**\n\n在这一阶段，云盛海宏使用的是微服务+ MySQL 分库分表的方式。立项之初，团队调研时考虑到数据垂直切分的模式短时间内较稳定，MySQL 集群的开发难易程度对团队来说又比较好掌握，所以选定了 MySQL 。\n\n随着业务的飞速发展，很多问题超出了团队的原始预期，MySQL 集群对于复杂报表分析支持不足，团队尝试引入 Oracle 分担这部分需求，再通过 Otter 进行数据的实时同步，保障两边的数据完整。对于 TOB 业务来说，内部报表非常关键，且对数据精度要求极高，冷热数据变化频繁，Oracle 的引入很好解决了实时报表方面的问题。\n\n此后，云海零售系统支撑了业务高速发展的五年，实现了很多小目标，比如实现了全国各地区、各大区的海量数据的存储，实现了数据实时共享，也达到了业务可视化的目标。但是随着业务的扩展和需求难度的增加，慢慢地出现了一些新的挑战。首先，整个架构基于 MyCAT 做分库分表，在日常维护中，如果有新的业务，比如要增加表或者调整表，维护层面会增加人力成本，需要人工调整配置，然后再调用配置，需要花费很多精力。\n\n其次，当时的 Otter 同步渠道已经有 110 +，使用起来也没有那么理想。比如源端加表，目标端没有加表，或者是仅仅是字段的调整也可能导致一些同步的中断，这需要大量人力维护。最主要的是 Oracle 也遇到了一些瓶颈，例如海量数据无法扩展、聚合库分析时效差等问题。\n\n**阶段二：解决数据爆发式增长导致聚合库分析时效性差**\n\n2020 年之前，Oracle 的单点性能已经无法横向扩展，团队开始积极寻求替代方案。此时，团队开始接触到 TiDB ，并于当时 InfoQ 举办的 ArchSummit 大会上听到了时任 PingCAP 联合创始人兼 CTO 黄东旭的详细讲解，后又经过详细的对比测试，主要集中在大数据量的查询以及复杂 SQL 的查询性能两方面，发现 TiDB 可以解决 Oracle 存在的问题并且非常便捷。在内部小规模试用取得显著效果之后，云盛海宏最终决定快速推进 TiDB 集群的部署工作。\n\n![云盛海宏当前使用 TiDB 的情况.png](https://img1.www.pingcap.com/prod/Ti_DB_9f849dd274.png)\n\n<center>决定将 TiDB 部署到生产时的压测方案（利用了 Percona 公司的开源工具 Percona-playback 实施的压测）</center>  \n\n“2020 年，疫情爆发，这对我们的业务带来了很大冲击，我们开始发力做线上业务，技术侧最直接的压力来自于库存管理模块的变化。原本，从接到需要对接淘宝、京东、唯品会、抖音等平台的需求到最终落地需要三个月甚至半年的时间，但因为我们前期已经切换到了 TiDB ，技术栈层面做好了充足的准备，最终只用了两周时间就完成了单平台库存管理模块的调整”，洪亮如是说道。\n\n![云盛海宏零售系统当前架构.png](https://img1.www.pingcap.com/prod/_d9bd9b7d69.png)\n<center>2020 年引入 TiDB 之后的架构图</center>\n\n就内部工程师而言，TiDB 的部署推进得也非常顺利。首先，云盛海宏的主要业务都是在 MySQL 的基础上构建的，TiDB 完全兼容 MySQL 协议，从 MySQL 迁移到 TiDB 是比较顺利的。其次，TiDB 的日常运维、扩容、缩容非常方便，原来 DBA 按月或者季度为周期需要在凌晨一次性完成十几个实例的数据迁移，维护工作量巨大，而且数据迁移风险极高，一旦出现问题后果非常严重，引入 TiDB 之后基本不需要做迁移动作，更别提 MySQL 日常巡检、归档和备份这些动作耗费的时间。最后，MySQL 分库分表带来的局限性无法让团队快速应对变化，公司组织架构的每一次调整都会对业务带来一定冲击，团队需要快速消化这种冲击，TiDB 的引入让整个技术栈更具弹性。\n\n**阶段三：向全面部署分布式数据库迈进，初步探索架构云化**\n\n目前，云盛海宏内部已经完成了 MySQL 到 TiDB 的迁移，从最初的 4.0 版本到目前线上的 5.4.2 版本，每一次升级 TiDB 都会带来比较实用的特性和功能。接下来，云盛海宏会尝试从 Oracle 到 TiDB 的迁移，逐渐收拢数据库集群，更进一步降低运维负担。在云盛海宏内部，Oracle 不会承担太多核心业务和写操作，迁移基本面向 AP 类的数据和业务，所以这部分相对来说比较容易，团队重心会放在前端数据迁移，包括数据准确性校验。\n\n采访中，洪亮表示目前内部的 TiDB 集群的机器规模已经达到 100 台，已经部署了两个 TiDB 集群，分别承担前端和后台的业务负载，计划在 2024 年前完成第三个 TiDB 集群的部署，承担前文所述的 AP 类业务，也就是目前 Oracle 承担的财务报表分析负载。届时，云盛海宏的所有业务将全部运行至 TiDB 集群，Oracle 集群将逐渐停用。\n\n除此之外，整体架构将会逐渐云化。当前，云盛海宏部分应用做了私有云化，未来会尝试将一些环境公有云化，比如开发、测试、培训、生产等。\n\n## 数据库设计核心问题探讨\n\n在零售行业，云盛海宏算得上是对技术投入较大的公司之一，而且结合其业务范围和体量，技术架构的搭建是存在一定难度的，数据库选型和架构演进需要考虑因素很多。在这个过程中，团队也摸索出了一些经验。\n\n**零售业有没有可能完全舍弃 Oracle ？**\n\n在零售领域，有一定历史的企业内部早期肯定部署着 Oracle 数据库，尤其是对精度要求极高的财务数据，那时可替代的国产数据库并不多。如今，国产数据库越来越成熟，可供选择的空间也越来越大，很多企业都开始尝试迁移至其他数据库。\n\n从云盛海宏的经验来看，零售领域未来完全有机会舍弃 Oracle ，即便是要求极高的财务报表数据的处理也可以由国产数据库来负责。\n\n选型上，企业需要提前根据业务特点做好压测，迁移之前也需要做好相关预案，云盛海宏从 MySQL 到 TiDB ，从 Oracle 到 TiDB 都做好了充分的备案。\n\n**从成本视角来看，分布式数据库值吗？**\n\n现在谈到成本，基本涵盖软件授权费用、软件服务费用、硬件采购费用以及日常维护费用等众多维度，企业内部情况不同也存在差异。\n\n从云盛海宏的经验来看，TiDB 相比 Oracle 在软件授权费用上肯定是具备明显优势的；在软件服务费用方面，TiDB 本身的生态和社区建设（包括文档）相对比较完善，但不排除一些国产数据库因为成熟度不足而尚无法投入人力建设成熟的服务生态，这一点需要根据选型情况具体判断；在硬件采购费用方面，云盛海宏使用前后差异不大；在日常维护方面，TiDB 的门槛低、易维护节约了大量人力成本。\n\n如果与管理 MySQL 集群相比，数据备份、硬件故障处理、主从节点管理等相对都比较麻烦，但 TiDB 基本可以做到轻量级维护，后期云化之后可能会更进一步降低运维成本。\n\n**要不要全面云化？**\n\n如前文言，云盛海宏其实未来会逐步云化，其团队内部对此也有很多考虑。\n\n采访中，洪亮表示从整个集群而不是单个数据库的角度出发，云化在机房管理、网络安全、高可用、容灾等层面会比本地部署更有优势。如今，TiDB 和阿里云也有合作，云化是比较容易进行的，尤其是针对原有技术栈基于 MySQL 的企业。\n\n**智能化运维值不值得初期就考虑？**\n\n最近两年，很多数据库都在积极整合 AI 能力，以期让部署、运行、运维等全过程更具智能化。对云盛海宏而言，企业内部对落地 AI 的诉求相对而言没那么迫切。\n\n“智能化运维或者说引入 AI 能力取决于底层的基础建设是否到位，如果存算分离或者是运维能力没有提升，AI 就像是空中楼阁。只有底层基础打好了，智能化运维才能发挥出更大作用。比如，MySQL 的一些指标监控肯定没有 TiDB 完善，没有这些指标，AI 监控就无从谈起了。”","author":"赵钰莹","category":4,"customUrl":"mysql-migrate-to-oracle-to-tidb","fillInMethod":"writeDirectly","id":489,"summary":"这样一家零售领域的老牌企业是如何一步步转向原生分布式数据库的？整体的架构变迁思路是怎样的？实践过后又是如何从成本视角评价 Oracle 和国产分布式数据库的......本文就上述问题逐一进行了探讨。","tags":["MySQL 架构演进"],"title":"从 MySQL 到 Oracle 再到全面 TiDB ，云盛海宏的数据库架构实践"}},{"relatedBlog":{"body":"TiDB 的一大特性就是和 MySQL 高度兼容，目标是让用户能够无需修改代码即可从 MySQL 迁移至 TiDB。要达成这个目标，需要完成两个提升兼容性的任务，分别是「语法兼容」和「功能行为兼容」。\n\n**本次活动聚焦于语法兼容，提升 TiDB SQL Parser 对 MySQL 8.0 的语法支持。对于新的贡献者而言，除了能将理论知识运用到实践上以外，还可以从中体验参与一个开源项目的整体流程与规范。**\n\n我们把 TiDB Parser 整体看作一个函数，输入是 SQL 字符串，输出是用于描述 SQL 语句的抽象语法树（AST）。在这个转换的过程中涉及到的组件有两个：一个是 lexer，负责将字符流变成 Token，并赋予它们类别标识，这个过程叫「Tokenize」；另一个是 parser，负责将 Token 转为树状结构，便于将来遍历和转换，这个过程叫「Parse」；TiDB 使用了 parser 生成工具 goyacc，它能够根据 `parser.y` 生成 `parser.go`，其中包含了名称为 `Parse` 的函数接口，供 TiDB 直接使用。更多关于 TiDB Parser 以及 Lex & Yacc 的信息请参考 [TiDB SQL Parser 的实现](https://pingcap.com/blog-cn/tidb-source-code-reading-5/)。\n\n## 参与流程\n\n参与流程分为 7 步：**领任务  -> 写 test case -> make test -> coding -> 补充 test case -> make test -> 提 PR**。\n\n### 1. 从 Issue 领取任务\n\n我们会在 [Improve parser compatibility](https://github.com/pingcap/tidb/issues/11486) 周期性发布不兼容的 Bad SQL Case 组，每组 Case 都会构成一个 Issue，Contributor 选择某个 Issue，在它的下方评论：**Let me fix it**。在我们将 Contributor 的 Github ID 添加到 Index Issue 中后，即完成任务的领取。\n\n### 2. 编写测试用例\n\n根据 Issue 描述的情况在 [`parser_test.go`](https://github.com/pingcap/parser/blob/master/parser_test.go) 中编写测试用例，除了 Issue 中提到的 Case 以外，可以适当添加更多的 Case。保证目标 SQL Case 语句能够通过 Parser 解析，并且通过 Restore 还原为预期的 SQL。\n\n### 3. 执行所有测试\n\nparser 根目录下运行 `make test`，确保第一次测试失败，并且失败的 Case 是第 2 步编写的。\n\n### 4. 编码\n\nContributor 修改文法规则。对于涉及到语义层面的规则变动，需要同步修改 AST 节点的数据结构（AST 节点定义在 `parser/ast` 中）。TiDB 通过 AST 树生成执行计划，修改 AST 节点可能会影响 TiDB 的行为，因此应尽量保持原有结构，不改变原有的属性，如果确实有修改 AST 树必要，我们会帮助 Contributor 检查 TiDB 的行为是否正常。另外，AST 节点其中的两个接口方法是 `Accept` 和 `Restore`，分别用于遍历子树和通过 AST 树还原 SQL 字符串。应确保它们的行为都符合预期。\n\n另外，还要检查新加的规则是否存在冲突问题。「冲突」可以被理解为当 parser 读到某个 token 时，有两种或以上的方式来构造语法树，从而导致歧义。goyacc 所生成的 parser 采用了 `LALR(1)` 方法进行解析，冲突有两类：一类是两条规则都能被用于归约，称为 `reduce-reduce` 冲突。另一类是既能使用一条规则归约，又能按照另一条规则移进下一个 token，称为 `shift-reduce` 冲突。可以通过指定优先级的方式消除冲突，具体可以参考 yacc 的 [%precedence 和 %prec 指示](https://www.gnu.org/software/bison/manual/html_node/Precedence.html#Precedence)。\n\n编码完成后在项目根目录下运行 `make parser`，这时会执行 goyacc 重新生成 `parser.go` 文件。\n\n### 5. 补充 test case\n\n根据实际情况尽可能提升测试覆盖率。\n\n### 6. 执行所有测试\n\nparser 根目录下运行 `make test`，确保测试通过。\n\n### 7. 提交 PR\n\t\n提交 PR 之前请先阅读 [contributing 指南](https://github.com/pingcap/tidb/blob/master/CONTRIBUTING.md)。下面是 PR 的模板，逐项填写即可。\n\n```\t\n### What problem does this PR solve?\n\n#### [ Put the subtask title here ]\n\nIssue: [ put the subtask issue link here ]\n\n#### MySQL Syntax:\n\n[ describe MySQL syntax here ]\n\n#### Bad SQL Case:\n\n[ give a SQL statement example that passes MySQL but fails TiDB parser ]\n\n[ give a SQL statement example that passes MySQL but fails TiDB parser ]\n\t\n...\n\t\n### Check List\n\t\nTests\n\n- Unit test\n```\n\n## 示例\n\n**下面以添加 「REMOVE PARTITIONING」 语法支持为例解释说明整个过程**。\n\n### 1. 从 Issue 领取任务\n\n从 [这里](https://github.com/pingcap/tidb/issues/11486) 找到 `REMOVE PARTITIONING` 子任务。[子任务 Issue](https://github.com/pingcap/parser/issues/402) 中有若干不兼容的 Case。\n\n首先，手动测试任一 Case，发现在 MySQL 下输出：\n\n```\nError 1505: Partition management on a not partitioned table is not possible\n```\n\n而在 TiDB 下输出：\n\n```\nError 1064: You have an error in your SQL syntax; check the manual that corresponds to your TiDB version for the right syntax to use line 1 column 20 near \"remove partitioning\"\n```\n\n这意味着 parser 无法识别 remove 关键字。\n\n确认了问题存在后，到 [该 Issue](https://github.com/pingcap/parser/issues/402) 下评论：**Let me fix it**，完成任务领取。\n\n### 2. 编写测试用例\n\n在 `parser_test.go` 下寻找合适位置添加测试用例，这里我们选择在 `func (s *testParserSuite) TestDDL(c *C)` 的 [末尾](https://github.com/pingcap/parser/pull/396/files#diff-688912c3f38a8a80d6bdc16c02088d69R2172) 添加：\n\n```\n// for remove partitioning\n{\"alter table t remove partitioning\", true, \"ALTER TABLE `t` REMOVE PARTITIONING\"},\n{\"alter table db.ident remove partitioning\", true, \"ALTER TABLE `db`.`ident` REMOVE PARTITIONING\"},\n{\"alter table t lock = default remove partitioning\", true, \"ALTER TABLE `t` LOCK = DEFAULT REMOVE PARTITIONING\"},\n```\n\n这里每一个 test case 分成了三部分，第一列是用于测试的 SQL 语句，第二列是「是否期望第一列的语句 parse 通过」，第三列是「从语法树 restore 后期望的 SQL 语句」。具体可以参考 [`TestDDL.RunTest()`](https://github.com/pingcap/parser/blob/53c769c5836485c83d5f339faab97ef5d853d560/parser_test.go#L308)。\n\n### 3. 执行所有测试\n\nparser 根目录下运行 `make test`，确保第一次测试失败，并且 fail 的 case 是第 2 步编写的。\n\n```\nFAIL: parser_test.go:1664: testParserSuite.TestDDL\n\nparser_test.go:2148:\n    s.RunTest(c, table)\nparser_test.go:318:\n    c.Assert(err, IsNil, comment)\n... value *errors.withStack = line 1 column 20 near \"remove partitioning\"  (\"line 1 column 20 near \\\"remove partitioning\\\" \")\n... source alter table t remove partitioning\n```\n\n错误信息和期望的一致，则可以开始进行下一步。\n\n### 4. 编码\n\n#### 4.1 修改 `parser.y` 文件\n\n首先从 [MySQL 文法](https://github.com/mysql/mysql-server/blob/8.0/sql/sql_yacc.yy) 中找到 remove partitioning 的定义：\n\n```\nalter_table_stmt: ALTER TABLE_SYM table_ident opt_alter_table_actions\n| ALTER TABLE_SYM table_ident standalone_alter_table_action\n\nopt_alter_table_actions: opt_alter_command_list\n| opt_alter_command_list alter_table_partition_options\n\nalter_table_partition_options: partition_clause\n| REMOVE_SYM PARTITIONING_SYM\n```\n\n经过分析可得该语法只能出现在 SQL 语句的最后一个部分，并且只能出现一次。\n\n在 `parser.y` 中找到 `AlterTableStmt`，其中一条规则是：\n\n```\n\"ALTER\" IgnoreOptional \"TABLE\" TableName AlterTableSpecListOpt PartitionOpt\n```\n\n其中最后一个符号 PartitionOpt 和 MySQL 中 `partition_clause` 非常相似，为了支持 remove partitioning，容易想到引入一条规则：\n\n```\nAlterTablePartitionOpt: PartitionOpt | \"REMOVE\" \"PARTITIONING\"\n```\n\n将 `PartitionOpt` 的语义动作移到 `AlterTablePartitionOpt` 中，`REMOVE PARTITIONING` 部分先返回 `nil`，并添加 parser 警告，表示目前 parser 能够解析但 TiDB 尚未支持该功能。修改后的规则如下：\n\n```\nAlterTablePartitionOpt:\n\tPartitionOpt\n\t{\n\t\tif $1 != nil {\n\t\t\t$$ = &ast.AlterTableSpec{\n\t\t\t\tTp: ast.AlterTablePartition,\n\t\t\t\tPartition: $1.(*ast.PartitionOptions),\n\t\t\t}\n\t\t} else {\n\t\t\t$$ = nil\n\t\t}\n\t}\n|\t\"REMOVE\" \"PARTITIONING\"\n\t{\n\t\t$$ = nil\n\t\tyylex.AppendError(yylex.Errorf(\"The REMOVE PARTITIONING clause is parsed but ignored by all storage engines.\"))\n\t\tparser.lastErrorAsWarn()\n\t}\n```\n\n由于 `REMOVE` 和 `PARTITIONING` 都是新添加的关键字，如果不做任何处理，lexer 扫描的时候只会将它们看作普通的标识符。于是需要在 `parser.y` 的 `%token` 字段上补充声明，其中一个目的是为该字符串产生一个 `tokenID`（一个整数），供 lexer 标识。另外 `goyacc` 也会对 `parser.y` 中所有的字符串常量进行检查，如果没有相应的 `token` 声明，会报 `Undefined symbol` 的错误。\n\n为支持这两个关键字，我们在文件开头的 `token` 字段添加声明。由于 `REMOVE` 和 `PARTITIONING` 都是非保留关键字，它们应被添加在含有 `The following tokens belong to UnReservedKeyword` [注释](https://github.com/pingcap/parser/blob/53c769c5836485c83d5f339faab97ef5d853d560/parser.y#L274) 的下方。此外，非保留字说明它们能够作为标识符 `Identifier`，因此在 `Identifier` 规则下的 [`UnRerservedKeyword`](https://github.com/pingcap/parser/blob/53c769c5836485c83d5f339faab97ef5d853d560/parser.y#L3717) 中也应加上 `REMOVE` 和 `PARTITIONING`。\n\n关于如何确定一个关键字是保留的还是非保留的，可以参考 [MySQL 文档](https://dev.mysql.com/doc/refman/8.0/en/keywords.html)。\n\n#### 4.2 增加「关键字-`tokenID`」映射\n\n前文提到，添加声明是为了让 lexer 能够识别关键字并赋予对应的 `tokenID`，对于 lexer 而言，它需要一个从关键字字符串到 `tokenID` 的映射关系。在 TiDB parser 中，这个映射关系就是 [`misc.go`](https://github.com/pingcap/parser/blob/53c769c5836485c83d5f339faab97ef5d853d560/misc.go) 中的 `tokenMap` 结构。\n\n在这个例子中，我们往 `tokenMap` 中添加 `remove` 和 `partitioning`（如果不添加，会使关键字一致性的检查测试失败）。\n\n#### 4.3 修改 AST 节点\n\n到目前为止，我们已经让 `goyacc` 生成的 parser 能够解析 remove partitioning 语法。但是，解析完成后并没有返回有效的数据结构（4.1 中我们返回了 `nil`），这意味着 parser 不能够根据语法树重新生成原 SQL 语句。\n\n所以，要修改现有的 AST 节点，使它们能够以某种形式保存 remove partitioning 信息。回顾目前规则层面的修改：\n\n```\nAlterTableStmt:\n\"ALTER\" IgnoreOptional \"TABLE\" TableName AlterTableSpecListOpt PartitionOpt\n```\n\n已改为：\n\n```\nAlterTableStmt:\n\"ALTER\" IgnoreOptional \"TABLE\" TableName AlterTableSpecListOpt AlterTablePartitionOpt\nAlterTablePartitionOpt:\n      PartitionOpt | \"REMOVE\" \"PARTITIONING\"\n```\n\n其中几个非终结符对应的数据结构如下：\n\n```\nAlterTableSpecListOpt -> []AlterTableSpec\nPartitionOpt -> PartitionOptions\n```\n\n关于修改节点，有几种方案可以选择：\n\n1. 增加一个新的节点 struct，表示 `AlterTablePartitionOpt`。其中包含 `PartitionOptions` 和一个 bool 值，表示是否为 remove partitioning。\n\n2. 将 remove partitioning 看作 `PartitionOptions`，在内部添加一个 bool 成员 `isRemovePartitioning` 以做区分。\n\n3. 将 `PartitionOpt` 和 remove partitioning 都看作 `AlterTableSpec`，为 `AlterTableSpec` 的添加一个类型，单独表示 remove partitioning。\n\n经过比较和分析，我们发现第一个方案会引入新的数据结构，有较大的概率会引起现有的 TiDB 测试不过，为此可能要修改 TiDB 方面的代码，工作量大的同时提高了 parser 的复杂度，因此作为备选方案；第二个方案没有引入新的数据结构，但是修改了现有的数据结构（加了个 bool 成员）；第三个方案即没有添加也没有修改数据结构，并且能够以较少的代码完成任务，作为首选方案。\n\n**在以上的选择中，我们遵循「尽量不修改 AST 节点结构」的原则。**\n\n按照方案三，观察 [`AlterTableSpec`](https://github.com/pingcap/parser/pull/396/files#diff-688d51c34d61e5d538b15582305c9a8dL1768)，其定义如下：\n\n```\ntype AlterTableSpec struct {\n  node\n  ...\n  Tp              AlterTableType\n  Name            string\n  ...\n}\n```\n\n其中一个成员是 `Tp`，它所属的类型包含了 `AlterTable` 的许多操作，例如 `AddColumn`，`AddConstraint`，`DropColumn` 等。我们的任务是添加一个 [类似的 Type](https://github.com/pingcap/parser/pull/396/files#diff-688d51c34d61e5d538b15582305c9a8dR1708)，让它能够表示 remove partitioning。\n\n到这里，解析完 SQL 语句生成的 AST 树已经包含 remove partitioning 的信息了。接下来要处理 Restore，让它能够从 AST 树还原出 SQL 语句。`AlterTableSpec` 的 Restore 十分简单，加一个 case 即可，这里不再赘述。\n\n#### 4.4 完善 `parser.y`\n\n第一次修改 `parser.y` 的时候我们在新加规则的语义动作中返回了 `nil`，原因是尚未确定 AST 是否需要修改，以及如何修改。而到这一步，这些都已经确定下来了，把 remove partitioning 看作 `AlterTableSpec` 类型：\n\n```\n|       \"REMOVE\" \"PARTITIONING\"\n      {\n              $$ = &ast.AlterTableSpec{\n                     Tp: ast.AlterTableRemovePartitioning,\n              }\n             yylex.AppendError(yylex.Errorf(\"The REMOVE PARTITIONING clause is parsed but ignored by all storage engines.\"))\n             parser.lastErrorAsWarn()\n      }\n```\n\n注意，这里必须抛出警告，表示虽然目前 parser 能够解析该语法，但实际上 TiDB 并未支持相应的功能。\n\n#### 5. 补充 test case\n\n这里，所有的代码修改引入的分支结构都能够被现有的测试覆盖，因此在提升覆盖率上没有需求。当然，如果想要测试更多类似的 case，可以将它们添加到前面提到的 `TestDDL` 函数中。\n\n#### 6. 执行所有测试（`make test`）\n\n```\ngofmt (simplify)\nsh test.sh\nok      github.com/pingcap/parser       4.294s  coverage: 62.1% of statements in ./...\nok      github.com/pingcap/parser/ast   2.090s  coverage: 42.3% of statements in ./...\nok      github.com/pingcap/parser/auth  1.155s  coverage: 1.3% of statements in ./... [no tests to run]\nok      github.com/pingcap/parser/charset       1.094s  coverage: 2.0% of statements in ./...\nok      github.com/pingcap/parser/format        1.114s  coverage: 2.5% of statements in ./...\n?       github.com/pingcap/parser/goyacc        [no test files]\nok      github.com/pingcap/parser/model 1.100s  coverage: 3.5% of statements in ./...\nok      github.com/pingcap/parser/mysql 1.102s  coverage: 1.3% of statements in ./... [no tests to run]\nok      github.com/pingcap/parser/opcode        1.099s  coverage: 1.4% of statements in ./...\nok      github.com/pingcap/parser/terror        1.091s  coverage: 2.3% of statements in ./...\nok      github.com/pingcap/parser/types 1.106s  coverage: 7.0% of statements in ./...\n\n```\n\n**确保所有的 test 都是 ok 的状态。**\n\n#### 7. 提交 PR\n\n按照 PR 模板逐项填写。\n\n```\n### What problem does this PR solve?\n\n#### Fix compatibility problem about keyword REMOVE PARTITIONING\n\nIssue: pingcap/parser#402\n\n#### MySQL Syntax:\n\nalter_specification:\n...\n  | REMOVE PARTITIONING\n\n### Bad SQL Case:\n\nalter table t remove partitioning;\nalter table t lock = default, remove partitioning;\nalter table t drop check c, remove partitioning;\n\n### Check List\n\nTests\n- Unit test\n\n```\n\n**需要特别指出的是，我们鼓励各位 Contributor 多使用 `make test`。当不知道从何处入手或者失去目标时，`make test` 输出的错误信息或许能够引导大家进行思考和探索**。\n\n>Tips: [完整的 PR 示例](https://github.com/pingcap/parser/pull/396)\n\n## FAQ\n\n以下是在增加 remove partitioning 语法支持时遇到的问题和解决方法。\n\n**Q1. 为什么不在 `PartitionOpt` 中直接添加规则？** \n\n**A1**：`PartitionOpt` 用于匹配含有 `partition by` 的 SQL 语句，除了 `Alter Table` 语句以外，它还被 `Create Table` 使用，而 `remove partitioning` 只存在于 `alter table` 语句中，因此不能在 `PartitionOpt` 中添加规则。\n\n**Q2. 执行 make test 时报错：** \n\n```\nparser.y:1100:1: undefined symbol \"PARTITIONING\"\nparser.y:1100:1: undefined symbol \"REMOVE\"\nmake[1]: *** [Makefile:19: parser] Error 1\n```\n\n**A2**：在 yacc 中，出现在规则中的字符串，要么是 `token`（终结符），要么是非终结符。这里引用一段 yacc 的规范：\n\n```\nNames refer to either tokens or nonterminal symbols.\nNames representing tokens must be declared; this is most simply done by writing\n%token   name1 name2 . . .\n```\n\n所以，修复方法是在 `parser.y` 的 `%token` 字段上添加 `PARTITIONING` 和 `REMOVE` 的声明。\n\n**Q3. 执行 make test 时报错：** \n\n```\n c.Assert(len(tokenMap)-len(aliases), Equals, keywordCount-len(windowFuncTokenMap))\n... obtained int = 454\n... expected int = 456\n```\n\n**A3**：这是关键字的一致性检查出了问题，解决方案是补充 `tokenMap`（它是关键字到 `token ID` 的映射，被 scanner 用来判断某个字符串是否为关键字）。\n\n**Q4. 执行 make test 时报错：** \n\n```\nFAIL: parser_test.go:1666: testParserSuite.TestDDL\nparser_test.go:2166:\n    s.RunTest(c, table)\nparser_test.go:351:\n    c.Assert(restoreSQLs, Equals, expectSQLs, comment)\n... obtained string = \"ALTER TABLE `t`\"\n... expected string = \"ALTER TABLE `t` REMOVE PARTITIONING\"\n... restore ALTER TABLE `t`; expect ALTER TABLE `t` REMOVE PARTITIONING\n```\n\n**A4**：这个错误说明 parser 已经解析通过，但不能从语法树中恢复原 SQL 语句的 remove partitioning 部分。此时应检查相应 AST 节点的 Restore 方法是否正确处理了 `REMOVE PARTITIONING`。\n\n***最后欢迎大家加入 [TiDB Contributor Club](https://pingcap.com/community-cn/)，无门槛参与开源项目，改变世界从这里开始吧！***  \n> 点击查看更多 [成为 Contributor 系列文章](https://pingcap.com/zh/blog/?tag=Contributor)","author":"谢腾进 赵一霖","category":3,"customUrl":"30mins-become-contributor-of-tidb-20190808","fillInMethod":"writeDirectly","id":175,"summary":"本次活动聚焦于语法兼容，提升 TiDB SQL Parser 对 MySQL 8.0 的语法支持。对于新的贡献者而言，除了能将理论知识运用到实践上以外，还可以从中体验参与一个开源项目的整体流程与规范。","tags":["TiDB","社区","Contributor"],"title":"三十分钟成为 Contributor | 提升 TiDB Parser 对 MySQL 8.0 语法的兼容性"}}]}}},
    "staticQueryHashes": ["1327623483","1820662718","3081853212","3430003955","3649515864","4265596160","63159454"]}