【问题标题】:SQL Strip Longest Common PrefixSQL Strip 最长公共前缀
【发布时间】:2020-02-10 05:31:13
【问题描述】:

我有一个表 tbl1 有两列 col1col2 包含字符串:

col1    | col2
--------+--------
bar     | foo
foo     | foobar
bar1foo | bar2foo

对应的SQL转储:

CREATE TABLE `tbl1` (
  `col1` varchar(20) COLLATE latin1_general_ci NOT NULL,
  `col2` varchar(20) COLLATE latin1_general_ci NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=latin1 COLLATE=latin1_general_ci;

INSERT INTO `tbl1` (`col1`, `col2`) VALUES
('bar', 'foo'),
('foo', 'foobar'),
('bar1foo', 'bar2foo');

在大多数情况下,条目的字符串共享一个公共前缀。我需要一个去除那些常见前缀的查询。预期结果:

bar  | foo
     | bar
1foo | 2foo

到目前为止我的方法:

SELECT
SUBSTR(`col1`, 1+GREATEST(LENGTH(`col1`), LENGTH(`col2`)) - CEIL(LENGTH(TRIM(TRAILING '0' FROM HEX(ABS(CONV(HEX(REVERSE(`col1`)),16,10) - CONV(HEX(REVERSE(`col2`)),16,10)))))/2)),
SUBSTR(`col2`, 1+GREATEST(LENGTH(`col1`), LENGTH(`col2`)) - CEIL(LENGTH(TRIM(TRAILING '0' FROM HEX(ABS(CONV(HEX(REVERSE(`col1`)),16,10) - CONV(HEX(REVERSE(`col2`)),16,10)))))/2))
FROM tbl1

简短说明:字符串取反(REVERSE),转换成整数(HEXCONV),相互相减(-ABS),转换成十六进制表示(@987654333 @), 0's 从末尾修剪 (TRIM),从最长字符串的长度 (-, LENGTHGREATEST) 中减去此结果的长度,然后由SUBSTR 获取结果。

我的方法存在问题:

  • 不适用于长度超过 64 位的字符串。
  • 不适用于包含多字节字符的字符串
  • 又长又丑
  • 性能不佳。

【问题讨论】:

  • 这不是sql擅长的。只是说'

标签: mysql sql string prefix


【解决方案1】:

此代码有效,尽管它冗长而丑陋并且(也许)性能不佳

select 
  substring(t.col1, g.maxlen + 1) col1, 
  substring(t.col2, g.maxlen + 1) col2
from tbl1 t inner join (
  select t.col1, t.col2,
    max(case when left(col1, tt.n) = left(col2, tt.n) then tt.n else 0 end) maxlen
  from tbl1 t inner join (
    select 1 n union all select 2 union all  select 3 union all  select 4 union all 
    select 5 union all  select 6 union all  select 7 union all  select 8 union all  
    select 9 union all  select 10 union all  select 11 union all  select 12 union all 
    select 13 union all  select 14 union all  select 15 union all  select 16 union all 
    select 17 union all  select 18 union all  select 19 union all  select 20
  ) tt on least(length(t.col1), length(t.col2)) >= tt.n 
  group by t.col1, t.col2
) g on g.col1 = t.col1 and g.col2 = t.col2   

请参阅demo
对于 MySql 8.0+,您可以使用 recursive CTE,在这种情况下,不需要事先了解列的长度:

with 
  recursive lengths as (
    select 1 n
    union all
    select n + 1
    from lengths
    where n < (select max(least(length(col1), length(col2))) from tbl1)
  ),
  cte as (
    select t.col1, t.col2,
      max(case when left(col1, l.n) = left(col2, l.n) then l.n else 0 end) maxlen
    from tbl1 t inner join lengths l      
    on least(length(t.col1), length(t.col2)) >= l.n 
    group by t.col1, t.col2                                
  )  
select 
  substring(t.col1, c.maxlen + 1) col1, 
  substring(t.col2, c.maxlen + 1) col2
from tbl1 t inner join cte c 
on c.col1 = t.col1 and c.col2 = t.col2  

请参阅demo
结果:

| col1 | col2 |
| ---- | ---- |
|      | bar  |
| bar  | foo  |
| 1foo | 2foo |

【讨论】:

  • 对于这个解决方案,需要先验知道字符串的长度。但是,它对我有用。谢谢!
【解决方案2】:

可悲的是,最通用和最有效的方法可能是一个巨大的case 表达式。但是,这只适用于一定的长度:

select substr(col1, prefix_length + 1),
       substr(col2, prefix_length + 1)
from (select tbl1.*,
             (case when left(col1, 10) = left(col2, 10) then 10
                   when left(col1, 9) = left(col2, 9) then 9
                   . . .
                   else 0
              end) as prefix_length
      from tbl1
     ) t;

实际上,您可以使用递归 CTE 来做到这一点,这是最通用的方法:

with recursive cte as (
      select col1, col2, 1 as lev, col1 as orig_col1, col2 as orig_col2
      from tbl1
      union all
      select substr(col1, 2), substr(col2, 2), lev + 1, orig_col1, orig_col2
      from cte
      where left(col1, 1) = left(col2, 1)
     )
select col1, col2
from (select cte.*,
             dense_rank() over (partition by orig_col1, orig_col2 order by lev desc) as seqnum
      from cte
     ) x
where seqnum = 1;

虽然性能肯定会比您的解决方案或大量的case 表达式更差,但它可能并没有那么糟糕,您可能会发现它足以满足您的目的。

Here 是一个 db两种解决方案的小提琴。

【讨论】:

  • 我接受了您的回答,因为它为任意长度的字符串提供了解决方案。
猜你喜欢
  • 2021-09-11
  • 1970-01-01
  • 2022-11-22
  • 2018-09-30
  • 2013-04-14
  • 2012-02-01
  • 2020-07-05
  • 2022-01-11
  • 2021-10-12
相关资源
最近更新 更多