ARTICLE DETAIL

资讯详情

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

SQL substring字符串截取函数详解:语法差异、数据清洗实战与避坑指南

SQL substring字符串截取函数详解:语法差异、数据清洗实战与避坑指南 数据团队有个写了三年的存储过程里面全是字段拼接和截取逻辑前阵子我接手维护第一眼看到SUBSTRING这个函数出现了两百多次。说实话刚看到的时候我有点头皮发麻但翻完那几百行 SQL 之后反而踏实了——因为那套逻辑虽然冗长却把字符串截取的用法写得非常全。从那之后我就觉得字符串截取这个函数值得好好聊一聊。substring是 SQL 中最常用的字符串截取函数名字不同数据库里叫法略有差异SUBSTRING、SUBSTR但核心功能都一样从一段字符串里按位置切出指定长度的内容。我做数据分析这几年从订单编号拆分、手机号脱敏、日志字段解析到 ETL 清洗几乎每个环节都会用上它。这篇内容不搞学院派那套我把常用语法、各数据库差异、实操案例和踩过的坑一次写清楚适合正在学 SQL 的入门者、做数据开发的后端同学以及写了多年 SQL 但没仔细研究过这个函数细节的老手。1. substring到底解决什么问题1.1 核心作用从字符串里精确切出一段如果你刚接触 SQL可能觉得截取字符串不就是剪一刀吗实际上它的价值远比想象中大。substring的本质可以理解成按坐标取子串——你告诉数据库从第几个字符开始给我截多长有的数据库还支持截到第几个字符。数据库就会返回你指定的那段内容。比如最常见的写法SELECT SUBSTRING(Hello World, 7, 5); -- 结果World这里7表示从字符串第 7 个字符开始5表示截取 5 个字符。这就是substring最核心的逻辑。注意别把它想成从下标 7 往后数 5 个——下标数字取决于数据库的计数规则这一点后面我会单独拿出来说因为它坑了不少人。1.2 典型业务场景不只是取个字符这么简单字符串截取看起来只是切一刀但在实际业务里它能解决很多看似棘手的数据问题字段格式化比如日期字段收到了20250414这种纯数字存储要展示成2025-04-14用SUBSTRING把年、月、日分别切出来再拼接几行 SQL 就搞定。订单编号解析很多系统的订单号带业务含义比如BJ20250414001表示北京、2025年4月14日、当天第001单拆分统计时就必须用截取函数把地区、日期、序号分别取出来。数据脱敏手机号中间四位打码、身份证出生日期提取、银行卡号尾号显示这些场景几乎绕不开字符串截取。日志清洗服务端日志里经常混着时间戳、请求路径、参数等结构化文本截取关键片段后才能做统计分析。还有更进阶的用法——和CHARINDEX、LOCATE、INSTR这类查找函数配合先定位某个分隔符的位置再截取指定区间的内容。这种组合用法在做复杂文本解析时非常强大比如从keyvalue格式的字符串里提取某个字段的值。后面我会用完整的案例带大家过一遍。2. 各数据库的 substring 差异对照2.1 MySQL / MariaDBSUBSTRING 与 SUBSTR 并存MySQL 里SUBSTRING和SUBSTR是同一个功能可以混用参数上支持两种写法-- 写法一从第3位开始截取到末尾 SELECT SUBSTRING(数据库开发, 3); -- 结果库开发 -- 写法二从第3位开始截取2个字符 SELECT SUBSTRING(数据库开发, 3, 2); -- 结果库开MySQL 还有几个变体值得注意-- 从右往左截取MySQL特有 SELECT SUBSTRING(abcdef, -3); -- 结果def -- 从第2位开始截往前数3位有点逆向取值的意思 SELECT SUBSTRING(abcdef, 2, -3); -- 结果bc第二种写法在日常开发中几乎不会用但面试题里出现过了解即可。MySQL 的字符串默认以字符为单位处理所以中文按CHARACTER计长度这点比某些数据库要友好。实际开发中MySQL 里我更常用LEFT、RIGHT和SUBSTRING_INDEX因为SUBSTRING需要精确定位索引而SUBSTRING_INDEX按分隔符切分更省事。比如-- 按点号切分IP取前三段 SELECT SUBSTRING_INDEX(192.168.1.100, ., 3); -- 结果192.168.12.2 SQL ServerSUBSTRING 的 1-based 下标SQL Server 的SUBSTRING语法和 MySQL 类似但有一个关键点索引从 1 开始其实大部分数据库的字符串位置计数都从1开始但总有例外。下面的例子很直观SELECT SUBSTRING(SQL Server 2025, 5, 6); -- 结果ServerSQL Server 中字符串位置计数从 1 开始所以上面是从第 5 个字符开始取 6 个字符。这里有一个容易被忽略的细节在 SQL Server 里如果起始位置加上长度超出了字符串总长度它不会报错而是返回从起始位置到末尾的内容如果起始位置本身大于字符串长度则返回空字符串而不是 NULL。这个行为和 MySQL 略有差异做数据迁移时特别容易踩坑我给一个对比表场景MySQL 行为SQL Server 行为SUBSTRING(abc, 5, 2)返回空字符串返回空字符串SUBSTRING(abc, 2, 10)返回bc返回bcSUBSTRING(abc, 0, 2)MySQL 中 0 被视为 1返回abSQL Server 中报错SUBSTRING(abc, -1, 2)MySQL 从右往左数返回bcSQL Server 报错你看同样的写法在不同的数据库里表现差别很大。如果你负责跨数据库迁移这类细节必须提前摸清楚否则线上跑挂了再排查代价就不是改一行 SQL 那么简单。SQL Server 还有一个细节字符串函数通常搭配LEN计算长度但LEN会忽略尾部空格这在截取尾部文本时会出问题。如果需要精确到字节长度要用DATALENGTH。我自己就踩过一次——某张表的备注字段以空格结尾用LEN判断截取位置结果数据错位了。2.3 PostgreSQL函数重载与 POSITION 配合PostgreSQL 的substring写得比较标准有标准的 SQL 写法也有扩展写法-- 标准 SQL 写法 SELECT substring(PostgreSQL from 7 for 4); -- 结果SQL -- 常见写法和 MySQL 类似 SELECT substring(PostgreSQL, 7, 4); -- 结果SQL -- 正则截取PostgreSQL特有 SELECT substring(abc123def, from [0-9]); -- 结果123注意最后这种正则截取是 PostgreSQL 的特色可以在截取的同时做模式匹配效率有时比先regexp_matches再取数组下标更高。和 PostgreSQL 配合时我通常用POSITION定位分隔符-- 提取邮箱地址的用户名部分 SELECT substring(userexample.com from 1 for position( in userexample.com) - 1); -- 结果user不过要注意PostgreSQL 中position返回的是 1-based 的索引这跟substring的起始位置规则是一套的配合起来不用调整偏移。相比之下MySQL 的LOCATE和 SQL Server 的CHARINDEX返回的也是 1-based 索引所以通用思路都一样。2.4 Oracle / DMSUBSTR 的特殊参数含义Oracle 以及国内常用的达梦DM数据库用的是SUBSTR而不是SUBSTRING而且它有一个完全不同的特性起始位置可以传负数。负数表示从字符串末尾往前数。SELECT SUBSTR(Hello World, -5, 3) FROM DUAL; -- 结果Wor上面表示从末尾往前数第 5 个字符就是 W开始取 3 个字符。这在从右往左解析时很好用不用先算LENGTH再减。Oracle 的高效截取还可以写-- 从第2个字符到第5个字符 SELECT SUBSTR(abcdef, 2, 4) FROM DUAL; -- 结果bcde如果第二个参数不传表示从起始位置截到末尾SELECT SUBSTR(abcdef, 3) FROM DUAL; -- 结果cdef还有一个容易忘记的细节Oracle 的SUBSTR对 NULL 的处理与多数数据库一致——传入 NULL 返回 NULL。但在DUAL表中测试时如果直接传 NULL 字面量会有隐式转换建议用CAST(NULL AS VARCHAR2(10))来测避免类型默认长度干扰测试结果。达梦数据库的SUBSTR与 Oracle 几乎一致毕竟达梦在设计时兼容了 Oracle 的语法体系。不过达梦的部分版本中截取函数的第三个参数长度如果传 0行为可能与 Oracle 有细微差别建议实测确认。3. 实操案例用 substring 完成日常数据清洗3.1 场景一从订单编号中提取业务标识我现在还留着一个老代码的截图——那时我们订单编号规则是区域编码(2位) 日期(8位) 流水号(6位)比如BJ20250414000001。运营要按区域分析订单量当时组里有人用LIKE BJ%硬匹配后来区域从 2 个扩到 4 个字母了那哥们儿直接傻眼。正确的解法是用SUBSTRING把区域编码、日期、流水号拆开SELECT order_no, SUBSTRING(order_no, 1, 2) AS region_code, SUBSTRING(order_no, 3, 8) AS order_date, SUBSTRING(order_no, 11, 6) AS sequence_no FROM orders WHERE order_date 2025-01-01;如果区域编码扩到 4 位只需把截取位置整体后移 2 位就行完全不用改逻辑。这就是规范截取的好处把字段结构显式化比写死匹配规则健壮得多。如果编号规则是固定前缀 可变长度还可以用CHARINDEX定位分隔符-- 假设格式REGION-20250414-000001 SELECT order_no, SUBSTRING(order_no, 1, CHARINDEX(-, order_no) - 1) AS region_part FROM orders;这里的思路是先找到第一个-的位置再截取它前面的部分。这种写法不受前缀长度影响只要分隔符固定就行。3.2 场景二手机号/身份证字段脱敏数据安全法实施后很多公司要求对敏感字段做脱敏。手机号中间四位打码是基础需求用SUBSTRING配合字符串拼接就能实现-- MySQL 写法 SELECT phone, CONCAT(SUBSTRING(phone, 1, 3), ****, SUBSTRING(phone, 8, 4)) AS masked_phone FROM users;如果是 SQL Server拼接方式略有差异但逻辑类似SELECT phone, SUBSTRING(phone, 1, 3) **** SUBSTRING(phone, 8, 4) AS masked_phone FROM users;身份证号的出生日期提取也是常见需求-- 假设身份证号110101199001011234 SELECT id_card, SUBSTRING(id_card, 7, 8) AS birth_date, CASE WHEN SUBSTRING(id_card, 17, 1) IN (1,3,5,7,9) THEN 男 ELSE 女 END AS gender FROM citizens;这里有个细节值得提醒身份证号长度是 18 位但早期数据里可能有 15 位旧号。直接按 18 位截取15 位号码会错乱。稳妥的做法是先根据长度分支处理SELECT id_card, CASE WHEN LENGTH(id_card) 18 THEN SUBSTRING(id_card, 7, 8) WHEN LENGTH(id_card) 15 THEN CONCAT(19, SUBSTRING(id_card, 7, 6)) ELSE NULL END AS birth_date FROM citizens;这种先判长度再截取的思路在做数据清洗时非常重要——脏数据的处理优先级永远要高于功能本身的实现。3.3 场景三嵌套截取搞定 JSON 片段有些系统在小字段里存了简化 JSON比如商品表里spec_info字段存了{color:red,size:L}。严格来说这种设计不符合数据库规范化但现实里确实存在。用嵌套截取提取某个 key 的值可以这样写-- MySQL 提取 color 字段的值 SELECT spec_info, SUBSTRING( spec_info, LOCATE(color:, spec_info) 9, LOCATE(, spec_info, LOCATE(color:, spec_info) 9) - LOCATE(color:, spec_info) - 9 ) AS color FROM products;这段逻辑看着复杂拆开理解就是用LOCATE找到color:的位置加上 9 个字符得到值内容的起始位置因为color:正好 9 个字符从值内容的起始位置往后找下一个引号得到值结束位置两者相减得到长度。如果嫌可读性差也可以分步写用子查询或 CTE 一步步算WITH base AS ( SELECT spec_info, LOCATE(color:, spec_info) AS start_marker FROM products ), positioned AS ( SELECT spec_info, start_marker 9 AS value_start, LOCATE(, spec_info, start_marker 9) AS value_end FROM base ) SELECT spec_info, SUBSTRING(spec_info, value_start, value_end - value_start) AS color FROM positioned;多步骤拆解虽然代码看起来长一些但可读性和可维护性都显著提升排查问题也容易定位。这个案例说明一个核心思路当截取条件不固定时先定位再截取永远比硬编码位置更可靠。4. substring 最容易踩的坑4.1 索引从 0 还是从 1不同数据库默认不一致这个坑是我见过最多的。很多从 Java 转 SQL 的人第一反应是字符串索引从 0 开始因为 Java 的substring就是从 0 开始的。但 SQL 里绝大多数数据库的字符串位置是从 1 开始的包括 MySQL、SQL Server、PostgreSQL。更烦人的是MySQL 里有LEFT(str, n)和RIGHT(str, n)它们不受起始位置影响直接用起来反而不会犯错。但如果数据库是 OracleSUBSTR还提供了 0 位置的兼容处理——SUBSTR(abc, 0, 2)和SUBSTR(abc, 1, 2)结果都是ab官方文档里写的是0 被当作 1 处理。这就更增加了困惑。我的建议是在代码里显式写出注释标注索引从 1 开始第 1 个字符位置为 1尤其是团队协作的项目里这行注释能省掉不少沟通成本。4.2 长度参数超出字符串长度时不同数据库表现不同前面提到过起始位置加上长度超出总长度时大多数数据库会返回从起始位置到末尾的内容这符合直觉。但这里有个容易被忽略的场景SELECT SUBSTRING(abcd, 2, 100);MySQL 返回bcdSQL Server 也返回bcdPostgreSQL 还是bcdOracle 的SUBSTR同样是bcd。这说明多数主流数据库对超长截取的处理是一致的——不报错截到末尾为止。但如果你把长度参数写成0或负数就有差异了数据库SUBSTR(abc, 2, 0)结果SUBSTR(abc, 2, -1)结果MySQL空字符串空字符串部分版本会告警SQL Server报错参数长度不能为负数报错Oracle空字符串等价于 NULL返回ab负数表示往前截PostgreSQL空字符串空字符串Oracle 的SUBSTR负数长度表示从起始位置往前数这和其他数据库完全不同。做跨数据库兼容时这里几乎必踩。稳妥的写法是先判断长度是否为正数再执行截取。CASE WHEN LENGTH(str) start_pos THEN SUBSTRING(str, start_pos, length) ELSE NULL END4.3 SUBSTRING 与 LEFT / RIGHT 混用时思路不统一LEFT和RIGHT是从字符串两端截取SUBSTRING是从中间任意位置截取。很多同事写代码时三种函数混用结果逻辑混乱。我一般建议团队定一个规范从开头取固定长度用LEFT语义更清晰。从末尾取固定长度用RIGHT不用算起始位置。从中间取用SUBSTRING必须明确起始位和长度。举个例子从身份证号取出生日期中间 8 位用SUBSTRING(id_card, 7, 8)最直观但也可以写成LEFT(RIGHT(id_card, 12), 8)——能跑但阅读时要在脑子里绕一圈完全没有必要。当然了有些数据库还提供了SUBSTRING_INDEX这种特殊函数它是按分隔符截取而不是按位置截取和SUBSTRING定位逻辑不同不要混在一起记忆。MySQL 的SUBSTRING_INDEX(a,b,c, ,, 2)返回a,b这是按分隔符计数而非按字符位置计数两者解决的问题域不同。4.4 中文和多字节字符的截取问题字符串截取在中文场景下有个老大难问题字符集和字节数。大部分数据库的SUBSTRING默认按字符character处理这意味着SUBSTRING(数据库开发, 1, 2)返回数据两个汉字没问题。但有个特殊情况如果你的数据库或表使用的是GBK 编码且函数操作的是字节byte那么截取结果就可能出现半个汉字的情况——显示为这样的乱码。SQL Server 有个函数SUBSTRING是按字符处理的但当列类型是VARCHAR且排序规则是中文二进制Chinese_PRC_BIN时某些版本会按字节计算稍有疏忽结果就乱了。我的解决经验是判断数据库的字符函数语义是按字符还是按字节。MySQL 的SUBSTRING按字符SQL Server 的VARCHAR涉及非 Unicode 数据时要注意排序规则Oracle 的SUBSTR按字符但SUBSTRB按字节。如果按字节处理先考虑转成 Unicode 类型比如 SQL Server 的NVARCHAR、Oracle 的NVARCHAR2转完之后按字符截取就安全了。实在绕不开字节时先计算字节长度保证截取边界落在完整字符边界上。这里给个极端案例一段文本同时包含数字、ASCII 字符和中文需要截取 10 个显示位。如果按字节截会乱码如果按字符截宽度又不等。实际场景里我遇到过最终方案是用CHAR_LENGTH先把中文字符算成 2 个宽度再通过辅助函数逐字累加判断边界。你放心绝大多数业务不会走到这一步但真遇到了你知道有解决的路径就行。5. 进阶substring 的复杂组合技巧5.1 与定位函数配合先找位置再截内容上一部分提到了先定位再截取这里展开讲。三类定位函数在不同数据库里叫法不同数据库函数名返回含义MySQLLOCATE(substr, str)/INSTR(str, substr)子串首次出现的位置从 1 开始SQL ServerCHARINDEX(substr, str)子串首次出现的位置从 1 开始PostgreSQLPOSITION(substr IN str)子串首次出现的位置从 1 开始OracleINSTR(str, substr)子串首次出现的位置从 1 开始这四个函数做的事几乎一样只是参数顺序不同。我的记法MySQL 的LOCATE把要查的子串放前面Oracle 的INSTR把原串放前面SQL Server 的CHARINDEX和 MySQL 的LOCATE参数顺序一致。写跨库脚本时这顺序能节约不少翻文档的时间。配合截取时最经典的就是按分隔符切分-- MySQL提取邮箱的域名部分 SELECT SUBSTRING(userexample.com, LOCATE(, userexample.com) 1); -- 结果example.com -- SQL Server同样的方式 SELECT SUBSTRING(userexample.com, CHARINDEX(, userexample.com) 1, LEN(userexample.com));注意 SQL Server 必须提供长度参数所以要用LEN兜底这一点和 MySQL/Oracle 不同。这类差异是很烦人的我一般建议在团队里维护一份各数据库字符串函数对照表能省掉大量踩坑时间。如果字符串里分隔符出现多次还可以用嵌套定位-- 提取第二个分隔符和第三个分隔符之间的内容 -- 假设路径/api/v1/orders/2025 SELECT SUBSTRING( /api/v1/orders/2025, LOCATE(/, /api/v1/orders/2025, LOCATE(/, /api/v1/orders/2025) 1) 1, LOCATE(/, /api/v1/orders/2025, LOCATE(/, /api/v1/orders/2025, LOCATE(/, /api/v1/orders/2025) 1) 1) - LOCATE(/, /api/v1/orders/2025, LOCATE(/, /api/v1/orders/2025) 1) - 1 );看完直接自闭有没有这种嵌套可读性太差实践里我绝不这样写而是拆成多步子查询。上面的例子纯属展示如果硬写会怎样实际别学。5.2 与 CASE 结合做逻辑判断字符串截取常常不是孤立操作而是配合条件判断实现业务规则。比如订单号里带了支付渠道标识不同渠道的统计口径不一样SELECT order_no, SUBSTRING(order_no, 1, 2) AS channel_code, CASE SUBSTRING(order_no, 1, 2) WHEN WX THEN 微信支付 WHEN AL THEN 支付宝 WHEN YL THEN 银联 ELSE 其他 END AS channel_name FROM orders;CASE也可以是范围判断-- 判断订单号中日期部分是否在促销期间 SELECT order_no, CASE WHEN SUBSTRING(order_no, 3, 8) BETWEEN 20250401 AND 20250415 THEN 大促订单 ELSE 普通订单 END AS order_type FROM orders;这时候SUBSTRING返回的其实是字符串和数字/日期比较前要注意隐式转换。我一般喜欢在比较时强制类型避免数据库自己玩转换把索引弄失效-- 推荐写法显式转成日期 CASE WHEN CAST(SUBSTRING(order_no, 3, 8) AS DATE) BETWEEN DATE 2025-04-01 AND DATE 2025-04-15 THEN 大促订单 END5.3 在 ETL 与报表中的正确使用姿势ETL 场景下的字符串截取往往是批量执行的最容易出问题的不是单条函数本身而是大量数据中的边界情况。比如你按固定位置截取某个字段99.9% 的数据是合规的但总有那么几条数据短一截或长一截。这时候不管 SQL 怎么写都会出现错位。我的处理顺序是先抽样查看数据分布用MIN(LENGTH(field))、MAX(LENGTH(field))看看字段长度的范围。对不合规数据单独标记不要直接让截取函数处理先建一张异常表把长度不在预期范围的数据捞出来。再针对正常数据做截取异常数据等人工确认。这个流程能帮你避免清洗完才发现数据错了但已经覆盖了原表这种灾难。我给团队定的规矩是任何涉及字符串截取的批量更新必须先做一次自查查询再执行 UPDATE。自查查询就是SELECT COUNT(*)看边界数据量不为 0 就停下来研究。在报表场景中字符串截取通常用于生成维度字段比如把时间戳字段按小时截取、把 URL 按路径分段。这里有一个提效的小技巧如果同一份报表每天跑且截取逻辑完全固定可以考虑把截取结果在写入时就生成好做成独立字段避免查询时反复计算。存储空间换查询性能在报表场景里多数时候是划算的。6. 写在维护 SQL 这些年后的一点体会用了这么多年 SQLsubstring给我的感觉像一把精密的手术刀——它本身很小但配合定位函数、条件判断和长度计算能解决大量实际业务问题。它不像JOIN、窗口函数那样自带光环但恰恰是这种基础函数的细节差异在关键时刻能卡住一整条数据链路。我自己在项目里总结了一套很实用的习惯把每个截取逻辑写成函数而不是在存储过程里到处硬编码截取位置。比如订单编号的解析逻辑我习惯先把截取规则单独抽出来用变量保存起始位置和长度再统一调用SUBSTRING。这样业务规则调整时只改变量赋值不动核心逻辑。还有个习惯是写注释。字符串截取最怕别人接手时看不懂位置参数怎么来的。我一般是紧跟一行注释-- 截取区域编码业务规则前2位表示大区编码 SUBSTRING(order_no, 1, 2) AS region_code别小看这条注释关键时刻连调三天查不出 bug最后发现是边界情况没处理而注释里写的规则正好帮你快速对回去排查效率能高不少。最后分享一个特别实用的调试技巧。如果截取结果不符合预期先别急着改 SQL把中间结果拆开看-- 调试模式先看每个参数算出来的值 SELECT field, CHARINDEX(-, field) AS dash_pos, LEN(field) AS field_len, SUBSTRING(field, 1, CHARINDEX(-, field) - 1) AS left_part FROM raw_data;先确认dash_pos、field_len这些参数在不同行数据里的值是否符合预期再去看SUBSTRING的结果。很多时候问题不在截取函数本身而是定位函数返回了你没预期到的位置值。这种参数先于结果排查的思路能帮你从源头定位问题而不是盯着结果瞎猜。
返回列表