news 2026/9/16 3:45:25

SQL Server数据类型选型实战:从隐式转换到性能优化的避坑指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SQL Server数据类型选型实战:从隐式转换到性能优化的避坑指南

1. 我背过的三口大锅:先看清数据类型选错到底错在哪

聊SQL Server,很多人第一反应是索引、是锁、是事务隔离级别,但真正让我在半夜被电话叫起来的,十次里有七八次都是数据类型惹的祸。说个真实经历:某次上线了一个订单查询功能,测试环境跑得飞快,到了生产库一查,响应时间从几十毫秒直接飙到十几秒。一开始怀疑是服务器资源问题,排查半天,最后看执行计划,发现SQL Server在索引列上做了一次隐式转换,导致索引全部失效——罪魁祸首就是查询参数用的类型和列定义的类型不一致。

这个经验让我明白一件事:数据类型不是"能存数据就行"的表层选择,它决定了存储空间、写入效率、查询性能、精度边界,甚至决定了你会不会在线上数据对不上账的时候背上一口大锅。

如果把常见事故归类,其实就三口锅:

锅的类型典型表现最常踩的场景
隐式转换锅索引失效、查询变慢、CPU飙升VARCHAR列与NVARCHAR参数比较
精度丢失锅金额误差、汇总对不上FLOAT存钱、DECIMAL精度位数不够
存储膨胀锅数据库体积异常大、IO开销高该用TINYINT用了INT、该用VARCHAR用了NVARCHAR

这三口锅背后的逻辑是相通的:你对SQL Server如何存储、如何比较数据没有足够的敬畏心。接下来我按类型分类展开,每一类都会把踩坑链路和修复方案一起讲清楚。

2. 字符型战场:VARCHAR与NVARCHAR的选择绝不是顺手的事

字符类型是重灾区。很多人建表的时候,看到字符串就一把梭写VARCHAR(50),看到中文就换NVARCHAR(50),至于为什么、对不对,基本不做深入思考。但字符类型里藏着的坑,比你想的深得多。

2.1 存中文到底该用VARCHAR还是NVARCHAR

先说结论:如果一个列可能包含中文或其他Unicode字符,推荐直接用NVARCHAR。这不是"win10以后无所谓"的事,而是跟编码存储机制有关。

在SQL Server里,VARCHAR对应的是非Unicode编码,具体代码页取决于数据库的Collation排序规则,比如Chinese_PRC_CI_AS对应的是GBK代码页(936)。也就是说,VARCHAR存中文不是"不能存",而是它的存储规则跟着排序规则走,如果数据库代码页设置得不对,或者数据库迁移到不同Collation的服务器上,乱码问题会成片爆发。

NVARCHAR则是Unicode(UTF-16)存储,每个字符固定占用2字节(补充平面字符是4字节),它不依赖代码页,任何语言、任何排序规则下都能正确存储和比较。

我见过最典型的锅是这样:两个系统做接口对接,A系统把VARCHAR字段定义为VARCHAR(50),存"张三"没问题;后来B系统传入的是NVARCHAR参数,SQL Server在比较时会把VARCHAR列隐式转换为NVARCHAR,转换过程中如果遇到扩展字符,就会产生乱码或"?"。更麻烦的是,这种隐式转换发生在索引列上时,查询计划从Seek变成Scan,性能瞬间崩掉。

2.2 定长还是变长:CHAR、NCHAR与VARCHAR、NVARCHAR的取舍

很多人觉得"定长字符性能更好",于是在姓名、手机号这类固定长度字段上用了CHAR(10)或NCHAR(10)。但SQL Server存储定长字段时,会为每个值补满空格,存储上没有节省,写入时还要多处理填充和裁剪逻辑。

移动端应用里手机号、身份证号这种字段,看似固定长度,实际上有各种异常情况——比如手机号可能有国际区号、座机号、分机号,身份证号历史上也有15位的老号。用CHAR一旦遇到超长或不足长度的数据,就是一顿拼字符串的麻烦。

