Division de la chaîne en plusieurs lignes dans Oracle


104

Je sais que cela a été répondu dans une certaine mesure avec PHP et MYSQL, mais je me demandais si quelqu'un pourrait m'apprendre l'approche la plus simple pour diviser une chaîne (délimitée par des virgules) en plusieurs lignes dans Oracle 10g (de préférence) et 11g.

Le tableau est le suivant:

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

Je souhaite créer ce qui suit:

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

J'ai vu quelques solutions potentielles autour de la pile, mais elles ne représentaient qu'une seule colonne (étant la chaîne délimitée par des virgules). Toute aide serait grandement appréciée.


2
Pour obtenir des exemples d'utilisation de la clause REGEXP, XMLTABLEet MODEL, consultez Fractionner les chaînes délimitées par des virgules dans une table à l'aide d'Oracle SQL
Lalit Kumar B

Réponses:


121

Cela peut être un moyen amélioré (également avec regexp et se connecter par):

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 : Voici une explication simple (comme dans, "pas en profondeur") de la requête.

  1. length (regexp_replace(t.error, '[^,]+')) + 1utilise regexp_replacepour effacer tout ce qui n'est pas le délimiteur (virgule dans ce cas) et length +1pour obtenir le nombre d'éléments (erreurs) présents.
  2. Le select level from dual connect by level <= (...)utilise une requête hiérarchique pour créer une colonne avec un nombre croissant de correspondances trouvées, de 1 au nombre total d'erreurs.

    Aperçu:

    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)) fait du casting de types d'oracle.
    • Le cast(multiset(.....)) as sys.OdciNumberListtransforme plusieurs collections (une collection pour chaque ligne de l'ensemble de données d'origine) en une seule collection de nombres, OdciNumberList.
    • La table()fonction transforme une collection en un ensemble de résultats.
  4. FROMsans jointure crée une jointure croisée entre votre ensemble de données et le multiset. En conséquence, une ligne dans l'ensemble de données avec 4 correspondances se répétera 4 fois (avec un nombre croissant dans la colonne nommée "valeur_colonne").

    Aperçu:

    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))utilise le column_valuecomme paramètre nth_appearance / occurrence pour regexp_substr.
  6. Vous pouvez ajouter d'autres colonnes de votre ensemble de données (à t.name, t.projecttitre d'exemple) pour une visualisation facile.

Quelques références à des documents Oracle:


7
Il faut se méfier! Une expression régulière du format '[^,]+'pour analyser les chaînes ne renvoie pas l'élément correct s'il y a un élément nul dans la liste. Voir ici pour plus d'informations: stackoverflow.com/questions/31464275/…
Gary_W

13
depuis 11g, vous pouvez utiliser à la regexp_count(t.error, ',')place de length (regexp_replace(t.error, '[^,]+')), ce qui peut apporter une autre amélioration des performances
Štefan Oravec

1
485 secondes avec "normal" CONNECT BY. 0,296 seconde de cette façon. Tu gères! Il ne me reste plus qu'à comprendre comment cela fonctionne. :-)
Bob Jarvis - Réintégrer Monica le

@BobJarvis a ajouté une modification pour expliquer ce qu'il fait. Les corrections orthographiques / grammaticales sont les bienvenues.
Nefreo

"La réponse acceptée a des performances médiocres" - quelle est la réponse acceptée dans ce sujet? Veuillez utiliser les liens pour référencer l'autre article.
0xdb

28

les expressions régulières sont une chose merveilleuse :)

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
salut pouvez-vous s'il vous plaît me clarifier pourquoi la requête ci-dessus donne des lignes en double si je n'ai pas utilisé de mot clé distinct dans la requête
Jagadeesh G

2
Cette requête est inutilisable en raison de @JagadeeshG, en particulier sur de grandes tables.
Michael-O

3
Extrêmement lent, il y a une meilleure réponse ci
MoreCoffee

La raison de la lenteur est que chaque combinaison de Names est connectée, ce qui peut être vu si vous supprimez distinct. Malheureusement ajouter and Name = prior Nameà la connect byclause provoque ORA-01436: CONNECT BY loop in user data.
mik

Vous pouvez éviter l' ORA-01436erreur en ajoutant AND name = PRIOR name(ou quelle que soit la clé primaire) et AND PRIOR SYS_GUID() IS NOT NULL
David Faber

28

Il y a une énorme différence entre les deux ci-dessous:

  • fractionnement d'une seule chaîne délimitée
  • fractionnement de chaînes délimitées pour plusieurs lignes dans une table.

Si vous ne limitez pas les lignes, la clause CONNECT BY produira plusieurs lignes et ne donnera pas la sortie souhaitée.

Outre les expressions régulières , quelques autres alternatives utilisent:

  • XMLTable
  • Clause MODEL

Installer

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>

Utilisation de 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>

Utilisation de la clause 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
Pouvez-vous expliquer plus en détail pourquoi il doit y en avoir ('"' || REPLACE(text, ',', '","') || '"')et les crochets ne peuvent pas être supprimés? Les documents Oracle ([ docs.oracle.com/database/121/SQLRF/functions268.htm ) ne sont pas clairs pour moi. C'est ça XQuery_string?
Betlista

@Betlista c'est une expression XQuery.
Lalit Kumar B du

La solution XMLTABLE, pour une raison quelconque, échoue constamment à afficher la dernière entrée pour des lignes de longueur mixte. Par exemple. row1: 3 mots; rangée2: 2 mots, rangée3: 1 mot; row4: 2 mots, row5: 1 word - ne produira pas le dernier mot. L'ordre des lignes n'a pas d'importance.
Gnudiff

7

Quelques autres exemples de la même chose:

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
/

Vous pouvez également utiliser 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


Sachez que cela comma_to_table()ne fonctionne qu'avec des jetons qui correspondent aux conventions de dénomination des objets de base de données Oracle. Il se lancera sur une corde comme '123,456,789'par exemple.
APC

7

Je voudrais proposer une approche différente utilisant une fonction de table PIPELINED. C'est un peu similaire à la technique du XMLTABLE, sauf que vous fournissez votre propre fonction personnalisée pour diviser la chaîne de caractères:

-- 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));

