Optimize long running select query against Oracle database - sql

I'm not an DBA expert, we have an existing Oracle query to extract data for a particular day , the problem we've is if the business volume for a day is extremly large, the query takes 8+ hours and timedout. We cannot do optimization inside the database itself, then how do we usually handle extreme case like this? I've pasted the query below with content masked to show the SQL structure, looking for advises on how to optimizae this query or any alternative way to avoid timeout.
WHENEVER SQLERROR EXIT 1
SET LINESIZE 9999
SET ECHO OFF
SET FEEDBACK OFF
SET PAGESIZE 0
SET HEADING OFF
SET TRIMSPOOL ON
SET COLSEP ","
SELECT co.cid
|| ',' || DECODE(co.cid,'xxxxx','xxxx',null,'N/A','xxxxx')
|| ',' || d.name
|| ',' || ti.rc
|| ',' || DECODE(cf.side_id,1,'x',2,'xx',5,'xx','')
|| ',' || cf.Quantity
|| ',' || cf.price
|| ',' || TO_CHAR(time,'YYYY-mm-dd hh24:mi:ss')
|| ',' || DECODE(co.capacity_id,1,'xxxx',2,'xxxx','')
|| ',' || co.type
|| ',' || cf.id
|| ',' || CASE
WHEN (cf.account_id = xxx OR cf.account_id = xxx) THEN SUBSTR(cf.tag, 1, INSTR(cf.tag, '.')-1) || '_' || ti.ric || '_' || DECODE(cf.side_id,1,'xx',2,'xx',5,'xx','')
WHEN INSTR(cf.clientorder_id, '#') > 0 THEN SUBSTR(cf.clientorder_id, 1, INSTR(cf.clientorder_id, '#')-1)
ELSE cf.clientorder_id
END
|| ',' || co.tag
|| ',' || t.description
|| ',' || CASE
WHEN cf.id = xxx THEN 'xxxx'
ELSE (SELECT t.name FROM taccount t WHERE t.account_id = cf.account_id)
END as Account
FROM clientf cf, tins ti, thistory co, tdk d, tra t
WHERE cf.sessiondate = TO_DATE('xxxxxx','YYYYMMDD')
AND cf.orderhistory_id = co.orderhistory_id
AND cf.reporttype_id = 1
AND ti.inst_id = cf.inst_id
AND (ti.rc LIKE '%.xx' or ti.rc LIKE '%.xx' or ti.rc LIKE '%.xx' )
AND d.de_id = t.de_id
AND t.tr_id = co.tr_id
AND nvl(co.type_id,0) <> 3
AND cf.trid not in (SELECT v2.pid FROM port v2 WHERE v2.sessiondate = cf.sessiondate AND v2.exec_id = 4)
ORDER BY co.cid, time, cf.quantity;

