本文是《PostgreSQL技术问答》系列文章中的一篇。关于这个系列的由来,可以参阅开篇文章:
《PostgreSQL技术问答00 - Why Postgres》
文章的编号只是一个标识,在系列中没有明确的逻辑顺序和意义。读者进行阅读时,不用太关注这个方面。
本文讨论的内容是PostgreSQL中一个非常重要而常用的功能集合: String字符串。
什么是String
String的原意是“绳子”,也有串的意思。在信息技术的语境中,就借助了这个形态,将一系列序列化的字符连接起来的样子,称为字符串。在早期的计算机技术情景中,它主要针对英文,就是一系列字符的序列,但后来的技术发展需要支持更广泛的语言和文字,所以对于一些表意语言比如中文而言,它就不仅仅是字符了,也可以是一个文字的序列,所以其实更应当被称为“字串”。 当然,在一般情况下,我们还是尊重传统,称其为“字符串”。
String操作一般有那些方式
在一般应用开发的日常中,我们可以总结抽象出String操作的一般方法和范式,这些形式可能在任何语言和系统中,都会需要,比如:
-
字符串合并和连接(Contact),将两个或者多个字符串连接在一起
-
获得子字符串,或者拆分成字符串数组
-
字符串数组合并(Join),将一个数组中的字符串连接起来,可选连接符如逗号
-
在字符串中特定位置中插入内容或者覆盖内容
-
字符串修整(Trim), 去除字符串头和尾的空字符或者指定字符
-
字符串填充(Pad), 在字符串前面或者后面添加内存,将字符串扩展到固定的长度,编码常用
-
转换成大小写
-
获取字符串的长度
-
比较两个字符串的内容
-
在字符串中检索,如找到匹配的子字符串和位置
-
正则表达式操作
-
字符串内容和类型转换,如字符串转数字、JSON、Array等
在PG中,对于上述内容,都提供了对应的操作符和函数,来实现这些操作和处理。还需要注意,除了常规的字符串处理之外,在数据库中,可能还涉及到字符串和记录和记录集之间的操作(如记录集转字符串或者逆向转换)。
下面,我们就来看看Postgres中是如何对字符串类型的数据进行操作和处理的。
Postgres中,如何处理和操作字符串
按照字符串类型数据处理的一般方法论,Postgres提供了一套相关的数据类型、操作符和函数,来实现在数据库中,存储、修改和查询字符串数据的操作。这些内容和特性大致如下。
字段类型
在数据库中存储和操作字符串,首先需要在创建数据库表时,声明库表字段的数据类型。示例如下:
-- 创建字符串类型列的表
create table tuser (
id serial primary key,
name varchar(30),
descp text
);
CREATE TABLE
-- 检查类型
defaultdb=> \d tuser;
Table "public.tuser"
Column | Type | Collation | Nullable | Default
--------+-----------------------+-----------+----------+-----------------------------------
id | integer | | not null | nextval('tuser_id_seq'::regclass)
name | character varying(30) | | |
descp | text | | |
Indexes:
"tuser_pkey" PRIMARY KEY, btree (id)
可以看到,示例中,使用了最常见的两种字符串数据类型,varchar需要指定最大长度,而text直接声明即可。
字符串构建、合并、切片、和修剪
下面是一些简单的示例:
// 构建、连接、切片修剪
with T(id, t1, t2) as (values
(1, 'Hello', 'World'),
(2, 'Tom', 'Jerry')
) select id, t1 || t2, substring(t1,1,3), substring(t2,length(t2)-2, length(t2)) from T;
id | ?column? | substring | substring
----+------------+-----------+-----------
1 | HelloWorld | Hel | rld
2 | TomJerry | Tom | rry
(2 rows)
// 从头或者尾截取
select left(str1,4), right(str1,3)
from (values ('hello World')) as D (str1);
left | right
------+-------
hell | rld
(1 row)
// conact函数
select concat(str1,str2,str3),concat_ws(',',str1,str2,str3)
from (values ('hello World', 'welcome','bye')) as D (str1,str2,str3);
concat | concat_ws
-----------------------+-------------------------
hello Worldwelcomebye | hello World,welcome,bye
(1 row)
-- 字符串重复
select repeat('World ', 4);
repeat
--------------------------
World World World World
(1 row)
这里的要点包括:
- 使用单引号表示和构建字符串
- 可以使用 || 操作符,或者concat方法
- 使用合并操作符时,支持字符串和其他标量的混合类型操作
- concat/concat_ws是可变参数列表,和字符串数组有差异
修剪、修补
主要使用trim, pad等相关函数。
-- 各种修剪
defaultdb=> with D(str1,str2) as (values (' 中国 ', '人民'))
select 'N' || str1 || str2 from D
union all
select 'A' || trim(str1) || str2 from D
union all
select 'L' || ltrim(str1) || str2 from D
union all
select 'R' || rtrim(str1) || str2 from D;
?column?
-------------
N 中国 人民
A中国人民
L中国 人民
R 中国人民
(4 rows)
-- 补足
defaultdb=> with D(v) as (values (12), (456))
select lpad(v::text,10, '0'), rpad(v::text,10, '0') from D;
lpad | rpad
------------+------------
0000000012 | 1200000000
0000000456 | 4560000000
(2 rows)
修剪操作,通常用于去除字符串头和尾的空格和换行等不可见字符,一般是一种数据清理操作,减少数据内容和格式问题带来的隐患。补足操作,一般用于标准化编码,保持编码的长度一致和固定。
Postgres提供的trim和pad函数功能更加强大,它可以指定修剪和补足使用的字符串,而且支持多个字符。
字符串搜索和前缀检查
在字符串中,搜索给定的内容,是一个常见的需求和操作。还有一种情况,就是判断字符串是以某种形式开头,这种在进行编码判断的时候,非常有效。
-- 搜索位置
select strpos(str1, 'World'), strpos(str1, 'world'),
starts_with(str1,'hello'), starts_with(str1,'Hello')
from (values ('hello World')) as D (str1);
strpos | strpos | starts_with | starts_with
--------+--------+-------------+-------------
7 | 0 | t | f
(1 row)
需要注意的是,和一般编程语言不同,字符串检索位置是从1开始的,如果找不到,会返回0(一般是-1)。而且,Postgres没有提供“后缀检查”,就是类似ends_with的函数。
转换大小写,首字母大写、字符串反序
对于主要使用中文的应用环境中,这种操作的机会其实不太多。
-- 大小写,首字母大写
select lower(str1), upper(str1), initcap(str1) from (values ('hello World')) as D (str1);
lower | upper | initcap
-------------+-------------+-------------
hello world | HELLO WORLD | Hello World
(1 row)
-- 反序回文
select reverse(str1)
from (values ('hello World')) as D (str1);
reverse
-------------
dlroW olleh
(1 row)
内容修改
字符串的内容修改,包括插入、搜索替换和位置覆盖等常见操作。下面是一些简单的示例:
-- 插入字符串,没有原生函数
select substr(str1,1,5) || ' beatiful ' || substr(str1,6) from (values ('hello World')) as D (str1);
?column?
-----------------------
hello beatiful World
(1 row)
-- 基于搜索替换
select replace(str1, 'World', 'China') from (values ('hello World')) as D (str1);
replace
-------------
hello China
(1 row)
-- 基于位置覆盖
select overlay(str1 placing 'China' from 2 for 4)
from (values ('hello World')) as D (str1);
overlay
--------------
hChina World
(1 row)
-- 字符映射转换
select translate('12345', '143', 'ax');
translate
-----------
a2x5
(1 row)
比较字符串
是的,字符串也是可以比较的。比较的依据,如果是ASCII码字符,则依次比较字符编码顺序,如果是UTF8,则转换为字节数组后,依次比较数组元素的值。字符串比较的功能也是非常重要和常用的,比如经常用于过滤和匹配条件检查。
defaultdb=> select 'AAA'='AAA', 'AAA'='aaa', 'AAA' > 'BBB', '中国' > '中华';
?column? | ?column? | ?column? | ?column?
----------+----------+----------+----------
t | f | f | t
(1 row)
字符串格式化
字符串格式化,笔者理解其实就跟C语言中的打印字符串(sprintf)一样,就是使用一个模板字符串,其中定义了可替换的类型变量,然后使用参数来填充和替换这些变量,就得到了最后格式化后的字符串。
SELECT format('Testing %s, %s, %s, %%', 'one', 'two', 'three');
Result: Testing one, two, three, %
SELECT format('|%10s|', 'foo');
Result: | foo|
SELECT format('|%-10s|', 'foo');
Result: |foo |
和C语言,format的核心是一种叫做格式化字符串的规范,其中的形式和细节比较多,这里就只展示最常用的方式,不再展开了。
字符串和其他类型的转换
字符串可以方便的和Number、Array、Record等进行相互转换。
-- 和数字的相互转换
select concat(str1,i1), i1+i2::int from (values ('hello World', 123, '256')) as D (str1,i1,i2);
concat | ?column?
----------------+----------
hello World123 | 379
(1 row)
-- 日期格式化
SELECT to_char(current_date, 'YYYY-MM-DD');
to_char
------------
2024-07-12
(1 row)
-- ASCII字符和编码
select ascii ('B'), chr(68);
ascii | chr
-------+-----
66 | D
(1 row)
-- 数组转换,
select string_to_array('A,B,C',','),
split_part('D,E,F,G',',',-2), array_to_string('{1,2,3}'::text[],';');
string_to_array | split_part | array_to_string
-----------------+------------+-----------------
{A,B,C} | F | 1;2;3
(1 row)
-- 分割部分
select split_part('abc,def,ghi,jkl', ',', -2);
split_part
------------
ghi
(1 row)
-- record to string
select string_agg(D.c::text, ',') from lateral (values (1),(2),(3) ) as D(c);
string_agg
------------
1,2,3
(1 row)
-- string to record
SELECT * FROM UNNEST(STRING_TO_ARRAY('apple,banana,orange', ',')) with ordinality as T(name,id);
name | id
--------+----
apple | 1
banana | 2
orange | 3
(3 rows)
-- 简化方式
SELECT * FROM string_to_table('apple,banana,orange', ',') with ordinality as T(name,id);
name | id
--------+----
apple | 1
banana | 2
orange | 3
(3 rows)
在使用字符串的转换的时候,要特别注意处理之前,需要合适的数据类型。所幸,Postgres提供了::这种同样的类型转换操作符来简化操作,虽然看起来有点奇怪,但它的功能和适用性是很强的。
作为开发者,我们也知道,关于字符串模式检查,最强大的功能集就是正则表达式,这个方面PG也有所支持,但由于这些部分内容较多,笔者准备在另行撰文专门讨论。
总结一下常用的字符串函数?
这里将日常比较常用的字符串相关的操作符和处理函数总结一下:
- text || text → text / text || anynonarray 字符串合并操作符
- text ^@ text → boolean 判断字符串起始于字符串
- text IS [NOT] [form] NORMALIZED → boolean,检查字符串是否是特定的Unicode格式
- concat / concat_ws 连接字符串,简单连接和使用分隔符
- trim / btrim / ltrim / rtrim 各种修整字符串的方式
- length / bit_length / octet_length / char_length 字符串长度,位长度,字节长度,字符长度
- lower / upper/ initcap 转换为小写,大写,首字符大写
- lpad / rpad 左补位,右补位
- left / right 按照长度截取开始和结束的子字符串
- position / strpos 检索子串在字符串中的位置
- starts_with 判断字符串起始于,好像没有ends_with?
- substring 获取子字符串,支持其止方式,模式匹配方式也可以使用模式字符串
- replace 替换字符串
- reverse 反序字符串
- repeat 重复字符串
- ascii / chr ascii字符数字码相互转换
- string_to_array / string_to_table 将字符串分解转换为数组和记录集
- split_part 分解为数组后,获取其中一个元素
- to_hex / to_ascii 编码转换
- translate 将字符串依次按照模式字符串中的字符替换
- overlay 字符串覆盖
- md5 计算字符串的MD5值
- unistr/ normalize / unicode编码字符串的解码
- pg_client_encoding 获取当前客户端字符集
- parse_ident 解析标识
- quote... 引述相关函数,转为字面量,笔者理解就是增加
- regexp... 正则表达式计算相关函数群
- format 格式化字符串
这其中的regexp正则表达式相关操作,和format格式化相关操作,内容比较多,也自成体系,笔者后续会撰文详细探讨。
PG中有那些字符串类型
一些有Postgres使用经验的开发者,可能会注意到,在Postgres中,对于字符串的支持,提供了好几种数据类型,包括char(character)、varchar(character varying)、bpchar和text几种。 它们可能适用不同的需求和场景,作为开发者有必要了解其中的差异,才能避免由于适用不当造成的性能问题。
- text
text就是文本,在PG中它是可变长度的字符串类型。在技术上,PG支持1GB的text字符串数据,但是在应用中,text类型没有长度限制,也不应当设置。
- varchar
varchar就是可变长度的字符串。但是在使用(创建表)的时候,需要指定一个最大的长度。如varchar(255)表示最多可以存储255个字符的字符串。如果向这个数据列中,插入超过这个最大长度的字符串,则会触发操作错误,并收到错误消息。
- char
和varchar相对,char就是一种定长的字符串。在很多场景中,我们都可以看到定长字符串的使用,比如中国省市县三级行政区划编码、中国手机电话号码长度、居民身份证号码长度、ICAO机场编码等等。由于是确定的长度,数据库系统在存储和处理这些信息的时候,是非常确定和高效的。如果能够确认这些业务特点,则应当优先选择这种类型。
- bpchar
bpchar的意思是(Blob Char,Binary Large Object 二进制大对象字符串)。它是一个定长的二进制形式的字符串类型,最大可选长度为8K字节。它在形式上是一个字符串,但实际上是二进制数据(字节数组)。保留字符串形式的目的是,它可以以此支持一些标准的字符串操作方式,方便开发和使用。当然Postgres也提供存储的Blob数据格式,给任意形式的编码化的信息使用。
在这几种类型中,varchar和text的使用是比较普遍的,它们都用于存储不能确定长度的字符串数据。但需要注意,它们的使用还是略有差异。如果确实能够确认字符串的长度,不会超过一个限制,则建议使用varchar,比如电子邮件地址、电话号码、人名、证件编号、简短的描述信息等等的;而对于可能比较大的篇幅的文本,则可以使用text,比如博客文章、产品信息和手册文本等等。
在技术细节方面,Postgres对于varchar和text的存储和处理也稍有不同。相对而言,varchar的处理更类似于标准数据库字段,系统的存储和处理效率更高;而相对的text则更像一种可动态挂接的外部数据块,相对处理性能更低,但灵活性更高。如果对性能比较敏感,需要认真评估数据类型选择对实际业务应用的影响,最后做一些实验和测试工作。
字符串的长度,是以什么作为依据的
在Postgres系统的日常使用中,有经验的开发者会发现,数据库中,字符串长度的计算,好像在不同的情况下,是不一样的。这个问题,需要在一些具体的情况下,予以关注,否则容易造成应用开发和使用中超限或者出错的情况。
下面是一个简单的例子:
defaultdb=> elect length('Hello'::varchar), length('Hello'::bytea), length('中国'::varchar), length('中国'::bytea);
length | length | length | length
--------+--------+--------+--------
5 | 5 | 2 | 6
(1 row)
可以看到,PG对于字符串长度的计算,有其内部规则,并不是简单的字节占用的大小。笔者简单的理解,就是如果字符串是UTF-8编码,则使用UTF-8的字符长度;如果是纯ASCII编码,就是使用ASCII字符长度。要检查字符串作为字节数组(二进制形式)的长度,需要进行类型的转换。
小结
本文探讨了PostgreSQL中,一个非常基础和核心的数据类型和特性:String,即字符串。具体包括了字符串操作的一般方法论和模型,常见的PG字符串操作,字符串数据类型等相关的问题。