Résultats:

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

Le problème avec ce type d'approche est que souvent l'optimiseur ne connaîtra pas la cardinalité de la fonction de table et devra faire une estimation. Cela peut potentiellement nuire à vos plans d'exécution, cette solution peut donc être étendue pour fournir des statistiques d'exécution pour l'optimiseur.

Vous pouvez voir cette estimation de l'optimiseur en exécutant un EXPLAIN PLAN sur la requête ci-dessus:

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 |
----------------------------------------------------------------------------------------------

Même si la collection n'a que 3 valeurs, l'optimiseur a estimé 8168 lignes pour elle (valeur par défaut). Cela peut sembler hors de propos au début, mais il peut suffire à l'optimiseur de décider d'un plan sous-optimal.

La solution consiste à utiliser les extensions d'optimisation pour fournir des statistiques pour la collection:

-- 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;

Test du plan d'exécution résultant:

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 |
----------------------------------------------------------------------------------------------

Comme vous pouvez le voir, la cardinalité sur le plan ci-dessus n'est plus la valeur estimée 8196. Ce n'est toujours pas correct car nous passons une colonne au lieu d'une chaîne littérale à la fonction.

Quelques ajustements au code de fonction seraient nécessaires pour donner une estimation plus précise dans ce cas particulier, mais je pense que le concept global est assez bien expliqué ici.

La fonction str2tbl utilisée dans cette réponse a été initialement développée par Tom Kyte: https://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:110612348061

Le concept d'association de statistiques avec des types d'objets peut être exploré plus en détail en lisant cet article: http://www.oracle-developer.net/display.php?id=427

La technique décrite ici fonctionne en 10g +.


4

REGEXP_COUNT n'a été ajouté qu'après Oracle 11i. Voici une solution Oracle 10g, adoptée à partir de la solution d'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;

Comment puis-je ajouter un filtre pour cela, disons que je veux filtrer avec uniquement le nom = '108'. J'ai essayé d'ajouter un where après la clause from mais je me suis retrouvé avec des doublons.
DRTauli

4

À partir d'Oracle 12c, vous pouvez utiliser JSON_TABLEet JSON_ARRAY:

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

Et requête:

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;

Production:

┌──────┬─────────┬──────────────────┬──────┐
 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 
└──────┴─────────┴──────────────────┴──────┘

démo db <> fiddle


1
Je concède que c'est une astuce intelligente, mais franchement, cela me dérangerait si je le rencontrais dans une base de code.
APC

@APC Ceci est juste une démonstration de ce qui est possible avec SQL. Si je dois utiliser un tel code dans ma base de code, je l'envelopperais certainement dans une fonction ou je laisserais un commentaire prolongé :)
Lukasz Szozda

Bien sûr. C'est juste que ce fil est l'un des succès les plus populaires pour la tokenisation de chaîne avec Oracle, donc je pense que nous devrions inclure des mises en garde sur les solutions les plus exotiques, pour protéger les innocents d'eux-mêmes :)
APC

3

Voici une implémentation alternative utilisant XMLTABLE qui permet la conversion en différents types de données:

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

... ou si vos chaînes délimitées sont stockées dans une ou plusieurs lignes d'une table:

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
;

Je pense que cette solution fonctionne pour Oracle 11.2.0.3 et les versions ultérieures.
APC

2

J'aimerais ajouter une autre méthode. Celui-ci utilise des requêtes récursives, ce que je n'ai pas vu dans les autres réponses. Il est pris en charge par Oracle depuis 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

Il est assez flexible avec le caractère de division. Changez-le simplement dans les INSTRappels.


2

Sans utiliser connect by ou 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

J'ai eu le même problème et xmltable m'a aidé:

SELECT id, trim (COLUMN_VALUE) text FROM t, xmltable (('"' || REPLACE (text, ',', '", "') || '"'))


0

Dans Oracle 11g et versions ultérieures, vous pouvez utiliser une sous-requête récursive et des fonctions de chaîne simples (qui peuvent être plus rapides que les expressions régulières et les sous-requêtes hiérarchiques corrélées):

Configuration d'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;

Requête :

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

Sortie :

NOM | PROJET | ERREUR
---: | : ------ | : ----
 108 | test | Err1
 109 | test2 | Err1
 108 | test | Err2
 108 | test | Err3

db <> violon ici


-1

J'avais utilisé la fonction DBMS_UTILITY.comma_to _table en fait son fonctionnement le code comme suit

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; 

j'avais utilisé mes propres noms de table et de colonne


5
Sachez que cela comma_to_table()ne fonctionne qu'avec des jetons qui correspondent aux conventions de dénomination des objets de base de données Oracle. Il se lancera sur une corde comme '123,456,789'par exemple.
APC

pouvons-nous implémenter en utilisant des tables temporaires?
Smart003

1
Euh, étant donné toutes les autres solutions réalisables, pourquoi voudrions-nous utiliser des tables temporaires qui entraînent une surcharge massive de matérialisation des données?
APC
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.