Dynamically build select statement in Oracle 12c - sql

I have posted the similar question before but the solution to this question seems like it will be completely different, thus I hope this does not qualify for a repost.
Req:
I have 2 columns in a table named SETUPS with the following columns:
ID INTEGER NOT NULL
RPT_SCRIPT CLOB NOT NULL
RPT_SCRIPT has select statements in each record. Below is a statement in the clob column WHERE ID = 1:
SELECT ID,
Title,
Desc,
Type,
LVL_CNT,
TYPE_10 VALUE_10,
TYPE_9 VALUE_9,
TYPE_8 VALUE_8,
TYPE_7 VALUE_7,
TYPE_6 VALUE_6,
TYPE_5 VALUE_5,
TYPE_4 VALUE_4,
TYPE_3 VALUE_3,
TYPE_2 VALUE_2,
TYPE_1 VALUE_1
FROM SCHEMA.TABLE
WHERE ID = 1;
Currently I am writing these select statements manually for all records.
SETUPS.ID is mapped to another master table META.ID in order to build the select statement.
The column names with pattern TYPE_%, i.e. TYPE_1, come from the META table; there are total of 20 columns in the table with this pattern but in this example I've used only 10 because META.LVL_CNT = 10. Similarly if META.LVL_CNT = 5 then only select columns TYPE_1,TYPE_2,TYPE_3,TYPE_4,TYPE_5.
The column aliases, i.e. VALUE_1, are values which come from the corresponding column where META.ID = 1 (as in this example).
ID will always be provided, so it can be used to query table META.
EDIT
The column aliases which come from META table will never have a pattern as I have shown in my example, but with LVL_CNT, at runtime we will know the number of columns. I tried to #Asfakul's provided logic and built a dynamic sql using the column names retrieved dynamically but when using EXECUTE IMMEDIATE INTO I realized I don't know how many columns will be retrieved and therefore wouldn't be able to dynamically generate the alias name with this method.
Need an approach to automatically build this select statment using above information.. how can I achieve this? Please provide any examples.

You can use this as the basis
declare
upper_level number;
t_sql varchar2(1000);
l_sql varchar2(1000);
begin
select lvl_cnt into upper_level from
SETUPS S,META S
where s.id=m.id
l_sql:='SELECT ID,
Title,
Desc,
Type,'||
upper_level
for lvl in 1..upper_level
loop
t_sql:=t_sql||'type_'||lvl||','
end loop;
l_sql:=l_sql||t_sql
l_sql:=rtrim(l_sql,',');
l_sql:=l_sql||' FROM SCHEMA.TABLE
WHERE ID = 1;';
end

I recommend this approach, if you already know how to build dynamic SQL, then use this concept to build your query:
SELECT 'TYPE_' || LEVEL
FROM DUAL
CONNECT BY LEVEL <= 10 --10 could be a variable

Related

How should I temporarily store data within a PL/SQL procedure?

