Разделение строки на несколько строк в Oracle


104

Я знаю, что на этот вопрос в какой-то степени ответили PHP и MYSQL, но мне было интересно, может ли кто-нибудь научить меня простейшему подходу к разделению строки (с разделителями-запятыми) на несколько строк в Oracle 10g (предпочтительно) и 11g.

Таблица выглядит следующим образом:

Name | Project | Error 
108    test      Err1, Err2, Err3
109    test2     Err1

Я хочу создать следующее:

Name | Project | Error
108    Test      Err1
108    Test      Err2 
108    Test      Err3 
109    Test2     Err1

Я видел несколько потенциальных решений для стека, однако они учитывали только один столбец (являющийся строкой, разделенной запятыми). Любая помощь будет принята с благодарностью.


2
Для примеров использования REGEXP, XMLTABLEи MODELп, см Split , разделенных запятыми строк в таблице с помощью Oracle SQL
Лалита Кумар B

Ответы:


121

Это может быть улучшенный способ (также с regexp и connect by):

with temp as
(
    select 108 Name, 'test' Project, 'Err1, Err2, Err3' Error  from dual
    union all
    select 109, 'test2', 'Err1' from dual
)
select distinct
  t.name, t.project,
  trim(regexp_substr(t.error, '[^,]+', 1, levels.column_value))  as error
from 
  temp t,
  table(cast(multiset(select level from dual connect by  level <= length (regexp_replace(t.error, '[^,]+'))  + 1) as sys.OdciNumberList)) levels
order by name

РЕДАКТИРОВАТЬ : Вот простое (например, «не углубленное») объяснение запроса.

  1. length (regexp_replace(t.error, '[^,]+')) + 1используется regexp_replaceдля удаления всего, что не является разделителем (в данном случае запятой), и length +1для получения количества элементов (ошибок).
  2. select level from dual connect by level <= (...)Использует иерархический запрос , чтобы создать столбец с увеличением числа совпадений найдено, от 1 до общего числа ошибок.

    Предварительный просмотр:

    select level, length (regexp_replace('Err1, Err2, Err3', '[^,]+'))  + 1 as max 
    from dual connect by level <= length (regexp_replace('Err1, Err2, Err3', '[^,]+'))  + 1
    
  3. table(cast(multiset(.....) as sys.OdciNumberList)) выполняет кастинг типов оракулов.
    • В cast(multiset(.....)) as sys.OdciNumberListПреобразует несколько семейств (один сборник для каждой строки в исходном наборе данных) в единый набор чисел, OdciNumberList.
    • table()Функция превращает коллекцию в результирующий.
  4. FROMбез соединения создает перекрестное соединение между вашим набором данных и мультимножеством. В результате строка в наборе данных с 4 совпадениями будет повторяться 4 раза (с увеличением числа в столбце с именем "column_value").

    Предварительный просмотр:

    select * from 
    temp t,
    table(cast(multiset(select level from dual connect by  level <= length (regexp_replace(t.error, '[^,]+'))  + 1) as sys.OdciNumberList)) levels
    
  5. trim(regexp_substr(t.error, '[^,]+', 1, levels.column_value))использует в column_valueкачестве параметра nth_appearance / ocurrence для regexp_substr.
  6. Вы можете добавить некоторые другие столбцы из вашего набора данных ( t.name, t.projectв качестве примера) для упрощения визуализации.

Некоторые ссылки на документы Oracle:


7
Осторожно! Регулярное выражение формата '[^,]+'для синтаксического анализа строк не возвращает правильный элемент, если в списке есть нулевой элемент. См. Здесь для получения дополнительной информации: stackoverflow.com/questions/31464275/…
Gary_W

13
с 11g вы можете использовать regexp_count(t.error, ',')вместо length (regexp_replace(t.error, '[^,]+')), что может принести еще одно улучшение производительности
Štefan Oravec

1
485 секунд при "нормальном" CONNECT BY. Так 0,296 секунды. Ты жжешь! Теперь все, что мне нужно сделать, это понять, как это работает. :-)
Боб Джарвис - Восстановить Монику