我的实际选择原则是:

  • 明确知道长度上限且长度完全固定的编号(如某些系统内的流水号),才考虑CHAR/NCHAR;
  • 大多数"看似固定"的业务字段,比如姓名、手机号、邮箱,一律用VARCHAR/NVARCHAR变长类型;
  • 需要存文件内容、JSON、XML的大文本,直接上VARCHAR(MAX)NVARCHAR(MAX),但要注意MAX类型不能直接建索引,得靠全文索引或者拆表处理。

2.3 隐式转换:字符类型选择错误的连锁反应

这一小节值得反复看。SQL Server有一个数据类型优先级表,当两个不同类型的值进行比较时,低优先级的一方会被隐式转换为高优先级的一方。数据类型优先级上,NVARCHAR高于VARCHARDATETIME高于CHARINT高于SMALLINT

举个实际例子:

-- 表结构 CREATE TABLE Orders ( OrderNo VARCHAR(20) NOT NULL PRIMARY KEY, Amount DECIMAL(18,2) ); -- 查询语句,参数是NVARCHAR DECLARE @param NVARCHAR(20) = N'SO20240001'; SELECT * FROM Orders WHERE OrderNo = @param;

因为NVARCHAR优先级更高,SQL Server会把OrderNo列从VARCHAR隐式升到NVARCHAR再比较,索引列上带函数式的转换,执行计划就变成了索引扫描——数据量一上去,慢是必然的。

排查这类问题的方法很直接:打开执行计划,找CONVERT_IMPLICIT字样。如果你在计划里看到它出现在WHERE条件对应的索引列上,恭喜你,踩坑了。

修复方式有两种:一是修改列类型为NVARCHAR,二是修改查询参数类型为VARCHAR。根据我的经验,优先改查询参数类型,因为列类型往往已经关联了大量表和存储过程,改动成本高,而查询端的修正只需要在应用层传参时保持一致即可。

3. 数值型真相:INT、BIGINT、DECIMAL、FLOAT的边界与取舍

数值类型看着简单,实际是精度事故的重灾区。尤其是金额、比率、统计指标这三类数据,选错类型导致的结果往往不是"性能变差",而是"数据直接错了"。

3.1 范围边界:INT溢出比你想象的更常见

很多开发习惯性用INT存一切整数,因为"够用"。但INT的范围是-2,147,483,6482,147,483,647,约21亿。听起来很大,但在某些场景下真的会爆:

  • 订单ID:一个日活百万的平台,几年后订单量就可能突破21亿;
  • 计数器:累计下载量、累计访问量,一旦营销活动爆量,瞬间打到边界;
  • 时间戳:用INT存Unix时间戳,2038年问题在SQL Server里同样是存在的;
  • 自增主键:IDENTITY(1,1)用INT做主键,一旦达到上限,INSERT直接报主键溢出错误,且无法自动重置。

我有一个项目就是把自增主键从INT升级为BIGINT,原因是某个中间表在双十一单日涌入了近千万条流水。迁移的时候花了整整一个窗口期,因为在SQL Server 2019之前,修改自增列类型需要ALTER TABLE ALTER COLUMN,这个操作会重建表,几亿行的表重建时间和磁盘空间都是大麻烦。

所以建表时,主键、流水号、状态计数这类会持续增长的值,直接建议用BIGINT起步。BIGINT范围是正负9百亿亿,在你我可见的未来很难触顶。

3.2 DECIMAL的精度与标度:金额计算最核心的决策

存钱的地方不能碰FLOAT不能碰FLOAT不能碰FLOAT,重要的事说三遍。FLOAT是近似数,内部按二进制浮点存储,0.1 + 0.2在FLOAT里都会得到一个极细微的偏差。虽然99%的场景这个偏差不影响显示,但一旦做累计汇总、银行对账、财务审核,几分钱的误差就足以让人崩溃。

正确做法是使用DECIMAL/NUMERIC类型。定义格式为DECIMAL(p, s),其中p是总位数(精度),s是小数点后位数(标度)。比如DECIMAL(18,2)表示一共18位,小数部分2位,整数部分16位,最大金额为999,999,999,999,9999.99(16位整数)。

