“无效的推文”这几个字,我第一次在某在线题库里刷到的时候,第一反应是内容审核后台的告警列表。点进去才发现,它是一道非常经典的数据库题:题目编号1683,给定一张推文表,表里有推文ID和正文内容,只要正文字符数严格大于15,就把这条推文判定为无效,要求查出所有无效推文的ID。题目难度标着“简单”,但我见过不少人在这道题上翻车,翻车原因高度统一:没搞清字符数和字节数的区别。这篇文章就把这道题从题目拆解、SQL实现、常见坑,到真实业务里的扩展用法完整梳理一遍。不管你是正在刷题准备面试,还是日常需要写数据清洗脚本,都能从里面找到能直接用的东西。
1. 题目到底在问什么:从表结构到判定规则
1.1 推文表长什么样
题目虽然只给了文字描述,但背后通常对应着这样一张表:
CREATE TABLE Tweets ( tweet_id INT PRIMARY KEY, content VARCHAR(200) );tweet_id是推文的唯一编号,content是推文正文。题目要求:写一条SQL,返回所有content中字符数严格大于15的tweet_id。先别急着写代码,这里面有几个细节值得抠一抠。
第一,content字段用的是VARCHAR(200),不是TEXT。VARCHAR后面的数字表示最多能存200个字符,而不是200个字节。这一点很多人会忽略,但它直接关系到后面理解CHAR_LENGTH和LENGTH的区别。第二,tweet_id是主键,所以结果天然不会重复,不需要额外去重。第三,VARCHAR(200)意味着单条推文最多200个字符,而题目把无效线划在15字符,说明这不是在考存储上限,而是在考条件过滤。
1.2 判定标准里的“严格大于”是什么
题目说的是“字符数严格大于15”,翻译成SQL就是> 15,而不是>= 15。也就是说,一条推文如果正好15个字符,它是合法的,不应该出现在结果集里。这个边界条件看起来简单,但面试和笔试里专门有人在这里失分。
举个例子:如果有一条推文内容是“Hello, world!!”,去掉引号后正好15个字符,那它不属于无效推文。只有长度到达16个字符及以上的推文,才会被当成无效。很多人在读题的时候只看到“大于15”,写的时候顺手就写了>= 15,结果把边界值也圈进去,整道题就错了。
1.3 这道题不是在考SQL语法,而是在考字符串认知
如果只是在考WHERE条件怎么写,那这道题没有资格成为经典题。它真正的考点是:你知不知道“字符数”在不同数据库里有不同的计算方法?英文、数字、中文、emoji在存储时占用的字节数分别是多少?用错了函数,表面上结果一样,实际在多语言环境下会错得离谱。后面几节我会重点展开。
2. 核心考点:CHAR_LENGTH 和 LENGTH 差在哪
2.1 一个字符不等于一个字节
这是整个题目最大的坑。在常见的开源关系型数据库里,如果表使用的是utf8mb4这类多字节字符集,那么:
- 英文字母、数字、半角标点:1个字符占1个字节
- 绝大多数中文汉字:1个字符占3个字节
- emoji等特殊符号:1个字符占4个字节
LENGTH()统计的是字节数,CHAR_LENGTH()统计的是字符数。比如字符串"hello world!!!"有15个英文字符,它的字节数也是15,两个函数算出来一样。但换成中文,比如"今天是个好日子啊",共8个汉字,CHAR_LENGTH返回8,而LENGTH返回24(8乘3)。如果题目要求“字符数大于15”,那这条8个字的推文明明合法,用LENGTH判断却会把它当成无效推文,这就是误判。
用生活里的类比:统计一个班有多少人,是按人来数,不是把所有人体重加起来。字节数是“体重”,字符数才是“人数”。除非业务明确按字节限制(比如短信按字节计费),否则做内容长度判断时应该优先使用字符数。
2.2 为什么推荐用 CHAR_LENGTH
用户眼里“一条推文有多长”,看到的是一排文字有多少个字符,不是它们在磁盘上占了多少字节。一条中文推文写了200个字,它就是200个字符,不应该因为编码变成600字节就显得“更长”。很多数据库提供了两个长度函数,一个按字符统计,一个按字节统计,目的就是让开发者根据业务语义选择。
在绝大多数内容展示、用户输入限制、文本校验场景里,业务规则说的“长度”都是字符数。比如某个社交应用规定推文最长280个字符,这280就是用户能打出来的字符个数,和底层编码无关。所以这道题的标准解法里,必须用CHAR_LENGTH。如果你拿不准,可以在动手前问自己一句:需求里说的“长度”,是用户输入框里能看到的长度,还是数据库存储空间占用的长度?前者选字符数函数,后者才选字节数函数。
2.3 不同数据库里的长度函数并不统一
这里要特别提醒:不同数据库对“长度”的实现差别很大。同样是统计字符串长度,可能一个函数名在不同库里代表完全不同的含义。
| 数据库类型 | 按字符数统计 | 按字节数统计 | 注意事项 |
|---|---|---|---|
| 某常见开源关系型数据库 | CHAR_LENGTH() | LENGTH() | LENGTH统计字节数,CHAR_LENGTH统计字符数 |
| 另一种常见开源数据库 | char_length() 或 character_length() | length() 对二进制类型统计字节数 | 对普通文本类型,length()默认统计字符数 |
| 某商业数据库 | LEN() | DATALENGTH() | LEN不统计末尾空格,DATALENGTH统计字节数 |
| 另一种商业数据库 | LENGTH() | LENGTHB() | 名字只差一个B,含义完全不同 |
这个表格给的是通用规律,实际使用前一定要查自己数据库的官方文档确认。不要靠记忆背函数名,因为版本差异、字符集差异都会改变结果。我遇到过有人从开源数据库跳到商业数据库后,还用LENGTH做字符数判断,结果中文长度全部翻倍,排查了半天才发现是函数含义不同。
3. 基础解法:一条SELECT,两种写法
3.1 标准答案长这样
SELECT tweet_id FROM Tweets WHERE CHAR_LENGTH(content) > 15;这条SQL的执行逻辑很直接:数据库逐行扫描Tweets表,对每一行的content调用CHAR_LENGTH函数计算字符数,判断是否大于15,把满足条件的tweet_id输出。由于tweet_id是主键,结果不会重复,不需要加DISTINCT。
如果你用的是某个默认把字符串按字符数处理的数据库,也可以写成CHARACTER_LENGTH(content),效果一样。但最稳妥的写法还是CHAR_LENGTH,它在大多数支持标准SQL的数据库里都可用。
3.2 加上ORDER BY,稳一点
题目说了返回顺序无要求,所以不排序也不会判错。但我个人建议顺手加一个ORDER BY tweet_id:
SELECT tweet_id FROM Tweets WHERE CHAR_LENGTH(content) > 15 ORDER BY tweet_id;理由很简单:稳定的输出顺序方便你验证结果。在本地测试、对比样例、写自动化断言的时候,一个固定顺序能让差异对比变得容易。数据量小的时候,排序带来的性能开销可以忽略不计;数据量大的时候,这个排序也不一定成为瓶颈,因为需要排序的已经是过滤后的结果集。
3.3 也可以用子查询,但没必要
有些人习惯先算长度、再过滤,于是写成子查询:
SELECT tweet_id FROM ( SELECT tweet_id, CHAR_LENGTH(content) AS len FROM Tweets ) t WHERE len > 15;这种写法在功能上正确,但多了一层派生表,可能让优化器多一步物化操作。除非你需要同时使用长度字段做其他计算,否则直接在WHERE里用函数更简洁。SQL的简洁性不只是好看,还意味着更少出错的环节。
4. 踩坑实录:我见过的三种典型错误
4.1 错误一:用LENGTH处理中文推文
这是最高频的错误,没有之一。很多人在本地测试时,用的样例数据全是英文,LENGTH和CHAR_LENGTH算出来的结果一模一样,测试通过后就提交了。但一旦把数据换成中文、日文、韩文或者带表情符号的文本,结果立刻错乱。
比如一条推文内容是"😀😀😀😀",4个emoji字符,CHAR_LENGTH返回4,小于15,合法;但LENGTH返回16(4个字符乘4字节),大于15,会被误判为无效。这种错误在真实业务里非常危险,因为内容审核系统一旦误判,用户正常发的短表情都会被屏蔽。
怎么避免?记住一句话:LENGTH是字节数,CHAR_LENGTH是字符数。如果你在面试中遇到这道题,甚至可以主动向面试官确认:“这里的超过15,指的是字符数还是字节数?”这个问题本身就是加分项,因为这能体现你对多语言文本处理的理解。
4.2 错误二:在WHERE里使用SELECT别名
有人图省事,写成这样:
SELECT tweet_id AS id FROM Tweets WHERE id > 0;这里的id是SELECT子句里定义的别名,但SQL的逻辑执行顺序是先FROM、再WHERE、再SELECT。也就是说,WHERE执行的时候,别名还没生成,数据库根本不知道id是什么。除非你把它放到HAVING子句里,否则会直接报错。放到HAVING虽然能解决,但在这个场景里没必要,直接在WHERE写原始列名tweet_id最干净。
4.3 错误三:把“大于15”写成“大于等于15”
边界条件出错,属于读题不仔细。大于15意味着15本身合法,16才开始无效。为了防止自己看错,建议写条件之前心里默念一遍“不包含15,不包含15”,或者直接看样例数据:如果样例里有一条正好15字符的推文,它就不应该在结果集里。
4.4 错误速查表
| 错误写法 | 后果 | 正确写法 |
|---|---|---|
WHERE LENGTH(content) > 15 | 中文字符和emoji被误判 | WHERE CHAR_LENGTH(content) > 15 |
WHERE CHAR_LENGTH(content) >= 15 | 15字符的合法推文被误伤 | WHERE CHAR_LENGTH(content) > 15 |
WHERE id > 0(id是别名) | 数据库报错或结果错误 | 使用原始列名,或改为HAVING |
WHERE content IS NULL OR CHAR_LENGTH(content) > 15 | 正确处理NULL,但题目若要求非空则无需加 | 按题目要求决定 |
5. 扩展思考:无效推文不止“超长”这一种
5.1 用CASE WHEN做多级分类
真实业务里,“无效推文”的定义往往比一道题复杂得多。你可能同时要处理空内容、纯空格、超长、包含敏感词等多种情况。如果你是个数据工程师,接到一个需求说要统计无效推文占比,你会怎么实现?
一个很实用的方式是先用CASE WHEN把每条推文分个类:
SELECT tweet_id, CASE WHEN content IS NULL OR CHAR_LENGTH(TRIM(content)) = 0 THEN 'empty' WHEN CHAR_LENGTH(content) > 15 THEN 'too_long' ELSE 'valid' END AS status FROM Tweets;这里的TRIM(content)去掉了首尾空格,避免一条由空格组成的“空内容”被当成有效推文。这个片段可以直接拿去做数据质量报表的底表,后面再按status分组统计。
5.2 什么时候才需要按字节数限制
如果业务规则是“某短信接口单条内容不能超过15个字节”,那上面那个15字符的阈值就不适用了。短信计费常常按实际发送的字节数或者分段条数计算,比如一条中文短信如果按字节数超了,就可能被拆成多条,费用翻倍。此时你应该用字节数函数:
SELECT tweet_id FROM Tweets WHERE LENGTH(content) > 15;但要注意,这个15字节能容纳的字符数非常有限:英文能放15个,中文只能放5个(15除以3)。所以在写代码之前,必须确认需求的计量单位。单位错了,所有阈值都失去意义。
5.3 不要忘记NULL的三值逻辑
SQL里NULL参与比较时,结果不是TRUE也不是FALSE,而是UNKNOWN。所以WHERE CHAR_LENGTH(content) > 15会把content为NULL的行排除掉,因为对NULL调用函数返回NULL,NULL大于15的结果是UNKNOWN,WHERE只保留TRUE。
如果“无效”的定义包含“内容缺失”,你就得显式处理:
SELECT tweet_id FROM Tweets WHERE content IS NULL OR CHAR_LENGTH(content) > 15;这道基础题默认content非空,不需要这一步,但真实数据里空值无处不在。我见过不少人在线上查数据时,因为没处理NULL,统计结果凭空少了一部分记录,排查半天才发现是NULL被三值逻辑过滤了。
6. 从一道题看真实数据清洗场景
6.1 长度校验为什么重要
推文内容在上架之前要经过长度校验。如果一个字段定义成VARCHAR(15),而程序塞进去一个20字符的字符串,数据库可能直接报“值过长”错误,也可能静默截断,把用户的文字切掉一半,导致内容损坏。与其等数据库来处理,不如在写入前就用SQL规则把超长数据检查出来。
这道题里的“无效判定”,本质上就是一个数据质量规则。在真实应用里,长度校验通常放在用户提交时,前端先限制输入框的maxlength,后端再校验一次,数据库最后兜底。三层都做,才能保证不会出现“前端能提交、后端报错”的割裂情况。
6.2 三层校验的统计口径必须统一
前端校验用户输入长度时,通常用JavaScript的length属性,但它统计的是UTF-16码元数量:大部分汉字算1个,但某些生僻字和emoji可能算2个。后端如果用Java的String.length(),统计的是UTF-16编码单元,同样是emoji会算2个。而数据库的CHAR_LENGTH按Unicode字符算,一个emoji算1个。
这三者如果不对齐,就会出现用户在手机上输入一个emoji,前端说长度没超,后端说超了,数据库说没超的混乱局面。真正专业的做法是统一按Unicode字符数作为业务口径,前端用[...str].length或者Intl.Segmenter统计实际字符数,后端用代码点方式统计,数据库用CHAR_LENGTH。这道题虽然只讲了数据库层,但它提醒我们:长度统计从输入框到数据库,每一步都要对齐。
6.3 一条语句生成数据质量报告
很多公司会有定时任务扫描内容表,计算无效内容占比。下面这条SQL可以直接跑:
SELECT COUNT(*) AS total_cnt, SUM(CASE WHEN CHAR_LENGTH(content) > 15 THEN 1 ELSE 0 END) AS invalid_cnt, AVG(CASE WHEN CHAR_LENGTH(content) > 15 THEN 1.0 ELSE 0.0 END) AS invalid_ratio FROM Tweets;SUM配合CASE计算满足条件的行数,AVG直接算出比例。这条语句输出的invalid_ratio可以用来监控内容质量,超过某个阈值就触发告警。如果你不需要保留无效明细,只想看一个健康度指标,这个方法比先查明细再在程序里统计要高效得多。
7. 性能优化:推文表上亿行时怎么查
7.1 函数会让索引失效的真相
WHERE CHAR_LENGTH(content) > 15看起来很简单,但它会对content的每一行做函数计算。即使content上有普通索引,索引里存的是原始字符串值,不是计算后的长度值,优化器无法直接通过索引定位满足条件的行,只能全表扫描。在几千行的小表上无所谓,但推文表一旦上亿行,这种写法会拖垮查询。
你可以用EXPLAIN查看执行计划,通常会发现type是ALL,意味着全表扫描。要解决这个问题,不能指望数据库自动帮你做函数索引,得换个思路。
7.2 方案一:冗余一个长度列
最直接的做法是增加一个content_len整型列,在写入数据时同步计算好字符长度,然后在这个列上建索引:
ALTER TABLE Tweets ADD COLUMN content_len INT; -- 插入时同步维护,不要单独UPDATE INSERT INTO Tweets(tweet_id, content, content_len) VALUES (?, ?, CHAR_LENGTH(?)); CREATE INDEX idx_content_len ON Tweets(content_len);查询改成:
SELECT tweet_id FROM Tweets WHERE content_len > 15;条件列变成了普通整型字段,索引能高效命中,性能会好很多。代价是需要业务层在写入时多算一个值,并且在修改内容时记得更新这个字段。这个方案的实质是“用空间换时间”,日常系统设计里很常用。
7.3 方案二:使用生成列自动维护
如果不想在应用层手动维护冗余列,可以看看你使用的数据库是否支持生成列。较新版本的开源关系型数据库支持这样的语法:
ALTER TABLE Tweets ADD COLUMN content_len INT GENERATED ALWAYS AS (CHAR_LENGTH(content)) STORED;STORED表示把计算结果物理存储下来,同时可以在这一列上建索引。数据库会在插入或更新时自动计算content_len,应用程序完全不用关心。查询时同样走列上的索引,干净利落。不过,生成列在某些数据库里只支持虚拟形式,不存储也不建索引,具体能力要在实际环境里确认。
7.4 方案三:从业务入口拦截
最彻底的优化不是让查询更快,而是让“无效推文”根本进不了表。应用层在写入前校验长度,超长就直接拒绝:
if len(content) > 15: return reject("tweet too long")数据库里仍然保留长度列和索引,主要给后台审核、离线分析用。这样做的好处是,业务主流程不会产生无效数据,需要查询无效数据时也能走索引快速扫描。分层思路很清晰:在线写入时拦截,离线分析时索引。
8. 简单题背后,我更想分享的三点经验
8.1 简单题最能暴露基础盲区
这道题加起来不到20行SQL,但把字符串函数、边界条件、NULL处理、索引优化全部串起来了。我在带人的时候,如果对方能主动聊到字符数和字节数的区别,我会判断他确实处理过多语言数据;如果对方上来就写LENGTH(content) > 15,我会提示一句“你确认这个表的字符集吗?”一个人对基础细节敏感不敏感,从这道题就能看出来。
8.2 实际项目中先确认“长度”的业务口径
我在实际写数据清洗脚本时,第一件事永远是确认业务方的“长度”是字符还是字节,是Unicode字符还是UTF-16码元。同一个词,在不同口径下结果差很远。比如“😀”这个emoji,在数据库的CHAR_LENGTH里算1,在某些编程语言里算2。如果不统一,就会出现A接口说超长、B接口说不超长的诡异现象。标准做法是把字符数定成唯一口径,所有环节都按这个口径实现。
8.3 我把这些经验沉淀成了一张检查清单
- 明确需求:字符数还是字节数?
- 明确数据库:对应的长度函数是哪个?
- 边界条件:
> 15还是>= 15? - 空值处理:NULL算不算无效?
- 是否需要去重和排序?
- 大表查询:能不能通过冗余列加索引优化?
这张清单不只是给这道题用,你接到任何一个文本长度校验需求,都可以照着过一遍。大部分线上事故,都是这几个问题里有一个没想清楚造成的。