@BobJarvis добавил правку, чтобы объяснить, что он делает. Правописание / грамматические исправления приветствуются.
Nefreo

«Принятый ответ плохо работает» - какой ответ принят в этой теме? Пожалуйста, используйте ссылки для ссылки на другой пост.
0xdb

28

регулярные выражения - замечательная штука :)

with temp as  (
       select 108 Name, 'test' Project, 'Err1, Err2, Err3' Error  from dual
       union all
       select 109, 'test2', 'Err1' from dual
     )

SELECT distinct Name, Project, trim(regexp_substr(str, '[^,]+', 1, level)) str
  FROM (SELECT Name, Project, Error str FROM temp) t
CONNECT BY instr(str, ',', 1, level - 1) > 0
order by Name

1
привет, не могли бы вы объяснить мне, почему приведенный выше запрос дает повторяющиеся строки, если я не использовал отдельное ключевое слово в запросе
Jagadeesh G

2
Этот запрос нельзя использовать из-за @JagadeeshG, особенно для огромных таблиц.
Michael-O

3
Чрезвычайно медленно, ниже есть лучший ответ
MoreCoffee

Причина медлительности в том, что каждая комбинация Names связана, что можно увидеть, если удалить distinct. К сожалению добавление and Name = prior Nameв connect byпункт причин ORA-01436: CONNECT BY loop in user data.
mik

Вы можете избежать ORA-01436ошибки, добавив AND name = PRIOR name(или любой другой первичный ключ) и AND PRIOR SYS_GUID() IS NOT NULL
Дэвид Фабер

28

Есть огромная разница между двумя нижеприведенными:

  • разделение одной строки с разделителями
  • разделение строк с разделителями на несколько строк в таблице.

Если вы не ограничиваете строки, то предложение CONNECT BY создаст несколько строк и не даст желаемого результата.

Помимо регулярных выражений , есть еще несколько альтернатив:

  • XMLTable
  • Предложение MODEL

Настроить

SQL> CREATE TABLE t (
  2    ID          NUMBER GENERATED ALWAYS AS IDENTITY,
  3    text        VARCHAR2(100)
  4  );

Table created.

SQL>
SQL> INSERT INTO t (text) VALUES ('word1, word2, word3');

1 row created.

SQL> INSERT INTO t (text) VALUES ('word4, word5, word6');

1 row created.

SQL> INSERT INTO t (text) VALUES ('word7, word8, word9');

1 row created.

SQL> COMMIT;

Commit complete.

SQL>
SQL> SELECT * FROM t;

        ID TEXT
---------- ----------------------------------------------
         1 word1, word2, word3
         2 word4, word5, word6
         3 word7, word8, word9

SQL>

Использование XMLTABLE :

SQL> SELECT id,
  2         trim(COLUMN_VALUE) text
  3  FROM t,
  4    xmltable(('"'
  5    || REPLACE(text, ',', '","')
  6    || '"'))
  7  /

        ID TEXT
---------- ------------------------
         1 word1
         1 word2
         1 word3
         2 word4
         2 word5
         2 word6
         3 word7
         3 word8
         3 word9

9 rows selected.

SQL>

Использование предложения MODEL :

SQL> WITH
  2  model_param AS
  3     (
  4            SELECT id,
  5                      text AS orig_str ,
  6                   ','
  7                          || text
  8                          || ','                                 AS mod_str ,
  9                   1                                             AS start_pos ,
 10                   Length(text)                                   AS end_pos ,
 11                   (Length(text) - Length(Replace(text, ','))) + 1 AS element_count ,
 12                   0                                             AS element_no ,
 13                   ROWNUM                                        AS rn
 14            FROM   t )
 15     SELECT   id,
 16              trim(Substr(mod_str, start_pos, end_pos-start_pos)) text
 17     FROM     (
 18                     SELECT *
 19                     FROM   model_param MODEL PARTITION BY (id, rn, orig_str, mod_str)
 20                     DIMENSION BY (element_no)
 21                     MEASURES (start_pos, end_pos, element_count)
 22                     RULES ITERATE (2000)
 23                     UNTIL (ITERATION_NUMBER+1 = element_count[0])
 24                     ( start_pos[ITERATION_NUMBER+1] = instr(cv(mod_str), ',', 1, cv(element_no)) + 1,
 25                     end_pos[iteration_number+1] = instr(cv(mod_str), ',', 1, cv(element_no) + 1) )
 26                 )
 27     WHERE    element_no != 0
 28     ORDER BY mod_str ,
 29           element_no
 30  /

        ID TEXT