精度设置经验:

  • 金额字段统一用DECIMAL(18,2)起步,如果涉及汇率、利率等需要更多小数位的场景,可以到DECIMAL(18,4)DECIMAL(20,6)
  • 百分比、折扣可以位更多,但最终计算时注意结果的小数位数是"左操作数精度加上右操作数精度",超出p时SQL Server会做舍入;
  • DECIMAL(38, 0)虽然精度最大,但完全没有小数位,用它做汇总时可能发生整数溢出。

另外一个被忽略的坑:DECIMAL类型的运算结果精度是动态推算的。比如两个DECIMAL(18,2)相乘,结果是DECIMAL(37,4)。如果你把这个结果存入一个DECIMAL(18,2)列,SQL Server会直接对超出部分做截断(实际上是四舍五入),而这种舍入在多步计算中不断累积,最终就变成了"明明每步都对但总数对不上"的玄学问题。我的经验是:中间计算过程不要插回DECIMAL列,先算出最终结果再入表。

3.3 存储空间:TINYINT到BIGINT的阶梯式浪费

数值类型的存储空间是有阶梯的:

类型存储大小范围
TINYINT1字节0~255
SMALLINT2字节-32,768~32,767
INT4字节-21亿~21亿
BIGINT8字节正负9百亿亿

如果一个只有三个状态值的状态列(比如0/1/2)用了INT,单行多占3字节;一个千万行的表,这一个字段就白白多占了30MB。单个字段60MB看着不多,但一个表里四五个这样的字段,再加上索引对每个字段都做复制,存储和IO开销就是几倍级别的差距。

我之前接手一个数据仓库的优化任务,发现多个事实表里有大量INT类型的标志位字段(0/1/2),改成了TINYINT之后,表体积缩水了差不多1/4。重建聚集索引时IO时间也明显下降,因为每页能存下的行数变多了。

4. 日期时间类型:DATETIME、DATETIME2、DATE、TIME怎么选

日期时间类型也是重灾区,而且这里的坑特别隐蔽——因为"看起来能用"和"真的用对了"之间隔着一层精度和范围的理解。

4.1 精度与范围:DATETIME的3.33ms你察觉不到但迟早会吃大亏

SQL Server早期的DATETIME精度是3.33毫秒(即百分之一秒),范围是1753年到9999年。这意味着:

  • 你存入'2024-01-01 12:00:00.000'没问题;
  • 但如果存入'2024-01-01 12:00:00.001',它会被舍入到最近的3.33ms倍数,可能在回读时变成00.003
  • 1753年之前的日期(比如历史人物数据、天文数据)存不进去。

DATETIME2是后来的改进版本,精度可到100纳秒(7位小数),范围从0001年到9999年。新项目一律用DATETIME2,这个决定可以帮你省掉未来一大堆精度和范围问题。根据网上其他博主和微软官方文档的推荐,DATETIME2是当前的默认选择。

日期时间列的类型选择:

需求场景推荐类型原由
只需要日期DATE4个字节,查询时不会有多余的时间
只需要当天时间TIME3~5字节,与日期解耦
普通业务时间戳DATETIME2(3)毫秒精度够用,兼容性好
需要亚毫秒精度DATETIME2(7)100ns精度,避免四舍五入
跨时区业务DATETIMEOFFSET保留时区偏移,避免时间读出来对不上

4.2 DATETIMEOFFSET与时区:全球化业务绕不开的类型

如果你做跨境电商、海外SaaS,或者任何需要统一时间基准的业务,DATETIMEOFFSET是你应该使用的类型。它除了存储时间本身还存储与UTC的偏移量,比如2024-05-01 15:00:00 +08:00。存储大小随精度不同为8~10字节,比DATETIME2略大。

一个真实的案例:某公司做东南亚业务,用户下单时间存在DATETIME2里,没有统一时区。服务器在香港、数据库在美东、用户在泰国,三方各自看到的时间都不一样。后来统一改成DATETIMEOFFSET,在应用层用UTC时间写入,展示时再转换到本地时区,问题才彻底解决。