I am very new to PL/SQL. I have data in an initial table, named 'FLEX_PANEL_INSPECTIONS' that I am attempting to summarise in a second table, named 'PANEL_STATUS_2' using a PL/SQL procedure. However, due to the nature of the data, I have had to write a case statement in order to correctly summarise the data from FLEX_PANEL_INSPECTIONS. I have therefore created a third, intermediate table to bridge the two (named 'PANEL_STATUS_1') since the case statement will not allow columns in the group by clause which specifically order the data (to the extent of my knowledge - I get an error when I try and do this). I do not want to be storing data in the intermediate table - is there any way that I can either make it temporary (i.e. exist only while the procedure runs so that data from 'PANEL_STATUS_1' is not retained); create a view within the procedure, or remove the need for the intermediate table altogether?
Any help or criticism of my mistakes / misunderstanding of PL/SQL would be greatly appreciated. Here is the code I have written:
create or replace procedure PANEL_STATUS_PROCEDURE (panel_lot_id in number) as
begin
--Populate intermediate table with information about the status of the panels.
insert into PANEL_STATUS_1 (FLEX_LOT_ID, FLEX_PANEL_DMX, FLEX_PANEL_STATUS)
select FLEX_LOT_ID, FLEX_PANEL_DMX,
--Sum the status values of the 4 panel inspections. A panel passes if and only if this sum = 4.
case sum (FLEX_PANEL_STATUS)
when 4 then 1
else 0
end as new_panel_status
from FLEX_PANEL_INSPECTIONS
where FLEX_LOT_ID = panel_lot_id
group by FLEX_LOT_ID, FLEX_PANEL_DMX;
--Add information about the machine ID and the upload time to this table.
insert into PANEL_STATUS_2 (FLEX_LOT_ID, FLEX_PANEL_DMX, FLEX_PANEL_STATUS, MACHINE_ID, UPLOAD_TIME)
select distinct PANEL_STATUS_1.*, MACHINE_ID, UPLOAD_TIME
from PANEL_STATUS_1, FLEX_PANEL_INSPECTIONS
where (FLEX_PANEL_INSPECTIONS.FLEX_LOT_ID = PANEL_STATUS_1.FLEX_LOT_ID
and FLEX_PANEL_INSPECTIONS.FLEX_PANEL_DMX = PANEL_STATUS_1.FLEX_PANEL_DMX)
and FLEX_PANEL_INSPECTIONS.FLEX_LOT_ID = panel_lot_id;
end PANEL_STATUS_PROCEDURE;
/
You can create your temp table as
create global temporary table gtt_panel_status
( column datatype ... )
on commit [delete|preserve] rows;
(specifying either delete or preserve in the on commit clause).
However you usually don't need a temp table. You might try a with clause (CTE), or else an inline view along lines of select x, y, z from (select your subquery here).
Edit: actually looking at your query some more, I think what you a actually need is an analytic sum, i.e. a total without aggregating. For example, something like this:
create or replace procedure panel_status_procedure
( panel_lot_id in number )
as
begin
-- Add information about the machine ID and the upload time to this table.
insert into panel_status_2
( flex_lot_id
, flex_panel_dmx
, flex_panel_status
, machine_id
, upload_time )
select distinct
flex_lot_id
, flex_panel_dmx
, case sum(flex_panel_status) over (partition by flex_lot_id, flex_panel_dmx)
when 4 then 1
else 0
end
, machine_id
, upload_time
from flex_panel_inspections pi
where pi.flex_lot_id = panel_lot_id;
end panel_status_procedure;

SQL Select statement - default field value when field doesn't exist

