前阵子帮朋友看一个小库存系统的表结构,打开数据库我人差点没坐住:一张库存表里,一个字段叫“标签”,存的是“红,大,棉,男款”,另一个字段叫“供应商”,把负责人电话、地址、折扣比例全怼在一个字符串里。查询全靠 like 和字符串截取,每次报错都得从头捋。这问题根子不在于 SQL 写得不够好,而在于建表时压根没按数据库范式来设计。
这不是个别现象。很多项目一开始都能跑,等数据量上来、需求一改,各种脏数据、重复数据、更新异常就开始大面积爆发。根子往往不在 SQL,而在建表的时候没按“规矩”来。数据库圈子里的这套规矩,就是范式——从第一范式到第三范式最常用,往上是 BCNF、第四第五范式,普通人能用到的基本就是前三个。
这篇文章我想用管冰箱、写账本这类日常事做类比,把范式这层窗户纸捅破,然后给你一套真正能用、能照着改的实战方法。不管你是刚转行的数据分析师、在校学生,还是被历史表结构折磨多年的老开发,这套逻辑搞明白了,后面所有的表设计都会顺很多。
1. 为什么会有范式这套规矩
1.1 没有范式的时候,表到底有多乱
想理解范式,先看一张反例表。假设我们要管公司员工和部门,最直觉的表长这样:
| 员工ID | 员工姓名 | 部门 | 部门经理 | 办公地点 | 办公电话 |
|---|---|---|---|---|---|
| 001 | 张三 | 研发部 | 王老师 | A栋301 | 8001 |
| 002 | 李四 | 研发部 | 王老师 | A栋301 | 8001 |
| 003 | 王五 | 市场部 | 赵老师 | B栋501 | 8002 |
这表看起来挺正常对吧?但懂行的人一眼就能看出问题:部门经理、办公地点、办公电话其实不依赖员工 ID,它们只依赖“部门”。只要研发部来一个新员工,这三列信息就被复制一遍。一千个研发部员工,就有一千份一模一样的“王老师、A栋301、8001”。
复制本身不可怕,可怕的是要改的时候。王老师升职了,你要把研发部的经理改成“刘老师”,那就得 update 这张表里所有研发部的行。万一漏掉几行,你会看到同一个部门有两种经理,报表一发出去,领导立刻发现不对劲。这就是更新异常。
反过来,如果把某部门的最后一个员工删掉,部门本身的信息也跟着没了——这叫删除异常。要是新成立一个部门,还没有分员工,你甚至没法把部门先录进去——这叫插入异常。
三大异常,全是这种“一个表啥都装”装出来的。范式就是针对性地回答一个问题:你表里的每一列,到底应该由谁来决定。
1.2 范式不是教条,是设计目标
很多人把范式背成几条定义,考试能过,做项目还是乱来。我建议你把范式理解成一系列“分家”的步骤:每个范式都在回答“如果这张表里有多余的依赖关系,怎么拆开才不丢信息”。
- 1NF:不许一个单元格里塞数组和菜篮子,拆到每行每列都是原子值。
- 2NF:复合主键的表里,非主键列不能只依赖主键的一部分,拆掉部分依赖。
- 3NF:非主键列不能依赖另一个非主键列,拆掉传递依赖。
- BCNF:把函数依赖的左边都变成候选键,算是 3NF 的加强版。
- 4NF/5NF:处理多值依赖、连接依赖,属于学术和极端复杂场景。
展开讲之前先说个原则:范式不是越高越好。项目里绝大多数表到 3NF 就够了,有时候还要故意往后退一退,回到第二甚至第一范式来换性能。这点我放到后面细说,先把三层基础范式吃透。
2. 第一范式:别在单元格里塞一篮子菜
2.1 原子性的生活化理解
第一范式(1NF)最简单的说法:每一行、每一列交叉的那个格子里,只能存一个值,不能存列表、数组,也不能存那种“用逗号分隔的一堆东西”。
打个比方。你管一个冰箱,冰箱的每个抽屉就是一个字段。按 1NF 的要求,一个抽屉只放一类东西,而且你贴了标签说“鸡蛋”,里面就只能放鸡蛋,不能放半抽屉鸡蛋加半抽屉葱。放在数据库里,意思就是“兴趣爱好”这个字段,不能存成“篮球, 阅读, 钢琴”,而应该把兴趣爱好拆成多行,或者拆到另一张专门存“用户-兴趣”关系的表里。
有人会说,那我用逗号分隔查询的时候用 find_in_set 不也能查吗?能查,但你会遇到一系列副作用:统计兴趣人数要写奇怪的函数,给用户加一个兴趣得读出整段字符串再拼接,删一个兴趣更是碰运气。数据量上万以后,这种字段就是长期技术债。
2.2 违反 1NF 的三种典型场景
实际业务里,违反 1NF 最常见的就三类:
- 逗号分隔字符串:像前面说的“标签”“兴趣”字段。看起来省了一张表,实际把查询、统计、更新的复杂度全堆在 SQL 里。
- JSON 当万能字段:很多团队图省事,把不确定的字段全塞进一个 JSON 列。小规模原型没问题,但一旦要对 JSON 里面的某个值做条件查询,索引用不上,性能瞬间垮掉。
- 一个字段存多种含义:比如“联系方式”字段里既有手机号又有座机号,或者“地址”字段里塞省市区街道全拼在一起。虽然从数据库角度看它是个字符串,但里面混了多种业务含义。
2.3 怎么改成符合 1NF
处理 1NF 的实操套路分两种:能拆字段就拆字段,拆不了就拆行。
场景一:字段含义混杂。比如“地址”拆成“省”“市”“区”“街道”。别怕字段多,数据库多几个字段的成本远低于后面写一串 substring_index 解析的成本。
场景二:一个属性有多个值。比如用户有多个兴趣。要么在“用户”表里建“兴趣1、兴趣2、兴趣3”这种垃圾设计(千万别学),要么建一张子表:
CREATE TABLE user_interest ( user_id INT NOT NULL, interest VARCHAR(32) NOT NULL, PRIMARY KEY (user_id, interest) );这样每一个兴趣单独一行,主键是 (user_id, interest),想统计“有多少人对篮球感兴趣”就是一句简单的 count,想给用户加兴趣就插入一行。很多范式问题,到最后都是靠这种“把一个字段的多值拆成多行”解决的。
2.4 1NF 的一个争议:这年头要不要严格 1NF
有时候我也会用 JSON 字段,但要区分场景。比如埋点事件里的自定义参数,上千种业务自定义字段,你不可能给每种参数建一张表,这时候 JSON 是合理的。关键是:对需要检索、计算、关联的属性,必须拆出来成独立字段;而对只做展示、存档、不参与核心逻辑的属性,JSON 可以容忍。1NF 不是让你把所有 JSON 都干掉,而是让你别把 JSON 当成所有设计问题的遮羞布。
注意:如果你决定用 JSON,一定要把其中会被频繁过滤的字段用虚拟列+索引的方式补出来,否则上线半年后 DBA 会找你谈心。
3. 第二范式:半张表依赖半把钥匙,当然要拆
3.1 先看什么是“部分依赖”
第二范式(2NF)是在 1NF 基础上往前走的。它只适用于“复合主键”的表——也就是主键是两列或更多列组成的情况。如果一张表的主键本身就是单列,那它天然满足 2NF,不需要额外操作。
很多人设计表时主键都喜欢用自增 id,于是 2NF 在他们那儿几乎没存在感。但复合主键的场景其实比想象中多:选课表(学号+课程号)、订单明细表(订单号+商品序号)、角色权限表(角色ID+权限ID),这些都逃不掉。
举具体例子。有一张“学生选课表”:
| 学号 | 课程号 | 课程名称 | 学分 | 成绩 |
|---|---|---|---|---|
| S001 | C01 | 数据库 | 4 | 90 |
| S002 | C01 | 数据库 | 4 | 85 |
| S001 | C02 | 算法 | 3 | 95 |
主键是 (学号, 课程号)。但“课程名称”和“学分”只依赖课程号,和学号没关系。这意味着同一个课程,被 100 个学生选,课程名称和学分就重复 100 遍。改学分的时候要 update 100 行,万一漏几行,同一个课程在系统里就有两个学分,后面算绩点直接乱套。
这就是部分依赖:有些列只依赖复合主键的一部分。有部分依赖存在,就不满足 2NF。
3.2 拆解方法:去掉那个多余的依赖
处理方式很直接:把真正依赖于“课程号”的列抽出去,单独建“课程表”。
课程表:
| 课程号 | 课程名称 | 学分 |
|---|---|---|
| C01 | 数据库 | 4 |
| C02 | 算法 | 3 |
选课表(去掉了课程名称和学分):
| 学号 | 课程号 | 成绩 |
|---|---|---|
| S001 | C01 | 90 |
| S002 | C01 | 85 |
| S001 | C02 | 95 |
选课表里保留课程号的外键,需要课程名称时通过 join 把课程表带出来就行。这样课程信息只存一份,改学分只需要改一行,删除学生也不会误删课程,课程未开课也能先录入课程表。2NF 直接解决了这组插入、删除、更新异常。
3.3 实操中识别部分依赖的三个判断法
我在实际评审表结构时,识别部分依赖基本靠三个问题:
- 这张表的主键是不是复合的?不是,那 2NF 直接跳过。
- 每个非主键列能不能用主键的全部列来解释?能,继续。不能,说明它只依赖其中一部分,这就违规了。
- 这列会不会在主键的一部分相同时出现大量重复?如果会,基本可以判定为部分依赖。
第三个问题特好用。比如订单明细表 (订单号, 商品序号) 为主键,“商品名称”“商品单价”是不是只跟商品 ID 有关?是,那就拆出去。很多外表的设计,其实就是从这种局部重复里拆出来的。
3.4 关于“先放一起再拆”的顺序
有些人会有疑问:为什么一开始不直接设计成三张表,非要先放一张里然后再拆?这个问题问得特别对。现实里你如果是一个人从零设计,完全可以直接设计好。但范式这套操作真正的用武之地,在于“已经有一张跑了几年的线上老表”的场景:你不知道怎么把一张混杂了所有信息的表安全地拆开,范式给了你明确的拆法和依据。拆的时候不是随便拆,是根据“谁的依赖关系更紧密”来切,这样才不会丢数据、不会破坏关系。
所以学 2NF,重点不是背定义,是掌握“识别多余依赖”的眼睛。眼睛练出来了,面对历史遗留脏表,你就有了一把精密手术刀。
4. 第三范式:别让列像传话游戏那样拐弯
4.1 传递依赖:A 决定 B,B 决定 C,于是 A 绕远路决定了 C
第三范式(3NF)针对的是传递依赖。它的定义是:非主键列不能依赖于另一个非主键列。
还是回到开头那个员工表。主键是员工 ID,员工 ID 决定部门,部门决定部门经理。于是员工 ID 绕了个弯,也“决定”了部门经理。但部门经理显然不是员工本人的属性,它属于部门。这种“非主键列之间还有依赖关系”的结构,就是传递依赖。
它造成的后果和 2NF 类似:部门信息跟着员工复制。但和 2NF 不同的是,2NF 的连接点是复合主键的一部分,3NF 的连接点是另一个非主键列,诊断起来更隐蔽。
我见过最典型的反例是一张“订单表”:
| 订单号 | 客户ID | 客户姓名 | 客户等级 | 客户所在城市 |
|---|---|---|---|---|
| O001 | C100 | 老李 | 金卡 | 上海 |
| O002 | C100 | 老李 | 金卡 | 上海 |
| O003 | C200 | 小张 | 银卡 | 北京 |
订单号是主键,但客户姓名、客户等级、客户所在城市这些信息明明只由客户 ID 决定。客户信息跟着订单复制一遍又一遍,客户改地址要更新其所有历史订单,想想就头大。
4.2 拆法:剥掉中间那一环,建依赖层的“专职表”
处理 3NF 的办法同样是拆,把依赖链条拆直:
客户表:
| 客户ID | 客户姓名 | 客户等级 | 客户所在城市 |
|---|---|---|---|
| C100 | 老李 | 金卡 | 上海 |
| C200 | 小张 | 银卡 | 北京 |
订单表(精简版):
| 订单号 | 客户ID | 订单金额 | 下单时间 |
|---|---|---|---|
| O001 | C100 | 299 | 2024-01-01 |
| O003 | C200 | 88 | 2024-01-02 |
客户改地址?改客户表的某一列,订单表一行不用动。查订单详情需要客户信息?join 一下客户表,逻辑清清楚楚。
4.3 什么时候 3NF 会自动满足
有个偷懒但好用的判断法:如果一张表的主键是单列自增 id,而且这张表里根本没有“另一个业务主键”出现,比如“用户表”里的 id、手机号、昵称、注册时间,那它基本天然满足 2NF 和 3NF,因为手机号、昵称、注册时间全部直接依赖于 id,非主键列之间也不存在相互依赖。
真正需要你动手做 3NF 的,往往是那些“把别的表的字段搬进来”的表:订单表搬了客户字段,商品表搬了分类字段,报销单表搬了员工字段。遇到这种表,多想一句:我搬进来的这列,是不是应该由这张表的主键直接决定?如果答案是否,就要警惕传递依赖。
4.4 3NF 与 2NF 的实战联动
实际项目里,2NF 和 3NF 经常一起出现。我们看一个稍微复杂的例子:一个“仓库调拨明细”表,主键是(调拨单号, 商品ID),字段包括商品名称、商品规格、调出仓库ID、仓库名称、调拨数量。
老规矩,第一刀先解决部分依赖:商品名称、商品规格都和商品 ID 有关,和调拨单号没关,拆出商品表;仓库名称和仓库 ID 有关,拆出仓库表。第二刀看看还有没有传递依赖:调拨单号决定调出仓库 ID,仓库 ID 决定仓库名称,虽然拆完单表后仓库名称已经进仓库表了,但如果当初只拆了商品,把仓库名称留在明细表里,依然是 3NF 违规。所以很多时候你要先按 2NF 的思路拆,再按 3NF 的思路复查一遍,两把刀轮流上。
这套“先拆部分依赖,再拆传递依赖”的操作,本质上是在给数据做血缘梳理。表拆清楚了,后面做数据仓库、做权限分级、做接口设计,都会省一大堆心。
5. 范式实战:从零设计一个在线书店的数据库
光讲概念不够尽兴,咱们拿一个具体业务走一遍全流程。假设我现在要设计一个在线书店系统,先丢一张最原始的“大表”出来——很多新手第一次设计系统,最直觉的想法就是这样:
| 订单ID | 顾客姓名 | 顾客城市 | 商品名称 | 商品作者 | 商品价格 | 购买数量 |
|---|---|---|---|---|---|---|
| O001 | 张三 | 上海 | 数据库讲义 | 刘老师 | 59 | 1 |
| O001 | 张三 | 上海 | 算法导论 | 李老师 | 79 | 1 |
| O001 | 张三 | 上海 | 数据库讲义 | 刘老师 | 59 | 2 |
| O002 | 李四 | 北京 | 数学之美 | 王老师 | 49 | 1 |
5.1 第一刀:处理重复购买的商品
先看这条数据:O001 订单里“数据库讲义”出现了两次,分别是 1 本和 2 本。这说明什么?不是数据录重复了,而是我们把“买了几种书”和“每种书买了几本”的关系描述不清。真正的结构是:订单里有“明细行”,每一行是一次购买的“某一种商品”的明细,同一种商品在一个订单里只会有一行,数量放在明细行里。
这一刀做下去,其实已经暗合 1NF 原子性的精神:一行记录一个不可再分的购买动作。你不可能让同一种商品拆成两行再分别写数量,那样查询、统计都扭曲。
5.2 第二刀:按 2NF 拆出商品表
这张原始表的主键如果硬定,得是(订单ID, 商品名称),因为同一订单可以买多种商品。可是“商品名称”“商品作者”明显都只依赖“商品”这个实体,和订单没关系,算是典型的部分依赖。
按 2NF 拆:商品名称、商品作者、商品价格,抽出去成一张“商品表”,商品 ID 作为主键。于是订单明细行不再存“商品名称文字”这么一坨,而是存“商品ID”,通过外键引用商品表。
为什么商品价格也放商品表?因为现在这个业务里价格是商品目录价。如果你做促销活动、每个订单的真实成交价可能不同,那“成交价”就必须留在明细行里,而不是跟着商品表走。这种“同一字段在不同上下文里该放哪”的取舍,是表设计最容易翻车的地方,后面我会专门做一张对照表。
5.3 第三刀:按 3NF 拆出顾客表
再看“顾客姓名”“顾客城市”。它们明显由“顾客实体”决定,而不是由订单决定。如果只依赖订单 ID,那么每个订单都要复制一遍顾客姓名和城市,顾客搬家时,历史上几十张订单全要更新,这就是传递依赖的另一个变种。
按 3NF 拆:顾客信息进“顾客表”,订单表里只留顾客 ID。于是订单表变成:
| 订单ID | 顾客ID | 下单时间 |
|---|---|---|
| O001 | C1 | 2024-03-01 |
| O002 | C2 | 2024-03-02 |
订单明细表变成:
| 订单ID | 商品ID | 购买数量 | 成交价 |
|---|---|---|---|
| O001 | B1 | 1 | 59 |
| O001 | B2 | 1 | 79 |
| O001 | B1 | 2 | 59 |
| O002 | B3 | 1 | 49 |
看这版,是不是清爽多了?但细看,O001 里 B1 又出现了两行。这说明订单明细的主键设计还有问题:同一订单同一商品只能有一行,应该把订单 ID 和商品 ID 组成联合主键,或者至少加唯一索引。在真正的订单系统里,通常还会加入“下单批次号”或“行号”,让每一明细行拥有唯一性。这里的要点是:范式只告诉你拆分原则,具体到唯一性约束、索引设计,还得自己补。
5.4 拆完之后的全景
最终的四张表:
顾客表
| 顾客ID | 顾客姓名 | 顾客城市 |
|---|---|---|
| C1 | 张三 | 上海 |
| C2 | 李四 | 北京 |
商品表
| 商品ID | 商品名称 | 商品作者 | 目录价 |
|---|---|---|---|
| B1 | 数据库讲义 | 刘老师 | 59 |
| B2 | 算法导论 | 李老师 | 79 |
| B3 | 数学之美 | 王老师 | 49 |
订单表
| 订单ID | 顾客ID | 下单时间 |
|---|---|---|
| O001 | C1 | 2024-03-01 |
| O002 | C2 | 2024-03-02 |
订单明细表
| 订单ID | 商品ID | 购买数量 | 成交价 |
|---|---|---|---|
| O001 | B1 | 1 | 59 |
| O001 | B2 | 1 | 79 |
| O001 | B1 | 2 | 59 |
| O002 | B3 | 1 | 49 |
这样从顾客视角查订单,顾客表→订单表→订单明细表→商品表,链路清晰;从订单视角查顾客,反过来 join 也一样容易。要统计某个作者的书卖了多少本,join 两三次就能算出来,还不用考虑重复复制信息带来的维护痛苦。
5.5 一套完整的建表 SQL 建议
把上面的设计落成库表,一套干净的建表语句大致长这样:
CREATE TABLE customer ( customer_id INT PRIMARY KEY AUTO_INCREMENT, customer_name VARCHAR(50) NOT NULL, city VARCHAR(50) NOT NULL ); CREATE TABLE product ( product_id INT PRIMARY KEY AUTO_INCREMENT, product_name VARCHAR(100) NOT NULL, author VARCHAR(50) NOT NULL, price DECIMAL(10, 2) NOT NULL ); CREATE TABLE orders ( order_id INT PRIMARY KEY AUTO_INCREMENT, customer_id INT NOT NULL, order_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_orders_customer FOREIGN KEY (customer_id) REFERENCES customer(customer_id) ); CREATE TABLE order_item ( order_id INT NOT NULL, product_id INT NOT NULL, quantity INT NOT NULL, deal_price DECIMAL(10, 2) NOT NULL, PRIMARY KEY (order_id, product_id), CONSTRAINT fk_item_order FOREIGN KEY (order_id) REFERENCES orders(order_id), CONSTRAINT fk_item_product FOREIGN KEY (product_id) REFERENCES product(product_id) );建表的 SQL 表面上一看都是 create table,但每一行的约束都在兑现前面拆表时下的判断:外键保证引用完整,联合主键保证同订单同商品唯一,价格字段用 decimal 而不是 float,避免金额精度出鬼。
6. 范式的边界:哪些场景要科学地“造反”
6.1 性能优先时:反范式不是妥协,是另一种严谨
把范式讲到五张表六张表之后,你一定会遇到一个问题:查询的 join 太多了。一个列表页要联五张表,数据库压力大,接口响应慢,这时候就需要“反范式”——刻意把某些字段复制回来,用空间换时间。
最常见的反范式场景有三类:
- 冗余计数字段:比如订单表里加一列“商品种类数”,每次下单选完商品就顺手 update 一下,查询列表页时直接展示,不用再 count 明细表。
- 冗余热门展示字段:比如商品表里存“分类名称”,而不是 join 分类表。分类改名时批量 update 一次商品表就行。
- 大数据分析场景:数据仓库里的宽表,经常把几十张表 join 好然后落成一张大宽表,供报表直接查。这跟 OLTP 事务系统追求的低冗余完全不是一个逻辑。
关键区别在于:事务系统讲究“写入时的一致性”,冗余数据一旦多了,更新就麻烦;分析系统讲究“查询时的速度”,冗余数据能省掉每次报表跑批的 join 成本。前者反范式要极其克制,后者反范式是常规操作。
6.2 反范式怎么反才不翻车
我见过太多“反范式反过头”的案例,最经典的坑是:冗余了字段,但没人负责同步。
比如你在订单表里冗余了“顾客姓名”,顾客改名后忘了同步订单表,报表里顾客名字就对不上了。正确的反范式必须配套三件事:
- 明确冗余字段的同步时机:是在业务写入时同步,还是定时任务批量同步,还是通过消息队列异步同步。
- 明确冗余字段的归属方:必须有一个“源表”,其他都是副本。源表改了,副本必须跟着改,谁改的、什么时候改的要有记录。
- 明确可容忍的延迟:有些报表场景第二天才同步也能接受,有些交易场景一秒都不能忍。
还有一种更精细的做法:用触发器或存储过程自动维护冗余字段。比如 MySQL 里可以建触发器,在订单明细插入后自动更新订单表的“商品种类数”。触发器好处是同步逻辑内聚,坏处是调试困难、对性能有一定影响,小表没问题,大表要慎重。我更推荐在业务代码里显式维护,至少出问题时能根据日志定位。
6.3 范式等级选择:一个可以参考的决策表
| 业务场景 | 推荐范式等级 | 理由 |
|---|---|---|
| OLTP 核心交易表(订单、支付) | 3NF 优先 | 一致性最关键,写入多、更新多 |
| OLTP 配置表(商品、分类) | 3NF | 冗余少,维护成本低 |
| 统计报表宽表 | 1NF 或反范式 | 查询性能优先,允许冗余 |
| 日志、事件流水 | 1NF 即可 | 写入量巨大,字段以 JSON 兜底 |
| 多级分类树 | 3NF+特殊优化 | 加上闭包表或路径枚举,避免深 join |
这张表不是标准答案,而是个思考方向。核心思路是:先想清楚这个表的读多还是写多,一致性要求多高,再决定范式和冗余的平衡点。
7. 从范式中真正学到的东西
7.1 第一课:识别冗余
范式给我的核心训练不是记住定义,而是看到一张表就能条件反射地问:哪一列是被别的列决定的?哪一列会在很多行里重复出现?这种直觉在代码评审和库表评审时特别有用。别人拿一张表过来,我扫一眼就能看出“这个部门经理不应该在员工表里”“这个客户城市放在订单表是个隐患”。冗余有时候藏得很深,但范式给了你一套寻找它的系统方法:一切从主键出发,非主键列要么完全依赖主键,要么就该去别的表。
7.2 第二课:拆分的胆量
很多人不敢拆表,怕业务代码要改太多。但我的经验是,越晚拆越难拆。表结构一上生产,就有数据、有线上代码、有各种报表依赖,再想动就像给飞驰的汽车换轮胎。所以新表设计时,多花十分钟拆干净,比上线后加班一个月救火划算得多。如果确实要改老表,原则也是先加新结构、同步数据、验证无误后再废弃旧结构,别想着一步到位。
7.3 第三课:当规范与性能打架时
范式不是枷锁。用不用反范式,取决于业务对一致性和性能的权衡。只要同步机制可靠、延迟可控、边界清晰,反范式是完全科学的设计。最怕的是那种“因为赶进度随手冗余,事后没人维护”的脏设计。
我自己每设计完一张新表,都会用三个问题自审:主键是什么?其他列都直接依赖主键吗?有没有哪一列其实根本不归这张表管?三个问题问完,问题基本就暴露了。范式定义可以忘,这三个问题建议焊在脑子里。