Розбиття рядка на кілька рядків в 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

Це може бути вдосконалений спосіб (також за допомогою регулярного вибору та підключення):

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

EDIT : Ось просте (як у "не в глибині") пояснення запиту.

  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 / входженням параметра для regexp_substr.
  6. Ви можете додати деякі інші стовпці зі свого набору даних ( t.name, t.projectяк приклад) для легкої візуалізації.

Деякі посилання на документи Oracle:


7
Остерігайся! Зворотний формат формату '[^,]+'для розбору рядків не повертає правильний елемент, якщо в списку є нульовий елемент. Дивіться тут для отримання додаткової інформації: stackoverflow.com/questions/31464275 / ...
Gary_W

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

1
485 секунд при "нормальному" З'ЄДНУЄТЬСЯ. 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.
МІК

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

28

Існує величезна різниця між наступними двома:

  • розщеплення однієї обмеженої струни
  • розділення розділених рядків на кілька рядків у таблиці.

Якщо ви не обмежите рядки, то пункт CONNECT BY створить кілька рядків і не дасть бажаного результату.

Окрім регулярних виразів , деякі інші варіанти використовують:

  • XMLTable
  • Стаття МОДЕЛЬ

Налаштування

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.
Лаліт Кумар Б

Рішення XMLTABLE чомусь постійно не вдається вивести останній запис для рядків змішаної довжини. Напр. рядок1: 3 слова; рядок2: 2 слова, рядок3: 1 слово; row4: 2 слова, row5: 1 слово - не виведе останнє слово. Впорядкування рядків не має значення.
Gnudiff

7

Ще кілька прикладів того ж:

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()працює лише з маркерами, які відповідають умовам іменування об'єктів бази даних. Він буде кидатись на рядок, як '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

Проблема такого типу підходу полягає в тому, що часто оптимізатор не буде знати кардинальність функції таблиці, і їй доведеться здогадуватися. Це може бути потенційно шкідливим для ваших планів виконання, тому це рішення можна розширити, щоб забезпечити статистику виконання оптимізатора.

Ви можете побачити цю оцінку оптимізатора, виконавши ПОЯСНИЙ ПЛАН за вищезазначеним запитом:

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 steps::::P11_QUESTION_ID:110612348061

Поняття асоціації статистики з типами об'єктів можна детальніше вивчити, прочитавши цю статтю: http://www.oracle-developer.net/display.php?id=427

Описана тут методика працює в 10g +.


4

REGEXP_COUNT не додано до Oracle 11i. Ось 10-річне рішення Oracle, прийняте з рішення 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;

Як я можу додати фільтр для цього, скажемо, що хочу фільтрувати лише з ім'ям = '108'. Я спробував додати пункт де після пункту, але в кінцевому підсумку з дублікатами.
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

Без використання підключення за допомогою або 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 допоміг мені:

Виберіть ідентифікатор, обрізання (COLUMN_VALUE) тексту від t, xmltable (('' '|| ЗАМІНА (текст,', ',' "," ') ||' "))


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 | тест | Помилка1
 109 | тест2 | Помилка1
 108 | тест | Помилка2
 108 | тест | Помилка3

db <> скрипка тут


-1

Я використовував функцію _table DBMS_UTILITY.comma_to, фактично її код працює наступним чином

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

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