1. 约束条件的价值与设计思路1.1 数据完整性约束到底保护了什么我见过太多项目数据校验全写在应用层结果上线半年就开始出脏数据。用户表里出现两条一模一样的手机号积分表里出现负数订单表的外围表数据被删了结果订单还挂在那边。这些问题的根源只有一个应用层校验不是百分之百可靠的。并发请求、程序异常返回、运营同学直接改库、接口调用方绕过业务代码任何一条链路出问题脏数据就有机会钻进你的表里。数据库约束的本质是把这个守门的动作从业务代码下沉到了存储引擎层。你不需要在每次插入前都写一遍查一下有没有重复你只需要在建表的时候声明这列不允许重复之后的每一次写入数据库会自动完成校验。这个过程既绕过了应用层bug也绕过了直接连数据库的操作是所有数据操作都必须经过的关卡。所以我的习惯一直是应用层做校验是为了给用户一个友好的提示数据库约束做校验是为了守住数据最后的底线。两者不是二选一的关系而是各管一段。约束条件能够解决的问题归纳起来就三类实体完整性行和行之间能不能区分开、域完整性某列的数据值合不合理、引用完整性表与表之间的关联关系是否成立。理解了这三类再看接下来的各个约束类型你会发现所有的语法细节都是围绕这三件事展开的。1.2 列级约束与表级约束约束在定义方式上分成两种列级约束和表级约束。在SQL语法层面它们的区别很明确——列级约束直接跟在某个列定义的后面只作用于该列表级约束写在CREATE TABLE语句中所有列定义完之后一般以CONSTRAINT或某种约束关键字开头可以同时作用于多个列。我举个最简单的例子。给一张学生表设置学号非空可以直接写CREATE TABLE student ( stu_id INT NOT NULL, name VARCHAR(50) );这里的NOT NULL就是列级约束。如果你想把姓名和身份证号绑定成一个组合唯一约束意思是这两个字段的值组合起来不能重复那列级约束就办不到了必须写成表级约束CREATE TABLE student ( stu_id INT NOT NULL, name VARCHAR(50), id_card VARCHAR(18), CONSTRAINT uk_student_name_idcard UNIQUE (name, id_card) );表级约束之所以存在核心原因就是组合场景。复合主键、复合唯一键、外键定义这些几乎都是表级约束的舞台。另一个实际开发中的使用习惯是为了便于后续管理即使是单个列的唯一约束或检查约束我通常也用表级语法显式地写上约束名这样可以一眼看出约束的用途后面删改也方便。1.3 约束命名规范与约束信息的获取说到显式命名很多初学者不知道这是个坑。MySQL有个特点你在创建约束时不写名字它会自动生成一个名字。主键一般叫PRIMARY唯一约束叫列名外键叫表名_ibfk_数字这个数字还是从1开始递增的。看起来问题不大可等你某天想删掉某个外键时就会发现你得先去查它到底叫order_ibfk_2还是order_ibfk_3猜错一步就是报错。所以我在团队里的规范是所有约束统一使用约束类型_表名_列名的格式命名。例如唯一约束uk_student_id_card外键约束fk_order_user_id检查约束ck_user_age默认约束一般不需要单独命名查看一张表上已经存在的约束有两个途径最常用。一是直接看建表语句SHOW CREATE TABLE student;二是查系统表SELECT TABLE_NAME, CONSTRAINT_NAME, CONSTRAINT_TYPE FROM information_schema.TABLE_CONSTRAINTS WHERE TABLE_SCHEMA 你的库名 AND TABLE_NAME student;我建议排查问题时优先用SHOW CREATE TABLE因为输出信息是现成的完整SQL连字符集、索引、约束带不带选项都一目了然排查外键失败这类问题时信息量最够。2. 六大约束逐个拆解语法、原理、避坑2.1 NOT NULL 非空约束不允许未赋值NOT NULL是最好理解的一个约束它规定列的值不允许是NULL。但有一个概念必须分清楚NULL和空字符串是两码事。NULL表示这个值没有被赋值空字符串是赋了一个值为空格的字符串。NOT NULL约束只拒绝前者是可以正常插入的。很多业务上的必填项其实要求的是不能为空字符串那就需要在应用层或者用CHECK约束去补充单靠NOT NULL不够用。在InnoDB存储引擎里NULL并不是完全不占空间。对于变长字段NULL在记录头里需要额外的标志位来记录Nullable列的存储管理也更复杂一些。所以如果一列确实在业务上必须有值直接加上NOT NULL还能顺带节省一部分存储和计算开销虽然影响不算大但积少成多。动手修改一张已经存在的表时这里有个经典事故给一个有数据的列加上NOT NULL约束时如果该列当前已经存在NULL值那么ALTER TABLE会直接报错。常规做法是先把NULL值改成具体的值再执行加约束的语句UPDATE student SET phone WHERE phone IS NULL; ALTER TABLE student MODIFY COLUMN phone VARCHAR(20) NOT NULL;2.2 UNIQUE 唯一约束内容不能重复唯一约束的含义是一列或一组列中的值不能重复出现。它在MySQL中有一个隐藏的副产品创建唯一约束时会自动生成对应的唯一索引后续的查询如果恰好走了这个索引读数据也会变快所以唯一约束不只是检查重复还能当普通索引用。关于NULL的处理是唯一约束最容易踩坑的地方。MySQL允许一个被UNIQUE约束的列上存在多个NULL值。理由是标准SQL认为NULL不等于任何值包括另一个NULL所以每次都算不重复。实际业务里如果你的需求是手机号要么不被填写填了就必须唯一那这种特性正好符合需求。组合唯一约束的行为值得单独说。如果定义了UNIQUE (a, b)那么(1, 2)和(1, 2)会冲突但(1, 2)和(1, 3)不会(1, 2)和(2, 1)也不会。要注意的是如果a列是NULL那么即便b列重复也不会算冲突因为MySQL认为带有NULL的组合每行都是不确定的。这一点经常导致联合唯一失效排查时要特别警惕。加唯一约束最怕的就是表里已经有重复数据。比如我遇到过一张用户表历史上有好几条手机号相同的记录加唯一约束时直接报错给出的错误信息会提到重复的键值。解决办法只能是先用SQL找出重复数据合并或清理掉然后再加约束。2.3 PRIMARY KEY 主键约束表里的唯一身份证主键在MySQL里是非空唯一的组合加强版。一张表只能有一个主键它的值不能为NULL也不能重复而且InnoDB里主键直接决定了聚簇索引的结构——数据行在物理上就是按照主键顺序组织的。这就是为什么主键的选择会影响写入性能如果你用UUID这种随机字符串当主键数据插入时经常要把已有的行挪位置性能会明显变差而自增整型主键顺序插入性能最好。所以几乎所有的业务表我都会用一个无业务含义的自增主键CREATE TABLE user ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, username VARCHAR(64) NOT NULL, PRIMARY KEY (id) );注意BIGINT UNSIGNED的选择不建议用INT。一张表的自增主键一旦达到21亿INT就装不下了。虽然单表要写21亿行很难但表一旦被频繁删除插入AUTO_INCREMENT的值也会不断消耗INT实际可用的范围远没有想象中宽裕。主键也可以定义成复合主键比如成绩表用(student_id, course_id)共同做主键。但复合主键有几个副作用一是所有从表去引用它时外键要包含全部主键列非常笨重二是联合主键的索引占空间更大。我现在的习惯是业务逻辑上需要组合唯一时用UNIQUE约束主键一律用无业务含义的自增列既干净又稳定。2.4 FOREIGN KEY 外键约束表与表的血缘纽带外键约束是约束体系里最能体现引用完整性的部分。它保证从表里的某个值一定能在主表里找到对应的记录避免出现订单属于一个不存在的用户这种孤儿数据。使用外键必须先满足几个硬性条件两张表都要用InnoDB引擎MyISAM不支持外键被引用的列必须是主键或有唯一约束的列外键列和引用列的类型必须一致字符集也要一致在从表上外键列通常会自动创建索引如果没索引会帮忙加上。写一个典型例子CREATE TABLE user ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, username VARCHAR(64) NOT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE user_order ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT UNSIGNED NOT NULL, amount DECIMAL(10,2) NOT NULL, PRIMARY KEY (id), CONSTRAINT fk_order_user_id FOREIGN KEY (user_id) REFERENCES user (id) ON DELETE CASCADE ON UPDATE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;ON DELETE和ON UPDATE是外键动作的关键选项控制主表的数据被删除或更新时从表该怎么办。常用的有四种CASCADE主表删除/更新从表跟着删除/更新SET NULL从表对应的列变为NULL前提是该列允许NULLRESTRICT拒绝执行主表的删除/更新操作NO ACTIONInnoDB里和RESTRICT效果一样MySQL会立即检查并拒绝。我个人的建议是如果外键关系的场景里从表数据通常也应该跟着主表一起消失比如订单明细跟随主订单删除那就用ON DELETE CASCADE如果从表数据删除后会引起后续统计异常比如审计日志引用了用户表用户注销时日志不能删那就不该加CASCADE而是用RESTRICT阻止删除或者改用SET NULL让日志保留但用户引用置空。外键在开发中是有争议的不少团队因为分库分表或导入性能问题干脆不用外键全靠应用层维护。这个取舍没有绝对的对错。但如果你的业务是强一致性要求、单库部署、数据量可控外键是成本最低的防数据错乱手段。它把原来写一遍都嫌烦的先查后插逻辑变成数据库引擎自动完成的原子操作正确性更高。2.5 CHECK 检查约束给字段加一道业务门槛CHECK约束用来限制列的值必须满足某个表达式比如年龄在0到150之间性别只能是M或F。语法上很直接CREATE TABLE user ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, age INT NOT NULL, PRIMARY KEY (id), CONSTRAINT ck_user_age CHECK (age 0 AND age 150) );这里必须强调一个版本相关的历史问题MySQL 8.0.16之前CHECK约束是只解析、不强制的。也就是说你写上去不会报错但插入非法数据也能成功官方文档当时甚至不认为这算CHECK约束的实现。一直到8.0.16版本MySQL才开始真正强制执行CHECK约束。如果你还在用8.0.15或更早的版本想实现类似效果只能靠触发器或者ENUM枚举这是很多老项目没写CHECK的原因。CHECK表达式里有一些限制不允许使用子查询不允许调用存储函数也不允许使用变量和自增列。它的能力边界就是当前行的列的算术和比较运算想跨表校验就别指望约束了那是外键的活。2.6 DEFAULT 默认值约束懒惰但必要的兜底DEFAULT约束解决的是插入时没给这个列赋值那它就用一个默认值的问题。比如创建时间列用户注册时没传时间系统默认取当前时间CREATE TABLE user ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, username VARCHAR(64) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) );我经常看到初学者把DEFAULT和NOT NULL搞混加了DEFAULT的列仍然是Nullable的如果你插入时显式指定了NULL那它照样存NULL。想让不给值用默认值给了NULL直接报错就得DEFAULT和NOT NULL一起用。需要特别注意的是在MySQL 8.0的某些版本里DATETIME列想用表达式作为默认值时DEFAULT (表达式)必须带括号比如DEFAULT (CURRENT_TIMESTAMP)不带括号在老版本没问题在8.0.13之后的表达式默认值语法下有兼容风险。这个细节在跨版本迁移时很容易踩。用DEFAULT约束还有一个实际好处批量插入时SQL语句可以少写很多列代码也简洁不少。但反过来写INSERT时如果不显式声明列数据容易错位所以业务代码里我一般还是会显式列出列名只是在确实需要省略的少数场景才依赖默认值。3. 约束的增删改与实操过程3.1 建表时定义约束的完整案例把前面六种约束放到一个综合案例里更能看清它们如何协同工作。这里模拟一个简单的电商场景用户表、商品表、订单表、订单明细表。CREATE TABLE member ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, username VARCHAR(64) NOT NULL, phone VARCHAR(20), age INT, level TINYINT NOT NULL DEFAULT 1, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), CONSTRAINT uk_member_username UNIQUE (username), CONSTRAINT uk_member_phone UNIQUE (phone), CONSTRAINT ck_member_age CHECK (age 0 AND age 120), CONSTRAINT ck_member_level CHECK (level BETWEEN 1 AND 9) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE product ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, product_name VARCHAR(128) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0, PRIMARY KEY (id), CONSTRAINT ck_product_price CHECK (price 0), CONSTRAINT ck_product_stock CHECK (stock 0) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE orders ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, member_id BIGINT UNSIGNED NOT NULL, order_no VARCHAR(32) NOT NULL, total_amount DECIMAL(10,2) NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), CONSTRAINT uk_orders_order_no UNIQUE (order_no), CONSTRAINT fk_orders_member_id FOREIGN KEY (member_id) REFERENCES member (id) ON DELETE RESTRICT ON UPDATE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE order_item ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, order_id BIGINT UNSIGNED NOT NULL, product_id BIGINT UNSIGNED NOT NULL, quantity INT NOT NULL, price DECIMAL(10,2) NOT NULL, PRIMARY KEY (id), CONSTRAINT fk_item_order_id FOREIGN KEY (order_id) REFERENCES orders (id) ON DELETE CASCADE, CONSTRAINT fk_item_product_id FOREIGN KEY (product_id) REFERENCES product (id) ON DELETE RESTRICT ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这个设计的几个思路我说明一下订单表引用会员表时用ON DELETE RESTRICT会员不能随便删除得先处理订单避免财务数据丢失。订单明细引用订单表时用ON DELETE CASCADE主订单删除明细一起删掉符合业务直觉。订单明细引用商品表时用ON DELETE RESTRICT商品被下单后不应该直接删掉得下架处理而不是物理删除。stock字段用CHECK (stock 0)做底线保护比只在应用层判断库存更稳。3.2 用ALTER TABLE追加与删除约束约束定义后不是不能改。常见的修改场景是给已有表补约束或者删除某个约束。语法分类很清楚。追加唯一约束ALTER TABLE member ADD CONSTRAINT uk_member_email UNIQUE (email);追加检查约束ALTER TABLE member ADD CONSTRAINT ck_member_age CHECK (age 0);追加默认值或非空约束用MODIFY COLUMNALTER TABLE member MODIFY COLUMN level TINYINT NOT NULL DEFAULT 1;删除约束时不同约束有不同的写法。P人删除唯一约束或主键时有个关键坑因为唯一约束在底层是靠索引实现的所以删除唯一约束不是用DROP CONSTRAINT而是用DROP INDEXALTER TABLE member DROP INDEX uk_member_phone;如果当初创建时没有显式命名系统生成的唯一约束索引名就是列名本身用SHOW CREATE TABLE查一下确认准确的名字再删。删除主键ALTER TABLE member DROP PRIMARY KEY;删除外键ALTER TABLE order_item DROP FOREIGN KEY fk_item_order_id;删除外键和删除外键产生的索引是两码事。DROP FOREIGN KEY只删约束不删索引这个索引是MySQL自动创建的如果确实不需要了可以再用DROP INDEX单独删。但如果这个索引同时被别的查询用到删了会影响性能动手前先看执行计划。给已有表追加外键时还要注意先后顺序先确保被引用的主表列是主键或唯一键再确保从表目标列上有索引没有的话MySQL会自动创建最后再执行ADD CONSTRAINT。如果表里已经存在不满足引用关系的数据加外键也会直接失败。3.3 约束与索引的关系及性能影响约束和索引在MySQL里是强绑定的但两者的目标完全不同。索引是为了加速查询约束是为了保证数据合法。只是由于实现方式约束总会附带索引的副作用。主键约束在InnoDB里直接参与聚簇索引的构建全表的物理排序都围绕主键唯一约束一定会生成一个唯一索引插入时检测冲突也就是在查这个索引外键约束在从表上会自动创建索引用来加速外键检查同时避免每次DML操作都做全表扫描NOT NULL、CHECK、DEFAULT不会生成索引但会导致写入时多一步校验逻辑。性能上的代价主要体现在写入路径上。每次INSERT或UPDATE唯一约束需要做一次索引查找来判断是否重复外键约束需要去主表查被引用的记录是否存在CHECK约束要计算一次表达式。数据量小的时候这些开销可以忽略但如果你在导入几百万行历史数据时还开着所有约束导入速度会明显下降。解决批量导入性能问题业界的常规操作是先临时关掉外键检查导入完成后再打开SET foreign_key_checks 0; -- 执行批量导入 SET foreign_key_checks 1;但这招有风险如果导入的数据本身引用关系就是错的你开着检查它进不去关了检查它进去了等再打开开关时数据库不会主动重新校验已有的脏数据。所以关检查导入前必须先自己在脚本里确保数据的引用关系是完整的这属于用效率换风险的权衡不能盲目用。4. 常见问题与排查技巧实录4.1 外键创建失败的典型原因外键是我被问得最多的一类约束问题报错信息反反复复就那么几种。我把实际工作中见过的原因整理一下。第一类是类型不一致。主表主键是BIGINT从表外键定义成INT创建外键时MySQL直接报错错误信息里会包含foreign key constraint is incorrectly formed这种关键字。两个列的类型必须完全一致注意不只是数字类型INT和BIGINT都不行必须统一。第二类是字符集和排序规则不一致。主表是utf8mb4从表是latin1连接条件都跑不通外键自然建不上。即使两张表的CHARACTER SET一致排序规则也可能不同比如一张表用utf8mb4_general_ci另一张用utf8mb4_0900_ai_ci这也会导致建外键失败。检查时用SHOW CREATE TABLE 主表; SHOW CREATE TABLE 从表;重点看DEFAULT CHARSET和COLLATE两处。第三类是被引用的列不是索引列。被引用的列必须是主键或者有唯一约束的列或者是已建索引列的最左前缀。比如主表只有一个(a, b)的联合索引你想引用b列就不行。第四类是已有数据不满足外键约束。从表里存在某个值在主表里找不到对应记录创建外键时就会报错。这种情况下错误提示比较明显里面会直接提到一个具体的键值。先用一条LEFT JOIN查出来哪些是孤儿数据清理后再建外键。4.2 唯一约束与NULL的相爱相杀唯一约束和NULL的问题是很多业务事故的源头。前面提到UNIQUE允许多个NULL存在这本身不是问题问题在于业务理解。我接手过一个项目用户表用char(11)的手机号列建了唯一约束业务上认为未填写手机号的用户应该只允许存在一个NULL但MySQL实际允许多个NULL。结果运营统计时发现几万个用户都是NULL谁也没办法区分他们到底是不是同一个人。如果你真的需要NULL也算重复的效果可以这样绕过把这列改成不允许NULL默认值是空字符串然后在唯一约束上就会把空字符串当成一个普通的重复值来校验。但要考虑业务上是否真的能代表未填写。另一个常见坑是在MySQL的排序规则是大小写不敏感的情况下比如默认的utf8mb4_general_ci唯一约束会把Abc和abc判定为重复。如果你的业务区分大小写建唯一约束前必须把列或表的排序规则改成utf8mb4_bin否则用户注册时输入了大小写不同的用户名就会莫名撞车。4.3 约束检查时机与严格模式MySQL的约束检查默认是即时执行的每执行一条SQL就立刻检查是否违反约束违反就报错。这个行为好理解但有一个隐藏的开关值得注意——foreign_key_checks。这个变量影响的是外键检查时机设为0后你可以先删除主表记录再慢慢处理从表数据最后把开关打开。它和事务配合时要注意在事务里关闭外键检查删除主表数据再提交前重新打开如果从表还有对应记录并不会在你打开开关的那一刻立刻报错而是等你下一次DML操作才触发检查。所以不要天真地以为打开开关就万事大吉脏数据照样留在库里。严格模式sql_mode对约束影响也很大。比如往NOT NULL列插入NULL在非严格模式含NO_ZERO_DATE等宽松设定下MySQL会把这个NULL转成空字符串或0并给一个警告数据照样插进去而严格模式会直接报错。项目刚启动时我强烈建议把sql_mode设为包含STRICT_TRANS_TABLES的配置否则那些警告会掩盖掉大量本应该被拦截的非法数据。唯一的坏处是应用层要处理更多报错但这恰恰是让你尽早发现问题的机制。4.4 字符集与排序规则对约束的影响除了外键字符集和排序规则还影响唯一约束的判定这一点容易被忽略。排序规则决定了字符串的比较方式*_general_ci是大小写不敏感、重音不敏感*_bin是二进制比较大小写敏感utf8mb4_0900_ai_ci是8.0默认的也大小写不敏感。创建唯一约束时MySQL是按列的排序规则来做重复判定的。举个例子。用户名列用utf8mb4_general_ci插入Admin后再插入admin唯一约束会拦截因为两个值在比较时被认为是相同的。如果你的注册逻辑允许用户名区分大小写那这个唯一约束就会误伤正常用户。解决办法是把该列的排序规则改成utf8mb4_binALTER TABLE member MODIFY COLUMN username VARCHAR(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL;但改排序规则会影响此前已有的全部数据字母的大小写内容并不会变只是后续比较规则变了改之前一定要结合业务评估。变更后原来因为大小写撞车而被拒的记录不会自动合并只会让新的插入规则改变。4.5 常见报错速查表我把这几年实际处理过的高频约束报错整理成一个速查表方便排查时对照。报错关键词典型原因处理建议Column ... cannot be null违反NOT NULL约束检查ORM传参和默认值设置显式传NULL就是不该被允许Duplicate entry ... for key ...违反唯一约束或主键查出重复数据合并清理后重试确认排序规则是否误伤Cannot add foreign key constraint外键创建失败用SHOW CREATE TABLE检查类型、字符集、索引、已有数据Failed to open the referenced table被引用表不存在或引擎非InnoDB确认表名、库名、引擎类型Check constraint ... is violated违反CHECK约束查看表达式定义确认写入值是否真的合法Incorrect table definition主键或外键定义结构错误通常是被引用列不是唯一索引或列数不匹配Data too long for column值超出列长度先看字符集下字节长度是否超出调列长度或用TEXT但要小心索引限制最后一列的处理建议只有思路没有万能脚本因为每张表的脏数据分布不一样。排查时我的标准流程是先SHOW CREATE TABLE看结构再SELECT实际违反约束的数据最后用UPDATE或DELETE清掉脏数据再回到ALTER TABLE补约束。再说一个最容易忽略的问题约束是有状态的如果你在事务里先HOLD住某行锁再在另一条链路里尝试触发外键检查可能造成死锁。表现上是应用日志里出现Deadlock found但根本原因往往和外键的锁顺序有关。遇到这种问题先从SHOW ENGINE INNODB STATUS里看最后发生死锁的SQL再回头调整外键设计或事务操作顺序。关于约束我最后想补一句实际的体会约束不是越强越好也不是越多越好。外键和复杂CHECK在保证数据正确性的同时确实拖慢了写入性能、增加了表结构变更的成本。合理的做法是核心业务表把该加的约束都加齐非核心的业务表只保留主键和必要的唯一约束。毕竟数据进到库里出了问题再想修复代价永远比当初多写一行约束要大得多。我个人在项目中还有一个习惯每次建完表都顺手导一份SHOW CREATE TABLE到文档库里标注每张表引入了哪些约束、删除时要注意什么依赖关系。长期运维下来这个动作能在你三个月后回头改表时帮你省下大量排查时间。如果接下来你要做分库分表或者优化大批量导入记住一个原则约束是数据质量的兜底兜底可以挪动但不可以消失。