永洪排序失效引发的思考:数值排序 vs 文本排序,列值相同自动合并错误,完美的排序方式:ROW_NUMBER() OVER (ORDER BY 多级排序)

一次排序失效引发的思考:数值排序 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 级排序键:

sql 复制代码
ORDER 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. 第 1 级:按 dept_name,财务部 → 销售部

  2. 第 2 级:财务部内按 sort_order,1 → 2 → 2 → 3;销售部内 1 → 2

  3. 第 3 级 :财务部合计(1)与费用(3)不比较(已被第 2 级分开);员工(2)在第 3 级都是 NULL

  4. 第 4 级:财务部员工内按 grand 降序:2000 → 1000;销售部只有一行员工

  5. 第 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 降序 换数据会变
按数据集物理顺序 实际上就是缓存里的行序 极少数版本,且刷新缓存会变

经验教训:永远不要依赖永洪的无序,永远使用自定义的数值列或业务要求的数值列来排序。
关键原因分析:

  1. 永洪端"无序"并不意味着"按 SQL 顺序"。永洪作为一个 BI 工具,有自己的数据缓存、分组、聚合逻辑。即使数据集 SQL 里有 ORDER BY,永洪在渲染自由表时,可能重新按维度字段排序,或按数据到达顺序展示,或按某种默认规则。

  2. ORDER BY 里的 CASE WHEN 表达式比较"隐晦" 。如果是列名的话,比如 sort_order DESC,永洪可能能识别;但 CASE WHEN 表达式作为一个计算列,永洪端可能不"识别"它作为排序依据,导致排序被忽略。

  3. 永洪的"无序"设置实际是按数据集返回的物理顺序。但如果数据被缓存、被重新组织过,物理顺序可能变了。

所以问题的关键可能不是 SQL 的 ORDER BY 写错了,而是永洪前端是否真正尊重 SQL 的 ORDER BY。

最终结论

  1. 不是 SQL 的 ORDER BY 写错了 。无论写 CASE WHEN sort_order IN (1, 3) 还是 CASE WHEN sort_order = 1,SQL 端结果都是对的。

  2. 是永洪自由表的排序配置在起作用

  3. 加部门费用行前看起来正常是巧合(只有 2 类时现象不明显),加之后现象更明显。

  4. 修复方案:永洪端排序配置里,明确指定排序列。

最开始以为是脚本排序写错了没生效,所以后面更换了排序方式。

之前的排序方式即使是正确的,永洪前端也没办法指定具体的排序列。


永洪前端能指定的"排序列",必须是 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 位) 000009999999899
1234.56 9999998765.44 9999998765.44(13 位) 009999998765.44

按字符比较:000009999999899 vs 009999998765.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 就是前面讲过的多级排序,只是被"包"进了窗口函数里。数据库做的事:

  1. 拿整个结果集(6 行)

  2. 按 ORDER BY 的规则排序(就是之前那 5 级排序)

  3. 给排好序的每行依次编号 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 列(如 '合计'、'明细'、'费用'),让前端按它区分。

  • 避免使用可能重复的值(如部门名)作为区分标签,改用固定前缀。

五、总结

  1. 排序失效的根本原因:字符串拼接排序无法正确处理数值,尤其是小数、负数和 NULL 导致长度不一致,字典序与数值序不一致。

  2. 推荐做法 :使用 ROW_NUMBER() OVER (ORDER BY ...) 生成整数排名 sort_rank,按它排序。数值字段直接参与比较,稳定且跨库。

  3. 列名相同的隐患:UNION ALL 后若不同语义的行使用了相同的维度值组合,会被前端合并。通过添加区分标签或字段避免。

  4. 通用原则:

    • 排序键含数值 → 用数值排序,不要拼字符串。

    • 多来源 UNION ALL → 保证维度组合唯一,或增加类型列。

相关推荐
shehuiyuelaiyuehao12 天前
算法46,分治快排,排序数组
java·算法·排序算法·排序
Logic10114 天前
C语言/数据结构字符串题解:找出DNA序列中未配对的独特序列——排序+遍历(O(nlogn))
c语言·数据结构·字符串·数组·排序·时间复杂度·算法题
2601_9622998820 天前
排序,然后再使用
python·排序·列表·关键函数·装饰-排序-去装饰
Jasmine_llq22 天前
《P15803 [GESP202603 七级] 物流网络》
排序·贪心离线枚举策略(核心解题思想·分批处理 / 双指针扫描·dijkstra单源最短路算法·无向图建图(邻接表)·最短路拼接计算候选答案·边界补充:普通无优惠最短路
aqiu1111111 个月前
【算法刷题】蓝桥杯/AtCoder:删除元素后的中位数问题(Symmetry / Median)
算法·蓝桥杯·排序·中位数
aqiu1111111 个月前
【算法刷题】蓝桥杯:0星际争霸(大数比较 + 多关键字自定义排序)
算法·排序·自定义排序
aqiu1111111 个月前
【算法刷题】蓝桥杯:怎么倒茶(贪心算法 + 结构体排序)
贪心·排序
OuO-21 个月前
笔试强训 Day 43:kotori 和抽卡(二)、ruby 和薯条、循环汉诺塔
数学·前缀和·二分查找·动态规划·排序·滑动窗口·循环汉诺塔
Tisfy2 个月前
LeetCode 3731.找出缺失的元素:哈希 / 排序
算法·leetcode·哈希算法·排序·哈希表