How can I tune my Oracle SQL query to run faster? [duplicate] - sql

This question already has answers here:
How to convert comma separated values to rows in oracle?
(6 answers)
Closed 2 years ago.
I have over 10,000 records in class_period table. When I run the query shown below, it takes too much time to fetch the data.
Can you please help me - how can I speed up the query?
WITH DATA AS
( SELECT distinct class_time , class_id
from class_period
)
SELECT distinct class_id, trim(regexp_substr(class_time, '[^:]+', 1, LEVEL)) class_time
FROM DATA
CONNECT BY regexp_substr(class_time , '[^:]+', 1, LEVEL) IS NOT NULL
sample data attached as image
enter image description here
required data attached as image
enter image description here
i am using oracle 11g.

Fix your query so you don't need to use DISTINCT. The problem with your method is that you are using a hierarchical query with multiple rows of input and no way of correlating each level of the hierarchy to the previous level so the query will correlate it to ALL the items at the previous level of the hierarchy and you will get exponentially more and more duplicate rows generated at each depth. This is incredibly inefficient.
Change from using regular expressions to simple string functions.
Instead you can use:
WITH bounds ( class_id, class_time, start_pos, end_pos ) AS (
SELECT class_id,
class_time,
1,
INSTR( class_time, ':', 1 )
FROM data
UNION ALL
SELECT class_id,
class_time,
end_pos + 1,
INSTR( class_time, ':', end_pos + 1 )
FROM bounds
WHERE end_pos > 0
)
SELECT class_id,
CASE end_pos
WHEN 0
THEN SUBSTR( class_time, start_pos )
ELSE SUBSTR( class_time, start_pos, end_pos - start_pos )
END AS class_time
FROM bounds;
Which, for the sample data:
CREATE TABLE data ( class_id, class_time ) AS
SELECT 1, '0800AM:0830AM' FROM DUAL UNION ALL
SELECT 1, '0900AM' FROM DUAL UNION ALL
SELECT 2, '0830AM:0900AM:0930AM' FROM DUAL UNION ALL
SELECT 2, '1000AM' FROM DUAL;
Outputs:
CLASS_ID | CLASS_TIME
-------: | :---------
1 | 0800AM
1 | 0900AM
2 | 0830AM
2 | 1000AM
1 | 0830AM
2 | 0900AM
2 | 0930AM
db<>fiddle here
However, an even better method would be to change your model for storing the data and stop storing it as a delimited string and instead store it in a separate table or, maybe, as a collection in a nested table.
An example using a second table is:
CREATE TABLE data (
class_id NUMBER PRIMARY KEY
);
CREATE TABLE class_times (
class_id NUMBER REFERENCES data ( class_id ),
class_time VARCHAR2(6)
);
INSERT ALL
INTO data ( class_id ) VALUES ( 1 )
INTO data ( class_id ) VALUES ( 2 )
INTO class_times ( class_id, class_time ) VALUES ( 1, '0800AM' )
INTO class_times ( class_id, class_time ) VALUES ( 1, '0830AM' )
INTO class_times ( class_id, class_time ) VALUES ( 1, '0900AM' )
INTO class_times ( class_id, class_time ) VALUES ( 2, '0830AM' )
INTO class_times ( class_id, class_time ) VALUES ( 2, '0900AM' )
INTO class_times ( class_id, class_time ) VALUES ( 2, '0930AM' )
INTO class_times ( class_id, class_time ) VALUES ( 2, '1000AM' )
SELECT * FROM DUAL;
Then your query would be (assuming that you need other columns from data alongside the class_id):
SELECT d.class_id,
c.class_time
FROM data d
INNER JOIN class_times c
ON ( d.class_id = c.class_id );
Which outputs:
CLASS_ID | CLASS_TIME
-------: | :---------
1 | 0800AM
1 | 0830AM
1 | 0900AM
2 | 0830AM
2 | 0900AM
2 | 0930AM
2 | 1000AM
An example using a nested table is:
CREATE TYPE stringlist IS TABLE OF VARCHAR2(6);
CREATE TABLE data (
class_id NUMBER,
class_time stringlist
) NESTED TABLE class_time STORE AS data__class_time;
INSERT INTO data ( class_id, class_time )
SELECT 1, stringlist( '0800AM','0830AM' ) FROM DUAL UNION ALL
SELECT 1, stringlist( '0900AM' ) FROM DUAL UNION ALL
SELECT 2, stringlist( '0830AM','0900AM','0930AM' ) FROM DUAL UNION ALL
SELECT 2, stringlist( '1000AM' ) FROM DUAL;
Then your query would become:
SELECT d.class_id,
ct.COLUMN_VALUE AS class_time
FROM data d
CROSS APPLY TABLE ( d.class_time ) ct
Which outputs:
CLASS_ID | CLASS_TIME
-------: | :---------
1 | 0800AM
1 | 0830AM
1 | 0900AM
2 | 0830AM
2 | 0900AM
2 | 0930AM
2 | 1000AM
db<>fiddle here

