如果你在数据库客户端里执行过SELECT查询 BLOB 字段大概率见过这样的“奇观”明明业务上是一个文档内容或者图片结果列里显示的是0A3B2C1D4E5F这种十六进制长串甚至有些人会把这串东西当成字符串直接复制出去用结果怎么都对不上。很多人把 BLOB 挂嘴边念成 “bolb”其实就是 Binary Large Object 的缩写。这个类型的设计初衷就是装二进制大对象比如图片、压缩包、PDF、序列化后的对象字节流。它跟 VARCHAR2、CLOB 完全是两回事VARCHAR2 和 CLOB 存的是“字符”有字符集概念BLOB 存的是“字节”没有字符集参与。所以当你直接 SELECT BLOB 字段时客户端没法把它按字符解码只能把每个字节显示成十六进制给你看。这篇内容我会把 BLOB 字段显示为字符串的来龙去脉、转换方法、真实项目中的读法、还有乱码和文件嗅探这些坑一次讲清楚。适合正在被 BLOB 字段折磨的数据库开发、数据运维和做数据迁移的朋友看完至少能在 10 分钟内定位这个字段里面到底存的是什么然后用对的方式把它读出来。1. 为什么 BLOB 字段显示出来是那串“乱码”1.1 BLOB 没有字符集只有字节序列先做一个最基础的实验。有一张模拟表字段内容是一个字符串ABC但类型是 BLOBSELECT DOC_CONTENT FROM SIMPLE_TABLE WHERE ID 1;在我的数据库客户端里返回的不是ABC而是类似414243的一小段十六进制。0x41是字符A的 ASCII 码0x42是B0x43是C。工具为了让你“看见”二进制内容直接把每个字节映射成了两位十六进制符号这就是所谓的“BLOB 字段显示为字符串”的真相。所以先说结论你看到的不是“字符串”而是原始字节的十六进制可视化。BLOB 在存储层面是一串 0 和 1 组成的字节流数据库不知道也不关心里面是文本、图片还是视频。它只保证一点你写进去多少字节读出来还是多少字节绝不篡改。我见过不少同事拿着这串 hex 去找开发理论说数据坏了。实际上数据好端端地躺在那里只是读取方式不对。理解这一点后面所有的转换方法才有意义。1.2 客户端工具显示策略各不相同Oracle 官方推荐的客户端是 SQL Developer其他的如 PL/SQL Developer、DBeaver 也很常见。不同工具对 BLOB 的显示策略不一样PL/SQL DeveloperBLOB 列直接显示为十六进制字符串双击进入编辑区还能看到可视化内容。SQL Developer通常会显示(BLOB)或[BLOB]占位符需要点开才能看详情。DBeaver默认显示为十六进制字符串有的版本会显示为BLOB: 23 bytes这样的形式。如果你用 JDBC 读取rs.getObject()返回的也不是byte[]而是一个oracle.sql.BLOB对象需要调用它的getBytes()或者getBinaryStream()方法才能拿到二进制内容。所以“BLOB 显示为字符串”这个问题本质上横跨了客户端展示、JDBC 读取、字符集转换三个层面。别慌下面我先带你确认一个重要前提这个 BLOB 里面到底装的是什么。1.3 动手前先判断 BLOB 里是文本还是文件这是最重要的一步也是我踩过坑后才悟出来的。BLOB 转字符串前提是它存的确实是文本。如果里面是一张 JPEG 图片你还硬要转字符串得到的必然是乱码如果里面是一个 ZIP 压缩包按文本读出来更是毫无意义。快速判断方法就两招。第一看长度SELECT DBMS_LOB.GETLENGTH(DOC_CONTENT) FROM SIMPLE_TABLE WHERE ID 1;如果长度只有几百字节大概率是文本、配置、JSON 或者小序列化对象如果长度是几 MB 甚至几十 MB那基本可以肯定是文件、图片或压缩包。第二看文件头“魔数”。所谓魔数就是文件开头固定的几个字节每种常见文件格式都有自己的签名。我整理了一份在 Oracle 里做文件嗅探的对照表文件类型十六进制特征文件头原始值JPEG 图片FF D8 FFÿØÿPNG 图片89 50 4E 47.PNGPDF 文档25 50 44 46%PDFZIP/DOCX/XLSX50 4B 03 04PK..老版 DOC/XLSD0 CF 11 E0ÐÏ.àJSON 文本7B开头即{{XML 文本3C开头即在 SQL 里可以这样判断SELECT CASE WHEN DBMS_LOB.SUBSTR(DOC_CONTENT, 4, 1) HEXTORAW(FFD8FFE0) THEN JPEG WHEN DBMS_LOB.SUBSTR(DOC_CONTENT, 8, 1) HEXTORAW(89504E470D0A1A0A) THEN PNG WHEN DBMS_LOB.SUBSTR(DOC_CONTENT, 4, 1) HEXTORAW(25504446) THEN PDF WHEN DBMS_LOB.SUBSTR(DOC_CONTENT, 4, 1) HEXTORAW(504B0304) THEN ZIP/DOCX WHEN UTL_RAW.CAST_TO_VARCHAR2(DBMS_LOB.SUBSTR(DOC_CONTENT, 1, 1)) { THEN JSON ELSE UNKNOWN END AS FILE_TYPE FROM SIMPLE_TABLE WHERE ID 1;DBMS_LOB.SUBSTR返回的还是RAW类型HEXTORAW可以把它和魔数做二进制对照。这个方法在排查线上数据时非常管用一秒钟就能确认存储类型避免后面白折腾。2. 把 BLOB 转成字符串的几种核心方法2.1 UTL_RAW.CAST_TO_VARCHAR2 DBMS_LOB.SUBSTR 组合如果你已经确认 BLOB 里存的是文本最常用的转换方式就是DBMS_LOB.SUBSTR配合UTL_RAW.CAST_TO_VARCHAR2。DBMS_LOB.SUBSTR(blob, length, offset)从 BLOB 中截取指定字节长度返回类型是RAWUTL_RAW.CAST_TO_VARCHAR2(raw)负责把RAW类型逐字节转为VARCHAR2字符串。两个函数一组合文本内容就出来了SELECT UTL_RAW.CAST_TO_VARCHAR2( DBMS_LOB.SUBSTR(DOC_CONTENT, 2000, 1) ) AS TEXT_CONTENT FROM SIMPLE_TABLE WHERE ID 1;注意这里有个关键细节为什么不能写成TO_CHAR(DOC_CONTENT)因为 Oracle 不允许 BLOB 直接隐式转换成VARCHAR2会直接报ORA-00932: inconsistent datatypes。这就是必须借助UTL_RAW和DBMS_LOB两个包的原因。再强调一个使用边界UTL_RAW.CAST_TO_VARCHAR2转换后字符串长度不能超过 4000 字节一旦超出就会报ORA-06502字符缓冲区不足的错误。所以这段代码只适合确认 BLOB 内容小于等于 4000 字节的情况。实际上如果 BLOB 里存的真是文本超过几 KB 就建议直接转 CLOB 而不是靠 SQL 硬转下面会详细讲。2.2 超过 4000 字节的文本要分段读取遇到超过 4000 字节的 BLOB 文本DBMS_LOB.SUBSTR照样能截取但UTL_RAW.CAST_TO_VARCHAR2只能转 4000 字节以内的片段。所以标准做法是循环分段读取每次取 4000 字节以内的块逐段拼接。以下是把 BLOB 转换到 CLOB 的存储过程骨架DECLARE L_BLOB BLOB; L_CLOB CLOB : EMPTY_CLOB(); L_OFFSET NUMBER : 1; L_CHUNK NUMBER : 4000; L_LENGTH NUMBER; L_RAW RAW(4000); BEGIN SELECT DOC_CONTENT INTO L_BLOB FROM SIMPLE_TABLE WHERE ID 1; L_LENGTH : DBMS_LOB.GETLENGTH(L_BLOB); DBMS_LOB.CREATETEMPORARY(L_CLOB, TRUE); WHILE L_OFFSET L_LENGTH LOOP L_RAW : DBMS_LOB.SUBSTR(L_BLOB, LEAST(L_CHUNK, L_LENGTH - L_OFFSET 1), L_OFFSET); DBMS_LOB.APPEND(L_CLOB, UTL_RAW.CAST_TO_VARCHAR2(L_RAW)); L_OFFSET : L_OFFSET L_CHUNK; END LOOP; DBMS_OUTPUT.PUT_LINE(CLOB length || DBMS_LOB.GETLENGTH(L_CLOB)); END;这个循环每次取 4000 字节LEAST函数处理最后一段不足 4000 字节的情况UTL_RAW.CAST_TO_VARCHAR2把每段 RAW 转成字符串DBMS_LOB.APPEND把结果追加到 CLOB 中。这里没有用v_clob : v_clob || ...这种拼接方式因为大量拼接会产生不必要的临时 CLOB性能更差。如果你只是临时查看比较大的文本又不想写存储过程可以反复调整DBMS_LOB.SUBSTR的OFFSET参数分页查看SELECT UTL_RAW.CAST_TO_VARCHAR2(DBMS_LOB.SUBSTR(DOC_CONTENT, 4000, 1)) FROM SIMPLE_TABLE WHERE ID 1; SELECT UTL_RAW.CAST_TO_VARCHAR2(DBMS_LOB.SUBSTR(DOC_CONTENT, 4000, 4001)) FROM SIMPLE_TABLE WHERE ID 1;这就像翻书一样第一段看 1 到 4000第二段看 4001 到 8000。用于排查快速确认内容没问题但真正正式的处理脚本还得靠上面那个循环。2.3 中文乱码的根源与 UTL_RAW.CONVERT 的正确姿势很多人按上面的方法转出字符串后发现中文全是问号或者ä¸Â这种乱码第一反应是数据坏了其实不是而是字符集没有对上。场景还原一下某个老系统把文本以 GBK 编码存进了 BLOB 字段而当前数据库字符集是AL32UTF8。UTL_RAW.CAST_TO_VARCHAR2只做“字节到字符串”的机械转换它不会自动识别字节里的编码。GBK 编码的字节序列直接按 UTF-8 去解读自然就成了乱码。解决办法是在转字符串之前先用UTL_RAW.CONVERT做字符集转换。这个函数的签名是UTL_RAW.CONVERT(data, to_charset, from_charset)三个参数分别是原始数据、目标字符集、源字符集注意别把后两个参数写反了SELECT UTL_RAW.CAST_TO_VARCHAR2( UTL_RAW.CONVERT( DBMS_LOB.SUBSTR(DOC_CONTENT, 2000, 1), AL32UTF8, ZHS16GBK ) ) AS TEXT_CONTENT FROM SIMPLE_TABLE WHERE ID 1;这里把源 BLOB 里的字节按ZHS16GBK解码再转换成AL32UTF8的字节最后CAST_TO_VARCHAR2出来的字符串才是正常的中文。实际工作中字符集可能有ZHS16GBK、UTF8、AL32UTF8、US7ASCII等最麻烦的是源系统已经停产、没人能告诉你当初用的什么字符集。这种情况就靠试先用ZHS16GBK转乱码就换UTF8再乱就换AL32UTF8看哪次转出来中文是正常的。踩过这个坑之后我的体会是字符集问题越早发现越好。最好的方式是在当初写 BLOB 的代码处就统一约定编码比如固定StandardCharsets.UTF_8后续读 BLOB 的代码也明确用同一字符集中间千万别搞“编码猜测”那只会越猜越乱。2.4 BLOB 转 CLOB 的官方函数为什么不好用Oracle 官方其实提供了DBMS_LOB.CONVERTTOCLOB过程看起来专门用来转 CLOB但实际使用体验一言难尽DBMS_LOB.CONVERTTOCLOB( dest_clob IN OUT NOCOPY CLOB, src_blob IN BLOB, dest_offset IN OUT NOCOPY INTEGER, src_offset IN OUT NOCOPY INTEGER, amount IN INTEGER, blob_csid IN INTEGER, lang_ctx IN OUT NOCOPY INTEGER, warning OUT INTEGER );它需要你传入blob_csid字符集 ID比如NLS_CHARSET_ID(ZHS16GBK)同时还要维护dest_offset、src_offset的偏移量并处理lang_ctx、warning这些参数。写起来啰嗦不说稍微处理不好就容易报ORA-22922不存在的 LOB 值这类错误。相比之下前面那个自己写循环的方式反而灵活因为你在每一段之间都可以插入UTL_RAW.CONVERT做字符集处理这是官方过程做不到的。所以我在实际项目中一直没有用CONVERTTOCLOB而是维护了一个通用的转换函数代码可读性和稳定性都更好。2.5 各种转换方法的横向对比方法适用场景限制推荐度UTL_RAW.CAST_TO_VARCHAR2(DBMS_LOB.SUBSTR(...))短文本内容 4000 字节以内超出缓冲区报错五颗星循环分段 DBMS_LOB.APPEND长文本需要转成 CLOB需要写 PL/SQL 代码四颗星UTL_RAW.CONVERT源字符集与目标字符集不一致要知道源字符集四颗星配上前两个更好DBMS_LOB.CONVERTTOCLOB官方转换过程参数繁琐字符集 ID 需单独获取两颗星TO_CHAR(BLOB)无会直接报错不支持隐式转换零颗星3. 三种典型项目场景BLOB 到底怎么正确处理3.1 场景一BLOB 里存的是 JSON 配置文本很多 Java 系统会把一份前端传来的 JSON 整体塞进 BLOB 字段这个字段本质上存的是文本只是为了省去改表结构的麻烦打了个“万能字段”。查询时我一般这样处理SELECT ID, UTL_RAW.CAST_TO_VARCHAR2( DBMS_LOB.SUBSTR(JSON_BLOB, 4000, 1) ) AS JSON_PART1, UTL_RAW.CAST_TO_VARCHAR2( DBMS_LOB.SUBSTR(JSON_BLOB, 4000, 4001) ) AS JSON_PART2 FROM CONFIG_TABLE WHERE CONFIG_KEY NOTIFY_RULE;如果 JSON 内容没超过 8000 字节两段就能读完。要是 JSON 里还带着中文就用前面提到的UTL_RAW.CONVERT组合。这种场景最忌讳的是在 PL/SQL 里把 JSON 当字符串做正则解析因为 BLOB 转出来的字符串长度会被 SQL 函数限制得很难受超过 4000 字符就用CLOB来承接再用DBMS_LOB.INSTR做子串查找。如果是给 Java 后端用更好的方案是让代码直接拿 BLOB 流然后转成StringBlob blob rs.getBlob(JSON_BLOB); byte[] bytes blob.getBytes(1, (int) blob.length()); String json new String(bytes, StandardCharsets.UTF_8);必须指定字符集不指定就用平台默认字符集Windows 下就是 GBK跑 Linux 就是 UTF-8同一段代码在两个环境结果不一样。这种低级的坑我在生产环境见过不止一次。3.2 场景二BLOB 里存的是图片图片转字符串不是靠CAST_TO_VARCHAR2硬转而是转成 Base64。Base64 是一种把二进制字节映射成可打印 ASCII 字符的编码方式非常适合把图片放进 JSON 接口传输。Oracle 的UTL_ENCODE.BASE64_ENCODE有一个 BLOB 参数的重载返回 CLOB 类型可以直接把整个 BLOB 一次转完SELECT UTL_ENCODE.BASE64_ENCODE(PIC_BLOB) AS BASE64_TXT FROM PRODUCT_TABLE WHERE PRODUCT_ID 1001;如果这张图比较小前端拿到 Base64 字符串后直接拼在img标签的src属性里就行data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA...但是要留意Base64 编码会让数据体积膨胀大约 33%。原始图片 3 MB转完 Base64 大约 4 MB这个字符串放进 SQL 的结果集再通过网络传输性能和内存压力都会增加。实际项目里小图徽标用 Base64 没问题产品原图最好还是返回文件流 URL让静态服务器去处理。另外注意UTL_ENCODE.BASE64_ENCODE返回的 CLOB 在 SQL Developer 里默认不会直接展示整个内容需要点进去看。如果你是通过 JDBC 获取rs.getString()读取 CLOB 列得到的 Base64 字符串是完整的不受工具显示限制。3.3 场景三BLOB 里存的是 ZIP/DOCX 压缩包像 DOCX、XLSX 这种 Office 新格式本质上是 ZIP 压缩包文件头是50 4B。这种 BLOB 内容转字符串没有任何业务意义你要做的是把它完整地导出成文件。最稳妥的方式是 Java 后端读取二进制流后写成文件try (InputStream is rs.getBinaryStream(ATTACH_BLOB); FileOutputStream fos new FileOutputStream(target.docx)) { byte[] buffer new byte[8192]; int len; while ((len is.read(buffer)) ! -1) { fos.write(buffer, 0, len); } }如果用 PL/SQL 导出可以用DBMS_LOB.FILEOPEN配合UTL_FILE但需要注意 Oracle 服务器上的目录权限。我处理过的实际案例中最常见的坑是DOCX 文件头确实是50 4B 03 04但有人误以为它是普通文本硬要转字符串结果拿出来的内容开头是PK加上一堆不可读的二进制。遇到这类 BLOB第一步永远是保存原文件不要试图在 SQL 层做内容解析。DOCX 内部是由 XML 和媒体文件组成的 ZIP 包要解析内容必须借助 Python 的python-docx库或者 Java 的 POISQL 这边只需要把二进制流完整导出就好。3.4 场景四序列化对象字节流还有一类 BLOB存储的是 Java 对象序列化之后的字节流特征是开头两个字节通常是AC ED。这种数据既不是文本也不是标准文件它是 Java 对象序列化协议的魔数。排查时看到AC ED 00 05基本可以断定是ObjectOutputStream写入的。对这种 BLOBSQL 层什么都不能做只能在 Java 端用ObjectInputStream反序列化。而且一旦原对象的类结构发生变化比如字段名改了、类名改了、序列化 UID 变了反序列化就会直接失败这是比 BLOB 本身更棘手的问题。4. 常见问题排查与避坑速查4.1 为什么转换出来的字符串总感觉被截断了如果你用DBMS_LOB.SUBSTR拿文本看到的内容始终只有一段先怀疑DBMS_OUTPUT.PUT_LINE的输出限制。DBMS_OUTPUT单行输出上限是 32767 字节客户端会话设置还可能把这个限制降到 10000 甚至更低。也就是说不是数据被截断了是控制台没显示完。排查方法用DBMS_LOB.GETLENGTH看 BLOB 的实际长度用DBMS_LOB.GETLENGTH(L_CLOB)看转换后的 CLOB 长度两个长度对上了数据就完整换一个能输出的客户端或把结果写入日志表再验证即可。还有一种是读取时SUBSTR的AMOUNT参数用了字符数而不是字节数。BLOB 是二进制DBMS_LOB.SUBSTR的第三个数值参数AMOUNT是以字节为单位的中文一个字符在 UTF-8 下占 3 字节如果按字符数去截必然把字符拦腰截断转出来就是乱码。这里统一用字节思维去写。4.2 常见异常与解决方案速查表现象可能原因解决方案查询显示十六进制字符串客户端默认展示二进制用UTL_RAW.CAST_TO_VARCHAR2转换后再查转换后所有中文是问号??源字符集与数据库字符集不匹配UTL_RAW.CONVERT先转换字符集再转字符串转换后出现å»Â风格字符字节是 UTF-8数据库按 Latin-1 或 GBK 解析明确源字符集用UTL_RAW.CONVERT调到AL32UTF8报ORA-06502字符缓冲区不足一次转换超过 4000 字节分段读取或者转成 CLOBTO_CHAR(blob)直接报错BLOB 不支持隐式转字符串改用DBMS_LOB.SUBSTRUTL_RAW.CAST_TO_VARCHAR2文件开头是FFD8FF转出来乱码是图片不是文本转 Base64 而不是字符串文件开头是ACEDJava 序列化对象用 ObjectInputStream 反序列化JDBC 获取字符串乱码代码没指定字符集new String(bytes, UTF-8)显式指定4.3 生产环境实操经验和禁忌先讲禁忌。不要在不知道 BLOB 内容类型的情况下把它当成字符串去 UPDATE。我见过有人因为 CLOB 字段放不下就改成UPDATE ... SET TEXT SUBSTR(MY_BLOB, 1, 1000)这种写法会强制进行隐式转换结果存进去全是问号毫无意义。改数据之前先做类型确认、长度确认、内容抽样三步。再讲一个实用技巧。在排查阶段不要把 BLOB 内容整个捞出来先用DBMS_LOB.SUBSTR取前 4 个字节做魔数分析。很多工具比如 PL/SQL Developer 在双击 BLOB 单元格时会把整个 LOB 加载到编辑区如果 BLOB 有 100 MB这一步就会卡死。正确的做法是只取需要的片段。最后建议项目里建一个独立的“简易 BLOB 查看函数”封装DBMS_LOB.SUBSTR、UTL_RAW.CAST_TO_VARCHAR2、UTL_RAW.CONVERT三层逻辑加一个文件头自动分类排查任何表都能直接调用。开发规范里也最好明确能不用 BLOB 存文本就不用文本用 CLOB文件流才用 BLOB。这个看似简单的建议能帮后来人省下无数个加班晚上。我在实际处理这类问题的体会是BLOB 显示为字符串这个现象表面上是一个函数用法的技术问题底层其实是数据库类型认知的问题。BLOB 就该按二进制的思路去对待不要因为“看着像字符串”就按字符串的逻辑操作。只要先把内容类型、字符集、大小三个参数确认到位转换方案基本就是顺理成章的事了。