搞数据库开发这些年,最绕不开的一个需求,就是“把一对多关联的多项选择字段查出来”。做后台管理系统的人体会应该最深:商品有多个分类标签、文章有多个专题、用户有多个兴趣爱好、订单对应多个商品明细。这一类需求,就算不挂在“课程设计”里,也早晚会在实际项目里出现,而且十个人里有八个会写错,要么查出来一堆重复数据,要么因为用了不合适的关联方式把数据库拖到慢查询日志里躺平。
这篇文章就从实际业务场景出发,把“一对多关联的多项选择字段查询”这件事彻底聊透,包括数据模型应该怎么设计、查询方案怎么选、SQL怎么写才能既正确又高效、以及那些光看文档根本学不到的排坑经验。不管你是大作业里被老师点名要求“设计商品多标签系统”的学生,还是在公司里维护老系统的开发,这篇文章都能直接拿去用。
1. 先搞清楚“多选字段”到底该怎么存:数据模型设计是查询效率的起点
很多人一上来就急着写SQL,但查询效率的根子其实在表结构设计阶段就已经定了。一项选择字段看起来简单,实际上牵涉到“字段描述的是什么实体”和“一次选择的结果是单选还是多选”两个问题。如果只是单选,一张表加一个外键字段就行,用不着折腾。可一旦涉及到多选,数据模型从一开始就没设计好,后面不管怎么调优SQL都是拆东墙补西墙。
1.1 三种常见建模方案:逗号分隔、JSON数组、关联表
先看第一种“逗号分隔字符串”方案,比如在一个article表里加一个字段,存成“1,2,3,4”或者“娱乐,体育,科技”这种格式。这种方案在小项目里特别常见,因为直观,写入的时候拿个implode就拼进去了。但查询的时候,如果你要查“包含标签2的文章”,就不得不写FIND_IN_SET或者LIKE '%2,%',走不了索引,数据量一大,全表扫描是必然的。更坑的是,如果标签ID是两位数以上,比如12和2,一个LIKE '%2%'就把不该查出来的数据也带出来了,只能靠仔细设计分隔符和匹配规则去规避,维护成本极高。
第二种方案是在表里存JSON数组,比如MySQL 5.7之后支持的JSON字段类型,原本是["娱乐","体育"]这种形式。JSON方案的好处是读的时候很轻松,PHP、Java那边拿回来直接json_decode就能用,但坏处也很明显:如果要在SQL里根据JSON内部的值做关联、统计、去重,语法会变得非常别扭,得用JSON_CONTAINS、JSON_TABLE这类函数,索引只能通过虚拟列或者函数索引去碰运气,对开发者心智负担很大。它只适合“存起来、读出来、前端展示”这种低频查询场景,不适合做核心业务里高频的筛选过滤。
第三种方案,也是我要重点推荐的,就是建一张“关联表”,也就是中间表。比如商品表product、标签表tag、商品标签关系表product_tag。关系表里只放商品ID、标签ID,必要时加一个自增ID或者一个int类型的sort字段做排序,然后对(product_id, tag_id)建联合唯一索引、对tag_id单独建索引。这套设计,业界叫“实体-属性-值”的一种简配版,但比EAV更规范。它看着多了一张表,写起来要多写几条插入语句,可一旦数据量上万,查询和统计的需求一多,它带来的灵活性和性能优势,是前两种方案完全比不上的。
1.2 为什么我推荐关联表:索引优势和扩展性
我见过不少初学者对中间表有抵触心理,觉得“不就是多选几个标签吗,搞这么麻烦干什么”。但真实原因很简单:关联表是唯一能让查询完全走B+树索引的方案。MySQL、PostgreSQL、Oracle这些关系型数据库,最擅长的事情就是把一大撮数据按索引树去快速缩小范围。你用逗号分隔或者JSON数组,等于把一个本该是“多行数据”的东西硬塞进“一个字段”,数据库最拿手的那套查找机制就直接废掉了。
中间表的扩展性也更好。假设你的需求从“商品有哪些标签”扩展成“商品的标签是谁在什么时间打上的”,那你只需要在中间表加两个字段,比如operator_id和created_at,一切问题迎刃而解。但如果用的是逗号分隔方案,这种“操作的审计痕迹”根本无从记录。再比如说你想知道“同时有标签A和标签B的商品有哪些”,在中间表模型下一条JOIN或者EXISTS就能搞定,而在逗号分隔模型下得写一堆函数嵌套,逻辑很容易崩。
所以,第一步永远是:遇到多选字段,先建中间表。你要是已经接手了逗号分隔字段的老项目,也别慌,后面第二大部分会讲兼容这种历史数据时怎么在查询层面做补救。
2. 查询方案选型:EXISTS、IN、JOIN聚合、GROUP_CONCAT四大主流路线
数据模型定下来之后,真正的重头戏就是查询方案了。项目标题里的关键词是“高效”,所以我这里不会只丢几条能跑通的SQL出来,而是要把常见的几种写法摆在一起,告诉你各自的性能特征、适用场景和踩坑点。
2.1 精确多选匹配:EXISTS与IN的战争
业务里的多选查询,大部分是“精确匹配”逻辑。比如“找出同时拥有标签A和标签B的订单”,或者“找出拥有标签1、3、5中的任意一个的商品”。两种逻辑虽然都是多选,但SQL写法完全不同。
先说“同时拥有多个标签”这种交集型需求。最直观的初学者写法是JOIN两次中间表:
SELECT p.* FROM product p INNER JOIN product_tag pt1 ON pt1.product_id = p.id AND pt1.tag_id = 1 INNER JOIN product_tag pt2 ON pt2.product_id = p.id AND pt2.tag_id = 3;这样写能出结果,中间表上(product_id, tag_id)有联合主键的话,性能也不差。但问题在于,标签个数一旦是动态的,SQL字符串拼接会变得很啰嗦,而且JOIN一次中间表就会让结果集的中间行数膨胀一次,数据量大了以后,排序和DISTINCT的开销都会上升。
另一种写法是用EXISTS,我实际项目里用得最多的就是这种:
SELECT p.* FROM product p WHERE EXISTS (SELECT 1 FROM product_tag pt WHERE pt.product_id = p.id AND pt.tag_id = 1) AND EXISTS (SELECT 1 FROM product_tag pt WHERE pt.product_id = p.id AND pt.tag_id = 3);EXISTS这种半连接,子查询只要匹配到一行就会返回TRUE,不会产生中间行的膨胀,配合中间表上的联合主键,执行计划通常都很漂亮。而且从语义上来讲,它天然就是“交集”的高效实现,每多一个标签条件,就多一个EXISTS子句,逻辑清楚,不容易错。
再说“包含任意一个标签”这种并集型需求。用IN是最直接、也最好读的:
SELECT p.* FROM product p WHERE p.id IN (SELECT product_id FROM product_tag WHERE tag_id IN (1, 3, 5));这条SQL里,内层子查询先在product_tag上找到所有符合标签条件的product_id,然后外层主表根据ID做等值匹配。MySQL优化器对IN子查询通常会改写成半连接,性能相当不错。需要注意的一点是,IN列表里的值不能太多,如果标签个数成千上万,建议拆批或者改成临时表JOIN,否则优化器生成的执行计划可能会变得非常复杂。
这里对比一下IN和EXISTS的使用边界:IN适合“任意匹配一个”的并集逻辑,EXISTS适合“全部匹配”的交集逻辑。但在某些数据库版本里,IN和EXISTS在特定数据分布下执行计划会等价转换,所以最稳妥的还是用EXPLAIN去验证,不要凭感觉拍板。
2.2 模糊匹配与聚合匹配:GROUP_CONCAT的坑
除了精确匹配,还有一种频率很高的需求是“聚合匹配”和“模糊匹配”。什么叫聚合匹配?就是你希望把商品的标签拼成一串返回,比如在列表页显示“娱乐, 体育, 科技”这个字符串,那么就需要对中间表做GROUP BY后聚合。MySQL里最常用的聚合函数是GROUP_CONCAT,Oracle里有LISTAGG,PostgreSQL是STRING_AGG,SQL Server是STUFF加FOR XML PATH。
GROUP_CONCAT用起来很简单:
SELECT p.id, p.name, GROUP_CONCAT(t.name ORDER BY t.sort SEPARATOR ',') AS tag_names FROM product p LEFT JOIN product_tag pt ON pt.product_id = p.id LEFT JOIN tag t ON t.id = pt.tag_id GROUP BY p.id, p.name;但这里我劝你提前踩醒一个坑:GROUP_CONCAT的默认最大长度只有1024个字符,超过这个长度内容会被静默截断,你拿到手的数据莫名其妙少了一段。解决办法是在执行查询前设置group_concat_max_len参数,比如SET SESSION group_concat_max_len = 10240。如果是公司统一管理的数据库,参数改不了,那你就得考虑用自定义临时表或者拆分拼接逻辑。
再看模糊匹配场景。这个场景通常发生在老系统里,字段存的是逗号分隔字符串,你没法改表,只能查。这种情况下SQL写起来就很拧巴:
SELECT * FROM product WHERE FIND_IN_SET(2, tag_ids) > 0;或者用LIKE配合分隔符:
SELECT * FROM product WHERE CONCAT(',', tag_ids, ',') LIKE '%,2,%';这两条都能查出“包含标签2”的记录,但它们的共同特点是:索引大概率失效,小数据量无所谓,大数据量直接变成慢查询。真要优化的话,要么改表结构,要么在应用层另建关系映射,不能想着纯靠SQL把这问题根治。
2.3 方案对比与适用场景速查
为了让你做技术选型的时候一眼看明白,我把上面的内容整理成一个对照表。
| 需求类型 | 推荐方案 | 核心SQL特征 | 性能特点 | 注意事项 |
|---|---|---|---|---|
| 交集(同时拥有全部标签) | EXISTS | 每个标签一个EXISTS子查询 | 子查询走索引,不膨胀中间行 | 标签多时SQL会变长,可考虑动态拼接 |
| 并集(拥有任意一个标签) | IN / EXISTS | 子查询返回ID集合,主表等值匹配 | 优化器改写半连接后性能好 | IN列表过大时改用临时表JOIN |
| 聚合(把多行标签拼成字符串) | JOIN + GROUP_CONCAT | 按主表ID分组聚合 | 分组和聚合占资源,注意索引 | 注意group_concat_max_len限制 |
| 模糊/兼容老表(逗号分隔字段) | FIND_IN_SET / 拼接LIKE | 函数匹配表达式 | 索引失效,性能差 | 数据量大时必须做数据迁移改造 |
这个表就是我平常做技术评审时会给同事看的东西。不搞虚的,什么场景配什么SQL,直接对号入座。
3. 从建表到SQL调优的一次完整实操:以商品多标签系统为例
说了这么多理论,是时候落到实际操作上了。下面我用一个非常经典的业务模型——“商品多标签系统”来做一整套实验。这套流程我已经帮不同的课程设计和公司项目重复过很多次,你照着走一遍,基本就能掌握这类查询的完整套路。
3.1 建表与造数:准备一套可复现的测试数据
先建三张基础表:product商品表、tag标签表、product_tag商品标签关系表。MySQL和Oracle的语法差异主要在自增字段和分页上,我这里以MySQL为例,其他数据库思路完全相同。
CREATE TABLE product ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10, 2) NOT NULL DEFAULT 0.00, status TINYINT NOT NULL DEFAULT 1 COMMENT '1上架 0下架', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE tag ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL, sort INT NOT NULL DEFAULT 0, PRIMARY KEY (id), UNIQUE KEY uk_name (name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE product_tag ( product_id INT UNSIGNED NOT NULL, tag_id INT UNSIGNED NOT NULL, sort INT NOT NULL DEFAULT 0 COMMENT '标签排序,小值在前', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (product_id, tag_id), KEY idx_tag_id (tag_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;注意product_tag表的主键我故意设计成(product_id, tag_id)联合主键,这样既能保证同一商品不会重复打同一个标签,又可以让“按商品ID查标签”和“按标签ID查商品”两类查询都走覆盖索引。单独的idx_tag_id是给“从标签反查商品”准备的。
接下来造数据。这里分享一个我一直用的存储过程,循环插入1万商品、100个标签、30万条左右的关系数据,用来压测SQL性能完全够了。
DELIMITER $$ CREATE PROCEDURE insert_test_data() BEGIN DECLARE i INT DEFAULT 1; DECLARE j INT; DECLARE tag_count INT; SET SESSION group_concat_max_len = 10240; WHILE i <= 10000 DO INSERT INTO product (name, price) VALUES (CONCAT('商品', i), ROUND(RAND() * 1000, 2)); SET tag_count = 1 + FLOOR(RAND() * 8); SET j = 1; WHILE j <= tag_count DO INSERT IGNORE INTO product_tag (product_id, tag_id, sort) VALUES (i, 1 + FLOOR(RAND() * 100), j); SET j = j + 1; END WHILE; SET i = i + 1; END WHILE; END$$ DELIMITER ; CALL insert_test_data();这段不会特别快,大概几十秒到几分钟,取决于你机器性能。关键在于INSERT IGNORE的用法,它可以避免随机过程里踩到联合主键重复的值,保证造数不会中断。
3.2 核心查询语句示例:五个高频场景
造完数据,下面高频场景挨个过。
场景一:查出“同时拥有标签1和标签2”的商品。这是课程设计里最常被点名的需求之一。
SELECT p.id, p.name FROM product p WHERE EXISTS (SELECT 1 FROM product_tag pt WHERE pt.product_id = p.id AND pt.tag_id = 1) AND EXISTS (SELECT 1 FROM product_tag pt WHERE pt.product_id = p.id AND pt.tag_id = 2);场景二:查出“拥有标签1或标签2或标签3”的商品。
SELECT DISTINCT p.id, p.name FROM product p INNER JOIN product_tag pt ON pt.product_id = p.id WHERE pt.tag_id IN (1, 2, 3);这里注意我加了DISTINCT,因为一个商品可能同时打了标签1和标签2,JOIN之后会出现重复行。如果你的项目对SQL比较敏感,也可以换成EXISTS版本:
SELECT p.id, p.name FROM product p WHERE p.id IN (SELECT product_id FROM product_tag WHERE tag_id IN (1, 2, 3));这个写法避免了外层DISTINCT,我个人更推荐。
场景三:查出商品列表以及对应的标签名称聚合串。
SELECT p.id, p.name, GROUP_CONCAT(t.name ORDER BY pt.sort SEPARATOR '、') AS tag_names FROM product p LEFT JOIN product_tag pt ON pt.product_id = p.id LEFT JOIN tag t ON t.id = pt.tag_id WHERE p.status = 1 GROUP BY p.id, p.name ORDER BY p.id DESC LIMIT 20;场景四:统计每个标签下的商品数量,并按数量倒序。
SELECT t.id, t.name, COUNT(pt.product_id) AS cnt FROM tag t LEFT JOIN product_tag pt ON pt.tag_id = t.id GROUP BY t.id, t.name ORDER BY cnt DESC;场景五:查出“拥有标签1但不拥有标签2”的商品。这种“排除”型需求,用NOT EXISTS比较好读。
SELECT p.id, p.name FROM product p WHERE EXISTS (SELECT 1 FROM product_tag pt WHERE pt.product_id = p.id AND pt.tag_id = 1) AND NOT EXISTS (SELECT 1 FROM product_tag pt WHERE pt.product_id = p.id AND pt.tag_id = 2);这一组SQL覆盖了交集、并集、聚合、统计、差集五种类型,基本上该踩的坑都踩过了。
3.3 索引设计与慢查询优化:用EXPLAIN说话
SQL写对了只是第一步,跑得快不快,得看索引。拿场景一来说,EXISTS子查询的过滤条件是product_id = p.id AND tag_id = 1,这正好命中product_tag表的联合主键(product_id, tag_id),所以子查询可以用到主键等值查找,效率极高。我建议你执行完SQL以后,用EXPLAIN看一眼执行计划。
EXPLAIN SELECT p.id, p.name FROM product p WHERE EXISTS (SELECT 1 FROM product_tag pt WHERE pt.product_id = p.id AND pt.tag_id = 1) AND EXISTS (SELECT 1 FROM product_tag pt WHERE pt.product_id = p.id AND pt.tag_id = 2);执行计划里你应该看到:product表是全表扫描还是主键扫描取决于你查询条件里有没有过滤;而product_tag表的访问类型应该是ref或者eq_ref,key显示PRIMARY,rows大约等于1,这样的子查询代价非常低。
场景三里那个GROUP_CONCAT加上JOIN的SQL,执行计划的重点在GROUP BY是否有合适的索引。如果product表本身有过滤条件,比如WHERE p.status = 1,建议在product表上建一个包含status和created_at的复合索引,用来减少外层结果集规模。中间表JOIN列本身就都在联合主键里,不需要额外建索引。
优化慢查询的时候,我习惯先用慢查询日志定位SQL,然后从执行计划里找三样东西:type是不是ALL、key是不是NULL、rows是不是特别大。如果三项都中,那就要么补索引,要么改写SQL。这里我再强调一句:不要盲目给每个字段都加索引,加索引是有内存和写入代价的,联合索引的字段顺序也得是“等值条件在前、范围条件在后”,否则优化器没办法走最左前缀。
4. 常见问题与排查技巧实录
这一部分我从自己踩过的坑里挑几个特别典型的,整理成速查表,后面再展开说细节。这些场景网上很难找到统一答案,但项目里真的三天两头遇到。
| 问题现象 | 根本原因 | 解决办法 |
|---|---|---|
| 用IN子查询时返回结果重复 | 中间表存在多条符合条件的关系记录 | 主表加DISTINCT,或改用EXISTS |
| 关联查询速度慢,日志出现全表扫描 | 中间表缺索引,或JOIN字段没走索引 | 检查执行计划,补联合主键和二级索引 |
| GROUP_CONCAT结果被截断 | group_concat_max_len默认太小 | 调大会话参数,或换应用层拼接 |
| 查“同时拥有A标签和B标签”结果为空 | 错误使用了IN而不是交集逻辑 | 改用EXISTS或多个JOIN |
| 更新子查询时MySQL报错 | 同一张表不允许在FROM和UPDATE子句中同时出现 | 扣一层临时表,或用JOIN写法 |
| 分页数据重复或丢失 | JOIN导致中间行数膨胀,DISTINCT与LIMIT配合不当 | 先查主表ID再回表,避免直接对JOIN结果分页 |
4.1 IN子查询结果集过大的坑
IN子查询在数据量小时很舒服,在数据量大时会变成灾难。查询条件匹配了上万条product_tag记录,优化器要把这些结果全部带进内存构建一个哈希集合,再做外层匹配。如果构建集合的时间过长,执行计划可能不会选择物化而选择别的策略,结果就更不可控。我的经验是,IN列表或者子查询返回的ID数量超过几千级别,就果断改成临时表方案:
CREATE TEMPORARY TABLE tmp_tag_filter (tag_id INT PRIMARY KEY); INSERT INTO tmp_tag_filter VALUES (1), (2), (3); SELECT DISTINCT p.id, p.name FROM product p INNER JOIN product_tag pt ON pt.product_id = p.id INNER JOIN tmp_tag_filter tf ON tf.tag_id = pt.tag_id;临时表方案的好处是让优化器先物化过滤条件,再走JOIN,索引命中率高,而且SQL本身可读性也不差。用完别忘DROP TEMPORARY TABLE,避免连接池复用连接时残留数据。
4.2 DISTINCT与JOIN放大效应的真相
有一次我帮别人排查一个课程设计里的SQL,逻辑是对商品表JOIN标签中间表后做分页。结果第一页20条数据里出现了4条重复,翻页到后面还发现数据数量对不上。这就是经典的JOIN放大效应:一个商品打了5个标签,JOIN之后在结果集里就变成了5行;如果外层再JOIN另一张图片表,行数可能膨胀到几十倍。大量重复行会让分页显得错乱,也让无谓的排序开销变大。
解决方案很简单:不要直接对JOIN结果做分页。先查主表ID,再关联次要表。比如:
SELECT p.id, p.name FROM product p INNER JOIN product_tag pt ON pt.product_id = p.id WHERE pt.tag_id IN (1, 2) GROUP BY p.id ORDER BY p.id LIMIT 20;拿到这20个主表ID之后,再查一次这些ID的标签聚合信息,应用层把数据组装起来。这样每一层的数据都是干净、不膨胀的,分页也不会出问题。前期多一步代码组装,后期省下的时间是成倍的。
4.3 NULL与空集合的处理
多项选择字段还有一个隐藏坑:空集合。如果某个商品没有任何标签,那么LEFT JOIN之后tag表字段全是NULL;如果应用层拿了一个联合字符串去展示,很容易出现“NULL、体育、科技”这种奇怪前缀。SQL里可以用COALESCE去兜底,但更好的办法是在关联聚合阶段直接把空值处理掉:
SELECT p.id, p.name, COALESCE(GROUP_CONCAT(t.name ORDER BY pt.sort SEPARATOR '、'), '暂无标签') AS tag_names FROM product p LEFT JOIN product_tag pt ON pt.product_id = p.id LEFT JOIN tag t ON t.id = pt.tag_id GROUP BY p.id, p.name;另外注意,GROUP_CONCAT在聚合过程中默认会忽略NULL值,所以就算某行JOIN出了NULL,也不会混进拼接结果里,但结果串可能因此变成“体育、科技”而缺失某一个标签。这通常意味着中间表里有脏数据,比如tag_id指向了一个已删除标签。遇到这种情况就要做数据清洗了。
4.4 一个隐藏很深的分页问题
分页问题多说一点。场景三提到GROUP_CONCAT和GROUP BY,这种聚合型SQL一旦配合LIMIT,很多时候是先全部聚合、排序、再取前N条,数据量大的时候也会拖慢整体性能。更隐蔽的是,如果聚合字段不唯一,比如两个商品名称相同,GROUP BY p.id, p.name可能让MySQL按扩展列排序,结果导致分页跳变。解决思路还是那句话:先窄口子查出主表ID,再回表拼接标签。
我自己做后台列表查询的模板是固定的一套:先查ID,再查附加数据。这套模板看着是多写了几个方法,但在性能上几乎不会翻车,也特别容易被下一个接手的人理解。
5. 关于工具链和诊断技巧的补充
既然标题里提到了“高效”二字,最后聊两句排查工具。慢查询日志是必须打开的,它能在第一时间告诉你哪些SQL超出了阈值。MySQL里可以这样配置:
SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1;然后针对抓出来的SQL,用EXPLAIN ANALYZE(MySQL 8.0以上)去看每一步的真实消耗行数和时间,比EXPLAIN更准确。像Navicat、dbForge Studio这些图形工具,也都自带执行计划可视化功能,鼠标一点就能看到索引命中情况。做课程设计的时候,截图贴上EXPLAIN的结果在答辩里是很加分的,因为这显示的是一种“我能定位性能问题”的能力,而不是只会写函数堆代码。
数据库同步工具方面,如果你需要把某个库的查询结果同步到报表库、分析库,可以看看DataX、Flink CDC这类方案。它们的核心思路是把变动数据批量或实时推送到目标端,避免在业务高峰期执行复杂JOIN查询。不过这是另一个大话题了,跟标题里的单项查询关系不大,这里只是提一嘴,免得你碰到“列表查询越来越慢”的时候走偏方向。
6. 一段话总结我的实操体会
聊到最后,说点掏心窝的话。一对多关联的多项选择字段,从技术层面看是“建模、索引、SQL语义”三件事的组装。建模选了中间表,索引设计得当,SQL用EXISTS处理交集、用IN处理并集、用GROUP_CONCAT做聚合展示,这套组合拳打下来,绝大多数业务都不会成为慢查询的重灾区。
我在实际项目里踩过太多次坑:逗号分隔字段带来的模糊匹配悲剧、直接JOIN再做分页导致的数据重复、GROUP_CONCAT截断后前端展示莫名其妙少了标签。这些坑的本质都不是SQL不够难,而是没有提前把数据模型和查询方案想透。希望读这篇文章的你,能直接把里面这套方案拿去用,少走一些我当年走的弯路。
提示:实验完成后,记得把测试表和存储过程清掉,别留在共享数据库里凑热闹。临时表DROP掉,慢查询日志开源根据自己的需求关掉,生产环境干干净净才是好习惯。