MT0 spotted the big problem of your connect by filter allowing all rows to be read. You don't need to convert it to a recursive CTE, since you're already distincting all the columns you're projecting, that can be treated as your primary key (assuming it's not nullable or you don't want the null values).
You also need a special filter so that it doesn't get confused into thinking you've got an infinite loop.
WITH DATA AS
( SELECT distinct class_time , class_id
from class_period
)
SELECT distinct class_id, trim(regexp_substr(class_time, '[^:]+', 1, LEVEL)) class_time
FROM DATA
CONNECT BY regexp_substr(class_time , '[^:]+', 1, LEVEL) IS NOT NULL
and prior class_time = class_time
and prior class_id = class_id
and prior sys_guid() is not null
The prior sys_guid() is not null is the special filter to prevent it from erroring with ORA-01436: CONNECT BY loop in user data.
This should perform similarly to the recursive CTE.

Related

How to insert records into the table by joining the reference table

CREATE TABLE ref_table (
ref_id NUMBER(10),
code VARCHAR2(50),
val VARCHAR2(50),
constraint pk_ref_table primary key(ref_id)
);
insert into ref_table values(1,'maker','E_maker');
insert into ref_table values(2,'checker','E_checker');
insert into ref_table values(3,'sme','E_sme');
create table data_table ( id NUMBER(10),
e_id VARCHAR2(50),
maker VARCHAR2(100),
checker VARCHAR2(100),
sme VARCHAR2(100)
);
INSERT INTO data_table VALUES (
1,
11,
'owner_fn,owner_ln;owner_fn_2,owner_ln_2',
'owner_checker',
'sme1,sme_ln1;sme2,sme_ln2'
);
CREATE TABLE org_table (
e_id NUMBER(10),
ref_id NUMBER(10),
CONSTRAINT pk_org_table PRIMARY KEY ( e_id ),
CONSTRAINT fk_org_table_ref_id FOREIGN KEY ( ref_id )
REFERENCES ref_table ( ref_id )
);
ref_table -> This table is for the reference data.
data_table -> This is the table that contains the actual data.
org_table -> This is the table which I need to insert the data from data_table and ref_table.
My attempt:
MERGE INTO org_table ot USING ( SELECT
e_id,
regexp_substr(maker, '[^;]+', 1, level) maker,
regexp_substr(checker, '[^;]+', 1, level) checker,
regexp_substr(sme, '[^;]+', 1, level) sme
FROM
data_table
CONNECT BY e_id = PRIOR e_id
AND PRIOR sys_guid() IS NOT NULL
AND level <= regexp_count(maker, ';') + 1
AND level <= regexp_count(checker, ';') + 1
AND level <= regexp_count(sme, ';') + 1 ORDER BY E_ID ) S
on ( ot.e_id = s.e_id )
WHEN NOT MATCHED THEN
INSERT (
e_id,
ref_id )
VALUES
( s.e_id,
s.ref_id );
Tool used : SQL Developer version 20.4
Problem that I am facing:
SELECT
e_id,
regexp_substr(maker, '[^;]+', 1, level) maker,
regexp_substr(checker, '[^;]+', 1, level) checker,
regexp_substr(sme, '[^;]+', 1, level) sme
FROM
data_table
CONNECT BY e_id = PRIOR e_id
AND PRIOR sys_guid() IS NOT NULL
AND level <= regexp_count(maker, ';') + 1
AND level <= regexp_count(checker, ';') + 1
AND level <= regexp_count(sme, ';') + 1 ORDER BY E_ID
This query should give me the below result like whenever in the columns maker, checker or some contains ; then it should insert a new record for the same e_id but this is not inserting into the new line.
And then, I want to insert the record into the org_table with e_id from data_table and ref_id from ref_table and the output should be like
from the ref_table 1 is for maker column, 2 is for checker column and 3 is for some.
Expected Output:
+------+--------+--+
| e_id | ref_id | |
+------+--------+--+
| 11 | 1 | |
| 11 | 1 | |
| 11 | 2 | |
| 11 | 3 | |
| 11 | 3 | |
+------+--------+--+
1 - It came twice because there were two maker for the same e_id. So, from ref_table 1 is the id for the maker.
2 - It came only once because there was one checker for the same e_id. So, from ref_table 2 is the id for the checker.
3 - It came twice because there was two some for the same e_id. So, from ref_table 3 is the id for the sme.
How I will be able to join the columns from ref_table and data_table column value to get the desired result.