4.3 日期函数使用中的隐性坑

  • GETDATE()返回的是服务器本机时间,不是UTC。如果服务器时区设置错了,所有写入数据的时间都会偏。一个规避办法是使用SYSUTCDATETIME()获取UTC时间。
  • DATEDIFF对边界值的行为是"跨过多少个边界",比如计算年龄时直接DATEDIFF(YEAR,出生日期,GETDATE()),会把那些还没到生日的人算大一岁。
  • CONVERT带格式参数时,CONVERT(VARCHAR(10), GETDATE(), 112)返回20240501,但CONVERT(VARCHAR(10), GETDATE(), 101)返回的是05/01/2024。如果格式串写错,SQL不会报错,但结果会非常难解。
  • 还有一个常见坑是午夜边界BETWEEN '2024-01-01' AND '2024-01-02'会把1月2日一整天全部包含进来,因为'2024-01-02'被自动解析为2024-01-02 00:00:00.000。统计日报时这样写会把第二天的零点数据多算进去。正确写法是使用>= '2024-01-01' AND < '2024-01-02'

5. 容易被忽视的BIT、UNIQUEIDENTIFIER与旧时代遗留类型

这一章讲的是那些"平时不太起眼、关键时刻让人挠头"的类型。

5.1 BIT:一个Bool的N种误用

BIT是SQL Server的布尔类型,只有0、1和NULL。常见误用:

  • IS_ACTIVE这类标志位建成了CHAR(1),存'Y'/'N'。这让每条记录多占空间,更重要的是,查询时容易漏掉大写/小写、空格等脏数据;
  • 给BIT列建索引。BIT列只有三个值(0/1/NULL),索引的选择性极低,查询优化器大概率还是会全表扫描。如果你真的需要快速过滤有效/无效记录,可以考虑创建过滤索引(Filtered Index),比如WHERE IS_ACTIVE = 1,这样索引行数会小很多;
  • BIT列是不能设置为IDENTITY列的,要用INT类型配合CHECK约束模拟。

另外一个不算坑但值得注意的点:BIT在SQL Server内部存储时是按字节打包的。如果一个表里有8个BIT列,它们共享1个字节存储;9到16个BIT列则共享2个字节。这跟其他类型不同,不能简单按"每列1字节"算。

5.2 UNIQUEIDENTIFIER:GUID主键的碎片之痛

UNIQUEIDENTIFIER(即GUID类型)在分布式系统中很常见,因为它允许不同节点各自生成主键而不需要全局协调。但如果你直接拿它做聚集索引主键,问题就来了:GUID是随机生成的,插入顺序完全随机,聚集索引本身是按主键排序的物理顺序,随机插入导致页分裂频繁、索引碎片大量产生、写入性能急剧下降。

某次一个同事把分布式业务系统的主键从INT改为GUID,写入性能直接掉了近60%,究其根源就是碎片化太重。如果确实需要GUID做主键,建议采取以下方案之一:

  • NEWSEQUENTIALID()代替NEWID(),生成的GUID在同一个服务器内是递增的,能显著减少页分裂;
  • 把主键设计为自增BIGINT(顺序键),GUID列作为普通唯一索引列,负责跨系统业务关联;
  • 使用INT IDENTITY+ROWGUIDCOL标记GUID列,把两者各自的优势都留下来。

另外注意GUID在查询中不能跟字符串直接比较。如果你把GUID从应用层转成字符串再拼进WHERE条件,它会被隐式转换成字符串比较,索引用不上GUID的比较优化,扫描范围会放大。

5.3 TEXT、NTEXT、IMAGE三类遗留类型不用再用

这三类是老版本SQL Server用来存大文本和大二进制的,它们的问题包括:

  • 不能直接使用=LIKESUBSTRING等常规字符串操作,需要依赖TEXTPTR等函数;
  • 不支持在WHERE条件中直接比较,操作非常受限;
  • 不能参与GROUP BYORDER BYDISTINCT等常规查询。

从SQL Server 2005开始,微软就建议用VARCHAR(MAX)NVARCHAR(MAX)VARBINARY(MAX)替代它们。如果你的库里还有这类老类型,迁移时用一条ALTER语句即可:

ALTER TABLE dbo.OldTable ALTER COLUMN [Body] NVARCHAR(MAX);