I would firstly talk to the people who need the output of this query and ask them about the report and each individual column. Sometimes, some columns are not needed any more, sometimes the whole report. 8+ hours runtime is a good bargaining point ;-)
Next, I would put the original query to one side and start build an test query from scratch, bit by bit, for instance starting with clientf, taking all it's columns in the WHERE clause:
SELECT *
FROM clientf SAMPLE (0.1) cf
WHERE cf.sessiondate = TO_DATE('xxxxxx','YYYYMMDD')
AND cf.reporttype_id = 1;
If that's ok, I'd increase the sample size until 99%. If the runtime is already to long, you might suggest an index on clientf.sessiondate (or may be on clientf.reporttype_id, but that's unlikely helpful as it looks like to have too few distinct values).
Once that is done, I'd join the first table:
SELECT *
FROM clientf SAMPLE (0.1) cf
WHERE cf.sessiondate = TO_DATE('xxxxxx','YYYYMMDD')
AND cf.reporttype_id = 1
AND cf.trid NOT IN (SELECT v2.pid
FROM port v2
WHERE v2.sessiondate = cf.sessiondate
AND v2.exec_id = 4);
I'd compare NOT IN and WHERE NOT EXISTS, not expecting much differences.
Then I'd join the next table (prefering personally ANSI syntax), again starting with a small sample, again adding it's columns to the where clause:
SELECT *
FROM clientf SAMPLE (0.1) cf
FROM thistory co
ON cf.orderhistory_id = co.orderhistory_id
WHERE cf.sessiondate = TO_DATE('xxxxxx','YYYYMMDD')
AND cf.reporttype_id = 1
AND nvl(co.type_id,0) <> 3
AND cf.trid NOT IN (SELECT v2.pid
FROM port v2
WHERE v2.sessiondate = cf.sessiondate
AND v2.exec_id = 4);
I'd play around replacing nvl(co.type_id,0)<>3 with (co.type_id <>3 OR co.type_id IS NULL), monitoring carefully that the result is logically the same.
And so on...

Related

Combining a CASE WHEN statement & a column containing strings of type "Column 1 || '_' || Column 2" in Oracle SQL

I have a table consisting of three columns, which are called the following:
1) Month
2) Store_Type
3) City
I need this table to be expanded to contain five columns and the two columns that I wish to be added are detailed below.
Firstly, the query needs to create a new column called Store_Code. The Store_Code columns job is to store a numerical value which corresponds to what type of store it is.
I presume this would done using a CASE WHEN statement of the type:
SELECT Month,Store_Type,City,
CASE
WHEN Store_Type = 'Corner Shop' THEN '1'
WHEN Store_Type = 'Megastore' THEN '2'
WHEN Store_Type = 'Petrol Station' THEN '3'
....
ELSE '10'
END Store_Code
FROM My_Table
After this is complete, I need to create a column known as "Store_Key". The values contained within the Store_Key column need to be of the following form:
"The Month For That Row""The Store Type For That Row""The City associated with that row"_"The Store Code for that row"
I imagine the best way to create this column would be to use a query similar to the following:
SELECT (My_Table.Month || '_' || My_Table.Store_Type || '_' || My_Table.City || '_' ||
My_Table.Store_Code)
FROM My_Table
What I need is for these two separate queries to be combined into one query. I imagine this could be done by sub-setting the different SELECT queries but I am open to and grateful for any alternative solutions.
Thank you for taking the time to read through this problem and all solutions are greatly appreciated.
Do the case expression part inside a derived table (the subquery):
SELECT (My_Table2.Month || '_' || My_Table2.Store_Type || '_' || My_Table2.City || '_' ||
My_Table2.Store_Code)
FROM
(
SELECT Month,Store_Type,City,
CASE
WHEN Store_Type = 'Corner Shop' THEN '1'
WHEN Store_Type = 'Megastore' THEN '2'
WHEN Store_Type = 'Petrol Station' THEN '3'
....
ELSE '10'
END Store_Code
FROM My_Table
) My_Table2
If this is you trying to populate your new columns, then you need an update statement. I would use two updates to ensure you get the store_case committed for your store_code. Otherwise if you're deriving it in real time, the subquery select answer would be the way to go.
update my_table
set store_case =
case store_type
when 'Corner Shop' then 1
when 'Megastore' THEN 2
when 'Petrol Station' THEN 3
...
else 10
end case;
commit;
update my_table
set store_code = Month || '_' || to_char(Store_Type) || '_' || City || '_' || Store_Code;
commit;
Why to use sub query? It can be done within single query as following:
SELECT My_Table.Month || '_' || My_Table.Store_Type || '_' || My_Table.City || '_' ||
CASE
WHEN Store_Type = 'Corner Shop' THEN '1'
WHEN Store_Type = 'Megastore' THEN '2'
WHEN Store_Type = 'Petrol Station' THEN '3'
....
ELSE '10'
END as result
FROM My_Table
or you can use DECODE function as following:
SELECT My_Table.Month || '_' || My_Table.Store_Type || '_' || My_Table.City || '_' ||
DECODE(Store_Type,
'Corner Shop', '1',
'Megastore', '2',
'Petrol Station', '3'
....,
'10') -- this is default value same as else part of the case statement
as result
FROM My_Table
Cheers!!