Display 1,2,3,4 as 1-4 in oracle pl/sql

I have a requirement as- if the user selects checkboxes containing 1,2,3,4 of type 1 and 2,3 of type2 then I should display as (1-4)type1 , (2-3)type2 as the output. We have to do it in backend. I have used LISTAGG but couldn't achieve the desired output. The user selects the checkbox and we have the values stored in Oracle database. Any inputs will be greatly helpful.
For Ex, the following is my data.
Type
Options
1
1
1
2
2
2
1
3
2
3
1
4
Using LISTAGG I could obtain :
select
Type ,
listagg (Option, ',')
WITHIN GROUP
(ORDER BY Type) selectedOption from ( select .... )
Type
selectedOption
1
1,2,3,4
2
2,3
Desired Output:
Type
selectedOption
1
1-4
2
2-3
You can use MATCH_RECOGNIZE to find the range boundaries and then aggregate:
SELECT type,
LISTAGG(
CASE
WHEN range_start = range_end
THEN TO_CHAR( range_start )
ELSE range_start || '-' || range_end
END,
','
) WITHIN GROUP ( ORDER BY mn ) AS grouped_values
FROM table_name
MATCH_RECOGNIZE(
PARTITION BY type
ORDER BY value
MEASURES
FIRST( value ) AS range_start,
LAST( value ) AS range_end,
MATCH_NUMBER() AS mn
ONE ROW PER MATCH
PATTERN ( successive_values* last_value )
DEFINE successive_values AS NEXT( value ) <= LAST( value ) + 1
)
GROUP BY type
Which, for the sample data:
CREATE TABLE table_name ( type, value ) AS
SELECT 1, COLUMN_VALUE
FROM TABLE( SYS.ODCINUMBERLIST( 1, 2, 10, 17, 3, 15, 4, 16, 5, 7, 8 ) )
UNION ALL
SELECT 2, COLUMN_VALUE
FROM TABLE( SYS.ODCINUMBERLIST( 2, 3 ) )
Outputs:
TYPE | GROUPED_VALUES
---: | :---------------
1 | 1-5,7-8,10,15-17
2 | 2-3
db<>fiddle here

modify column value if condition in oracle