不过注意:VARCHAR(MAX)VARCHAR(10)在查询计划中的处理逻辑有差异。MAX类型的大值数据默认存储在LOB结构中,读取它的开销比普通行内数据大。如果只是存长文/JSON,这是合理选择;如果字段实际数据长度大部分都在几百字节以内,定义成NVARCHAR(500)反而更合适,因为它仍然走行内存储,扫描速度更快。

6. 如何快速定位数据类型引发的性能问题

前面都在讲"怎么选",这一节讲"已经踩坑了怎么救"。数据类型问题的临床表现通常是"生产环境某一类查询突然变慢""CPU莫名升高""某个存储过程偶尔超时"。如果你怀疑是数据类型的问题,排查链路可以按以下顺序走。

6.1 第一步:抓执行计划,找CONVERT_IMPLICIT

打开SSMS的"包含实际执行计划",跑一遍慢查询,在执行计划里搜索关键字CONVERT_IMPLICIT。只要出现它,就说明有隐式类型转换正在发生。

如果它发生在索引列上,下一步就是确定谁向谁转换。根据数据类型优先级表(从高到低),大概知道:DATETIME > VARCHAR > INTNVARCHAR通常比VARCHAR优先级高。低优先级向高优先级转,意味着高优先级这侧的字段/参数需要被转换,索引大概率失效。

这里有个容易误判的地方:不是所有CONVERT_IMPLICIT都致命。如果转换发生在常量上(比如参数是VARCHAR,列是VARCHAR,但传入的是N'字符串'),可能每次只需要转换一次,性能影响很小。找问题要优先看转换发生在索引列上还是发生在参数侧

6.2 第二步:查看表定义与应用层参数类型一致性

把慢查询涉及的表的列类型和应用程序传入参数的类型逐一对齐,注意以下典型错配:

应用层传入列定义后果
NVARCHARVARCHAR索引列隐式转换,索引失效
VARCHARNVARCHAR参数被升格,可能找不到值或产生隐式转换
INTBIGINT列被转成BIGINT,索引失效
日期字符串DATETIME2字符串被隐式转换,通常问题不大,但注意格式依赖
DECIMAL(10,2)DECIMAL(18,2)精度更高时可能不影响索引,但注意参数化问题

6.3 第三步:修正方案与验证方式

修正思路通常有三种,按优先级排序:

  1. 改参数类型:应用层把参数类型改成和列定义一致,改动量小,风险低;
  2. 改存储过程参数:如果是一批存储过程,统一修改入参类型,执行计划里隐式转换就会消失;
  3. 改列类型:只有确认列类型本身设计不合理(比如代码页引发乱码、VARCHAR改NVARCHAR),才走这一步,而且需要评估索引重建的时间和空间成本。

修正后的验证方式很简单:重新抓执行计划,确认CONVERT_IMPLICIT消失,索引Seek恢复。同时看逻辑读和CPU时间是否明显下降。根据我的经验,一个千万行的表如果在索引列上免掉了隐式转换,查询时间常常能从几秒降到几十毫秒。

6.4 数据迁移与类型变更的实操教训

如果你最终决定修改列类型,有几个实操教训值得记住:

  • ALTER TABLE ALTER COLUMN在SQL Server中会重建表,除非所有列都满足"行内更新"要求。带索引的列进行类型修改,索引会重建,锁表时间可能很长,必须在业务低峰期执行;
  • 如果只是增加VARCHAR长度(比如从VARCHAR(50)改到VARCHAR(100)),在大多数情况下是元数据变更,非常快。但修改NVARCHAR和VARCHAR之间的类型会触发全表扫描重建,时间和空间都要预留;
  • INTBIGINT通常也很快,因为底层存储布局变化不大,但INTDECIMAL(18,0)会触发全表重构。所以建表阶段对值域的预判很重要,后面想改代价是相当大的;
  • 对于超大表,不要直接用ALTER,可以先建新表、后台插入数据、切换表名,或者使用SQL Server 2016+的ALTER TABLE ... WITH (ONLINE = ON)(需要注意版本和索引限制)。

7. 最后一聊:把检查清单固化到流程里

