ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

深入理解SQL DDL:从表结构设计到生产环境避坑指南

深入理解SQL DDL:从表结构设计到生产环境避坑指南 做数据库开发这些年我见过太多人在表结构上反复返工。今天加个字段明天改个类型后天删个约束一个上线几年的业务表被改得面目全非。说到底很多开发对SQL数据定义DDL的理解还停留在“CREATE建表、DROP删表”这个粗浅层面从没把DDL当成一套需要仔细对待的知识体系。DDLData Definition Language数据定义语言确实是SQL里最基础的一块但基础不等于简单。它管的是整个数据库的“骨架”库、表、索引、视图、约束全是先有定义才能装数据的东西。骨架立得稳后面所有增删改查才不折腾骨架歪了天天线上补丁代价比想象中大得多。这篇文章我把DDL的核心内容从头到尾捋一遍从语法拆解到业务表设计再到生产环境实操和常见坑全程用实际SQL示例说话。刚接触数据库的新人可以把这当入门提纲写了一阵子SQL但没系统梳理过DDL的开发也能从中捡回不少容易忽略的细节。1. 先彻底搞清楚DDL到底是什么1.1 SQL语言里的“四大家族”SQLStructured Query Language结构化查询语言不是一条孤零零的语句而是一整套语言体系日常开发和运维嘴里常说的“写SQL”其实是对它的笼统叫法。如果按职责划分SQL大致会被分成四类DDLData Definition Language数据定义语言负责创建、修改、删除数据库对象比如建库、建表、加索引、加约束。DMLData Manipulation Language数据操纵语言负责对数据进行增删改查比如INSERT、UPDATE、DELETE、SELECT。DCLData Control Language数据控制语言负责权限管理比如GRANT、REVOKE。TCLTransaction Control Language事务控制语言负责事务管理比如COMMIT、ROLLBACK、SAVEPOINT。这四类里面DDL是入口中的入口。没有DDL先把表建出来DML连操作对象都没有没把字段类型定义好后面查询优化做得再多也是白搭。很多人觉得DDL好学是因为它语法不多二十来条就能列完但它难在“定义”背后的设计判断——字段用VARCHAR还是CHAR要不要允许NULL索引建在哪个列上这些选择一旦落地直接影响整个系统后续的性能和可维护性。1.2 DDL和DML最本质的区别玩的是“结构”还是“数据”把DDL和DML的区别讲透是绕不开的话题也是很多新人第一个犯迷糊的地方。我用一个生活里的例子类比数据库是一个仓库DDL负责设计仓库格局——隔几个房间、门开在哪儿、每间房多大DML负责往房间里搬东西、挪东西、扔掉东西。格局改的是基础设施搬东西只是在已有空间里操作。区别不是表面上“一个建表一个查表”那么简单还有几个很实际的技术差异第一个差异是事务回滚能力。绝大多数数据库里DDL语句是隐式提交的一条CREATE TABLE执行完不等你手动COMMIT它自己就提交了执行成功后想ROLLBACK基本没戏。DML则不同在事务块里执行了一半发现错了可以轻松ROLLBACK回滚。Oracle里这个特性尤其典型DDL执行期间和完成后的事务状态都是自动提交的。生产环境里不少人吃过这个亏——手一抖把表结构改了想撤销发现没有后悔药。第二个差异是影响范围。DML伤的是某几行数据修修补补就能恢复DDL动的往往是整张表的元数据定义影响的是这张表上所有后续操作。一个ALTER TABLE加字段如果表里几千万行数据执行期间可能引发全表重建或长时间锁表线上业务会直接卡住。第三个差异在权限模型里也很清晰。DCL里给普通账号授DML权限很容易但授DDL权限通常要非常谨慎。这也是为什么大公司里开发账号往往只有增删改查权限表结构变更要走专业的数据库管理员审核核心就在“DDL出错影响的是全局结构DML出错影响的只是局部数据”。1.3 DDL到底管了哪些数据库对象DDL的操作对象就是数据库里的各种“结构型对象”。常见的包括数据库SCHEMA创建、修改、删除一个数据库实例内部逻辑空间。表TABLE定义表结构、字段、约束、存储引擎等。索引INDEX加快查询速度的辅助结构。视图VIEW虚拟表本质是保存一段查询逻辑。触发器TRIGGER表上的事件监听器。存储过程和函数PROCEDURE / FUNCTION数据库内封装的逻辑代码。序列SEQUENCE生成自增数值的对象。每一类对象都有配套的DDL语法。平时用得最多的是表、索引、视图这三类后面我会重点展开。剩下的触发器、存储过程入门阶段可以先了解等有实际场景再深入。2. 核心DDL语法逐个拆解2.1 CREATE从建库到建表到建索引CREATE是DDL里最常用的命令用法按对象分类。先看建库CREATE DATABASE shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;这里的CHARSET和COLLATE很多人会忽略但字符集和排序规则一旦定错后面改起来非常痛。绝大多数MySQL项目建议直接用utf8mb4它支持emoji和生僻字排序规则里utf8mb4_unicode_ci按Unicode标准排序规则更科学。再看建表这是DDL里最核心的场景CREATE TABLE users ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键ID, username VARCHAR(50) NOT NULL COMMENT 用户名, email VARCHAR(100) DEFAULT NULL COMMENT 邮箱, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1启用 0禁用, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_username (username), KEY idx_status_created (status, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;这张表几乎涵盖了建表时要考虑的所有要素主键、唯一键、普通索引、默认值、自动更新时间、字段注释、表注释。创建索引单独也会用这条命令CREATE INDEX idx_email ON users(email); CREATE UNIQUE INDEX uk_email ON users(email);视图的创建则是用CREATE VIEW本质是保存一段查询逻辑CREATE VIEW v_active_users AS SELECT id, username, email FROM users WHERE status 1;视图有个好处是能封装复杂查询给上层应用提供简便入口但要注意视图是虚拟表底层还是去执行那段的SELECT不能因为视图名看着像表就忽略底层表查询的性能。2.2 ALTER改表结构的十八般武艺表结构上线后几乎一定会遇到变更需求ALTER TABLE就是干这个的。新人在ALTER上踩坑最多我按常用场景拆开说。添加字段ALTER TABLE users ADD COLUMN phone VARCHAR(20) DEFAULT NULL COMMENT 手机号 AFTER email;AFTER关键词控制新列插在哪个字段后MySQL里如果不指定默认加在最后。SQL Server对这种情况一般用ALTER TABLE ADD位置由系统决定不能指定AFTER关键字。修改字段类型或属性ALTER TABLE users MODIFY COLUMN username VARCHAR(100) NOT NULL COMMENT 用户名;这里要注意MODIFY和CHANGE的区别。MySQL里MODIFY只能改字段属性不能重命名CHANGE可以在改属性的同时改字段名ALTER TABLE users CHANGE COLUMN username login_name VARCHAR(100) NOT NULL COMMENT 登录名;删除字段ALTER TABLE users DROP COLUMN phone;添加和删除索引、约束也属于ALTER的范畴ALTER TABLE users ADD INDEX idx_phone (phone); ALTER TABLE users DROP INDEX idx_phone; ALTER TABLE users ADD CONSTRAINT fk_orders_user FOREIGN KEY (user_id) REFERENCES users(id);SQL Server里加约束的语法类似但删除约束的写法略有差异ALTER TABLE users DROP CONSTRAINT fk_orders_user。这种细节在跨数据库迁移时特别容易踩雷语法报错还只是小问题最怕的是语义不同造成数据损坏。2.3 DROP与TRUNCATE删表和清空表是两码事很多新手搞不清DROP、TRUNCATE、DELETE三者的区别这是面试高频点也是工作上出了事要背锅的痛点。用一句话先概括DROP删结构TRUNCATE清数据留结构DELETE按条件删数据。DROP TABLE users; -- 表结构和数据一起没了 TRUNCATE TABLE users; -- 清空数据表结构还在 DELETE FROM users WHERE id 1; -- 只删指定行一张表格梳理它们的核心差异对比项DROPTRUNCATEDELETE删除对象表结构数据仅数据符合条件的行数据是否可加WHERE否否可以事务回滚隐式提交不可回滚大多数数据库不可回滚事务内可回滚触发触发器不触发MySQL中不触发会触发自增计数直接删表重置归零不重置执行速度快快慢逐行删除生产环境里TRUNCATE是个危险操作它只需要一秒就把几千万行清空而且一般不能回滚。我见过误把TRUNCATE当DELETE用的线上事故好在表里有备份否则后果不敢想。所以日常操作前先确认自己写的是哪条命令把鼠标移到执行键前深呼吸三次。2.4 RENAME与COMMENT容易被忽略的辅助语法改表名RENAME TABLE users TO members; -- SQL Server: EXEC sp_rename users, members;改库名MySQL里没有直接命令常见的做法是通过备份恢复或者新建库再迁移数据这点也常被人在面试时问到。至于COMMENT它在DDL里是纯粹的“注释增强项”理论上不影响功能但实际对团队协作帮助极大ALTER TABLE users MODIFY COLUMN status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1启用 0禁用;我强烈建议所有表和字段都写注释。数据字典靠注释新人接手靠注释一年后你自己回来看代码也得靠注释。一个没有注释的数据库等维护的人离职接手的兄弟迟早要骂街。3. 从零设计一张业务表DDL完整实操3.1 需求拆解与字段规划光看语法容易犯困我拿一个电商订单表来走一遍完整的DDL设计流程。假设需求是订单基本信息和金额状态管理需要通过用户、状态、时间范围查询订单号绝对不允许重复同时要记录创建和更新时间。第一步是梳理字段清单。一张订单表至少要包含以下几个维度订单唯一标识主键id。业务单号order_no像“20250101000001”这类对接支付、物流都用它。用户信息user_id关联用户表。金额信息total_amount涉及钱一律用DECIMAL不用FLOAT或DOUBLE避免精度丢失。状态信息status下单、已支付、已发货、已完成、已取消等状态。时间信息created_at、updated_at、paid_at等。这一步不要急着写SQL先把业务方所有需求一条条列出来再反推需要哪些字段。我见过不少表刚上线就改了三轮基本都是需求复查没做透——要么漏了唯一订单号要么忘了平台来源字段只能反复ALTER往线上表加列。3.2 数据类型选择每个字段的“选型理由”字段类型定错了后期几乎都得返工。常见的几个选型原则整数类型主键通常用BIGINT考虑未来数据量INT最大约21亿很多大表都会撞到上限。状态字段用TINYINT就够不要为了“看起来整齐”都上INT。字符串类型用户名、昵称用VARCHAR(50)左右邮箱、地址这类长文本VARCHAR的长度按实际最坏情况留余量但也不要动不动就VARCHAR(1000)过长的变长字段在InnoDB里会影响索引和行存储。定长短字符串比如性别、状态码可以用CHARCHAR在读取时会省去变长处理但大多数场景VARCHAR已经足够。金额类型一定要用DECIMAL(10,2)这种定点数。FLOAT和DOUBLE是浮点数用二进制近似存储0.10.2都可能不等0.3做成账务系统就是事故。DECIMAL(10,2)能精确到分金额上限是99999999.99大多数中小业务够用。时间类型DATETIME和TIMESTAMP选谁简单说DATETIME不依赖时区范围更大1000年到9999年TIMESTAMP依赖数据库时区32位下上限到2038年。现代系统建议直接用DATETIME或者干脆存BIGINT毫秒时间戳看团队约定。3.3 约束设计主键、唯一键、非空、默认值约束是DDL里的“法律条款”目的就是保证数据合法不靠应用层自觉。主键约束一张表的身份证必须非空且唯一。InnoDB里主键还决定数据行物理排列顺序建议用自增BIGINT简单高效。业务主键虽然听着美好但要处理生成规则、重复风险复杂度很高大多数场景不推荐。唯一约束用户名、订单号这类不允许重复的业务值要加UNIQUE KEY。它既保证数据正确又顺便提供了唯一索引查询效率也有保障。非空约束能NOT NULL的字段尽量NOT NULL。空值处理逻辑IS NULL、COALESCE、COUNT相当繁琐而且很多索引在NULL列上效率会打折扣。默认值约束状态值给个DEFAULT 1时间给CURRENT_TIMESTAMP钱给0。默认值能避免程序漏传字段导致写入失败也是数据规范性的一道保险。外键约束理论上很美好保证表间引用完整性。但互联网大流量场景下一般不怎么用外键原因不外乎性能开销和分库分表导致外键失效。我个人的做法是核心系统在单库场景下可以保留外键分布式架构里宁可牺牲外键换成应用层补偿校验也别让外键卡住扩展。看一段完整的用户订单表DDLCREATE TABLE orders ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT 主键, order_no VARCHAR(32) NOT NULL COMMENT 订单号, user_id BIGINT UNSIGNED NOT NULL COMMENT 用户ID, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 订单金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 订单状态0待支付 1已支付 2已发货 3已完成 4已取消, consignee_name VARCHAR(50) NOT NULL COMMENT 收货人姓名, consignee_phone VARCHAR(20) NOT NULL COMMENT 收货人电话, shipping_address VARCHAR(255) DEFAULT NULL COMMENT 收货地址, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 下单时间, paid_at DATETIME DEFAULT NULL COMMENT 支付时间, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_created (user_id, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;注意这里唯一键选了订单号而不是主键订单号要防止同一笔订单重复创建联合索引idx_user_created覆盖了最频繁的“按用户和时间查订单”场景。3.4 索引设计什么时候建、建在哪里索引设计说到底是门权衡艺术。索引能让查询飞快但会拖慢写入还会占更多磁盘空间。常见的原则是高频查询条件加索引低频写表控制索引数量小表干脆别加索引扫描全表也就几十毫秒的事。联合索引要遵循最左前缀原则。比如idx_user_created(user_id, created_at)它可以支持user_id单独查也支持user_idcreated_at范围查但不能只按created_at查询时充分利用索引。字段顺序很讲究通常把区分度高的、查询条件更频繁的放前面。订单状态这种枚举字段区分度低放最左位置意义不大一般放最后。唯一索引除了约束功能还能帮优化器做优化。有了uk_order_no后查询WHERE order_no...MySQL能直接走唯一索引快速定位不用回表查主键再过滤。索引设计里还有一个容易忽略的点隐式类型转换会废掉索引。比如order_no列是VARCHAR查询写成WHERE order_no20250101000001数字类型传入会让数据库做隐式转换索引可能用不上这就是平时说的“慢SQL优化”现场。3.5 注释与命名规范长期维护的隐形刚需字段注释的作用在前面说过这里强调一下命名规范。库名、表名、字段名统一用小写加下划线比如user_orders、order_detail不要用驼峰也尽量别用保留字做名字。MySQL里ORDER是保留字直接CREATE TABLE order会语法报错逼你到处加反引号纯粹给自己挖坑。表名前缀建议按业务模块分组比如订单模块的所有表都叫orders_xxx用户模块叫user_xxx这样数据字典里一眼能看出归属。字段名也要统一主键统一叫id逻辑删除字段统一叫is_deleted时间字段统一created_at和updated_at。团队里只要把一条命名规范写成文档后面所有人照着执行数据库长期维护成本能降一大截。4. 生产环境执行DDL的那些经验教训4.1 高峰期别手贱直接改大表开发环境随便ALTER生产环境可就是另外一回事了。MySQL 5.6以前ALTER TABLE很多时候会锁住整张表期间所有读写全部阻塞一百万的表改结构都能让业务停摆几分钟。5.6之后引入在线DDL很多操作用ALGORITHMINPLACE可以在线执行但LOCKNONE也并不是万能的还需要持有几个版本都要兼容的元数据锁元数据锁本身也会阻塞后续的新查询。我先给一个实操场景某个线上订单表有5000万行运营临时要求加一个字段项目排期在下周三上线。如果周五下午直接扔一条ALTER TABLE上去很可能在线DDL执行十分钟这段时间所有写操作被卡住支付回调、库存扣减全部堵塞运维监控直接被报警刷屏。正确做法是提前在低峰期窗口执行或者用专业工具在线变更不管多急不要在交易高峰期赌国产数据库能扛住。4.2 MySQL和SQL Server的DDL差异跨库迁移最容易被坑很多团队用的是SQL Server看到MySQL的语法就原样拷过去十个里有八个报错。两个数据库在DDL上的差异相当大举几个常见点第一个是自增列语法。MySQL是AUTO_INCREMENTSQL Server是IDENTITY(1,1)。如果从SQL Server迁到MySQL字段定义写AUTO_INCREMENT没问题反向迁移时就要改成IDENTITY。第二个是修改表结构的语法细节。MySQL里改字段类型是MODIFY COLUMNSQL Server是ALTER COLUMN重命名字段SQL Server用sp_rename存储过程跟MySQL的ALTER TABLE CHANGE COLUMN完全两回事。第三个是数据类型命名差异。SQL Server用NVARCHAR存UnicodeMySQL里VARCHAR加utf8mb4字符集就够了SQL Server用GETDATE()拿当前时间MySQL是NOW()或CURRENT_TIMESTAMP。这些都是DDL写完后要立刻排查的兼容性问题。我建议团队在做跨数据库迁移时先写一份“数据库方言对照表”把常用DDL语法在两边的写法列出来逐条比对别指望一条SQL吃遍所有数据库。4.3 大表结构变更的稳妥方案大表几千万行甚至上亿行做结构变更直接ALTER风险太大生产环境基本都会用专业方案。MySQL生态里比较出名的是Percona Toolkit里的pt-online-schema-change以及GitHub开源的工具Appropriate namegh-ost。原理都是类似的创建一张影子表按原表结构加好新字段然后通过触发器或者Binlog同步数据最后在某个时间点切换表名实现近乎无感知的在线变更。实际使用时流程大概是创建一张空表结构同原表再手动加上需要变更的新字段。工具接管后续把原表数据分批拷贝到新表。拷贝期间用触发器或者日志同步增量数据。拷贝完成原表加锁一瞬间把新表重命名替换原表。这里有一个经验值大表变更前先评估数据量估算拷贝时长预留足够的变更窗口变更完成后要立即检查主外键、约束、索引是否被完整迁移。用这类工具也有坑比如触发器方式在高并发写入下可能拖慢线上gh-ost虽然不依赖触发器但要额外占用磁盘和Binlog空间。方案没有绝对最优只有结合自己业务压力量级去选。4.4 DDL变更的备份与版本管理虽然DDL执行完很难回滚但变更之前做好备份是完全来得及的。线上改表结构之前的固定动作是用mysqldump或者直接导出生效的建表语句保存到变更脚本里方便随时重建表结构。数据备份至少要有全量备份DDL影响元数据但数据级的误操作比如DROP DATABASE或者误删表也得靠备份兜底。再往细了说我习惯把每次表结构的变更都记录到数据库脚本里按版本号管理。比如scripts目录下有v1.0_init.sql、v1.1_add_phone.sql这样的文件每次上线一个文件。这样任何人想看这张表从出生到现在的变更史翻历史一并串起来就行。别小看这个习惯等出问题回溯的时候你才知道哪个版本改了哪个字段这比从数据库元数据里一点点拼回去高效多了。另外一个实操里特别有用的经验DDL脚本尽量写成幂等形式执行前先判断对象是否存在。例如MySQL的CREATE TABLE IF NOT EXISTS就是幂等的反复执行不会报错也不会覆盖数据。生产环境脚本如果能写成幂等出错重试的成本会低很多。5. 常见问题速查与排坑记录5.1 数据库对象操作时最容易卡住的环境问题这个章节专门整理了一些实际中高频出现的问题不一定全都是DDL语法本身的问题但都发生在“管数据库结构”这条线上。SQL Server安装相关的问题也常被问起。比如安装SQL Server时提示“无法启动Windows Management Instrumentation (WMI)服务”这个报错会让安装流程直接中断一般先从服务管理里确认Windows Management Instrumentation服务有没有被禁用把它设为自动启动再重新安装。再比如想删除一个SQL Server数据库提示“无法删除数据库因为它正在使用中”这其实不是DROP语句本身的问题而是当前有连接占用了目标库。常见的处理方式是执行ALTER DATABASE dbname SET SINGLE_USER WITH ROLLBACK IMMEDIATE强制断开连接再执行DROP。顺带说一句安装SQL Server 2008 R2时提示“无法卸载Microsoft SQL Server 2008 R2安装程序支持文件因为安装了其他依赖它的组件”这类报错多半和系统补丁顺序、残留组件有关需要按官方文档手动清理不建议硬来。5.2 字符集与排序规则引发的“幽灵报错”数据库对象大多带了字符集和排序规则属性。两个表如果排序规则不一致做JOIN联查可能报错或者统计结果让人看不懂。建表时统一用utf8mb4SQL Server里统一用Latin1_General_CI_AS这类并在文档里固定下来就能堵住大部分这类问题。MySQL还有一个经典坑建表时用了utf8没选utf8mb4然后用户输入了emoji表情结果写入报错。utf8在MySQL里最多只能存3字节字符emoji是4字节直接溢出。这个坑最容易被项目上线后的小功能踩中提前在DDL阶段用utf8mb4就能避免。5.3 NULL值、去重统计和表结构设计的关系“sql去除空值”“sql语句去重查询”是搜索热词但在DDL层面做对设计后面DML会省很多事。一张表如果很多字段允许NULL查询时写WHERE name IS NULL还是WHERE name 语义完全不同DISTINCT去重时NULL行也会和空字符串行分在一起还是分开不同数据库可能有细微差别。统计时COUNT(name)会把NULL跳过去COUNT(*)则不会这直接影响报表数字。我的建议很简单业务字段在设计时就尽量NOT NULL没有值就显式计算默认值。对“用户是否填写了昵称”这种需求宁可加一个默认值用空字符串还是“未填写”由业务规定。这样DML端写起来清爽统计结果也少了一堆边界处理。5.4 字段类型隐式转换引发的“慢SQL优化”问题搜索热词里的“慢SQL优化”也和DDL有直接关系。我们曾经排查过一条查询Python后端传参数时把数值型ID当字符串传SQL执行MySQL把字符串转成数字再拿去比较。看似结果一样但MySQL对带索引的列做隐式转换时优化器经常放弃走索引扫描全表几分钟都查不出结果。最后是重新确认参数类型让两边对齐查询才恢复正常。这个问题的根源就在建表初期主键到底用BIGINT还是VARCHAR金额是DECIMAL还是FLOAT类型定得不严谨SQL写的时候稍微不注意就触发隐式转换。慢SQL优化别只盯查询语句本身先回看表结构定义类型设计合理才能让索引发挥该有的作用。5.5 表结构变更顺序先改灰度还是先改线上最后分享一个实操心得。线上表加字段如果是兼容性变更比如只加一个允许NULL的新字段一般是先把表的DDL在线上执行掉再发应用代码如果是破坏性变更比如要删字段或者改字段语义那就要应用代码和数据库变更做好协同顺序。一个常见流程是先在灰度环境把数据库结构和应用代码同步更新跑一轮测试观察监控确认无异常后再往生产推。生产环境也是先改从库、再主从切换、再改原主库减少单点变更带来的风险。这些虽然是运维范畴的步骤但DDL作为触发变更的源点开发人员应该有这个意识。走了这么多弯路之后我现在的习惯很固定写SQL先列字段清单注释一定写清楚状态字段给默认值金额用定点数大表变更绝不一把梭线上执行前先看备份和变更脚本。这一套流程不一定最酷炫但至少没再出过因为DDL设计失误导致的线上事故。数据库表结构这东西建的时候几十秒隐患却能埋好几年宁可前期多花点时间想清楚也别等别人生产环境骂娘了再来补。
返回列表