为什么postgresql中的字符串排序结果会不一样

转载请注明出处,作者:archimeai

奇怪的排序

在不同的环境上,postgresql对同一个表的排序(order by)结果可能不同。

postgres=# create table t1(s text);
CREATE TABLE
postgres=# insert into t1 values (' b'),('b'),('a'),('c');
INSERT 0 4

postgres=# select * from t1 order by s collate "en_US.utf8";
 s
----
 a
  b
 b
 c
(4 rows)

postgres=# select * from t1 order by s collate "C";
 s
----
  b
 a
 b
 c

可以看到,当使用en_US.utf8 collation时,前面带了一个空格的b排到了a的后面,而当使用 C collation时,两者的顺序又反过来了,这是为什么呢?

不同的排序方法

核心参考资料:https://www.postgresql.org/docs/current/collation.html

从postgresql的文档中可知,字符串的排序是通过collate相关的参数来控制的。在不同的环境上,默认使用的collation可能不同,这就导致在不同的环境上order by的结果不同。

简单来讲,collation C的含义是按照字符编码的字节值进行排序(注意相同的字符在不同的编码方式下可能具有不同的编码值)。这种情况下,空格+b的utf8编码值是 0020 0062 (均为16进制),a的utf8编码为0061,b的utf8编码为0062,c的utf8编码为0063。0020最小,因此排在最前面。

postgres=# select * from t1 order by s collate "C";
 s
----
  b  -- 0020 0062
 a   -- 0061
 b   -- 0062
 c   -- 0063

collation en_US.utf8的含义是按照美国英语的规范对unicode字符进行排序,笔者将在下一节中介绍。

unicode中的排序规则

核心参考资料:

  • https://www.unicode.org/reports/tr10/
  • https://unix.stackexchange.com/questions/252419/unexpected-sort-order-in-en-us-utf-8-locale
  • https://unicode-org.github.io/icu/userguide/collation/concepts.html

Unicode中定义了复杂的字符串排序规则,这一规则可以被不严谨地简化为一个多趟排序的过程。注意只有在前一趟排序中存在无法比较大小的情况时,才需要下一趟排序对无法比较大小的值进行排序。在Unicode提供的all_keys.txt中,定义了所有Unicode字符在每一趟排序中的先后顺序。还是以文章一开头的排序为例子,所涉及的字符的排序顺序定义如下。

// 顺序定义在 https://www.unicode.org/Public/UCA/latest/allkeys.txt
0020  ; [*0209.0020.0002] # SPACE
0061  ; [.20A9.0020.0002] # LATIN SMALL LETTER A
0062  ; [.20C3.0020.0002] # LATIN SMALL LETTER B
0063  ; [.20DD.0020.0002] # LATIN SMALL LETTER C

上述定义中,第一个数字是字符在unicode中的编码值。后面方括号中有三个值,代表unicode排序中的权重(先后顺序,权重越小越靠前)

  • 第一个权重代表在第一趟排序中大小比较的依据,如果以.开头,就代表这个字符在第一趟排序中不能被忽略,这种字符被叫做base character。如果以*开头,就代表这个字符在第一、二、三、四趟排序中的情况都可以动态选择,这种字符被叫做variable character,注意空格就是variable character。在en_US.utf8中,第一、二、三趟排序需要忽略space,也即需要忽略空格。
  • 第二个权重代表在第二趟排序中大小比较的依据
  • 第三个权重代表在第三趟排序中大小比较的依据。
    第一趟排序仅对“base character"进行排序,忽略其它字符。

所以,上述order by语句在第一趟排序时假设情况如下:

 a  -- 20A9
 b  -- 20C3
  b -- 20C3  空格被忽略了
 c  -- 20DD

无法区分 空格+b 和b的大小,需要进行第二趟排序,只考虑上述两个值。注意因为是第二趟排序,需要使用第二个权重。

 b  -- 0020
  b -- 0020  空格被忽略了

还是无法区分大小,进行第三趟排序

 b  -- 0002
  b -- 0002  空格被忽略了

还是无法区分大小,怎么办呢?这个时候要用上一个特殊规定,即在en_US.utf8中,对于前三趟都区分不出大小的字符串,如果其中插入了一个variable character,则插入这个variable character会使得其排在前面。这样我们终于可以把空格+b排到b前面了。

b
 b

综合上面的情况,就有了postgresql中的排序结果

postgres=# select * from t1 order by s collate "en_US.utf8";
 s
----
 a
  b
 b
 c
(4 rows)

上述解释只是对unicode排序规则的一个非常简单化的解释,忽略了很多重要的细节,想要进一步详细了解的读者请移步阅读本节开头提供的参考资料。

其它参考资料

/usr/share/i18n/locales/en_US
https://linuxconfig.org/how-to-install-generate-and-list-locale-on-linux
https://superuser.com/questions/1551967/what-does-locale-gen-generate-in-linux

### PostgreSQL MAX() 函数与 MySQL MAX() 函数的区别 #### 功能概述 `MAX()` 是一种聚合函数,在 PostgreSQL 和 MySQL 中都用于查找指定列中的最大值。然而,尽管两者功能相似,但在某些细节上存在差异。 在 PostgreSQL 中,`MAX()` 可以应用于数值、日期时间以及字符串类型的字段[^1]。对于字符串类型的数据,它会按照字典序比较并返回最大的字符串值[^2]。 同样地,MySQL 的 `MAX()` 也支持数值、日期时间和字符串类型的字段[^3]。过,MySQL 对于字符串的最大值判断依据也是基于字典序排序规则[^4]。 #### 数据类型的支持 两种数据库系统均允许对多种数据类型应用此函数,但具体行为可能会因实现方式同而有所变化: - **PostgreSQL**: 支持更多复杂的数据结构比如数组等作为参数传递给 max 函数,并能正确处理 null 值的情况。 - **MySQL**: 主要集中在基础数据类型上的操作;当遇到 NULL 值时,默认忽略这些记录除非明确指定了如何对待它们。 #### 处理NULL值的方式 无论是哪种 RDBMS ,如果集合里全是 NULL,则最终结果也会是 NULL 。但是关于是否显示警告信息或者错误提示方面可能存在细微差别: - 在 PostgreSQL 中,如果有任何非 NULL 输入项则会报错也会发出警告消息直接给出有效输出即可; - 而在 MySQL 方面,虽然同样是跳过 NULL 记录来进行运算得到实际存在的最高值,但如果整个范围都是由 NULL 组成的话,那么除了返回 NULL 结果之外还可能附加一些额外的状态指示器告知用户发生了这种情况。 #### 使用场景举例 以下是两个简单例子展示两者的用法一致之处及潜在的同点: ##### 查询某表中价格最高的商品名称 (假设有一个名为 products 表含 price 列) **PostgreSQL 版本** ```sql SELECT product_name FROM products WHERE price = (SELECT MAX(price) FROM products); ``` **MySQL 版本** ```sql SELECT product_name FROM products ORDER BY price DESC LIMIT 1; ``` 这里可以看到虽然都能完成任务目标即找到最贵的产品名,但由于各自语法特性的缘故采用了略微同的策略达成目的——前者利用子查询配合条件过滤选出符合条件的一行或多行;后者则是先做降序排列再取第一条记录代表整体最大者。 #### 性能考量因素 最后值得一提的是性能方面的考虑。由于底层存储引擎设计原理各异加上索引机制等因素的影响,即使同样的逻辑表达式也可能因为所处环境一样而导致效率高低悬殊巨大。通常来说经过良好优化后的 B-tree 或 GiST 索引可以极大地加速涉及此类统计汇总类工作的进程速度^。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值