ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SQL Server字符集验证实战:从排序规则到writelog乱码排查

SQL Server字符集验证实战:从排序规则到writelog乱码排查 项目标题是“SQL Server 字符集验证报告”乍看像是个文档名但干过数据库这块的人都知道一份字符集验证报告背后往往藏着不少故事。我最近刚好在处理一套系统的SQL Server迁移评估原库是中文环境目标库是全新的实例上线前要把字符集、排序规则、代码页这些东西全部核对一遍。这类工作在交付清单里往往只是一行字但真做起来牵扯到实例级配置、库级配置、列级设置、客户端会话行为还有同步工具的数据流转规则每一层都可能埋雷。这篇内容把我实际的验证思路、查询脚本、碰到的典型报错以及排查过程整理出来希望能给正在做SQL Server迁移、升级或者异构系统对接的朋友一些参考。1. 为什么一张字符集验证清单能避免上线后的“乱码事故”1.1 字符集问题不是“显示乱码”那么简单很多人一听到字符集第一反应是“数据库里中文显示成问号了”。但字符集没配好远不止显示层的问题。最直接的影响有三个一是数据截断。按单字节长度设计字段结果存入多字节字符后长度超限写入直接报错。二是比较和排序结果异常。同样的汉字在不同排序规则下排序位置不同如果查询依赖ORDER BY或者JOIN条件结果可能和预期不一致。三是索引失效或者约束冲突。唯一索引按字节比较字符集不同可能导致“看起来相同的值”被判为不同或反之导致误判重复。所以字符集验证的本质是要确认“写入的字符”和“读取的字符”在每一层都以一致的方式被解释和存储。它贯穿存储引擎、协议、客户端驱动和应用程序代码任何一环错位数据就可能在你不知道的情况下被改变。1.2 什么时候必须做字符集验证不一定每次部署都要做全套验证但下面几类场景我建议老老实实走一遍第一类是实例迁移和升级。比如从SQL Server 2008 R2迁到2019或者从物理机迁到云RDS实例级排序规则往往被忽略而默认库、tempdb的排序规则会跟着实例走。第二类是异构系统对接。Java应用通过JDBC写入或者用Kettle、DataX之类的工具从MySQL、Oracle同步数据到SQL Server。源端字符集是UTF-8目标端如果是GBK系列排序规则中间就可能出现无法映射的字符。像最近大家经常搜到的“sql server writelog”相关报错很多就是写入日志时遇到了无法识别的字符编码。第三类是历史库改造。系统早期定义字段时没有显式指定NVARCHAR后来业务要支持繁体、生僻字或者外文原来的VARCHAR字段在代码页限制下根本装不下。遇到这些情况一份完整的字符集验证报告能提前量化风险而不是等到上线后被业务方反馈“这名字怎么变成乱码了”再回头看。2. SQL Server 字符集机制拆解排序规则、代码页、数据类型三者关系2.1 Collation 不只是排序规则SQL Server里字符集的概念并不像MySQL里那样直接叫charset它通过 Collation排序规则来综合表达。一个排序规则名称里同时包含三件事涉及的字符集/代码页、排序规则、是否区分大小写/重音等属性。以中文环境最常见的 Chinese_PRC_CI_AS 为例Chinese_PRC表示针对简体中文的排序规则CI表示Case Insensitive不区分大小写AS表示Accent Sensitive区分重音。但这个排序规则底层对应的 Windows 代码页是 936也就是 GBK。所以一个列如果用了这个排序规则那它只能完整表达GBK编码范围内的字符。而 Latin1_General_CI_AS 则对应代码页 1252西欧语言在这个规则下存储中文不是不能存但中文被当成“特殊符号”处理某些字符可能发生不可逆转换。这也是为什么有些老系统迁移后出现“繁体变简体、特殊符号变问号”的原因——代码页在写入时就把字符映射到了不同的码点。这里可以类比一下代码页就像一本“字符字典”数据库拿着字节去查字典字典里没有这个字就显示不出来。GBK字典比1252字典多收录了大量中文字符但缺乏某些西欧符号UTF-8字典最全但存储开销和比较逻辑又不同。2.2 四层配置优先级实例、库、列、表达式SQL Server的排序规则是分层的优先级从低到高大概是实例级安装时指定决定系统数据库master、model、tempdb的默认排序规则。数据库级创建库时指定如果没指定就继承实例的。这个级别在图形界面里经常被忽略。列级建表时为CHAR/VARCHAR/TEXT列显式指定。表达式级查询里的字符串比较和排序操作可以用 COLLATE 临时指定。实际验证时我从上往下逐层检查。一个常见的坑是实例和库都是Chinese_PRC_CI_AS但某张表的某些字段从老系统迁移过来时被定义成了Latin1_General_CI_AS于是这些字段里的中文在页面上显示正常因为应用层做了转码但直接在SSMS里查出来却是乱码。这种“部分乱码”最难排查因为不是你肉眼看到所有中文都错而是特定字段错。2.3 VARCHAR 与 NVARCHAR 的存储差异直接影响验证结论字符集验证绕不开数据类型的选择。VARCHAR是按代码页存储的单字节字符占1字节中文在GBK下占2字节。NVARCHAR使用Unicode UCS-2/UTF-16存储不管中英文一般占2字节。这就带来一个很重要的实践判断如果一个库的字符串列全部用了NVARCHAR那么即使排序规则是Latin1_General_CI_AS中文也能正常存储反之如果用VARCHAR而排序规则是Chinese_PRC_CI_AS也能存中文但换到英文排序规则的实例上做DBCC或者迁移时如果不转换列定义行为就会有差异。所以验证时不能只看排序规则名称还要看数据类型。很多老系统为了省空间用了VARCHAR虽然当时够用但现在要做字符集升级或迁移就很被动。3. 字符集验证实操一套可直接复用的检查脚本3.1 实例级与数据库级的快速体检第一次接触一个环境我会先跑一个总览查询把实例默认排序规则、各个数据库的排序规则、以及关键系统库的配置一次性拉出来SELECT SERVERPROPERTY(Collation) AS InstanceCollation, SERVERPROPERTY(SqlCharSetName) AS SqlCharSetName, SERVERPROPERTY(SqlSortOrderName) AS SqlSortOrderName; SELECT name, collation_name FROM sys.databases ORDER BY database_id;这里有一个容易忽略的点tempdb的排序规则是继承实例的它不允许单独修改。如果你的实例排序规则和业务库不一致那么临时表、临时变量以及某些join操作里字符串比较就可能出现collation冲突。实例级排序规则一旦设定修改起来非常麻烦官方支持的做法是重建实例或者用命令行工具配合重建系统库。所以验证报告里我通常把实例排序规则单独列为“高风险项”因为它的修改成本最高。我曾经遇到一个生产环境实例排序规则被当初实施的人设成了SQL_Latin1_General_CP1_CI_AS业务库却是Chinese_PRC_CI_AS平时查询不报错但一旦用临时表关联业务表且关联条件是字符串就会时不时冒出“Cannot resolve the collation conflict”的错误。因为表面看数据能读写这种隐患藏的特别深。3.2 列级和表达式级的细查列级检查是工作量最大的部分。我一般用下面这个脚本找到所有字符串类型字段及其排序规则SELECT t.name AS TableName, c.name AS ColumnName, ty.name AS DataType, c.max_length, c.collation_name FROM sys.columns c INNER JOIN sys.tables t ON c.object_id t.object_id INNER JOIN sys.types ty ON c.user_type_id ty.user_type_id WHERE c.system_type_id IN (167, 175, 231, 239, 35) AND c.collation_name IS NOT NULL ORDER BY c.collation_name;system_type_id的含义如下167对应VARCHAR175对应CHAR231对应NVARCHAR239对应NCHAR35对应TEXT。可以在系统里确认。这个脚本的价值在于它能一眼看出哪些列用的是非默认排序规则。比如一个库整体是Chinese_PRC_CI_AS突然冒出来一列是Latin1_General_CI_AS这基本就是历史遗留问题得单独评估。还有一个容易被忽略的地方是“用户定义的表类型”。有些存储过程会用自定义表类型做参数表类型的列也有排序规则定义。如果应用传入的字符串排序规则和表类型不一致也会在调用时报collation冲突。我在验证的时候会额外查一下系统表类型SELECT tt.name AS TableTypeName, c.name AS ColumnName, c.collation_name FROM sys.table_types tt INNER JOIN sys.columns c ON tt.type_table_object_id c.object_id WHERE c.collation_name IS NOT NULL;3.3 会话级行为验证连接字符串和客户端驱动服务端配置都OK不代表客户端读写没问题。SQL Server允许客户端在连接时指定语言和排序规则比如ODBC、JDBC连接串里的Language参数或者执行SET LANGUAGE Simplified Chinese来影响会话级别的日期格式和默认排序行为。我在验证报告里会专门做一组会话级测试模拟三种客户端连接SSMS自带、Java JDBC驱动、Python pyodbc用同一个中文串做写入、读取、比较三项操作确认结果一致。这里的经验是JDBC驱动的连接串里最好显式设置useUnicodetruecharacterEncodingutf-8如果不设置驱动会按平台默认编码发送字符串碰上实例代码页不是UTF-8的场景中文写入就可能变成问号。不过这不是SQL Server引擎的问题而是客户端驱动层解释差异导致的但最终用户只看到数据库里的数据是坏的。会话级验证还可以查当前会话的有效排序规则SELECT DATABASEPROPERTYEX(DB_NAME(), Collation) AS CurrentDBCollation, SERVERPROPERTY(Collation) AS InstanceCollation;如果应用里执行了ALTER DATABASE或者在代码里指定了不同的collation这里就能看出来。4. 经典故障实录writelog 报错与“无效的多字节字符”问题4.1 “出现无效字符。多字节字符集必须包含一前导字节且无结尾字节”是怎么发生的很多人在搜索这个问题说明它不是罕见现象。这个报错的本质是SQL Server在解析某个字符串时发现字节流不符合当前代码页的编码规范。GBK这类多字节字符集里一个中文字符由两个字节组成第一个字节叫前导字节第二个字节叫尾字节两者各有合法范围。如果字节流里突然出现一个前导字节后面跟的不是合法的尾字节就会报这个错。举个例子某系统的应用端把一段UTF-8编码的文本直接拼进SQL语句发送给SQL Server而SQL Server当前会话用的是GBK代码页。UTF-8里某些三字节字符的字节序列在GBK解析器看来可能是“一个合法前导字节一个非法尾字节”于是直接报错。这类问题高发于“writelog”相关的错误日志中因为写入日志文件时的结构化处理往往要求严格的编码转换。实际上很多事务在提交时SQL Server会把数据写进事务日志如果Record里包含无法按当前collation解释的字节就会触发类似报错。还有一种相当常见的触发路径是应用层使用MySQL或Oracle作为源库数据同步任务把源库的查询结果通过字符串拼接的方式生成INSERT语句再发给SQL Server。源库的字符集是UTF-8但同步代码没有做目标端代码页的转换某些生僻字比如一些冷僻人名用字在拼接SQL时被原样塞进去SQL Server解析SQL文本时就挂了。4.2 排查步骤分而治之找到截断点碰上这个报错我建议按以下顺序排查第一步先确认是哪条语句和哪个字段触发的。开启SQL Server Profiler或者扩展事件捕捉报错发生时的SQL文本重点看WHERE条件和INSERT VALUES里的中文字符。第二步确认连接串和客户端代码页。如果是Java应用检查JDBC URL是否设置characterEncodingutf-8以及应用服务器系统默认编码是否UTF-8。第三步确认目标表的列类型和排序规则。如果是VARCHAR看一下collation对应的代码页是不是能覆盖源端字符集如果目标列是NVARCHAR一般不会出现解析错误因为N...前缀会用Unicode解析。第四步尝试复现最小化。把报错SQL里的中文字符替换成正常的“测试中文”如果不再报错基本可以锁定是某个特殊字符导致的。这类问题最好的解决方式不是去改实例排序规则而是在应用层、同步层统一使用Unicode。比如同步程序生成SQL时用N...前缀或者把目标列类型改为NVARCHAR。在数据库侧增加排序规则检查只能发现问题不能解决根源上的编码不一致。4.3 验证报告里给这类故障的定级与建议在我的验证报告模板里这类问题我会定为“高风险·需应用改造”。原因是即使改数据库排序规则如果应用端仍然发送UTF-8字节流问题依旧只是数据库代码页变了报错形式可能从“解析错误”变成“数据写入错误”。定级高是为了让项目组重视安排应用侧和同步侧一起改造而不是只让DBA改配置。这里分享一个实操细节即使目标表是NVARCHAR如果应用使用VARCHAR传参没加N前缀SQL Server也会把参数隐式转换成表列的类型转换过程就可能发生字符丢失。所以在代码审查时我一般要求凡是写入NVARCHAR列的参数要么在SQL里显式加N前缀要么在驱动层面使用setString等相关API的Unicode绑定不要依赖默认转换。5. 迁移场景下的字符集兼容性专项从源端评估到目标端校验5.1 源端元数据采集不能只靠排序规则名称做SQL Server之间的迁移时最直观的办法是把源端所有字符串列的collation和数据类型拉出来和目标端的设计进行对比。但这里我想强调一个容易被忽略的事实如果源库列是NVARCHAR那么即使排序规则不同大多数情况下数据也能无损迁移因为底层数据是Unicode但如果源库列是VARCHAR且代码页和中文相关那就要特别小心。为了评估VARCHAR列的实际风险我会在源库做一个“最坏字符探测”找出所有可能包含非当前代码页字符的行。一个简单的探测方式是检查字符串里是否存在某些保留的替换字符如0xFFFD或DBCC PAGE里的可疑字节但对大多数场景我建议直接在源库跑一个对比查询SELECT COUNT(*) FROM tbl WHERE col CONVERT(NVARCHAR(MAX), col);这个查询的思路是如果VARCHAR列里的字符按代码页解析后再转为NVARCHAR会和原NVARCHAR表示一致说明该列内容在代码页内如果不一致说明该列可能已经存储了不属于代码页的乱码数据。这类数据迁过去后基本无法自动修复。5.2 目标端校验字符级抽样对比迁移完成之后不能只看行数和总字节数对不对要做字符级抽样对比。我常用的做法是源端和目标端各执行同样的聚合函数——包括CHECKSUM_AGG(BINARY_CHECKSUM(*))但更实用的是对关键字符串列做两张表的全量对比。如果数据量不大可以直接用EXCEPT做行对比如果数据量大就按主键分片抽样对比每个样例行里字符串列的BINARY_CHECKSUM。这里要注意BINARY_CHECKSUM在不同排序规则下可能因为大小写和全半角处理方式不同而产生差异所以最好用CHECKSUM在应用层做归一化后比较。我曾经遇到过一个案例迁移后行数一致某字段看起来完全正常但有一小部分字符在源端显示是“中文引号”目标端显示成“英文引号”。用BINARY_CHECKSUM一下就查出了差异原因是源端排序规则允许某些Unicode字符映射到相近的码点目标端则做了不同的规范化。最后定位下来是迁移工具在nvarchar转换时的映射规则问题这类问题只有字符级对比才能暴露。5.3 排序规则冲突的兼容方案在迁移过程中如果业务库之间、或者业务库与tempdb之间排序规则不同最常出现的错误是Cannot resolve the collation conflict between Chinese_PRC_CI_AS and Latin1_General_CI_AS.处理方案通常有三种第一在JOIN或WHERE条件里显式指定COLLATE把一边强制转换SELECT * FROM A INNER JOIN B ON A.Name COLLATE Chinese_PRC_CI_AS B.Name;第二统一数据库到实例排序规则。如果量不大可以在新库建库时就不指定排序规则让它继承实例然后重新导入数据。第三重建实例。这是最费事的修改实例级排序规则需要重建系统库和数据一般只在实例本来就是新建且业务数据可重新加载时使用。我个人的经验是优先选择第一种方案做临时兼容但最终还是要推动统一排序规则。因为SQL文本里到处写COLLATE看起来能跑但后续开发人员可能在一个没写COLLATE的查询里又踩一次坑而且SQL Server对COLLATE的索引利用率会有影响。6. 验证报告撰写建议从技术数据到可决策结论6.1 报告里必须有“影响面”和“风险等级”很多工程师写的验证报告通篇是查询结果截图和排序规则名称缺乏结论领导看完不知道该怎么办。好的验证报告应该包含几块验证范围、发现的问题清单、每项问题的风险等级、建议整改措施、整改后的验证结果。比如“某表某列用的Latin1_General_CI_AS与库级Chinese_PRC_CI_AS不一致”这条发现有经验的读者会知道这大概率是历史遗留但业务影响是什么需要确认该列是否被用来做索引、是否被JOIN、是否会被写入中文字符。因此报告中我会附一句该列主要用于记录用户输入的英文名称当前数据存量中未发现非拉丁字符但应用侧已规划支持中文建议将列类型修改为NVARCHAR或修改COLLATE。这种写法比单纯列一行“发现不一致”有价值得多。6.2 建议整改顺序低风险先行高风险专项整改顺序一般遵循“低风险先行、高风险专项”的原则。低风险项包括修改非关键列的排序规则、调整应用连接串的编码参数、统一临时表排序规则等。这些改动影响面小可以快速落地。高风险项包括修改实例级排序规则、列数据类型从VARCHAR改成NVARCHAR、批量更新历史数据。这些操作必须放到维护窗口并且要有回滚方案。比如VARCHAR改NVARCHAR时表大小可能膨胀一倍以上要考虑磁盘空间和索引重建时间。关于VARCHAR改NVARCHAR我补充一个实操细节如果一张大表有多个索引直接ALTER COLUMN会触发所有相关索引重建可能锁表很长时间。推荐的做法是新建一张新结构表用INSERT INTO ... SELECT ...分批导数据同时建好索引最后rename切表。SQL Server Enterprise版可以用分区切换降低停机时间但即使标准版分批导数据也比一次性ALTER好控制。6.3 持续监控而非一锤子买卖字符集验证不应该是一次性动作更应该变成持续监控的一部分。尤其在多系统并行期间新上的应用可能再次引入错误的编码习惯。我建议在每次版本发布前跑一次快速检查实例排序规则、库排序规则、可疑列清单、最近24小时是否出现collation冲突报错。扩展事件可以捕捉collation conflict也可以定期扫描错误日志里的相关关键字。SQL Server的错误日志不会把所有4104类错误都记录所以稳妥的做法是扩展事件会话落盘按天归档然后写个脚本解析关键字告警。7. 实操中容易踩的坑非官方文档常见内容7.1 大小写敏感性测试要慎用“中文”排序规则里的CI/AS是针对拉丁字符和重音符号的中文没有大小写概念所以用中文测CI是测不出区别的。我在做验证时会用英文测试用例比如a A来判断是否区分大小写用拼音字符或带音调的字符来判断是否区分重音。测试脚本示例SELECT CASE WHEN a A THEN CI ELSE CS END AS CaseSensitivity, CASE WHEN Nü Nu THEN AI ELSE AS END AS AccentSensitivity;7.2 SQL Server 2022 及后续版本排序规则差异最近在做2022版本验证时发现新版本安装默认排序规则可能和旧版本有所不同。老版本中文环境常见的是Chinese_PRC_CI_AS新安装的2022在部分场景下会默认成Chinese_PRC_90_CI_AS或其他变体。版本号90、100等表示排序规则版本对应Unicode标准版本差异。它们之间的主要影响不在常用汉字更多体现在生僻字和特殊符号的排序权重上。所以验证时如果源端是2008或2012目标端是2019或2022即使排序规则名称看着都是Chinese_PRC_CI_AS建议也做一次字符级抽样对比重点包含生僻字和特殊符号。7.3 “无后缀排序规则”与“90/100版本”的差异刚开始接触这块的人容易忽略Chinese_PRC_CI_AS 和 Chinese_PRC_90_CI_AS、Chinese_PRC_100_CI_AS 不是完全等价的。_90和_100后缀代表排序规则版本更新对相同字符串可能产生不同的排序和比较结果尤其是在Unicode字符串上。比如某些数据库迁移到高版本实例后原有索引统计信息里的排序规则版本不一致查询计划可能选择不同的索引。实际我碰到过一次一张大表的主键是NVARCHAR迁移后查询效率明显下降后来发现主键索引在源库是Chinese_PRC_CI_AS目标库因为连接字符串里临时指定collation导致索引统计信息未正确更新。解决办法并不复杂重新更新统计信息并重建索引后性能恢复正常。7.4 全文索引与字符集的关系全文索引对语言和字符集也很敏感。如果业务用了全文索引在做字符集验证时还需要检查全文索引的语言词库设置。中文全文索引需要加载中文分词器如果目标实例没有安装对应的语言组件全文查询可能建不了索引或者查询不到中文内容。这一块在常规验证报告里容易漏掉但是真踩到的时候特别耽误时间。8. 我的最终经验总结字符集验证的真正价值在于降低不确定性做了这么多字符集验证的案子我有一个很深的体会字符集问题之所以讨厌不是因为它有多难而是因为它往往潜伏很久才爆发。开发环境数据量小、字符样本简单所有字符都是常见汉字验证自然通过。等上了生产用户录入生僻字、繁体字、甚至特殊表情符号时问题才集中暴露。到那个时候数据已经写入修复成本比上线前高好几倍。所以我给团队定的规矩是凡是涉及跨实例迁移、跨驱动升级、跨语言环境部署的项目字符集验证必须纳入上线前检查清单并且验证的样本不能只挑“名字”和“地址”这类常见字段要专门构造一批边界样本包括生僻字、全角半角字符、带音调的外文字符、历史上曾被替换字占位的脏数据。同时验证报告里不要只报“一致”或“不一致”。把不一致分成“必须处理”“建议处理”“暂不处理但有跟踪”三档每一档对应明确的行动项和负责人。这样报告才有推动力。一点小技巧送给正在做验证的朋友当发现某列数据和库级排序规则不一致时先别急着改数据库先查应用代码里有没有对这个字段做排序或者比较的依赖特别是报表系统里的排序逻辑。很多时候数据库字段本身没问题是应用在代码里写死了ORDER BY某个中文字段而应用连接会话的排序规则因为连接串参数和数据库不一致导致排序结果在测试环境和生产环境不一样。这类问题修复不需要动数据库改应用连接串或SQL语句里的COLLATE就行代价小得多。字符集验证这件事表面上是一套查询脚本、一张对比报告本质上是在数据链路每一环上确认“同一个字符解释一致”。把这个确认做透了系统就能稳稳地接住各种语言和各种来源的数据不会在某个深夜突然抛出一条看不懂的报错。
返回列表