SQL Strip самый длинный общий префикс - PullRequest
1 голос
/ 13 октября 2019

У меня есть таблица tbl1 с двумя столбцами col1 и col2, содержащая строки:

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), преобразованы в целые числа (HEX и * 1018)*) вычитаются друг из друга (- и ABS), преобразуются в шестнадцатеричное представление (HEX), 0 обрезаются с конца (TRIM), длина этого результата вычитаетсяиз длины самой длинной строки (-, LENGTH и GREATEST), а затем используется SUBSTR для получения результата.

Проблемы с моим подходом:

  • Не работает со строками длиннее 64 бит.
  • Не работает со строками, содержащими многобайтовые символы
  • Очень длинные и некрасивые
  • Не имеет хорошей производительности.

Ответы [ 2 ]

1 голос
/ 13 октября 2019

К сожалению, самый общий и эффективный метод, вероятно, гигантское выражение 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, оно, вероятно, не так уж плохо, и вы можете найти его достаточным для ваших целей.

Здесь - это дБ <>возьмите оба решения.

1 голос
/ 13 октября 2019

Этот код работает, хотя он длинный и некрасивый и (возможно) несоответствующий :

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   

См. Демонстрационную версию . Для 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  

См. Демонстрационную версию . Результаты:

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