---------- --------------------------------------------------
         1 word1
         1 word2
         1 word3
         2 word4
         2 word5
         2 word6
         3 word7
         3 word8
         3 word9

9 rows selected.

SQL>

1
Не могли бы вы подробнее пояснить, почему они должны быть, ('"' || REPLACE(text, ',', '","') || '"')а скобки не снимаются? Документы Oracle ([ docs.oracle.com/database/121/SQLRF/functions268.htm ) мне не понятны. Это так XQuery_string?
Betlista

@Betlista это выражение XQuery.
Lalit Kumar B

Решение XMLTABLE по какой-то причине постоянно не выводит последнюю запись для строк смешанной длины. Например. row1: 3 слова; row2: 2 слова, row3: 1 слово; row4: 2 слова, row5: 1 слово - последнее слово не выводится. Порядок строк не имеет значения.
Gnudiff

8

Еще пара примеров того же:

SELECT trim(regexp_substr('Err1, Err2, Err3', '[^,]+', 1, LEVEL)) str_2_tab
  FROM dual
CONNECT BY LEVEL <= regexp_count('Err1, Err2, Err3', ',')+1
/

SELECT trim(regexp_substr('Err1, Err2, Err3', '[^,]+', 1, LEVEL)) str_2_tab
  FROM dual
CONNECT BY LEVEL <= length('Err1, Err2, Err3') - length(REPLACE('Err1, Err2, Err3', ',', ''))+1
/

Также можно использовать DBMS_UTILITY.comma_to_table и table_to_comma: http://www.oracle-base.com/articles/9i/useful-procedures-and-functions-9i.php#DBMS_UTILITY.comma_to_table


Имейте в виду, что comma_to_table()работает только с токенами, которые соответствуют соглашениям об именах объектов базы данных Oracle. Он будет швырять веревку, '123,456,789'например.
APC

7

Я хотел бы предложить другой подход с использованием табличной функции PIPELINED. Это несколько похоже на метод XMLTABLE, за исключением того, что вы предоставляете свою собственную функцию для разделения строки символов:

-- Create a collection type to hold the results
CREATE OR REPLACE TYPE typ_str2tbl_nst AS TABLE OF VARCHAR2(30);
/

-- Split the string according to the specified delimiter
CREATE OR REPLACE FUNCTION str2tbl (
  p_string    VARCHAR2,
  p_delimiter CHAR DEFAULT ',' 
)
RETURN typ_str2tbl_nst PIPELINED
AS
  l_tmp VARCHAR2(32000) := p_string || p_delimiter;
  l_pos NUMBER;
BEGIN
  LOOP
    l_pos := INSTR( l_tmp, p_delimiter );
    EXIT WHEN NVL( l_pos, 0 ) = 0;
    PIPE ROW ( RTRIM( LTRIM( SUBSTR( l_tmp, 1, l_pos-1) ) ) );
    l_tmp := SUBSTR( l_tmp, l_pos+1 );
  END LOOP;
END str2tbl;
/

-- The problem solution
SELECT name, 
       project, 
       TRIM(COLUMN_VALUE) error
  FROM t, TABLE(str2tbl(error));

Полученные результаты:

      NAME PROJECT    ERROR
---------- ---------- --------------------
       108 test       Err1
       108 test       Err2
       108 test       Err3
       109 test2      Err1

Проблема с этим типом подхода заключается в том, что оптимизатор часто не знает количество элементов табличной функции, и ему приходится делать предположения. Это может быть потенциально вредным для ваших планов выполнения, поэтому это решение можно расширить для предоставления статистике выполнения оптимизатору.

Вы можете увидеть эту оценку оптимизатора, запустив EXPLAIN PLAN для запроса выше:

Execution Plan
----------------------------------------------------------
Plan hash value: 2402555806

----------------------------------------------------------------------------------------------
| Id  | Operation                          | Name    | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                   |         | 16336 |   366K|    59   (0)| 00:00:01 |
|   1 |  NESTED LOOPS                      |         | 16336 |   366K|    59   (0)| 00:00:01 |
|   2 |   TABLE ACCESS FULL                | T       |     2 |    42 |     3   (0)| 00:00:01 |
|   3 |   COLLECTION ITERATOR PICKLER FETCH| STR2TBL |  8168 | 16336 |    28   (0)| 00:00:01 |
----------------------------------------------------------------------------------------------

Несмотря на то, что в коллекции всего 3 значения, оптимизатор оценил для нее 8168 строк (значение по умолчанию). Сначала это может показаться несущественным, но оптимизатору этого может быть достаточно, чтобы выбрать неоптимальный план.

Решение состоит в том, чтобы использовать расширения оптимизатора для предоставления статистики для коллекции:

-- Create the optimizer interface to the str2tbl function
CREATE OR REPLACE TYPE typ_str2tbl_stats AS OBJECT (
  dummy NUMBER,

  STATIC FUNCTION ODCIGetInterfaces ( p_interfaces OUT SYS.ODCIObjectList )
  RETURN NUMBER,

  STATIC FUNCTION ODCIStatsTableFunction ( p_function  IN  SYS.ODCIFuncInfo,
                                           p_stats     OUT SYS.ODCITabFuncStats,
                                           p_args      IN  SYS.ODCIArgDescList,
                                           p_string    IN  VARCHAR2,
                                           p_delimiter IN  CHAR DEFAULT ',' )
  RETURN NUMBER
);
/

-- Optimizer interface implementation
CREATE OR REPLACE TYPE BODY typ_str2tbl_stats
AS
  STATIC FUNCTION ODCIGetInterfaces ( p_interfaces OUT SYS.ODCIObjectList )
  RETURN NUMBER
  AS
  BEGIN
    p_interfaces := SYS.ODCIObjectList ( SYS.ODCIObject ('SYS', 'ODCISTATS2') );
    RETURN ODCIConst.SUCCESS;
  END ODCIGetInterfaces;

  -- This function is responsible for returning the cardinality estimate
  STATIC FUNCTION ODCIStatsTableFunction ( p_function  IN  SYS.ODCIFuncInfo,
                                           p_stats     OUT SYS.ODCITabFuncStats,
                                           p_args      IN  SYS.ODCIArgDescList,
                                           p_string    IN  VARCHAR2,
                                           p_delimiter IN  CHAR DEFAULT ',' )
  RETURN NUMBER
  AS
  BEGIN
    -- I'm using basically half the string lenght as an estimator for its cardinality
    p_stats := SYS.ODCITabFuncStats( CEIL( LENGTH( p_string ) / 2 ) );
    RETURN ODCIConst.SUCCESS;
  END ODCIStatsTableFunction;

END;
/

-- Associate our optimizer extension with the PIPELINED function   
ASSOCIATE STATISTICS WITH FUNCTIONS str2tbl USING typ_str2tbl_stats;

Тестирование полученного плана выполнения:

Execution Plan
----------------------------------------------------------
Plan hash value: 2402555806

----------------------------------------------------------------------------------------------
| Id  | Operation                          | Name    | Rows  | Bytes | Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                   |         |     1 |    23 |    59   (0)| 00:00:01 |
|   1 |  NESTED LOOPS                      |         |     1 |    23 |    59   (0)| 00:00:01 |
|   2 |   TABLE ACCESS FULL                | T       |     2 |    42 |     3   (0)| 00:00:01 |
|   3 |   COLLECTION ITERATOR PICKLER FETCH| STR2TBL |     1 |     2 |    28   (0)| 00:00:01 |
----------------------------------------------------------------------------------------------

Как видите, количество элементов в приведенном выше плане больше не является предполагаемым значением 8196. Это по-прежнему неправильно, потому что мы передаем функции столбец вместо строкового литерала.

Для более точной оценки в данном конкретном случае потребуется некоторая настройка кода функции, но я думаю, что здесь в значительной степени объясняется общая концепция.

Функция str2tbl, используемая в этом ответе, была первоначально разработана Томом Кайтом: https://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:110612348061

Концепцию связывания статистики с типами объектов можно подробнее изучить, прочитав эту статью: http://www.oracle-developer.net/display.php?id=427

Описанная здесь техника работает в 10g +.


4

REGEXP_COUNT не было добавлено до Oracle 11i. Вот решение Oracle 10g, заимствованное из решения Art.

SELECT trim(regexp_substr('Err1, Err2, Err3', '[^,]+', 1, LEVEL)) str_2_tab
  FROM dual
CONNECT BY LEVEL <=
  LENGTH('Err1, Err2, Err3')
    - LENGTH(REPLACE('Err1, Err2, Err3', ',', ''))
    + 1;

Как я могу добавить фильтр для этого, скажем, я хочу фильтровать только name = '108'. Я попытался добавить where после предложения from, но в итоге остались дубликаты.
DRTauli

4

Начиная с Oracle 12c вы могли использовать JSON_TABLEи JSON_ARRAY:

CREATE TABLE tab(Name, Project, Error) AS
SELECT 108,'test' ,'Err1, Err2, Err3' FROM dual UNION 
SELECT 109,'test2','Err1'             FROM dual;

И запрос:

SELECT *
FROM tab t
OUTER APPLY (SELECT TRIM(p) AS p
            FROM JSON_TABLE(REPLACE(JSON_ARRAY(t.Error), ',', '","'),
           '$[*]' COLUMNS (p VARCHAR2(4000) PATH '$'))) s;

Вывод:

┌──────┬─────────┬──────────────────┬──────┐
 Name  Project       Error         P   
├──────┼─────────┼──────────────────┼──────┤
  108  test     Err1, Err2, Err3  Err1 
  108  test     Err1, Err2, Err3  Err2 
  108  test     Err1, Err2, Err3  Err3 
  109  test2    Err1              Err1 
└──────┴─────────┴──────────────────┴──────┘

db <> демо скрипта


1
Я признаю, что это умный трюк, но, честно говоря, меня бы озадачило, если бы я наткнулся на него в кодовой базе.
APC

@APC Это просто демонстрация возможностей SQL. Если бы мне пришлось использовать такой код в своей кодовой базе, я бы определенно обернул его функцией или оставил бы расширенный комментарий :)
Лукаш Шозда