I'm wondering whether there is a way to display some default value for select statement where queries field doesn't exist.
For instance,
SELECT t.name, t.type, t.price, t.brand FROM some_table t;
If the 'brand' field doesn't exist in the some_table I would like this statement to display 'brand' as 'not available'.
Eventually I want to create a view from that select statement.
I'm just curious whether there is a way to do that in PL/SQL.
EDIT:
To avoid confusion, I want the statement to compile and work when the 'brand' column doesn't exist in the table.
You can use COALESCE, change null by not available
SELECT t.name, t.type, t.price, COALESCE(t.brand,'not available') AS brand FROM some_table t;
COALESCE is sql standard, but i dont know if Oracle have it.
EDIT:
I think you have to check the field exist in table first, someting like:
Select count(*) into v_column_exists
from user_tab_cols
where column_name = 'ADD_TMS'
and table_name = 'EMP';
If 1 then EXIST else NOT EXIST, after create the view based on the result.
1:
SELECT t.name, t.type, t.price, t.brand FROM some_table t;
2:
SELECT t.name, t.type, t.price, 'not available' AS brand FROM some_table t;
But i cant see the right way to use this in view.
Unless you go with some really heavy and clunky meta-data gathering where you simply query the master tables to get the data and then unpivot it all into rows, you cannot query a column that doesn't exist because the compiler will start hooking up his stuff and won't find the column.
You can bypass that by using dynamic sql, but then you'll simply have a runtime error instead since you're still querying a column that doesn't exist.
This means that your dynamic SQL will have to exclude the column if it's not in that table, at which point you're better simply removing the column from the static SQL. The only point where dynamic SQL would truly be better is if you have to query like 30+ tables and that you know what you're doing.
So basically ,why do you need to query columns that don't exist? In your case if it's only to be able to preserve an obsolete view, you'd be better to simply maintain your view when it requires updating.
I have just seen the question above. It seems very weird design or requirement. I am posting a code snippet which may suffice your problem but ideally this should not be like this.
--So i get chance to look into the question asked simple way to get a workaround for your problem is to fetch out the columns list from table
var p_lst refcursor;
SET serveroutput ON;
DECLARE
lv_sql LONG;
lv_tab_name VARCHAR2(100);
lv_col_chk VARCHAR2(1000 CHAR);
BEGIN
FOR I IN
(SELECT * FROM ALL_TAB_COLUMNS WHERE OWNER = 'AVROY' AND TABLE_NAME = 'EMP'
)
LOOP
lv_tab_name:=I.TABLE_NAME;
lv_sql :=lv_sql||','||i.column_name;
END LOOP;
lv_sql:='SELECT '||SUBSTR(lv_sql,2,LENGTH(lv_sql));
dbms_output.put_line(lv_sql);
lv_col_chk:=INSTR(UPPER(lv_sql),'BRAND',1);
dbms_output.put_line(lv_col_chk);
IF lv_col_chk = 0 THEN
lv_sql :=SUBSTR(lv_sql,1,LENGTH(lv_sql))||', ''Not_available'' as Brand_col FROM '||lv_tab_name;
dbms_output.put_line(LV_SQL);
ELSE
lv_sql:=SUBSTR(lv_sql,1,LENGTH(lv_sql))||' FROM '||lv_tab_name;
dbms_output.put_line(LV_SQL);
END IF;
OPEN :p_lst FOR lv_sql;
END;
PRINT p_lst;
SELECT t.name, t.type, t.price, NVL(t.brand,"Not available") FROM some_table t;

Query to write extra rows in Excel output

I'm trying to accomplish something that seems like it should be straightforward in MS Excel. I want to use a single SQL query - so I can pass it on to others to copy and paste - though I know the following could be achieved with other methods as well. Sheet 1 looks like this:
ID value value_type
1 minneapolis city_name
2 cincinnati city_name
I want an SQL query to return an "exploded" version of those two rows:
ID attr_name attr_value
1 value minneapolis
1 value_type city_name
2 value cincinnati
2 value_type city_name
There's much more I need to do, but this concept gets at the heart of the issue. I've tried a single SELECT statement, but can't seem to make it create two rows from one, and when I tried using UNION ALL I got a syntax error.
In Microsoft Query, how can I construct an SQL statement to create two rows from the existing values in one row?
UPDATE
thanks for the help so far. First, for reference, here is the default statement that recreates the table in Microsoft Query:
SELECT
`Sheet3$`.ID,
`Sheet3$`.name,
`Sheet3$`.name_type
FROM `path\testconvert.xlsx`.`Sheet3$` `Sheet3$`
So, following #lad2025's lead, I have:
SELECT
ID = `Sheet3$`.ID
,attr_name = 'value'
,attr_value = `Sheet3$`.value
FROM `path\testconvert.xlsx`.`Sheet3$` `Sheet3$`
UNION ALL
SELECT
ID = `Sheet3$`.ID
,attr_name = 'value_type'
,attr_value = `Sheet3$`.value_type
FROM `path\testconvert.xlsx`.`Sheet3$` `Sheet3$`
And the result is this error Too few parameters. Expected 4.
LiveDemo
CREATE TABLE #mytable(
ID INTEGER NOT NULL PRIMARY KEY
,value VARCHAR(11) NOT NULL
,value_type VARCHAR(9) NOT NULL
);
INSERT INTO #mytable(ID,value,value_type) VALUES (1,'minneapolis','city_name');
INSERT INTO #mytable(ID,value,value_type) VALUES (2,'cincinnati','city_name');
SELECT
ID
,[attr_name] = 'value'
,[attr_value] = value
FROM #mytable
UNION ALL
SELECT
ID
,[attr_name] = 'value_type'
,[attr_value] = value_type
FROM #mytable
ORDER BY id;
Ok, after going back to the original statement and working up from there as per the suggestions from #lad2025, I've come up with this statement which achieves what I was looking for in my original question:
SELECT
ID,
'name' AS [attr_name],
name AS [attr_value]
FROM `path\testconvert.xlsx`.`Sheet3$` `Sheet3$`
UNION ALL
SELECT
ID,
'name_type',
name_type
FROM `path\testconvert.xlsx`.`Sheet3$` `Sheet3$`
ORDER BY ID;
One of the main problems is that the new column names are only defined in the first SELECT statement. Also, brackets are ok, just not how #lad2025 was using them originally.
Microsoft Query is pretty finicky.