VBA: translate parsed structure to PostgreSQL

I'm looking to translate parsed structure to PostgreSQL. Hopefully, I am asking this correctly.
Is there code out there to do this already?
For more color, the need arose from this question/answer:
https://dba.stackexchange.com/questions/162784/postgresql-translating-user-defined-calculations-into-executable-calculation-in
Note this part of the question: "Use an off-the-shelf solution that can translate the parsed structure to SQL. Most languages have something that can do this, like SQL::Abstract. If not, you gotta create it."
Edit: We are using PostgreSQL 9.3.5, if it matters.
Probably query is too complicate, but it does what do you need :-)
Parametrs:
cmd - just your structure to parsing
op - possible operations
tables - jsonb object for translating table names from short form to full (probably you just mean than 'b' -> 'bbg' and 'p' -> 'pulls' instead 'bp' -> 'bbg_pulls'). I run this query on 9.6 and use jsonb. You can change it to just json for 9.3
WITH q AS (
WITH param AS (
SELECT '[bp2][-1]/[bp5]'::text AS cmd,
'+-/%*'::text AS op,
'{"bp": "bbg_pools"}'::jsonb AS tables
), precmd AS (
SELECT btrim(replace(translate(cmd, '[]', ',,'), ',,', ','), ',') AS precmd
FROM param
), split AS (
SELECT i,
split_part(precmd, ',', i) AS part
FROM (
SELECT generate_series(1, length(precmd) - length(translate(precmd, ',', '')) + 1) AS i,
precmd
FROM precmd
) AS a
) SELECT *,
CASE
WHEN part ~ ('^[' || op || ']$') THEN
' ) ' || part || ' ( '
WHEN tables->>(translate(part, '0123456789', '')) != '' THEN
'select val from '::text || (tables->>(translate(part, '0123456789', '0'))) || ' where id = ' || translate(part, translate(part, '0123456789', '0'), '')
WHEN part ~ '^[-]?[0-9]*$' THEN
' and val_date = (CURRENT_TIMESTAMP + (''' || part|| ' day'')::interval)::date '
ELSE
' ERROR '
END AS res
FROM param, precmd, split
ORDER BY i
)
SELECT 'SELECT (' || string_agg(res, ' ') || ')'
FROM q;
Some explanation (for better understanding you can try run query with SELECT * FROM q instead aggregating).
param CTE is just your paramters. In precmd I prepare cmd to split on parts and in split I do it.
Result of this query is:
SELECT (select val from bbg_pools where id = 2 and val_date = (CURRENT_TIMESTAMP + ('-1 day')::interval)::date ) / ( select val from bbg_pools where id = 5)

Comparing column by column between two rows in Oracle DB

