Skip to Main Content

Breadcrumb

Question and Answer

Chris Saxon

Thanks for the question, Narayana.

Asked: January 21, 2026 - 8:48 am UTC

Last updated: January 30, 2026 - 1:57 pm UTC

Version: 19c

Viewed 1000+ times

You Asked

table has two rows

select id,value from IIO_SYSTEM_PARAMETER;

200 1027/1029/1000
199 1027/1029/1000


when I ran the below query, it is giving 7 records, instead of 3 records

query1:

 SELECT
            REGEXP_SUBSTR(VALUE, '[^/]+', 1, LEVEL) DEST
        FROM
            IIO_SYSTEM_PARAMETER
        WHERE id =200
        CONNECT BY
            LEVEL <= LENGTH(VALUE) - LENGTH(REPLACE(VALUE, '/')) + 1
    ;



so, I created temp table with required row only

create table temp as
select  * from  IIO_SYSTEM_PARAMETER
        WHERE id =200;



and ran the string to rows query on temp table

query 2:

SELECT
            REGEXP_SUBSTR(VALUE, '[^/]+', 1, LEVEL) DEST
        FROM
            temp
        CONNECT BY
            LEVEL <= LENGTH(VALUE) - LENGTH(REPLACE(VALUE, '/')) + 1;


and gave 3 records as result(which is correct)

why query 1 is giving 7 records, but query 2 is giving 3 records .



complete code:

     DROP TABLE IIO_SYSTEM_PARAMETER CASCADE CONSTRAINTS;

CREATE TABLE IIO_SYSTEM_PARAMETER
(
  ID        INTEGER                             NOT NULL,
  MODULE    VARCHAR2(50 CHAR)                   NOT NULL,
  NAME      VARCHAR2(50 CHAR)                   NOT NULL,
  DESCR     VARCHAR2(200 CHAR),
  VALUE     VARCHAR2(2000 CHAR)                 DEFAULT ' ',
  COMMENTS  VARCHAR2(2000 CHAR)
);

Insert into IIO_SYSTEM_PARAMETER
   (ID, MODULE, NAME, DESCR, VALUE)
 Values
   (200, 'DF_USER_INPUT', 'U_GOLIVE_LOC_OrderOpt', 'List of Locations going live in Order Opt', '1027/1029/1000');
Insert into IIO_SYSTEM_PARAMETER
   (ID, MODULE, NAME, DESCR, VALUE)
 Values
   (199, 'DF_USER_INPUT', 'U_GOLIVE_LOC', 'List of Locations going live', '1027/1029/1000');

commit;

select id,value from IIO_SYSTEM_PARAMETER;

drop table temp;

create table temp as
select  * from  IIO_SYSTEM_PARAMETER
        WHERE id =200;
     
select count(*) from (
 SELECT
            REGEXP_SUBSTR(VALUE, '[^/]+', 1, LEVEL) DEST
        FROM
            temp
        CONNECT BY
            LEVEL <= LENGTH(VALUE) - LENGTH(REPLACE(VALUE, '/')) + 1);--3records



 SELECT
            REGEXP_SUBSTR(VALUE, '[^/]+', 1, LEVEL) DEST
        FROM
            IIO_SYSTEM_PARAMETER
WHERE id =200
        CONNECT BY
            LEVEL <= LENGTH(VALUE) - LENGTH(REPLACE(VALUE, '/')) + 1    ;--7 records


and Chris said...

The CONNECT BY LEVEL trick applies to every row in the input table. The WHERE clause filters the result of this hierarchy. So this links rows with ID 199 and 200, then hides those for ID 199.

Removing the WHERE clause and adding LEVEL and columns from the resource table may make this clearer:

select regexp_substr(value, '[^/]+', 1, level) dest, level, p.id
from   iio_system_parameter p
connect by nocycle level <= length(value) - length(replace(value, '/')) + 1;  

DEST    LEVEL    ID     
1027           1    200 
1029           2    200 
1000           3    200 
1000           3    199 
1029           2    199 
1000           3    200 
1000           3    199 
1027           1    199 
1029           2    200 
1000           3    200 
1000           3    199 
1029           2    199 
1000           3    200 
1000           3    199 


To overcome this, generate the rows in a LATERAL or CROSS APPLY subquery. This uses the values from each row of the source table to generate the output.

For example:

select regexp_substr(value, '[^/]+', 1, lvl) dest
from   iio_system_parameter, 
       lateral ( 
         select level lvl from dual 
         connect by level <= length(value) - length(replace(value, '/')) + 1    
       )