Oracle- create a temporary resultset for use in a query

How do I create a temporary result set for use in an SQL without creating a table and inserting the data?
Example: I have a list of, say 10 codes for example. I want to put this into a query, and then query the database to see which codes in this temporary list do not exist in a table.
If it was already in a table, I could do something like:
SELECT
ITEM_CODE
FROM
TEMP_ITEMS
MINUS
SELECT
ITEM_CODE
FROM
M_ITEMS
Is their a way without using PL/SQL, and pure SQL to create a temporary rowset before querying?
Please don't answer with something like:
SELECT 1 FROM DUAL
UNION ALL
SELECT 2 FROM DUAL
I am sort of thinking of something where I can provide my codes in an IN statement, and it turns that into rows for use in a later query.
Edit: so everyone knows my objective here, basically I sometimes get a list of product codes that I need to find which ones in the list are not setup in our system. I want a quick way to throw this into an SQL statement so I can see which ones are not in the system (rather than importing data etc). I usually put these into excel, then do a formula such as :
="'"&A1&"',"
So that I can create my comma separated list.
If you are using oracle 11g you can do this
with t as
(
select (column_value).getnumberval() Codes from xmltable('1,2,3,4,5')
)
SELECT * FROM t
WHERE NOT EXISTS (SELECT 1 FROM M_ITEMS M WHERE codes = M.ITEM_CODE);
or
with t as
(
select (column_value).getstringval() Codes from xmltable('"A","B","C"')
)
SELECT * FROM t
WHERE NOT EXISTS (SELECT 1 FROM M_ITEMS M WHERE codes = M.ITEM_CODE);
I would go with:
with t as (
select 1 as val from dual union all
select 2 as val from dual
)
select . . .
And then use "t" or whatever you call it, in the subsequent query block.
I'm not sure what the objection is to using the select method . . . just pop the values you want in a column in Excel and produce the code for each value by copying down the formula. Then paste the results back into your query interface.
If you want to use a temporary table, you can use the values clause. Alternatively, you can use string functions if you only want IN functionality. Put the values in a comma separated list and check to see if it matches a particular value:
where ','||<list>||',' like '%,'||col||',%'
This one is interesting because it's not a union and fit in a single select. You have to enter the string with delimiters ('a/b/c/def') two times though:
SELECT regexp_substr('a/b/c/def', '[^/]+', 1, ROWNUM) var,
regexp_substr('2/432/sd/fsd', '[^/]+', 1, ROWNUM) var2
FROM dual
CONNECT BY LEVEL <= length(regexp_replace('a/b/c/def', '[^/]', '')) + 1;
var var2
=== ====
a 2
b 432
c sd
def fsd
Note: Credits go to : https://stackoverflow.com/a/1381495/463056
So using the with clause it would give someting like :
with tempo as (
SELECT regexp_substr('a/b/c/def', '[^/]+', 1, ROWNUM) var,
regexp_substr('2/432/sd/fsd', '[^/]+', 1, ROWNUM) var2
FROM dual
CONNECT BY LEVEL <= length(regexp_replace('a/b/c/def', '[^/]', '')) + 1
)
select ...
or you can use it in a from clause :
select ...
from (
SELECT regexp_substr('a/b/c/def', '[^/]+', 1, ROWNUM) var,
regexp_substr('2/432/sd/fsd', '[^/]+', 1, ROWNUM) var2
FROM dual
CONNECT BY LEVEL <= length(regexp_replace('a/b/c/def', '[^/]', '')) + 1
) tempo
There are two approaches I would lean towards:
1. Global Temporary Table
Although you say you don't want to create a table, it depends on why you don't want a table. If you choose to create a Global Temporary table, the rows are only visible to the session that inserted them, so it's like having a private in-memory table but gives you all the benefits of a real table - i.e. being able to query and join to it.
2. Pipelined function
You can create a function that returns the results in a form that can be queried using the TABLE() operator. More info here: http://www.oracle-base.com/articles/misc/pipelined-table-functions.php
It's a bit hokey-looking. But you can parse a string into separate rows using regular expressions assuming you are using 10g or later. For example
SQL> ed
Wrote file afiedt.buf
1 SELECT REGEXP_SUBSTR('a,b,c,def,g', '[^ |,]+', 1, LEVEL) parsed_str
2 FROM dual
3* CONNECT BY LEVEL <= REGEXP_COUNT('a,b,c,def,g', '[^ |,]+')
SQL> /
PARSED_STR
--------------------------------------------
a
b
c
def
g
Personally, I would find a pipelined table function or a PL/SQL block that generates a collection easier to understand, but if you have to do it in SQL you can.
Based on your edit, if you are getting a list of product codes that is already in some sort of file, it would seem to make more sense to use an external table to expose the file as a table or to use SQL*Loader to load the data into a table (temporary or permanent) that you can query. Barring either of those options, if you really want to manipulate the list in Excel first, it would make more sense to generate an IN list in Excel and just copy and past that into your query. Generating a comma-separated list of codes in Excel only to parse that list into it's constituent elements in SQL seems like way too many steps.

