
提到MySQL经典50题很多准备面试或者刚入门的同学应该不陌生。我当年第一次拿到这份题集时根本没把它当回事觉得就是网上流传的学生成绩查询练习结果真正上手以后才发现这套题几乎把一个后端工程师和数据分析师在日常工作中会遇到的基础SQL场景全给覆盖了条件过滤、分组统计、多表连接、子查询、去重排序、分组取TopN……每一类都能在真实业务里找到对应。这套题最吸引我的地方是它不考任何花哨的语法所有问题都基于一张很简单的“学生-课程-成绩”模型。正因为模型足够简单你才能把注意力完全放在SQL本身的写法上。不管是准备面试、复习基础还是带新人练SQL都很合适。下面我就按自己刷题三遍之后的经验把这个题集背后的表结构设计、题型分类、踩坑细节和练习方法一次说清楚。1. 这套题到底在练什么整体设计与场景拆解1.1 “经典50题”的构成这套题在很多技术社区都能找到版本之间可能题目编号略有出入但核心模型高度一致四张表、一个选课成绩系统。student表存学生基础信息teacher表存教师信息course表存课程信息sc表存学生的选课记录和成绩。题目难度呈阶梯式递进前几题基本是单表查询中间开始多表关联后段会出现聚合、子查询、分组排序、去重等组合题少数版本还会加入日期处理、字符串处理和行列转换。整体来看50题把SQL查询中的常用知识点分成了几个层次我用平时带人练习的习惯给它们做了个分类题目类型覆盖知识点常见问法基础查询SELECT、WHERE、LIKE、IN、BETWEEN查询姓“张”的学生、查询成绩大于80分的学生分组统计GROUP BY、聚合函数、HAVING统计每门课的平均分、统计每个学生的选课门数多表连接INNER JOIN、LEFT JOIN、表别名查询选修了某门课程的学生名单子查询标量子查询、IN子查询、EXISTS查询比某学生成绩高的同学排序去重ORDER BY、DISTINCT、LIMIT查询成绩最高的前几名、去重查询教师名单高级组合窗口函数、自连接、变量每门课成绩前两名或按分数排名对刚入门的人来说这套题的梯度设计非常合理。你不需要一上来就面对复杂的业务表也不用懂存储过程、触发器只需要把DQL查询练熟就能覆盖日常工作中90%的取数需求。1.2 为什么选“学生-课程-成绩”这个模型学生选课这个场景本质上是一个标准的多对多关系建模一个学生可以选多门课一门课可以被多个学生选sc表作为中间关系表在记录选课关系的同时保存成绩。这种结构在业务系统里太常见了订单-商品、用户-角色、文章-标签全都是同一个套路。你把表名换掉50题里的查询逻辑直接就能迁移到电商后台、内容管理系统和运营报表里。比如“查询选修了数学课的学生”换到电商场景就是“查询购买了某个商品的用户”“统计每门课的平均分”换到运营场景就是“统计每个品类的平均客单价”。之所以很多人说SQL学不会是因为一上来就对着几十个字段的大宽表连业务含义都理不清更别说写关联查询了。而学生成绩模型只有四张表外键关系一目了然是理解和掌握关系模型的最短路径。这其实也回答了热搜里那个“学生课程成绩信息实体表设计mysql”的问题只要把这四张表的结构吃透绝大部分教学型业务表的设计思路你都大概有数了。1.3 什么人适合拿它练手不同阶段的人用这套题收获完全不一样。准备面试的求职者可以把50题当成高频面试题集来刷。很多面试官自己就刷过这套题面试时随口改个条件比如“查成绩第二高的学生”“查每门课的平均分排名”本质上都在这套题的射程范围内。在职的开发和数据分析师如果平时写SQL全靠复制粘贴那这套题补基础非常合适。我见过不少写业务SQL很熟练的同学遇到NOT IN里混入NULL就翻车遇到ONLY_FULL_GROUP_BY报错就慌其实这些坑在50题里都有对应场景。带新人的团队负责人也可以直接拿它当练习册配一套标准数据让新人自己建表、造数、刷题比打印一堆文档管用得多。2. 建表与数据准备先把地基打对2.1 四张核心表的字段设计与DDL开始刷题之前第一步一定是自己动手建表。别去网上复制现成的建表语句自己敲一遍你对字段类型、约束、主外键的理解会完全不一样。下面是我常用的建表脚本适配MySQL 8.0也基本兼容5.7CREATE TABLE student ( sid INT PRIMARY KEY AUTO_INCREMENT COMMENT 学生ID, sname VARCHAR(20) NOT NULL COMMENT 姓名, sage DATETIME COMMENT 出生日期, ssex VARCHAR(10) COMMENT 性别 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生表; CREATE TABLE teacher ( tid INT PRIMARY KEY AUTO_INCREMENT COMMENT 教师ID, tname VARCHAR(20) NOT NULL COMMENT 教师姓名 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT教师表; CREATE TABLE course ( cid INT PRIMARY KEY AUTO_INCREMENT COMMENT 课程ID, cname VARCHAR(30) NOT NULL COMMENT 课程名, tid INT COMMENT 授课教师ID, CONSTRAINT fk_course_teacher FOREIGN KEY (tid) REFERENCES teacher(tid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程表; CREATE TABLE sc ( sid INT NOT NULL COMMENT 学生ID, cid INT NOT NULL COMMENT 课程ID, score DECIMAL(5,2) COMMENT 成绩, PRIMARY KEY (sid, cid), KEY idx_cid (cid), CONSTRAINT fk_sc_student FOREIGN KEY (sid) REFERENCES student(sid), CONSTRAINT fk_sc_course FOREIGN KEY (cid) REFERENCES course(cid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT选课成绩表;几个字段设计上的细节说一下。sid、tid、cid用INT主键自增纯粹是为了查询方便和避免重复。sname、cname这类字符串字段长度不用给太大VARCHAR(20)和VARCHAR(30)足够给太大反而浪费存储空间还影响索引效率。sage用DATETIME而不是VARCHAR这是很多人容易犯的错日期字段一定要用日期类型否则ORDER BY排序、日期函数计算的时候会非常痛苦。score用DECIMAL(5,2)而不是FLOAT或INT因为成绩可能带小数而且DECIMAL在计算平均值和求和时更精确不会出现浮点数误差。sc表的主键用复合主键(sid, cid)这样保证同一个学生同一门课只有一条选课记录从数据库层面杜绝重复数据。这是数据建模里一个很重要的思想选课关系天然是联合唯一的把它设成复合主键后面刷题时很多去重逻辑就不用手动写了。2.2 建表时最常见的三个坑第一字符集必须用utf8mb4不要用utf8。MySQL里的utf8其实是utf8mb3最大只支持3字节存emoji、生僻字、部分特殊符号时会报错或乱码。utf8mb4才是真正的四字节UTF-8。排序规则方面MySQL 8.0默认是utf8mb4_0900_ai_ci5.7默认是utf8mb4_general_ci两者都不区分大小写日常练习没太大区别。如果有人问你“为什么MySQL字符串比较不区分大小写”答案就在排序规则里的_ci后缀它表示case-insensitive改成_bin或_cs后缀就会区分大小写了。第二外键约束可以建但要知道真实项目中往往不建。练习环境建外键能帮你理清表间关系插入数据时也能自动校验完整性。但生产环境外键会影响写入性能尤其是大并发写入时每插入一条子表记录数据库都要去父表校验一遍开销不小。很多互联网公司会在应用层保证数据一致性表结构层面反而不建外键。这个区别面试时经常被问到不要只是死记“生产不建外键”要能解释清楚原因。第三默认存储引擎一定要是InnoDB。MySQL 5.5之后InnoDB就是默认引擎了除非你主动改成MyISAM否则一般不会踩坑。但如果是在老项目里碰到表是MyISAM的又没有主键刷题时会发现很多在线DDL操作做不了而且不支持事务。练习时统一用InnoDB别给自己找麻烦。2.3 造数据的实操技巧建完表之后就是插入测试数据。网上各版本的50题自带的数据不一定完全一样但为了保证练习效果你自己造一套符合逻辑的数据即可不用纠结是否和某个版本一模一样。插入中文数据时最容易遇到乱码根因往往是客户端连接字符集和表字符集不一致。用命令行或客户端工具连接时先执行一句SET NAMES utf8mb4;再插入中文就不会乱码。如果你用的是Navicat或DBeaver这类图形工具一般在连接属性里设置好编码就不会有问题。如果不想手工一条条插可以用笛卡尔积批量造数。下面这段SQL可以在student表里生成1万个学生名日常练习时用不着这么多但理解这条语句本身就很有价值INSERT INTO student (sname, sage, ssex) SELECT CONCAT(学生, n), NOW(), 男 FROM ( SELECT a.n b.n * 10 c.n * 100 d.n * 1000 AS n FROM (SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) a CROSS JOIN (SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) b CROSS JOIN (SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) c CROSS JOIN (SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) d ) t;刷这套题时我反而建议插入的数据量控制在20到30条左右就行。数据量太大结果对不对很难一眼看清数据量太小又覆盖不了各种边界情况。比如为了测出“NOT IN遇到NULL返回空集合”的坑你必须在数据里故意留几个NULL成绩否则压根不会遇到这类问题。3. 经典题型拆解从单表到多表的核心写法3.1 单表查询与条件过滤别小看WHERE50题里最简单的部分就是单表查询但越简单的东西越能看出基本功。题目通常会这样问查询姓“张”的学生、查询成绩大于80分的记录、查询课程名以“数”开头的课程。SELECT sname, ssex FROM student WHERE sname LIKE 张%; SELECT sid, cid, score FROM sc WHERE score 80; SELECT cid, cname FROM course WHERE cname LIKE 数%;第一题考察LIKE通配符。%表示任意多个字符_表示单个字符所以LIKE 张%能匹配“张三”“张伟”等所有张姓学生。第三题的LIKE 数%同理。这里有个细节值得注意前导通配符会导致索引失效也就是LIKE %数这种写法没法走索引。数据量小的时候无所谓但在生产环境一个几千万行的表如果这么写查询基本就是全表扫描。另外WHERE里如果有多组条件MySQL一般会先根据索引和统计信息筛选出数据量最小的结果集再执行后续过滤。理解这个执行顺序可以帮助你写出更快的SQL而不是盲目加条件。3.2 聚合统计与GROUP BY分组思维是关键过了单表查询很快就会遇到一类题统计每门课程的平均分、统计每个学生的选课门数、统计各科成绩大于60分的人数。这类题的核心就是GROUP BY。SELECT cid, COUNT(*) AS stu_cnt, AVG(score) AS avg_score FROM sc GROUP BY cid;这行的意思是按课程ID把sc表分成若干组然后对每组分别计算选课人数和平均分。理解GROUP BY一定要抓住一个点分组之后SELECT里的非聚合列必须出现在GROUP BY里否则结果没有意义。比如你按cid分组然后又想select sname这时每个分组里可能有多个学生数据库该显示哪一个MySQL 8.0在ONLY_FULL_GROUP_BY模式下会直接报错。WHERE和HAVING的执行顺序也在这里体现。WHERE是在分组之前过滤原始数据HAVING是在分组之后过滤聚合结果。想筛选平均分大于80的课程正确的逻辑是先按cid分组再用HAVING avg(score) 80过滤。如果写成WHERE avg(score) 80数据库在分组前压根不知道平均分是多少直接报错。3.3 多表连接JOIN的三种形态要分清等开始查“选修了数学课的学生名单”“选修了某课程且成绩大于90分的学生”就要用多表连接了。最典型的三表关联SELECT s.sid, s.sname, c.cname, sc.score FROM student s JOIN sc ON s.sid sc.sid JOIN course c ON c.cid sc.cid WHERE c.cname 数学;这里的JOIN连接条件解决的是“表和表之间怎么对上”的问题WHERE过滤条件解决的是“要哪些数据”的问题两者各司其职。初学者常犯的错误是写完JOIN后把所有条件一股脑塞进WHERE这在结果上可能碰巧是对的但可读性和性能都会受影响。另一类经典题目是“查询没有选课的学生”这类题能帮你真正理解LEFT JOIN。思路是用student表做主表LEFT JOIN sc表如果某学生在sc表里没有匹配记录对应的sc.sid就是NULL。SELECT s.sid, s.sname FROM student s LEFT JOIN sc ON s.sid sc.sid WHERE sc.sid IS NULL;有些同学会写成NOT IN (SELECT sid FROM sc)这在数据没有NULL时结果是对的可一旦子查询结果集里有NULLNOT IN会返回空集合整个查询一条数据都查不出来。碰到这种情况优先用NOT EXISTS或上面的LEFT JOIN写法不容易踩坑。3.4 子查询与相关子查询筛选的艺术子查询在50题里占的比例不低常见的问法包括“查询比张三成绩高的同学”“查询每门课成绩超过平均分的学生”。先看非相关子查询。它先执行内层查询把结果给外层用来比较。比如“查询课程ID为1且成绩高于张三的学生”SELECT sid, score FROM sc WHERE cid 1 AND score ( SELECT score FROM sc WHERE sid 1 AND cid 1 );注意内层子查询和外层查询是两个独立的查询子查询能拿到结果后外层再用它做条件过滤。相关子查询则完全不同。它的内层查询要引用外层查询的字段每处理一行都要执行一次。比如“查询有任意一门课成绩大于90分的学生”可以写成SELECT sname FROM student s WHERE EXISTS ( SELECT 1 FROM sc WHERE sc.sid s.sid AND score 90 );这里EXISTS的作用是判断子查询是否返回了至少一行。从性能角度说EXISTS和IN的大致区别是外层表数据量小、内层表数据量大时EXISTS通常更合适反过来IN有时更好。但这只是经验法则真实情况要结合执行计划来看。3.5 排序、分页与去重分组TopN的多种做法排序、分页、去重几乎是所有SQL面试的必考点50题里关于“每门课的前两名”就是最典型的代表。如果你的MySQL是8.0窗口函数是最直观的写法SELECT cid, sid, score FROM ( SELECT cid, sid, score, ROW_NUMBER() OVER (PARTITION BY cid ORDER BY score DESC) AS rn FROM sc ) t WHERE rn 2;其中PARTITION BY cid表示按课程分组ORDER BY score DESC表示组内按成绩降序ROW_NUMBER()给每组内的每一行编号最后只需要取编号小于等于2的即可。如果你需要“并列名次”应该把ROW_NUMBER()换成RANK()或DENSE_RANK()。三者区别在于ROW_NUMBER()严格按顺序编号1、2、3RANK()遇到同分并列且会跳号DENSE_RANK()遇到同分并列不跳号。实际业务里的排行榜功能经常会用到RANK和DENSE_RANK这个区别必须记清楚。如果你的环境还是MySQL 5.7没有窗口函数可以用用户变量实现同样的效果SELECT cid, sid, score FROM ( SELECT cid, sid, score, IF(prev_cid cid, rn : rn 1, rn : 1) AS rn, prev_cid : cid FROM sc, (SELECT prev_cid : NULL, rn : 0) vars ORDER BY cid, score DESC ) t WHERE rn 2;这个写法理解起来稍微绕一点核心原理是让每行按(cid, score DESC)排序然后逐行比较课程编号是否变化如果编号没变行号自增如果编号变了行号重置为1。需要注意用户变量在MySQL 5.7里的赋值顺序依赖执行计划不能保证100%稳定所以生产环境真的遇到这类TopN问题我还是建议升级到8.0或者改用自连接实现。4. 那些容易翻车的SQL细节避坑与排查实录4.1 ONLY_FULL_GROUP_BY模式带来的烦恼MySQL 5.7开始默认开启ONLY_FULL_GROUP_BYMySQL 8.0也默认开启。在这种模式下SELECT后面的非聚合字段必须出现在GROUP BY中否则直接报错。比如网上很多老版本的答案里有这样的写法SELECT sname, ssex, COUNT(*) FROM student GROUP BY ssex;在MySQL 8.0里跑大概率报错因为sname没有出现在GROUP BY里。从逻辑上讲这个报错是有道理的按性别分组后每组里可能有多名学生到底取哪个sname结果是不确定的。MySQL宁可直接报错也不给你一个随机结果。遇到这个问题正确做法是重写SQL把需要展示的字段加进GROUP BY或者用聚合函数包住它。如果真的只是临时想关闭这个模式可以执行SET SESSION sql_mode (SELECT REPLACE(sql_mode, ONLY_FULL_GROUP_BY,, ));但我不建议长期关闭。这个模式能在开发阶段就帮你发现不少逻辑问题关闭之后短期省事长期埋雷。4.2 NULL值是真的坑NULL在SQL里不是“空字符串”也不是“0”它代表“未知”。几乎所有对NULL的常规比较运算结果都不是TRUE或FALSE而是UNKNOWN。这就带来了几个非常经典的坑。第一个坑WHERE score ! 80查不出score为NULL的行。从业务逻辑上讲成绩未知确实不能算“不等于80”但很多人刚接触时都会在这上面栽跟头。解决办法是显式加上OR score IS NULL。第二个坑COUNT(*)和COUNT(score)结果不一样。COUNT(*)统计行数COUNT(score)只统计score不为NULL的行数。如果某列允许NULL这两个函数的结果就很可能对不上。第三个坑NOT IN遇到子查询含NULL结果为空集合。这个前面已经说过最好用NOT EXISTS替代。刷题时如果发现“怎么查不到数据”第一反应就是检查涉及的表里有没有NULL值。4.3 多表JOIN字段重名与别名多表连接时如果两张表都有同名列SELECT里直接写列名会报Column sid in field list is ambiguous。解决办法就是加表别名或表前缀比如s.sid和sc.sid。这个错误对新手很常见面试官也经常故意把两张结构相似的表放一起看你能不能意识到歧义问题。用表别名还有一个好处就是SQL写起来更短尤其是表名很长时比如order_detail起个别名od整个语句看起来清爽很多。唯一要注意的是一旦在FROM里定义了别名后续的SELECT和WHERE里就得统一用别名不能再写原始表名。4.4 常见问题速查表把我在练习和带人过程中遇到的典型问题整理成了一张表方便你对照排查现象常见原因处理方式插入中文变成乱码客户端字符集与表字符集不一致连接后执行SET NAMES utf8mb4或修改客户端编码分组查询报ONLY_FULL_GROUP_BY错误非聚合列不在GROUP BY中重写SQL将字段加入GROUP BY或用聚合函数包裹NOT IN查询结果为空子查询结果含NULL改用NOT EXISTS或LEFT JOIN IS NULL多表查询报ambiguous连接的多个表存在同名字段给表加别名字段前加表前缀查询结果中倒数第一名和正数第一混淆没注意排序方向确认ORDER BY是ASC还是DESC数值字段求和有精度误差使用FLOAT或DOUBLE改用DECIMAL类型4.5 UPDATE语法里的隐藏知识点热搜词里有一条“mysql中更新子查询”这虽然不属于50题的主线但确实是个易错点。MySQL旧版本里UPDATE语句的子查询不能直接引用目标表比如你想把某学生的成绩改成这门课的平均分直接写UPDATE sc SET score (SELECT AVG(score) FROM sc WHERE cid 1) WHERE sid 1在某些版本会报错“You cant specify target table sc for update in FROM clause”。解决办法通常是把子查询再包一层临时表让MySQL认为它操作的不是目标表UPDATE sc SET score (SELECT avg_score FROM ( SELECT AVG(score) AS avg_score FROM sc WHERE cid 1 ) t) WHERE sid 1;这个知识点在面试中出现的频率不低而且能很好地检验你写UPDATE语句时是否考虑到了MySQL的执行限制。5. 从“会写”到“写好”练习之外还能学什么5.1 用EXPLAIN审视每一条SQL刷题刷到一定程度就不能只看结果对不对了还要看SQL写得好不好。MySQL里最直接的手段就是给SQL加EXPLAIN前缀查看执行计划。拿“查询某学生的选课和成绩信息”来举例EXPLAIN SELECT s.sname, c.cname, sc.score FROM sc JOIN student s ON sc.sid s.sid JOIN course c ON sc.cid c.cid WHERE sc.sid 1;执行后会返回一张执行计划表重点关注几列type表示访问类型从好到差大致是const、eq_ref、ref、range、index、ALLkey表示实际用到的索引如果是NULL说明没有走索引rows表示预估扫描行数数字越小越好Extra里如果出现Using filesort或Using temporary通常说明这条SQL还有优化空间。真正练习时你可以故意在WHERE条件里写一个函数比如WHERE YEAR(sage) 1990然后看执行计划会发现即使sage字段有索引也不会被用到这就是“不要在索引列上使用函数”的最直观证据。光看理论记不住亲手实验一次就理解了。5.2 索引基础50题背后的索引思维50题里的连接字段天然就是需要加索引的字段。sc表上sid和cid是复合主键已经覆盖了大部分按学生查询和按课程查询的场景。如果你在自己的表上刷题可以额外给高频查询字段建索引。以“查询姓名重复的学生”这类题为例如果student表数据量很大sname字段上建了索引GROUP BY sname HAVING COUNT(*) 1的效率会高很多。反过来如果sname没有索引这个分组查询大概率会全表扫描加临时表数据量一大就会很慢。建索引要注意最左前缀原则比如在(sid, cid)复合索引下你按sid查能走索引只按cid查反而走不了除非在cid上单独建索引。这也是为什么我在建表时特意在sc表的cid字段上加了KEY idx_cid。很多生产事故本质就是当初建表时没考虑好索引顺序导致线上慢查询一堆。5.3 面试变形题与真实场景映射经典50题只是起点刷完之后要会做场景映射。拿几道最常见的面试题来对照“每门课最高分”对应“每个部门最高工资”。解法思路完全一致都是PARTITION BY按组划分再在组内排序。“查询没有选课的学生”对应“查询从未下单的用户”。LEFT JOIN加IS NULL或者NOT EXISTS思路一字不差。“统计各科平均分”对应“统计各品类GMV”。换掉表名和字段名SQL结构保持不变。这种映射能力才是刷题真正的收获。你不必背下所有面试题只要把50题里的基础模型理解透遇到真实场景时把字段名替换一下SQL自然就写出来了。5.4 练习环境准备与进阶建议练习这套题不需要多高配置的服务器本地装一个MySQL客户端就够了。安装方式有很多种Windows直接下载安装包macOS用Homebrew或者用Docker一条命令拉一个容器来练docker run --name mysql-50 -e MYSQL_ROOT_PASSWORD123456 -p 3306:3306 -d mysql:8.0用容器练习的好处是环境干净、随时可以删掉重建不用怕把本机环境搞乱。连上数据库之后把建表语句执行一遍再插入数据就可以开始刷题了。50题刷完之后如果想进一步巩固MySQL的操作能力可以把一些重复出现的查询封装成存储过程。比如写一个接收课程ID参数、返回该课程成绩排名列表的存储过程顺带把流程控制、游标、动态SQL都过一遍。这个进阶方向对应了热搜里“mysql存储过程”确实值得花点时间但前提是基础查询题已经刷透不要本末倒置。6. 最后再分享一点个人经验我在不同阶段拿这套题反复验证过自己对MySQL的理解。第一遍刷题我追求的是写得出来能出正确结果就行第二遍刷题我开始注意看执行计划思考为什么这条SQL走了全表扫描那条走了索引第三遍再刷就想着怎么写才能让索引命中、怎么写更稳面对同样一个问题时开始比较不同的解题方案。如果你也是准备面试或者想把SQL基础打牢的人我的建议是先自己把表结构和数据建一遍再按题型分类去刷最后用EXPLAIN回头审视每一条SQL。这套题真不用背答案它值钱的地方在于让你把SQL思维刻进肌肉记忆。遇到不会的题别急着查卡住半小时再去看答案你会发现下一次看到类似问题思路会清楚很多。