Конечно. Просто этот поток является одним из наиболее популярных способов токенизации строк с помощью Oracle, поэтому я думаю, что мы должны включить предостережения в отношении более экзотических решений, чтобы защитить невиновных от самих себя :)
APC

3

Вот альтернативная реализация с использованием XMLTABLE, которая позволяет выполнять приведение к различным типам данных:

select 
  xmltab.txt
from xmltable(
  'for $text in tokenize("a,b,c", ",") return $text'
  columns 
    txt varchar2(4000) path '.'
) xmltab
;

... или если ваши строки с разделителями хранятся в одной или нескольких строках таблицы:

select 
  xmltab.txt
from (
  select 'a;b;c' inpt from dual union all
  select 'd;e;f' from dual
) base
inner join xmltable(
  'for $text in tokenize($input, ";") return $text'
  passing base.inpt as "input"
  columns 
    txt varchar2(4000) path '.'
) xmltab
  on 1=1
;

Я думаю, что это решение работает для Oracle 11.2.0.3 и более поздних версий.
APC

2

Хочу добавить еще один метод. Здесь используются рекурсивные запросы, чего я не видел в других ответах. Он поддерживается Oracle с 11gR2.

with cte0 as (
    select phone_number x
    from hr.employees
), cte1(xstr,xrest,xremoved) as (
        select x, x, null
        from cte0
    union all        
        select xstr,
            case when instr(xrest,'.') = 0 then null else substr(xrest,instr(xrest,'.')+1) end,
            case when instr(xrest,'.') = 0 then xrest else substr(xrest,1,instr(xrest,'.') - 1) end
        from cte1
        where xrest is not null
)
select xstr, xremoved from cte1  
where xremoved is not null
order by xstr

