一次排序失效引发的思考:数值排序 vs 文本排序,以及列名相同的隐患
本文记录一次实际开发中遇到的排序问题,从"排序乱序"出发,逐步排查到字符串拼接排序的不可靠性,并给出更稳定的数值排序方案。
同时附带一个容易被忽视的坑:UNION ALL 后列名相同导致前端合并行。
一、问题背景
有一张员工费用汇总报表,数据来自费用明细表,需要按部门展示:
-
部门合计行:该部门所有费用合计(含员工明细 + 部门公共费用)
-
员工明细行:每个员工的费用明细,按金额降序排列
-
部门费用行:员工为空(公共费用)的合计,单独一行
排序要求:部门合计 → 员工明细(金额降序)→ 部门费用。
在增加部门费用的行之前,排序正常
此时永洪前端排序选的是无序
sql
ORDER BY
dept_name,
sort_order, -- 部门(1) 在员工(2) 之前
CASE WHEN sort_order = 1 THEN grand END DESC, -- 部门行之间:金额降序
CASE WHEN sort_order = 2 THEN grand END DESC, -- 员工行之间:金额降序
yuanggh;
增加部门费用后
sql
-- ★ 部门的费用行:sort_order = 3(只统计员工为空的费用,排在最后)
empty_emp_rows AS (
SELECT
bum AS dept_name,
'部门的费用' AS yuanggh,
NULL AS yuangxm,
SUM(zc_zf) AS zc_zf,
SUM(zc_jp) AS zc_jp,
SUM(zc_jw) AS zc_jw,
SUM(zc_qtjt) AS zc_qtjt,
SUM(zc_ydfw) AS zc_ydfw,
SUM(zc_zs) AS zc_zs,
SUM(zc_total) AS zc_total,
SUM(yw_zf) AS yw_zf,
SUM(yw_total) AS yw_total,
SUM(grand) AS grand,
3 AS sort_order
FROM emp
WHERE (yuanggh IS NULL OR yuanggh = '')
GROUP BY bum
HAVING SUM(grand) != 0
)
排序失效
sql
ORDER BY
dept_name,
sort_order,
CASE WHEN sort_order IN (1, 3) THEN grand END DESC, -- 部门合计 / 部门的费用:金额降序
CASE WHEN sort_order = 2 THEN grand END DESC, -- 员工明细:金额降序
yuanggh;
ORDER BY的排序逻辑与优先级:多级排序的基本规则SQL 的
ORDER BY是逐级生效的:
- 先按第 1 级排 ,只有在第 1 级相等的行,才看第 2 级;第 2 级也相等的,才看第 3 级......以此类推。
有 5 级排序键:
sqlORDER BY dept_name, -- 第 1 级 sort_order, -- 第 2 级 CASE WHEN sort_order IN (1, 3) THEN grand END DESC, -- 第 3 级 CASE WHEN sort_order = 2 THEN grand END DESC, -- 第 4 级 yuanggh; -- 第 5 级第 1 级:
dept_name(部门名)先按部门名把所有行分组。同一个部门的行聚在一起,不同部门之间按部门名升序(默认 ASC)。
第 2 级:
sort_order同一个部门内,按
sort_order升序:
1= 部门合计 → 排最前
2= 员工明细 → 排中间
3= 部门费用 → 排最后到这一步,三类行已经分开了。
第 3 级:
CASE WHEN sort_order IN (1, 3) THEN grand END DESC这一级只对
sort_order = 1或3的行有值,其余行为 NULL。
对
sort_order = 1或3的行:按grand降序对
sort_order = 2的行:CASE返回 NULL,对排序没有贡献因为第 2 级已经把它们分开了,所以这一级只在"部门合计之间"和"部门费用之间"内部比较。同一个部门如果有多行合计(一般不会),或者有多行费用(一般不会),按金额降序。
第 4 级:
CASE WHEN sort_order = 2 THEN grand END DESC这一级只对
sort_order = 2的员工行有值,其余行为 NULL。
员工明细内部 :按
grand降序,金额大的排前面对合计/费用行:NULL,无影响
第 5 级:
yuanggh同一类内,如果金额也相同,按工号升序排。作为"终极裁决",避免同一金额下顺序不稳定。
排序过程:
第 1 级:按 dept_name,财务部 → 销售部
第 2 级:财务部内按 sort_order,1 → 2 → 2 → 3;销售部内 1 → 2
第 3 级 :财务部合计(1)与费用(3)不比较(已被第 2 级分开);员工(2)在第 3 级都是 NULL
第 4 级:财务部员工内按 grand 降序:2000 → 1000;销售部只有一行员工
第 5 级:金额相同时按工号
Oracle 默认:升序时 NULL 排最后,降序时 NULL 排最前。
乱序现象用数值化描述
| 行 | sort_order |
期望位置 |
|---|---|---|
| 部门合计 | 1 | 第一行 |
| 员工明细 | 2 | 中间 |
| 部门费用 | 3 | 最后一行 |
你实际看到的是:部门费用(sort_order=3)在第一行,部门合计(sort_order=1)在最后。
这个顺序,恰好是 sort_order 降序(3 → 2 → 1)。
交换
sort_order 后排序依旧没变,此时意识到永洪的无序机制,并不是完全按照SQL脚本的输出顺序来。
永洪的"无序"= 忽略数据集返回顺序,按它自己认为的规则重新排。
至于按什么排,需要观察。但既然交换
sort_order都不变,说明sort_order这一列根本没被永洪用作排序键。它一定是按别的字段或默认规则在排。
永洪"无序"到底按什么排
常见几种可能,从大到小:
可能性 说明 特征 按第一个维度列升序/降序 自由表默认把第一个维度列当排序键 换 sort_order没反应,换第一列就变按所有维度列逐级排 dept_name → yuanggh → ...整体按维度列字典序,不受 sort_order影响按第一个数值列降序 有些版本默认按第一个 measure 降序 换数据会变 按数据集物理顺序 实际上就是缓存里的行序 极少数版本,且刷新缓存会变
经验教训:永远不要依赖永洪的无序,永远使用自定义的数值列或业务要求的数值列来排序。
关键原因分析:
永洪端"无序"并不意味着"按 SQL 顺序"。永洪作为一个 BI 工具,有自己的数据缓存、分组、聚合逻辑。即使数据集 SQL 里有 ORDER BY,永洪在渲染自由表时,可能重新按维度字段排序,或按数据到达顺序展示,或按某种默认规则。
ORDER BY 里的 CASE WHEN 表达式比较"隐晦" 。如果是列名的话,比如
sort_order DESC,永洪可能能识别;但 CASE WHEN 表达式作为一个计算列,永洪端可能不"识别"它作为排序依据,导致排序被忽略。永洪的"无序"设置实际是按数据集返回的物理顺序。但如果数据被缓存、被重新组织过,物理顺序可能变了。
所以问题的关键可能不是 SQL 的 ORDER BY 写错了,而是永洪前端是否真正尊重 SQL 的 ORDER BY。
最终结论
不是 SQL 的 ORDER BY 写错了 。无论写
CASE WHEN sort_order IN (1, 3)还是CASE WHEN sort_order = 1,SQL 端结果都是对的。是永洪自由表的排序配置在起作用
加部门费用行前看起来正常是巧合(只有 2 类时现象不明显),加之后现象更明显。
修复方案:永洪端排序配置里,明确指定排序列。
最开始以为是脚本排序写错了没生效,所以后面更换了排序方式。
之前的排序方式即使是正确的,永洪前端也没办法指定具体的排序列。
永洪前端能指定的"排序列",必须是 SELECT 出来的字段。 而 SQL 的
ORDER BY是"结果集内部排序",它不产生新的字段,只影响返回行的顺序。
两者的本质区别
SQL 的 ORDER BY 永洪前端的排序列 依赖对象 查询里用到的字段 + 表达式 必须来自 SELECT 输出的列 排序逻辑 支持多级、CASE WHEN、NULLS LAST 只支持按列升/降,不支持表达式 生效范围 数据集返回时 前端渲染时 能否覆盖对方 会被前端覆盖 优先于 SQL 顺序
采用字符串拼接 生成 order_key,再用 ORDER BY order_key 排序。但在永洪报表中,员工明细行之间的金额降序乱序了。
sql
COALESCE(dept_name, '~') || '|' ||
CAST(sort_order AS TEXT) || '|' ||
LPAD(CAST(9999999999 - COALESCE(grand, 0) AS TEXT), 15, '0') || '|' ||
COALESCE(yuanggh, '') AS order_key
二、原因排查:字符串拼接排序为什么不可靠
其中 grand 是金额(NUMERIC),想通过 9999999999 - grand 取反,再用 LPAD 补齐 15 位,让金额大的字符串排在前面。
排序问题定位
order_key里对grand做降序时,用的表达式是:sql
LPAD(CAST(9999999999 - COALESCE(grand, 0) AS TEXT), 15, '0')问题在于:
grand是小数,CAST AS TEXT 后小数位数不一致。举例:
grand 9999999999 - grand转字符串 LPAD(15) 后 100 9999999899 9999999899(10 位)0000099999998991234.56 9999998765.44 9999998765.44(13 位)009999998765.44按字符比较:
000009999999899vs009999998765.44第 1 位
0=0,第 2 位0=0,第 3 位0<9→ grand=100 的行被排在了前面,而正确的顺序应该是 1234.56 排在前面。这就是"乱序"的根因。
修复:用固定宽度的字符串编码
把金额 乘 100 转成整数,再 LPAD,就能消除小数位数不一致的干扰:
sql
LPAD(CAST(CAST((999999999999 - ROUND(COALESCE(grand, 0), 2)) * 100 AS BIGINT) AS TEXT), 17, '0')
sql
-- ★ 修复 order_key:金额乘 100 转成整数 + LPAD,消除小数位数不一致
COALESCE(dept_name, '~') || '|' ||
CAST(sort_order AS TEXT) || '|' ||
LPAD(CAST(CAST((999999999999 - ROUND(COALESCE(grand, 0), 2)) * 100 AS BIGINT) AS TEXT), 17, '0') || '|' ||
COALESCE(yuanggh, '') AS order_key
2.1 核心问题:文本比较 ≠ 数值比较
数据库里,ORDER BY 一个字符串列时,是按字符逐位比较(字典序),而不是按数值大小。举例:
| 数值比较 | 文本比较(字符串) |
|---|---|
| 100 > 99 | "100" < "99"('1' < '9') |
| 1234.5 > 999.9 | "1234.5" < "999.9" |
| 10 = 10 | "0010" < "10"(长度不同) |
2.2 具体触雷:小数导致 LPAD 失效
grand 是小数(如 1234.56、100),CAST(... AS TEXT) 后字符串长度不一样:
| grand | 9999999999 - grand |
CAST AS TEXT | LPAD(15) 后 |
|---|---|---|---|
| 100 | 9999999899 | 9999999899(10 位) |
000009999999899 |
| 1234.56 | 9999998765.44 | 9999998765.44(13 位) |
009999998765.44 |
| -5000 | 10000009999 | 10000009999(11 位) |
00010000009999 |
长度不同,逐位比较时就乱了。原本应该是 1234.56 排在 100 前面,结果 100 反而排在前面。
2.3 其他潜在风险
-
负号 :
"-100"和"10"按字符串比较,结果可能不符合预期。 -
千分位逗号 :
"1,000.5"和"999.9"排序会完全相反。 -
NULL :
NULL || 'x'结果为NULL,整个order_key变 NULL,排序位置不可控。 -
全角/半角、空格:看似相同的字符串,排序时可能错位。
-
跨数据库:不同数据库的数字转字符串格式、排序规则(Collation)可能不同。
因此,只要排序键里包含数值,就应该用数值排序,而不是字符串拼接。
三、两种排序方式对比
3.1 文本排序(字符串拼接)
做法 :把多个字段拼接成一个字符串,ORDER BY 这个字符串。
适用场景:
-
排序键本身就是文本,且格式固定,例如日期字符串
'2026-09-24'、编码'A001'、'A002'。 -
不需要考虑数值大小,只需要按字典序。
风险:
-
一旦包含数值,必须精细处理补零、小数、负号、NULL,极易出错。
-
跨库兼容性差。
示例(不推荐用于数值):
sql
ORDER BY dept_name || '|' || sort_order || '|' || LPAD(grand, 15, '0')
3.2 数值排序(ROW_NUMBER() 窗口函数)
sql
ROW_NUMBER() OVER (
ORDER BY
dept_name,
sort_order,
CASE WHEN sort_order IN (1, 3) THEN grand END DESC NULLS LAST,
CASE WHEN sort_order = 2 THEN grand END DESC NULLS LAST,
yuanggh
) AS sort_rank
ROW_NUMBER()把完整的、复杂的排序逻辑压缩成一个整数 ,落到一个真实的输出列sort_rank上。
永洪前端只需要"按
sort_rank升序"复杂逻辑由 SQL 完成
两边各司其职,不会冲突
做法 :用 ROW_NUMBER() OVER (ORDER BY ...) 生成一个整数排名,再按这个整数排序。排序键可以是多个字段,且允许数值直接参与比较。
优点:
-
数据库按字段的真实类型比较,数值就是数值,文本就是文本。
-
支持
NULLS LAST/NULLS FIRST显式控制 NULL 位置。 -
不需要考虑补零、小数位数、负号。
-
跨库稳定(Oracle、PostgreSQL、MySQL 8+ 等都支持窗口函数)。
-
可读性好,业务逻辑一目了然。
这里用两个 CASE WHEN 把不同类别的行分开排序:部门合计/部门费用按金额降序,员工明细也按金额降序,互不干扰。
执行效果(示例):
| sort_rank | dept_name | yuanggh | grand |
|---|---|---|---|
| 1 | 财务部 | 财务部 合计 | 3500 |
| 2 | 财务部 | 002 | 2000 |
| 3 | 财务部 | 001 | 1000 |
| 4 | 财务部 | 财务部 费用 | 500 |
| 5 | 销售部 | 销售部 合计 | 8000 |
| 6 | 销售部 | 005 | 8000 |
排序完全由整数 sort_rank 决定,稳定可靠。
假设原始数据如下(4 个 CTE 合并后的行):
dept_name yuanggh sort_order grand 财务部 财务部 合计 1 3500 财务部 002 2 2000 财务部 001 2 1000 财务部 财务部 费用 3 500 销售部 销售部 合计 1 8000 销售部 005 2 8000
ROW_NUMBER() OVER (ORDER BY ...)会先按里面的 ORDER BY 把这 6 行排好,再给每行打一个 1、2、3...的序号 ,落到sort_rank列:
sort_rank dept_name yuanggh sort_order grand 1 财务部 财务部 合计 1 3500 2 财务部 002 2 2000 3 财务部 001 2 1000 4 财务部 财务部 费用 3 500 5 销售部 销售部 合计 1 8000 6 销售部 005 2 8000 最终结果集里多了一列
sort_rank,它的值就是"这行应该排第几"。
这个序号是怎么算出来的
ROW_NUMBER() OVER (ORDER BY ...)里的ORDER BY就是前面讲过的多级排序,只是被"包"进了窗口函数里。数据库做的事:
拿整个结果集(6 行)
按
ORDER BY的规则排序(就是之前那 5 级排序)给排好序的每行依次编号 1、2、3...
所以
sort_rank的本质就是把"排好的顺序"固化成一个整数。
为什么它能解决永洪的问题
永洪前端只能按"输出列"排序,不能按表达式排。
不用
sort_rank:SQL 排序逻辑里的CASE WHEN...在永洪里"看不见",永洪只能按dept_name、sort_order这类普通列排,"员工内按金额降序"的逻辑丢失。用
sort_rank:SQL 里已经把完整排序算好,输出成一个整数列。永洪前端只配置"按sort_rank升序",完整复刻 SQL 的排序。
四、列值相同的隐患:UNION ALL 后的意外合并
在同一张报表中,还遇到了另一个坑:不同语义的行使用了相同的列值,导致前端把它们合并成一行。
4.1 场景还原
dept_rows(部门合计)和 empty_emp_rows(部门费用)两部分的定义中,dept_name 都是 bum,yuanggh 也都被设置为 bum(或 COALESCE(?{业务单位}, '部门合计'))。当参数选中某个部门时,两行的 (dept_name, yuanggh) 完全相同,例如:
| dept_name | yuanggh |
|---|---|
| 财务部 | 财务部 |
| 财务部 | 财务部 |
在永洪自由表中,这两行会被识别为同一维度组合,从而合并成一行,导致"部门合计"和"部门费用"只显示了一个。
正确的说法
之前我说"UNION ALL 后列名相同导致合并",准确表述应该是:
两行在 SELECT 输出后,所有维度列(
dept_name、yuanggh等)的值完全相同,前端渲染时把它们当作同一维度组合,只显示一行。
关键词 是"值相同 ",不是"列名相同"
UNION ALL 只是"把多行拼在一起"的操作,本身不导致合并
总结
说法 准确性 列名相同导致合并 ❌ 不准确 GROUP BY 导致 SQL 层聚合 ✅ 正确,但不是这次合并的原因 两行维度列值相同导致前端合并 ✅ 这就是原因 UNION ALL 导致合并 ❌ UNION ALL 只是拼接 结论 :当前脚本已经把两行的
yuanggh改成不同值('财务部 合计'和'财务部 费用'),就不会再合并了。如果你现在还有合并现象,请确认一下两行的yuanggh输出值是否已经不同------如果相同,那才是问题所在。"前端合并"的条件是
不是"列名相同",而是"两行的所有维度列(= 展示时的分组列)的值完全相同"。
列名:
dept_name和yuanggh,前端本来就知道它们是"维度列"。决定合并的是值:值相同 → 合并;值不同 → 分开。
4.2 解决方案
给不同语义的行不同的标签,或者增加一个区分字段。例如:
sql
sql
-- 部门合计行
bum AS dept_name,
bum || ' 合计' AS yuanggh
-- 部门费用行
bum AS dept_name,
bum || ' 费用' AS yuanggh
这样 (dept_name, yuanggh) 就不重复了,前端不会再合并。
这里业务一定要求一致,还可以使用空格来区分。一个加空格,一个不加空格。
4.3 更通用的建议
-
在 UNION ALL 中,不同来源的行应尽量保证维度列的组合唯一。
-
如果维度列确实需要相同,可以额外增加一个
row_type列(如'合计'、'明细'、'费用'),让前端按它区分。 -
避免使用可能重复的值(如部门名)作为区分标签,改用固定前缀。
五、总结
-
排序失效的根本原因:字符串拼接排序无法正确处理数值,尤其是小数、负数和 NULL 导致长度不一致,字典序与数值序不一致。
-
推荐做法 :使用
ROW_NUMBER() OVER (ORDER BY ...)生成整数排名sort_rank,按它排序。数值字段直接参与比较,稳定且跨库。 -
列名相同的隐患:UNION ALL 后若不同语义的行使用了相同的维度值组合,会被前端合并。通过添加区分标签或字段避免。
-
通用原则:
-
排序键含数值 → 用数值排序,不要拼字符串。
-
多来源 UNION ALL → 保证维度组合唯一,或增加类型列。
-