自从背过几次锅之后,我在建表和Code Review阶段就增加了一套强制检查项,每次新表上线前都要过一遍:

检查项默认推荐备注
主键BIGINT IDENTITY大流量系统不要用INT
金额/累计指标DECIMAL(18,2)起禁用FLOAT/FLOAT依赖
普通字符串NVARCHAR(50/100/500)避免乱码和隐式转换
日期时间DATETIME2(3)精度足够且兼容性好
布尔标志BIT不要用CHAR(1)或INT存
大文本/JSONNVARCHAR(MAX)按实际长度考虑是否用MAX
跨时区时间DATETIMEOFFSET全球化业务必备
用途单一的枚举状态TINYINT能省空间,别用INT

这套表不一定适合所有业务,但至少提供了一种思考框架:每个字段的类型选择都要问一句"它的边界是什么、会不会增长、会不会跨时区、会不会有大文本"。

最后分享一个让我印象很深的教训。之前有个同事为了"统一风格",把所有字符串列都建成了NVARCHAR(50),结果表里有一列是记录"订单来源渠道编码",实际存的值是纯ASCII的短编码,比如"TAOBAO""JD""WX"。这列被一千万行的订单表外键关联,经常做JOIN。因为NVARCHAR和VARCHAR在字符宽度上的差异,存储体积比它应需要的大了不止一倍,索引页放了更少的键,IO和内存占用都受了拖累。后来这列改成VARCHAR(20)后,索引大小缩了差不多30%,查询性能也好了不少。这个案例让我明白:朴素地"全部NVARCHAR"并不总是最佳答案,真正的原则是按需选择、保持一致、预判增长

希望你看了这篇之后,在建表、写查询、排查慢SQL时能多留一个心眼。那些看起来不起眼的数据类型定义,往往就是线上事故真正的引爆点。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/16 3:45:00

电脑蓝屏别急着重装:5步定位法从代码到硬件排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/16 3:44:00

企业生态解析:制度之外的权力、利益与立场博弈

先说个我自己的经历。几年前我给一家做智能硬件的公司做组织诊断&#xff0c;创始人拉着我聊了半天业务&#xff0c;最后快走的时候&#xff0c;他叹了口气说&#xff1a;“制度定了那么多&#xff0c;KPI 也拆得很细&#xff0c;可一到跨部门协作就卡壳&#xff0c;大家都很有…

作者头像 李华
网站建设 2026/9/16 3:42:19

Doris Remote Catalog 实战指南:Arrow与JDBC协议选型与避坑

1. 项目概述&#xff1a;为什么 Remote Catalog 不是“开箱即用”&#xff0c;而是个需要亲手调教的精密仪器Doris Remote Catalog 这个词最近在数据湖和实时数仓的讨论区里出现频率越来越高&#xff0c;但凡聊到 Doris 对接 Hive、MySQL、Oracle 或者 Iceberg 这类外部数据源&…

作者头像 李华
网站建设 2026/9/16 3:42:15

MaAsLin3多变量模型:微生态关联分析的破局与实操指南

1. 为什么偏偏是MaAsLin3&#xff1a;多变量模型才是微生态关联分析的破局点做微生态研究的朋友应该都有过这种体验&#xff1a;拿到16S或者宏基因组数据&#xff0c;跑完差异丰度分析&#xff0c;筛出一堆p值小于0.05的菌&#xff0c;看着热图里红红绿绿一片&#xff0c;结果审…

作者头像 李华
网站建设 2026/9/16 3:41:43

2026最新揭秘:有没有做家具特卖的网站?避坑指南与建站实战

2026最新揭秘:有没有做家具特卖的网站?避坑指南与建站实战 找建站公司怕被坑高价,这是很多想做家具特卖生意老板们的共同焦虑。市面上报价从几千到几万不等,看着眼花缭乱,心里却没底,生怕花了大钱买个摆设,或者后期维护被绑死。其实,2026年最新的建站逻辑早就变了,不再是单纯比价格,而是看“性价比”和“…

作者头像 李华
网站建设 2026/9/16 3:41:08

MCP Client 不走 DashScope,改走 TaoToken 行不行?

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华