SQL Nested Subquery Referencing Grandparents Column

I have a particularly complicated query for a report. It selects several columns from a view, and it must build a column via aggregately concatenating several fields. To complicate things further, the concatenation must contain 3 fields even if there are 0 in reality (The concatenation is comma delimited, so empty fields will still be noticed).
We are using Oracle 11.1.0.7.0.
To provide backward compatibility (not necessary) we used the xmlagg function to perform the concatenation, I believe that has been around since Oracle 8 or 9.
This example will be simplified but I feel provides sufficient information. In other words, please do not focus on normalizing the table structure, this is strictly an example.
person_view
-----------
name
phone
address
position_id
position_table
--------------
position_id
position_title
So the query we currently have, and I admit to not being a SQL guru, is something like:
select
name,
phone,
address,
(select
xmlagg(xmlelement(e, position_title || ',')).extract('//text()')
from
(select
position_title
from
position_table
where
position_table.position_id = person_view.position_id and
rownum <= 3
union all select '' from dual
union all select '' from dual
union all select '' from dual
)
where
rownum <= 3
)
from
person_view
My actual error is that, it seems, the subquery that ensures at least 3 rows of input cannot reference the grandparents query to determine person_view.position_id.
I get ORA-00904: "PERSON_VIEW"."POSITION_ID": invalid identifier
Performance is not a huge concern, as this is a report that will not be run regularly, but I need to figure out a solution to aggregate this data with an absolute 3 columns of data. Any guidance to help rewrite the query, or allow the subquery to access the relevant grandparent column is greatly appreciated.
This is a limitation in Oracle SQL: you can't reference a parent query element from a subquery more than 1 level deep.
I would use a function in such a case:
CREATE OR REPLACE FUNCTION get_title(p_position_id NUMBER) RETURN VARCHAR2 IS
l_result LONG;
l_position_num NUMBER := 0;
BEGIN
FOR cc IN (SELECT position_title
FROM position_table
WHERE position_table.position_id = p_position_id
AND rownum <= 3) LOOP
l_result := cc.position_title || ',';
l_position_num := l_position_num + 1;
END LOOP;
RETURN l_result || rpad(',', 3 - l_position_num, ',');
END;
You query would look like this:
select
name,
phone,
address,
get_title(p.position_id) title
from person_view p