I have below values in table, and need to set valid_values =6 when found >6
ID VALUE VALID_VALUES
---------- --------------- ---------------------------------------------
555 OFF OFF,1,2,3,4,5,6,7,8,9,10
So after change desired output would be as below,
SQL> /
FIS_ID VALUE VALID_VALUES
---------- --------------- ---------------------------------------------
417 OFF OFF,1,2,3,4,5,6,6,6,6,6
You do not need to split and aggregate; instead you can use a regular expression to find either 2-or-more-digit numbers (i.e. [1-9]\d+) or 1-digit values higher than 6 (i.e. [789]) and could include leading zeroes if these may appear in your data set (since you are storing numbers as text):
SELECT id,
value,
REGEXP_REPLACE(
valid_values,
'0*[1-9]\d+|0*[789]',
'6'
) AS valid_values
FROM table_name
Which, for the sample data:
CREATE TABLE table_name ( ID, VALUE, VALID_VALUES ) AS
SELECT 555, 'OFF', 'OFF,1,2,3,4,5,6,7,8,9,10' FROM DUAL UNION ALL
SELECT 666, 'OFF', 'OFF,1,2,3,4,5,6,42,05,0123' FROM DUAL;
Outputs:
ID | VALUE | VALID_VALUES
--: | :---- | :----------------------
555 | OFF | OFF,1,2,3,4,5,6,6,6,6,6
666 | OFF | OFF,1,2,3,4,5,6,6,05,6
db<>fiddle here
You need to split, replace and aggregate as follows:
Select id, value,
Listagg(case when to_number(vals default null on conversion error) is not null
then case when to_number(vals) > 6 then 6 else vals end
else vals end) Within group (order by lvl) as valid_values
From
(Select id, value,
REGEXP_SUBSTR( t.valid_values, '[^,]+', 1, column_value ) ) , ',' ) as vals,
column_value as lvl
from your_table t,
TABLE(CAST(MULTISET(
SELECT level as lvl
FROM DUAL
CONNECT BY LEVEL <= REGEXP_COUNT( t.valid_value, '[^,]+' )
AS SYS.ODCIVARCHAR2LIS ) v
) group by id, value;
For this solution, you need to split using LAG analytic function, before replacing and aggregating as below :
select ID, VALUE
, listagg(
case when regexp_like(separate_value, '^\d+$')
then case when separate_value > 6
then '6'
else separate_value
end
else separate_value
end
, ',') within group (order by lvl) VALID_VALUES
from (
select ID, VALUE
, lvl, substr(VALID_VALUES, lag(pos, 1, 0)over(order by lvl)+1, pos - lag(pos, 1, 0)over(order by lvl)-1) separate_value
from (
select ID, VALUE, VALID_VALUES||','VALID_VALUES, level lvl, instr(VALID_VALUES||',', ',', 1, level)pos
from your_table
connect by level <= length(VALID_VALUES||',')-length(replace(VALID_VALUES||',', ','))
)
)
group by ID, VALUE
;

Transform some substrings from many rows to a new table

I want to transform this output from the row "topic"...
SMARTBASE/N0184/1/MOISTURE/value
SMARTBASE/N0184/1/MOISTURE/unit
SMARTBASE/N0184/1/MOISTURE/timestamp
SMARTBASE/N0184/1/CONDUCTIVITY/value
SMARTBASE/N0184/1/CONDUCTIVITY/unit
SMARTBASE/N0184/1/CONDUCTIVITY/timestamp
to a new table like:
SENSORS|MOISTURE(value)|MOISTURE(unit)|CONDUCTIVITY(value)|CONDUCTIVITY(unit)
N0184|0.41437244624|Raw VWC|0.5297062938712509|mS/cm
first line: values of topic(row), second line: values of value(row)(values of mqtt-topics)
but that's a sensor of 500++... SMARTBASE is not always SMARTBASE, so regexp _... is not a good idea ... At the end this should be saved as a view.
Is that even possible? I don't know how to implement it... or how to start with it. to transform a row in a table, I can use the pivot-function, but the rest, I don't know.
my main problem: How can I access the individual values of the topic?
Use REGEXP_SUBSTR to get the substring components of your topic column and then use PIVOT:
Oracle Setup:
CREATE TABLE table_name ( topic, value ) AS
SELECT 'SMARTBASE/N0184/1/MOISTURE/value', '0.414' FROM DUAL UNION ALL
SELECT 'SMARTBASE/N0184/1/MOISTURE/unit', 'Raw VWC' FROM DUAL UNION ALL
SELECT 'SMARTBASE/N0184/1/MOISTURE/timestamp', '2019-01-01T00:00:00.000' FROM DUAL UNION ALL
SELECT 'SMARTBASE/N0184/1/CONDUCTIVITY/value', '0.529' FROM DUAL UNION ALL
SELECT 'SMARTBASE/N0184/1/CONDUCTIVITY/unit', 'mS/cm' FROM DUAL UNION ALL
SELECT 'SMARTBASE/N0184/1/CONDUCTIVITY/timestamp', '2019-01-01T00:00:00.000' FROM DUAL;
Query:
SELECT SENSOR_TYPE,
SENSOR,
TO_NUMBER( moisture_value ) AS moisture_value,
moisture_unit,
TO_TIMESTAMP( moisture_timestamp, 'YYYY-MM-DD"T"HH24:MI:SS.FF3' ) AS moisture_timestamp,
TO_NUMBER( conductivity_value ) AS conductivity_value,
conductivity_unit,
TO_TIMESTAMP( conductivity_timestamp, 'YYYY-MM-DD"T"HH24:MI:SS.FF3' ) AS conductivity_timestamp
FROM (
SELECT REGEXP_SUBSTR( topic, '[^/]+', 1, 1 ) AS sensor_type,
REGEXP_SUBSTR( topic, '[^/]+', 1, 2 ) AS sensor,
REGEXP_SUBSTR( topic, '[^/]+', 1, 4 ) AS measurement_name,
REGEXP_SUBSTR( topic, '[^/]+', 1, 5 ) AS measurement_metadata_type,
value
FROM table_name
)
PIVOT(
MAX( value )
FOR ( measurement_name, measurement_metadata_type )
IN (
( 'MOISTURE', 'value' ) AS MOISTURE_value,
( 'MOISTURE', 'unit' ) AS MOISTURE_unit,
( 'MOISTURE', 'timestamp' ) AS MOISTURE_timestamp,
( 'CONDUCTIVITY', 'value' ) AS CONDUCTIVITY_value,
( 'CONDUCTIVITY', 'unit' ) AS CONDUCTIVITY_unit,
( 'CONDUCTIVITY', 'timestamp' ) AS CONDUCTIVITY_timestamp
)
)
Output:
SENSOR_TYPE | SENSOR | MOISTURE_VALUE | MOISTURE_UNIT | MOISTURE_TIMESTAMP | CONDUCTIVITY_VALUE | CONDUCTIVITY_UNIT | CONDUCTIVITY_TIMESTAMP
:---------- | :----- | -------------: | :------------ | :------------------------------ | -----------------: | :---------------- | :------------------------------
SMARTBASE | N0184 | .414 | Raw VWC | 01-JAN-19 12.00.00.000000000 AM | .529 | mS/cm | 01-JAN-19 12.00.00.000000000 AM
db<>fiddle here

Oracle/SQL - Need query that will select max value from string in each row

I need a graceful way to select the max value from a field holding a comma delimited list.
Expected Values:
List_1 | Last
------ | ------
A,B,C | C
B,D,C | D
I'm using the following query and I'm not getting what's expected.
select
list_1,
(
select max(values) WITHIN GROUP (order by 1)
from (
select
regexp_substr(list_1,'[^,]+', 1, level) as values
from dual
connect by regexp_substr(list_1, '[^,]+', 1, level) is not null)
) as last
from my_table
Anyone have any ideas to fix my query?
with
test_data ( id, list_1 ) as (
select 101, 'A,B,C' from dual union all
select 102, 'B,D,C' from dual union all
select 105, null from dual union all
select 122, 'A' from dual union all
select 140, 'A,B,B' from dual
)
-- end of simulated table (for testing purposes only, not part of the solution)
select id, list_1, max(token) as max_value
from ( select id, list_1,
regexp_substr(list_1, '([^,])(,|$)', 1, level, null, 1) as token
from test_data
connect by level <= 1 + regexp_count(list_1, ',')
and prior id = id
and prior sys_guid() is not null
)
group by id, list_1
order by id
;
ID LIST_1_ MAX_VAL
---- ------- -------
101 A,B,C C
102 B,D,C D
105
122 A A
140 A,B,B B
In Oracle 12.1 or higher, this can be re-written using the LATERAL clause:
select d.id, d.list_1, x.max_value
from test_data d,
lateral ( select max(regexp_substr(list_1, '([^,]*)(,|$)',
1, level, null, 1)) as max_value
from test_data x
where x.id = d.id
connect by level <= 1 + regexp_count(list_1, ',')
) x
order by d.id
;