如何替换字符串(文本列)中最后一次出现的字符?

Fma*_*nez 5 postgresql regular-expression replace

假设我在 Postgres 中有一个具有此值的文本字段:

'bar$foo$john$doe$xxx'
Run Code Online (Sandbox Code Playgroud)

我想将最后一次出现的美元 ( $) 字符替换为另一个字符,例如“-”。替换后该字段的内容应为:

'bar$foo$john$doe-xxx'
Run Code Online (Sandbox Code Playgroud)

Vér*_*ace 14

介绍:

这个问题涉及到一些横向思维。当反转后,任何字符的最后一次出现也是该字符在字符串中的第一次出现!所有解决方案(除了一个)都使用这种方法。

对于所提出的所有 5 个解决方案,我们有以下内容(此处提供了包含以下所有代码的小提琴。下面的每个解决方案都包含单独的每个解决方案的小提琴):

CREATE TABLE test
(
  id INT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
  t_field TEXT
);
Run Code Online (Sandbox Code Playgroud)

PRIMARY KEY仅第五个解决方案需要此。然后我们运行以下查询来填充表 - 第一个记录是 OP 自己的数据 - 其余的都是随机生成的!

操作:

INSERT INTO test (t_field)
VALUES ('bar$foo$john$doe$xxx');  -- OP's own data
Run Code Online (Sandbox Code Playgroud)

随机数据:

INSERT INTO test (t_field)
SELECT 
  LEFT(MD5(RANDOM()::TEXT), FLOOR(RANDOM() * (5 - 3 + 1) + 3)::INT) || '$' ||
  LEFT(MD5(RANDOM()::TEXT), FLOOR(RANDOM() * (5 - 3 + 1) + 3)::INT) || '$' ||
  LEFT(MD5(RANDOM()::TEXT), FLOOR(RANDOM() * (5 - 3 + 1) + 3)::INT) || '$' ||
  LEFT(MD5(RANDOM()::TEXT), FLOOR(RANDOM() * (5 - 3 + 1) + 3)::INT) || '$' ||
  LEFT(MD5(RANDOM()::TEXT), FLOOR(RANDOM() * (5 - 3 + 1) + 3)::INT)
FROM 
  GENERATE_SERIES(1, 29999);  --<<== Vary here
   