I need to write a query to compare column by column (ie: find differences) between two rows in the database. For example:
row1: 10 40 sometext 24
row2: 10 25 sometext 24
After the query executed, it should shows only the fields that have difference (ie: the second field)
Here's what I have done so far:
select table1.column1, table1.column2, table1.column3, table1.column4
from table1
where somefield in (field1, field2);
The above query will show me two rows one above another like this:
10 40 sometext 24
10 25 sometext 24
Then I have to manually do the comparison and it takes a lot of time b/c the row contains a lot of column.
So again my question is: How can I write a query that will show me only the columns that have differences??
Thanks
Use UNPIVOT clause (see http://www.oracle-developer.net/display.php?id=506) to turn columns into rows, then filter out the same rows (using GROUP BY HAVING COUNT and finally use PIVOT to get rows with different columns only.
To do this easily you need to query the metadata for the table to get each row. You can use the following code as a script.
Replace the define table_name with your table name and define yes_drop_it = NO. Put your raw WHERE syntax into the where_clause. The comparison logic always compares the first two rows returned for the where clause.
whenever sqlerror exit failure rollback;
set linesize 150
define test_tab_name = tst_cf_cols
define yes_drop_it = YES
define order_by = 1, 2
define where_clause = 1 = 1
define tab_owner = user
<<clearfirst>> begin
for clearout in (
select 'drop table ' || table_name as cmd
from all_tables
where owner = &&tab_owner and table_name = upper('&&test_tab_name')
and '&&yes_drop_it' = 'YES'
) loop
execute immediate clearout.cmd;
execute immediate '
create table &&test_tab_name as
select 10 as column1, 40 as column2, ''sometext'' as column3, 24 as column4 from dual
union all
select 10 as column1, 25 as column2, ''sometext'' as column3, 24 as column4 from dual
';
end loop;
end;
/
column cfsynt format a4000 word_wrap new_value comparison_syntax
with parms as (select 'parmquery' as cte_name, 'row_a' as corr_name_1, 'row_b' as corr_name_2 from dual)
select
'select * from (select ' || LISTAGG(cfcol || ' AS cf_' || trim (to_char (column_id, '000')) || '_' || column_name
, chr(13) || ', ') WITHIN GROUP (order by column_id)
|| chr(13) || ' from (select * from parmquery where row_number = 1) ' || corr_name_1
|| chr(13) || ', (select * from parmquery where row_number = 2) ' || corr_name_2
|| chr(13) || ') where ''DIFFERENT'' IN (' || LISTAGG ('cf_' || trim (to_char (column_id, '000')) || '_' || column_name, chr(13) || ', ') within group (order by column_id) || ')'
as cfsynt
from parms, (
select
'decode (' || corr_name_1 || '.' || column_name || ', ' || corr_name_2
|| '.' || column_name || ', ''SAME'', ''DIFFERENT'')'
as cfcol,
column_name,
column_id
from
parms,
all_tab_columns
where
owner = &&tab_owner and table_name = upper ('&&test_tab_name')
);
with parmquery as (select rownum as row_number, vals.* from (
select * from &&test_tab_name
where &&where_clause
order by &&order_by
) vals
) &&comparison_syntax
;

Phantom LONG datatype is crashing my SQL code - ORA-00997

I have the following code which is supposed to find each column in a database and ouptput the column name, table name, data type, number of null values, and number of rows.
The problem that I run into is that when I run it, it runs for about two minutes, and then complains about an 'illegal use of LONG datatype', but I am not using any LONG here.
If I edit my search to only select WHERE rownum < 100 (commented out in the following code), it works perfectly. Additionally, if I only do the SELECT statement, it runs just fine and outputs all of the correct SQL statements. (about 18000 of them) So I am guessing that the error is in the loop somewhere.
Any guidance on how to fix this?
SET SERVEROUTPUT ON;
declare
myCol1 varchar2(1000);
myCol2 varchar2(1000);
myCol3 varchar2(1000);
myCol4 number;
myCol5 number;
begin
for line in
(
SELECT
'SELECT ''' || atc.column_name || ''', ''' || atc.table_name || ''', ''' || atc.data_type || ''',
SUM(CASE WHEN temp.'|| atc.column_name || ' IS NULL THEN 0 ELSE 1 END) "Filled Values",
COUNT(temp.' || atc.column_name || ') "Total Records"
FROM all_tab_columns atc
JOIN '|| atc.table_name || ' temp ON atc.column_name = ''' ||
atc.column_name ||''' AND atc.table_name = ''' || atc.table_name || '''' AS SQLRow
FROM all_tab_columns atc --WHERE rownum < 100
)
loop
execute immediate line.Sqlrow into myCol1, myCol2, myCol3, myCol4, myCol5;
INSERT INTO results VALUES (myCol1, myCol2, myCol3, myCol4, myCol5);
end loop;
end;
SELECT * FROM results;
/
One of the tables being picked up has a LONG column. Your static code isn't referring to it directly, but the dynamic SQL you're generating is, e.g.
SELECT 'SQL_TEXT', 'OL$', 'LONG',
SUM(CASE WHEN temp.SQL_TEXT IS NULL THEN 0 ELSE 1 END) "Filled Values",
COUNT(temp.SQL_TEXT) "Total Records"
FROM all_tab_columns atc
JOIN OL$ temp ON atc.column_name = 'SQL_TEXT' AND atc.table_name = 'OL$'
It's complaining about the COUNT. You can't apply aggregates, even something that seems as simple as a count, to a LONG column. Or any built-in function; from the data types documentation:
In addition, LONG columns cannot appear in these parts of SQL
statements:
GROUP BY clauses, ORDER BY clauses, or CONNECT BY clauses or with the DISTINCT operator in SELECT statements
The UNIQUE operator of a SELECT statement
The column list of a CREATE CLUSTER statement
The CLUSTER clause of a CREATE MATERIALIZED VIEW statement
SQL built-in functions, expressions, or conditions
...
The ROWNUM filter just happens to be stopping before it encounters any LONG columns in the data dictionary.
To run this for everything else you'd need to exclude LONG columns from your query. You might want to restrict it you selected schemas though; reporting the data types of system tables/columns seems a little odd.
I'm not sure why you're joining back to all_tab_columns in your generated query. This would get the same result (for a column with a different data type in the same table):
SELECT 'SPARE2', 'OL$', 'VARCHAR2',
SUM(CASE WHEN temp."SPARE2" IS NULL THEN 0 ELSE 1 END),
COUNT(temp."SPARE2")
FROM SYSTEM."OL$" temp
COUNT only counts non-null values, so it'll give you the same result as the SUM (except the sum gives null if the table is empty). If you want to count all rows then count a constant, not the column name. So instead you could do:
SELECT 'SPARE2', 'OL$', 'VARCHAR2',
COUNT(temp."SPARE2"),
COUNT(1)
FROM SYSTEM."OL$" temp
You can give a null result for LONG and LOB values, rather than skipping those columns altogether, by changing the dynamic query based on the data type. You might also want to quote all the identifiers just in case you have mixed case or other problems:
for line in (
SELECT
'SELECT ''' || atc.column_name || ''', '
|| '''' || atc.table_name || ''', '
|| '''' || atc.data_type || ''', '
|| CASE WHEN DATA_TYPE IN ('LONG', 'CLOB', 'BLOB') THEN 'NULL'
ELSE 'COUNT(temp."' || atc.column_name || '")' END || ', '
|| 'COUNT(1) '
|| 'FROM '|| atc.owner || '."' || atc.table_name || '" temp ' AS SQLRow
FROM all_tab_columns atc
WHERE owner NOT IN ('SYS', 'SYSTEM') -- and others
) loop
Or use your SUM version if you want to get the not-null count for those too, I suppose, but with NVL so it reports zero for empty tables.

Oracle sql update using regexp_replace

I have almost 2000 rows in a table ("Sensor", which has many more than 2000 rows) in which I need to update one column, the sensorname.
Part of the replacement within the update is based on the contents of another table, deviceport.
Deviceport is related to the updating table through this deviceportid column -- sensor.deviceportid = deviceportid. Thus the actual update is different for every row. I don't want to have to write 2000 update statements, but I haven't been able to figure out what my "where" statement will say.
UPDATE sensor sn SET sn.sensorname = (
SELECT REGEXP_REPLACE(
sensorname,
'^P(\d)',
'J ' || (
SELECT d.deviceportlabel
FROM deviceport d
WHERE d.deviceportid = s.deviceportid
) ||
' Breaker \1'
)
FROM sensor s
WHERE REGEXP_LIKE( sensorname, '^P(\d)')
)
WHERE ...?
Any clues?
UPDATE sensor sn
SET sn.sensorname = 'J ' ||
(
SELECT d.deviceportlabel
FROM deviceport d
WHERE d.deviceportid = sn.deviceportid
) || ' Breaker ' || substr(sn.sensorname, 2, 1)
WHERE REGEXP_LIKE( sn.sensorname, '^P\d')
Try this:-
UPDATE sensor sn, deviceport d
SET sn.sensorname = REGEXP_REPLACE(
sn.sensorname,'^P(\d)', 'J '|| d.deviceportlabel||' Breaker \1')
WHERE d.deviceportid = sn.deviceportid
AND REGEXP_LIKE( sn.sensorname, '^P(\d)');