Это довольно гибко с символом разделения. Просто поменяйте его в INSTRзвонках.


2

Без использования connect by или regexp :

    with mytable as (
      select 108 name, 'test' project, 'Err1,Err2,Err3' error from dual
      union all
      select 109, 'test2', 'Err1' from dual
    )
    ,x as (
      select name
      ,project
      ,','||error||',' error
      from mytable
    )
    ,iter as (SELECT rownum AS pos
        FROM all_objects
    )
    select x.name,x.project
    ,SUBSTR(x.error
      ,INSTR(x.error, ',', 1, iter.pos) + 1
      ,INSTR(x.error, ',', 1, iter.pos + 1)-INSTR(x.error, ',', 1, iter.pos)-1
    ) error
    from x, iter
    where iter.pos < = (LENGTH(x.error) - LENGTH(REPLACE(x.error, ','))) - 1;

1

У меня была такая же проблема, и мне помог xmltable:

ВЫБРАТЬ id, обрезать (COLUMN_VALUE) текст FROM t, xmltable (('"' || REPLACE (text, ',', '", "') || '"'))


0

В Oracle 11g и более поздних версиях вы можете использовать рекурсивный подзапрос и простые строковые функции (которые могут быть быстрее, чем регулярные выражения и коррелированные иерархические подзапросы):