--
-- For this fiddle, we only have 30,000 (29,999 + the OP's original datum) records 
-- (although relative magnitudes appear good), the individual fiddles use
-- 300,000. 
--
-- 300,000 appears large enough to give reliable consistent results and small 
-- enough so that the fiddle doesn't fail too often - rarely fails on 30k.
--
--
-- You can vary this number, but please consider using the individual fiddles for
-- large numbers of records so as not to hit the db<>fiddle server too hard!
--
-- The home test VM used 10,000,000 records - 16 GB RAM, 1 CPU, SSD
--
Run Code Online (Sandbox Code Playgroud)

解决方案将按性能顺序呈现。它在 db<>fiddle 和家庭虚拟机(16GB RAM,SSD)上进行了测试,测试表中有 10M 记录 - 不要在 fiddle 上尝试 10M!每种方法都会被赋予一个因素,即它比虚拟机上最快的方法花费的时间多长。

bar$foo$john$doe-xxx在所有情况下, OP 的原始数据和测试查询都会获得所需的结果(LIMIT 2显示它们的行为符合预期 - 即用$连字符 ( )替换最后一个美元 ( -) 符号。您可以更改此限制小提琴来检查。

1:Postgresql字符串函数(参见手册),使用OVERLAY()STRPOS()AND REVERSE()(个人小提琴):

SELECT
  t_field, 
  OVERLAY(t_field PLACING '-' 
    FROM 
      LENGTH(t_field) + 1 - STRPOS(REVERSE(t_field), '$')
         ) AS result
FROM test;
Run Code Online (Sandbox Code Playgroud)
  • 性能:10M 记录的时间 = 8034.787 ms。
  • 与最快的比较 = 1.0 x

2:Reverse()和正则表达式函数REGEXP_REPLACE()(个人小提琴):

SELECT 
  REVERSE(REGEXP_REPLACE(REVERSE(t_field), '\$', '-'))
FROM
  test;
Run Code Online (Sandbox Code Playgroud)

正在做的事情(从内到外)是:

  • REVERSE()字符串,

  • REGEXP_REPLACE('xxx', '\$', '-')在反转的字符串上运行。

    请注意,这只会替换第一个实例,$因为'g'(全局)标志不存在 - 如果读取代码... , '-', 'g'),则所有美元都将被替换 - 并且您无论如何都可以使用(便宜得多)REPLACE()函数来做到这一点。

    另请注意,它$是一个正则表达式meta-character- 即它在正则表达式中具有特殊功能(它意味着字符串的最后一个字符),因此在替换它时必须使用反斜杠 ( \) 字符对其进行转义。

  • 然后,最后一步是将编辑后的字符串反转回其原始顺序,我们就得到了结果!

值得记住的是,正则表达式非常强大。不幸的是(释义),强大的能力带来了巨大的复杂性。正则表达式可能会变得非常复杂且难以理解 - 但它们非常值得深入研究 - 它们可以在高手的手中将一页页代码变成一行行代码!

首先尝试找到具有非正则表达式功能的不同解决方案(参见解决方案 1)始终是值得的,但它们有自己的位置,在这种情况下,它工作得相当好!上面链接的网站是开始探索它们的好地方。

  • 性能:10M 记录的时间 = 14298.643 ms。
  • 与最快的比较 = 1.77 x

3:使用 REGEXP_REPLACE() 的替代正则表达式(不使用 REVERSE() -请参阅 Evan Carroll 的答案(个人小提琴)):

SELECT
  t_field,
  REGEXP_REPLACE(t_field, '(.*)\$', '\1-' )
FROM test
LIMIT 2;
Run Code Online (Sandbox Code Playgroud)
  • 性能:10M 记录的时间 = 16316.768 ms。

  • 与最快的比较 = 2.03 x

4:仅替代字符串函数,使用SUBSTRING(), POSITION()and LENGTH()(单独的fiddle):

SELECT
  t_field,
  REVERSE(
  SUBSTRING(REVERSE(t_field) FROM 1 FOR POSITION('$' IN REVERSE(t_field)) - 1)
  || '-' ||
  SUBSTRING(REVERSE(t_field) FROM POSITION('$' IN REVERSE(t_field)) + 1 FOR (LENGTH(REVERSE(t_field)))))
 FROM test
LIMIT 2;
Run Code Online (Sandbox Code Playgroud)
  • 性能:10M 记录的时间 = 16316.768 ms。
  • 与最快的比较 = 2.34 x

5:(ARRAY手动 -v.缓慢但演示STRING_TO_ARRAY()UNNEST()以及1 (个人小提琴WITH ORDINALITY

1:请参阅 Erwin Brandstetter 的这些帖子( 1、23 )WITH ORDINALITY

个人小提琴展示了多种方法以及性能分析和一些讨论。包含在内只是为了完整性,在本例中并不是一个现实的选择。

尽管在这种特殊情况下,该ARRAY技术的性能不是很好(由于具有子查询),但服务器的大部分后端代码都使用ARRAYs,并且它们通常是解决各种问题的最佳方法。PostgreSQL 这个鲜为人知的角落非常值得了解。

首先要做的是:

SELECT
  UNNEST
  (STRING_TO_ARRAY(REVERSE((SELECT t.t_field 
                                    FROM test t
                                    WHERE t.id = 1
                                    )), '$'));
Run Code Online (Sandbox Code Playgroud)

结果(OP 的记录 -xxx由于 REVERSE(),注释排在第一位):

str
xxx
eod
nhoj
oof
rab
Run Code Online (Sandbox Code Playgroud)

该字符串按字符分为多个字段$

然后:

SELECT
  t.t_field,
  t.id, x.elem, x.num
FROM test t
LEFT JOIN LATERAL
  UNNEST(STRING_TO_ARRAY(REVERSE((SELECT t_field 
                                  FROM test
                                  WHERE test.id = t.id
                                  )), '$'))
  WITH ORDINALITY AS x (elem, num) ON TRUE
  LIMIT 5;
Run Code Online (Sandbox Code Playgroud)

结果:

             t_field    id    elem  num
bar$foo$john$doe$xxx     1     xxx    1
bar$foo$john$doe$xxx     1     eod    2
bar$foo$john$doe$xxx     1    nhoj    3
bar$foo$john$doe$xxx     1     oof    4
bar$foo$john$doe$xxx     1     rab    5
Run Code Online (Sandbox Code Playgroud)

我们需要 the 的原因WITH ORDINALITY是,如果没有 is,我们就无法区分字符串的第一个元素(即我们感兴趣的元素)和其他元素(elem, num)

然后,我们这样做:

SELECT
  (SELECT t_field FROM test WHERE test.id = tab.id),
  REVERSE(
  (STRING_TO_ARRAY((SELECT REVERSE(t_field) FROM test WHERE test.id = tab.id), '$'))[1]
  || '-' || 
  STRING_AGG(elem, '$'))
FROM
(
  SELECT
    t.id, x.elem, x.num
  FROM test t
  LEFT JOIN LATERAL
    UNNEST(STRING_TO_ARRAY(REVERSE((SELECT t_field 
                                    FROM test
                                    WHERE test.id = t.id
                                    )), '$'))
    WITH ORDINALITY AS x (elem, num) ON TRUE
) AS tab
WHERE tab.num > 1
GROUP BY tab.id
LIMIT 2;
Run Code Online (Sandbox Code Playgroud)

结果:

             t_field    result
bar$foo$john$doe$xxx    bar$foo$john$doe-xxx
7a29f$d06f$20e$21f$1b1  7a29f$d06f$20e$21f-1b1  -- will vary by fiddle run!
result
bar$foo$john$doe-xxx
Run Code Online (Sandbox Code Playgroud)

其作用是使用 作为$分隔符将反转的字符串聚合回其原始形式,但排除第一个元素 ( WHERE num > 1;)。代替第一个元素的是第一个元素 - 数组引用[1]+ 连字符 ( || '-' ||),因此,我们xxx-加上反转字符串的其他元素并将$它们分隔开。

然后我们只需将其应用于REVERSE()整个构造即可得到所需的结果!

  • 性能:10M 记录的时间 = 80715.198 ms。
  • 与最快的比较 = 10.04 x

有一个可能的解决方案不使用WITH ORDINALITYROW_NUMBER()代替) - 请参阅个人小提琴中的讨论。

表现

每个查询都会显示家庭虚拟机上 10M 记录的性能数据 - db<>fiddle(30,000 条记录)结果在相对大小方面相当接近地反映了它们。

因此,在这种情况下,如果可能,请使用基于字符串的方法,但正则表达式可以帮助减少SLOC计数,但它们可能会变慢 - 由 DBA/Dev 在速度和复杂性之间做出选择。


Eva*_*oll 8

使用regexp_replace

您可以使用正则表达式和捕获括号来完成此操作regexp_replace

SELECT regexp_replace(t.x, '(.*)\$', '\1-' )
FROM ( VALUES ('bar$foo$john$doe$xxx') ) AS t(x);
Run Code Online (Sandbox Code Playgroud)

将替换最后$一个-。最终结果是,

bar$foo$john$doe-xxx
Run Code Online (Sandbox Code Playgroud)

下面是它的工作原理,

  • 捕获最后一个之前的所有内容$\1保存它。
  • 抓住但没有抓住最后一个$
  • 留下其他一切...

然后它恢复捕获并\1添加一个,-使字符串的其余部分保持不变。