where  id =200;

DEST    LVL    ID     
1027         1    200 
1029         2    200 
1000         3    200 

Rating

  (3 ratings)

Comments

Using JSON_TABLE

Rajeshwaran Jeyabal, January 24, 2026 - 3:06 pm UTC

In addition to the Lateral and Cross apply , another option would be to use JSON_TABLE, something like this

demo@ORA26AI> select *
  2  from IIO_SYSTEM_PARAMETER ,
  3      json_table( json('['||replace(value,'/',',')||']') ,'$[*]'
  4          columns ( c1 number path '$' error on error) ) jt;

        ID VALUE                                  C1
---------- ------------------------------ ----------
       199 1027/1029/1000                       1027
       199 1027/1029/1000                       1029
       199 1027/1029/1000                       1000
       200 1027/1029/1000                       1027
       200 1027/1029/1000                       1029
       200 1027/1029/1000                       1000

6 rows selected.



i went with CTE MATERIALIZE method

Narayana, January 28, 2026 - 7:06 am UTC

i went with CTE MATERIALIZE method (after listening "Getting WITH it: Using CTEs for better performance")

with rws as (
select /*+ MATERIALIZE */ value as str FROM IIO_SYSTEM_PARAMETER t
                     WHERE    id=200
)
  select regexp_substr (
           str,
           '[^/]+',
           1,
           level
         ) dest
  from   rws
  connect by level <= 
    length ( str ) - length ( replace ( str, '/' ) ) + 1;

Chris Saxon
January 28, 2026 - 3:12 pm UTC

I would still go with the cross apply/lateral method - this works for any number of rows you fetch from IIO_SYSTEM_PARAMETER. Materialized CTEs only work if the CTE returns one row.

Other option

mathguy, January 29, 2026 - 11:26 pm UTC

It is not clear to me why the CTE needs to be MATERIALIZED, can you elaborate?

If you only need the result for a single ID like 200, and if ID is primary key (so there are no duplicates in that column), you could run this query:

select  regexp_substr(value, '[^/]+', 1, level) dest
from    iio_system_parameter
start   with id = 200
connect by   level <= length(value) - length(replace(value, '/')) + 1
        and  id = 200


Note that the CONNECT BY engine (with the associated START WITH condition, if present) is processed before the WHERE clause, despite the weird choice in the SQL standard to require the WHERE clause to appear first. And before applying the WHERE condition, the CONNECT BY mechanism does exactly what Chris described (the reason you get so many rows in the output). Moving the condition id = 200 to the START WITH and CONNECT BY clauses has the effect you were trying to get with your WHERE condition.

If you only need the results for a single id, it certainly makes more sense to write a subquery (as CTE or inline view, it makes no difference) to select the row with id = 200 first (no need to materialize); and if you need to do this for more than one input row, to use a lateral subquery. The query I suggested above is more along the lines of what we needed to do before Oracle 12 - when there was no LATERAL subquery, and no JSON tools. If you need to split all the strings (for all ID) in a single query, and you don't have access to LATERAL subqueries and to JSON tools, you would write something like this:

select  id, regexp_substr(value, '[^/]+', 1, level) dest
from    iio_system_parameter
connect by   level <= length(value) - length(replace(value, '/')) + 1
        and  prior id = id
        and  prior sys_guid() is not null
;


There's no more START WITH clause anymore (you can add it back if you need to select multiple ID's but not all of them, for example). CONNECT BY is a bit more complicated; PRIOR ID = ID means that each new token will come from the same string as the preceding one (exactly what your query was NOT doing - what Chris explained), and the last condition is a hack, to prevent Oracle from finding cycles in the results. (Don't worry about that one - if you ever need to use something like that, you can read more about it at that time.)

In any case, in Oracle 12 and newer, there is no reason to use this old method. Chris already showed the right way to do it - and the JSON trick is also a good alternative (at least for splitting strings supposed to aggregate numbers).
Chris Saxon
January 30, 2026 - 1:57 pm UTC

Thanks for sharing alternatives.

MATERIALIZE ensures the CTE is processed before it is merged with the outer query. This avoids the problem in the original queries of generating the hierarchy for every row. I don't think it's necessary in this case, but it removes that possibility.

CONNECT BY's not part of the SQL standard; legend goes it was included in the first release of Oracle Database to show it was capable functionality available in hierarchical databases.