Настройка Oracle :

CREATE TABLE table_name ( name, project, error ) as
 select 108, 'test',  'Err1, Err2, Err3' from dual union all
 select 109, 'test2', 'Err1'             from dual;

Запрос :

WITH table_name_error_bounds ( name, project, error, start_pos, end_pos ) AS (
  SELECT name,
         project,
         error,
         1,
         INSTR( error, ', ', 1 )
  FROM   table_name
UNION ALL
  SELECT name,
         project,
         error,
         end_pos + 2,
         INSTR( error, ', ', end_pos + 2 )
  FROM   table_name_error_bounds
  WHERE  end_pos > 0
)
SELECT name,
       project,
       CASE end_pos
       WHEN 0
       THEN SUBSTR( error, start_pos )
       ELSE SUBSTR( error, start_pos, end_pos - start_pos )
       END AS error
FROM   table_name_error_bounds

Выход :

ИМЯ | ПРОЕКТ | ОШИБКА
---: | : ------ | : ----
 108 | тест | Err1
 109 | test2 | Err1
 108 | тест | Err2
 108 | тест | Err3

db <> скрипка здесь


-1

Я использовал функцию DBMS_UTILITY.comma_to _table, которая фактически работает с кодом следующим образом

declare
l_tablen  BINARY_INTEGER;
l_tab     DBMS_UTILITY.uncl_array;
cursor cur is select * from qwer;
rec cur%rowtype;
begin
open cur;
loop
fetch cur into rec;
exit when cur%notfound;
DBMS_UTILITY.comma_to_table (
     list   => rec.val,
     tablen => l_tablen,
     tab    => l_tab);
FOR i IN 1 .. l_tablen LOOP
    DBMS_OUTPUT.put_line(i || ' : ' || l_tab(i));
END LOOP;
end loop;
close cur;
end; 

я использовал свои собственные имена таблиц и столбцов


5
Имейте в виду, что comma_to_table()работает только с токенами, которые соответствуют соглашениям об именах объектов базы данных Oracle. Он будет швырять веревку, '123,456,789'например.
APC

можем ли мы реализовать использование временных таблиц?
Smart003 02

1
Умм, учитывая все другие работоспособные решения, зачем нам использовать временные таблицы, которые связаны с огромными накладными расходами на материализацию данных?
APC
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.