I need a query that will get the column names of the result of another query. The other query can be anything - I can't make any assumptions about it but it will typically be some SELECT statement.
For example, if I have this table Members
Id | Name | Age
---|------|----
1 | John | 25
2 | Amir | 13
And this SELECT statement
SELECT Name, Age FROM Members
Then the result of the query I'm trying to write would be
Name
Age
In SQL Server, there is a function - sys.dm_exec_describe_first_result_set - that does this but I can't find an equivalent in Oracle.
I tried to use this answer but I can't use CREATE TYPE statements because of permissions issues and I probably can't use CREATE FUNCTION statements for the same reason.
Suppose you have a query like this:
select *
from (select deptno, job, sal from scott.emp)
pivot (avg(sal) as avg_sal for job in
('ANALYST' as analyst, 'CLERK' as clerk, 'SALESMAN' as salesman)
)
order by deptno
;
This produces the result:
DEPTNO ANALYST_AVG_SAL CLERK_AVG_SAL SALESMAN_AVG_SAL
---------- --------------- ------------- ----------------
10 1300
20 3000 950
30 950 1400
Notice the column names (like ANALYST_AVG_SAL) - they don't appear exactly in that form anywhere in the query! They are made up from two separate pieces, put together with an underscore.
Now, if you were allowed to create views (note that this does not create any data in your database - it just saves the text of a query), you could do this:
Create the view (just add the first line of code to what we already had):
create view q201028_vw as
select *
from (select deptno, job, sal from scott.emp)
pivot (avg(sal) as avg_sal for job in
('ANALYST' as analyst, 'CLERK' as clerk, 'SALESMAN' as salesman)
)
order by deptno
;
(Here I assumed you have some way to identify the query, an id like Q201028, and used that in the view name. That is not important, unless you need to do this often and for a large number of queries at the same time.)
Then you can find the column names (and also their order, and - if needed - their data type, etc.) by querying *_TAB_COLUMNS. For example:
select column_id, column_name
from user_tab_columns
where table_name = 'Q201028_VW'
order by column_id
;
COLUMN_ID COLUMN_NAME
---------- --------------------
1 DEPTNO
2 ANALYST_AVG_SAL
3 CLERK_AVG_SAL
4 SALESMAN_AVG_SAL
Now you can drop the view if you don't need it for anything else.
As an aside: The "usual" way to "save" queries in the database, in Oracle, is to create views. If they already exist as such in your DB, then all you need is the last step I showed you. Otherwise, were is the "other query" (for which you need to find the columns) coming from in the first place?
I would use the dbms_sql package and the following code example should show you how to start:
DECLARE
cursorID INTEGER;
status INTEGER;
colCount INTEGER;
rowCount INTEGER;
description dbms_sql.desc_tab;
colType INTEGER;
stringValue VARCHAR2(32676);
sqlCmd VARCHAR2(32767);
BEGIN
-- open cursor
cursorID := dbms_sql.open_cursor;
-- parse statement
dbms_sql.parse(cursorID, 'select * from user_tables', dbms_sql.native);
-- describe columns
dbms_sql.describe_columns(cursorID, colCount, description);
-- cursor close
dbms_sql.close_cursor(cursorID);
-- open cursor
cursorID := dbms_sql.open_cursor;
-- assemble a new select only using up to 5 the "text" columns
FOR i IN 1 .. description.COUNT LOOP
IF (i > 5) THEN
EXIT;
END IF;
IF (description(i).col_type IN (1, 112)) THEN
IF (sqlCmd IS NOT NULL) THEN
sqlCmd := sqlCmd || ', ';
END IF;
sqlCmd := sqlCmd || description(i).col_name;
END IF;
END LOOP;
sqlCmd := 'SELECT ' || sqlCmd || ' FROM user_tables';
dbms_output.put_line(sqlCmd);
-- parse statement
dbms_sql.parse(cursorID, sqlCmd, dbms_sql.native);
-- describe columns
dbms_sql.describe_columns(cursorID, colCount, description);
-- define columns
FOR i IN 1 .. description.COUNT LOOP
dbms_sql.define_column(cursorID, i, stringValue, 4000);
END LOOP;
-- execute
status := dbms_sql.execute(cursorID);
-- fetch up to 5 rows
rowCount := 0;
WHILE (dbms_sql.fetch_rows(cursorID) > 0) LOOP
rowCount := rowCount + 1;
IF (rowCount > 5) THEN
EXIT;
END IF;
dbms_output.put_line('row # ' || rowCount);
FOR i IN 1 .. description.COUNT LOOP
dbms_sql.column_value(cursorID, i, stringValue);
dbms_output.put_line('column "' || description(i).col_name || '" = "' || stringValue || '"');
END LOOP;
END LOOP;
-- cursor close
dbms_sql.close_cursor(cursorID);
END;
/
As astentx suggested, you can use a common table expression function to package the PL/SQL code into a SQL statement. This solution is just a single SQL statement, and requires no non-default privileges and does not create any permanent objects.
(The only downside is that not all SQL tools understand these kinds of WITH clauses, and they may throw an error expecting a different statement terminator.)
SQL> create table members(id number, name varchar2(100), age number);
Table created.
SQL> with function get_result_column_names(p_sql varchar2) return sys.odcivarchar2list is
2 v_cursor_id integer;
3 v_col_cnt integer;
4 v_columns dbms_sql.desc_tab;
5 v_column_names sys.odcivarchar2list := sys.odcivarchar2list();
6 begin
7 v_cursor_id := dbms_sql.open_cursor;
8 dbms_sql.parse(v_cursor_id, p_sql, dbms_sql.native);
9 dbms_sql.describe_columns(v_cursor_id, v_col_cnt, v_columns);
10
11 for i in 1 .. v_columns.count loop
12 v_column_names.extend;
13 v_column_names(v_column_names.count) := v_columns(i).col_name;
14 end loop;
15
16 dbms_sql.close_cursor(v_cursor_id);
17
18 return v_column_names;
19 exception when others then
20 dbms_sql.close_cursor(v_cursor_id);
21 raise;
22 end;
23 select *
24 from table(get_result_column_names(q'[select name, age from members]'));
25 /
COLUMN_VALUE
--------------------------------------------------------------------------------
NAME
AGE
Related
A junior SQL developer was trying to write an anonymous block to but is running into issues. The code should count how many items a person can afford based on their budget. The are sure that the SQL works fine, and their logic for counting the number of products is OK, but they don’t remember the right syntax for creating an anonymous block. Help them by finding and fixing the three errors in the following PL/SQL:
BEGIN
DECLARE
firstName VARCHAR(50) := 'Rob';
budget NUMBER = 600;
counter NUMBER;
CURSOR all_products AS
SELECT product_name, list_price FROM oe.PRODUCT_information;
counter := 0;
FOR items IN all_products LOOP
IF (items.LIST_PRICE <= budget) THEN
counter := counter + 1;
END IF;
END LOOP;
DBMS_OUTPUT.PUT_LINE(firstName || ', you can afford ' || TO_CHAR(counter) || ' items.');
END;
Two minor mistakes:
DECLARE should go first, BEGIN-END next
you missed colon sign for the BUDGET variable
Test case:
SQL> set serveroutput on
SQL> create table product_information (product_name varchar2(20), list_price number);
Table created.
SQL> insert into product_Information values('Some product', 100);
1 row created.
Your code, fixed:
SQL> DECLARE
2 firstname VARCHAR(50):= 'Rob';
3 budget NUMBER := 600; -- missing colon
4 counter NUMBER;
5 cursor all_products is
6 SELECT product_name,
7 list_price
8 FROM product_information; -- I removed OE. (as I don't have that schema)
9
10 BEGIN
11 counter := 0;
12 FOR items IN all_products LOOP
13 IF(items.list_price <= budget)THEN
14 counter := counter + 1;
15 END IF;
16 END LOOP;
17
18 dbms_output.put_line(firstname
19 || ', you can afford '
20 || TO_CHAR(counter)
21 || ' items.');
22 END;
23 /
Rob, you can afford 1 items.
PL/SQL procedure successfully completed.
SQL>
This code, works. It runs a row count the way you'd expect, I want to tweek it, mostly to do a count over a db_link for tables dictated as I see fit.
declare
n number;
begin
for i in (select table_name from user_tables) loop
execute immediate' select count(*) from '||i.table_name into n;
dbms_output.put_line('Table Name: '||i.table_name||' Count of Row''s: '||n);
end loop;
end;
/
So, this is the adapted code... it includes a variable with the name of the link. (The link works fine) But how to reference it is probably where I'm coming unstuck.
declare
l_dblink varchar2(100) := 'DB1';
n number;
begin
for i in (select table_name from my_tables) loop
execute immediate' select count(*) from '||i.table_name#||l_dblink into n;
dbms_output.put_line('Table Name: '||i.table_name||' Count of Row''s: '||n);
end loop;
end;
/
Can someone please have a look and tell me where I'm going wrong? I just want the SQL to pick up the table names from a local table, and then use the names to count the rows in those tables, which reside in the remote database.
Monkey is on the wrong tree and can't eat a banana.
SQL> create table my_tables (table_name varchar2(20));
Table created.
SQL> insert into my_tables values ('dual');
1 row created.
SQL> set serveroutput on
SQL> declare
2 l_dblink varchar2(100) := 'db1';
3 n number;
4 begin
5 for i in (select table_name from my_tables) -- has to be like this
6 loop -- vvv
7 execute immediate' select count(*) from '||i.table_name || '#' || l_dblink into n;
8 dbms_output.put_line('Table Name: '||i.table_name||' Count of Row''s: '||n);
9 end loop;
10 end;
11 /
Table Name: dual Count of Row's: 1
PL/SQL procedure successfully completed.
I have plsql function. And this function has query like as below:
select colum_name
from table_name
where filter_coloumn_name in ( select filter_coloumn_name from table_name_2);
My function is used by other query to return value from query. This query which use this function take more time. Even the site (front screen) down. Because table_name_2 has three million record. So that i must use some performance tuning method in my function. I change my function query like as below:
cursor my_cursor IS
select filter_coloumn_name from table_name_2;
TYPE cursor_array_type IS TABLE OF my_cursor%ROWTYPE INDEX BY BINARY_INTEGER;
m cursor_array_type;
TYPE cursor_table_type IS TABLE OF VARCHAR2(20) INDEX BY BINARY_INTEGER;
cursor_table_object cursor_table_type;
fetch_size NUMBER := 5000;
index_var number;
begin
index_var := 1;
open my_cursor;
loop
FETCH my_cursor BULK COLLECT
into m LIMIT fetch_size;
exit when my_cursor %notfound;
for i in 1 .. m.count loop
cursor_table_object (index_var) := m(i).filter_coloumn_name;
index_var := index_var + 1;
end loop;
end loop;
Close my_cursor;
select colum_name
from table_name
where filter_coloumn_name in (cursor_table_object);
select colum_name
from table_name
where filter_coloumn_name in (cursor_table_object);
Namely, i want fetch all values at one time, and then i want use this table object like as above. But i can't use table object with in condition expression in sql query.
I take PLS-00382: expression is of wrong type error.
I want to use this table object like as below:
select colum_name
from table_name
where filter_coloumn_name in ('bla', 'bla bla', 'bla bla bla');
Should i convert table object to the array ?
A suggestion, based on what you've said so far: how about using a table function? It makes it possible to use something like statement you wanted to use, i.e.
select colum_name from table_name where filter_coloumn_name in (cursor_table_object);
but which returns
PLS-00382: expression is of wrong type
Here's an example based on Scott's schema. Have a look:
SQL> create or replace function f_test
2 return sys.odcinumberlist
3 is
4 -- Cursor's SELECT should contain your 3-million-rows table
5 cursor cur_r is select deptno from dept where deptno < 30;
6 l_ret sys.odcinumberlist;
7 begin
8 open cur_r;
9 fetch cur_r bulk collect into l_ret;
10 close cur_r;
11 return l_ret;
12 end;
13 /
Function created.
SQL> -- How to use it?
SQL> select e.ename
2 from emp e join table(f_test) x on x.column_value = e.deptno;
ENAME
----------
MILLER
KING
CLARK
ADAMS
SCOTT
FORD
JONES
SMITH
8 rows selected.
SQL>
You must use exists instead of in, also table_name_2 must have index over filter_coloumn_name field.
Using cursor only what you get is full access to a big table.
I am trying to concatenate strings that are used as column names
I want to do something like:
Select someData as "ONE" || :someVariable) from sometable;
where someVariable is a bind variable, which does not work inside double quotes.
(The column should have the name "ONE2018" if someVariable = 2018.)
I tried it with single quotes and with the concat function. It doesn't work.
Is there some way to accomplish this?
EDIT:
With inspiration from littlefoots answer I tried
declare
customVariable number(4);
rc sys_refcursor;
begin
open rc for 'select 1 as bla' || :customVariable || ' from dual';
dbms_sql.return_result(rc);
end;
/
which does have the output
BLA2018
----------
1
I don't know how to put that into a PreparedStatement, but if used on it's own it works and might help someone else
An example based on Scott's EMP table which contains columns whose names begins with an E: ENAME and EMPNO.
You'd pass NAME or MPNO and get the result.
SQL> create or replace function f_one (par_column_name in varchar2)
2 return sys_refcursor
3 is
4 l_rc sys_refcursor;
5 l_str varchar2(200);
6 begin
7 l_str := 'select e' || par_column_name || ' from emp where rownum < 3';
8 open l_rc for l_str;
9 return l_rc;
10 end;
11 /
Function created.
SQL> select f_one('mpno') from dual;
F_ONE('MPNO')
--------------------
CURSOR STATEMENT : 1
CURSOR STATEMENT : 1
EMPNO
----------
7369
7499
SQL> select f_one('name') from dual;
F_ONE('NAME')
--------------------
CURSOR STATEMENT : 1
CURSOR STATEMENT : 1
ENAME
----------
SMITH
ALLEN
SQL>
In PL/SQL, you can specify the values for the IN operator using concatenation:
v_sql := 'select field1
from table1
where field2 in (' || v_list || ')';
Is it possible to do the same using a variable?
v_sql := 'select field1
from table1
where field2 in (:v_list)';
If so, how?
EDIT: With reference to Marcin's answer, how do I select from the resultant table?
declare
cursor c_get_csv_as_tables is
select in_list(food_list) food_list
from emp_food
where emp_type = 'PERM';
cursor c_get_food_list (v_food_table varchar2Table)is
select *
from v_food_table;
begin
for i in c_get_csv_as_tables loop
for j in c_get_food_list(i.food_list) loop
dbms_output.put_line(j.element);
end loop;
end loop;
end;
I get the following error:
ORA-06550: line 10, column 6:
PL/SQL: ORA-00942: table or view does not exist
ORA-06550: line 9, column 1:
PL/SQL: SQL Statement ignored
ORA-06550: line 15, column 34:
PLS-00364: loop index variable 'J' use is invalid
ORA-06550: line 15, column 13:
PL/SQL: Statement ignored
Like in #Sathya link, you can bind the varray (I took #Codo example):
CREATE OR REPLACE TYPE str_tab_type IS VARRAY(10) OF VARCHAR2(200);
/
DECLARE
l_str_tab str_tab_type;
l_count NUMBER;
v_sql varchar2(3000);
BEGIN
l_str_tab := str_tab_type();
l_str_tab.extend(2);
l_str_tab(1) := 'TABLE';
l_str_tab(2) := 'INDEX';
v_sql := 'SELECT COUNT(*) FROM all_objects WHERE object_type IN (SELECT COLUMN_VALUE FROM TABLE(:v_list))';
execute immediate v_sql into l_count using l_str_tab;
dbms_output.put_line(l_count);
END;
/
UPDATE: the first command can be replaced with:
CREATE OR REPLACE TYPE str_tab_type IS TABLE OF VARCHAR2(200);
/
then call:
l_str_tab.extend(1);
when ever you add a value
Unfortunately you cannot bind a list like this, however you can use a table function. Read this
Here's an example of usage based on your code:
declare
cursor c_get_csv_as_tables is
select in_list(food_list) food_list
from emp_food
where emp_type = 'PERM';
cursor c_get_food_list (v_food_table varchar2Table)is
select column_value food
from TABLE(v_food_table);
begin
for i in c_get_csv_as_tables loop
for j in c_get_food_list(i.food_list) loop
dbms_output.put_line(j.food);
end loop;
end loop;
end;
I used here a column_value pseudocolumn
Bind variable can be used in Oracle SQL query with "in" clause.
Works in 10g; I don't know about other versions.
Bind variable is varchar up to 4000 characters.
Example: Bind variable containing comma-separated list of values, e.g.
:bindvar = 1,2,3,4,5
select * from mytable
where myfield in
(
SELECT regexp_substr(:bindvar,'[^,]+', 1, level) items
FROM dual
CONNECT BY regexp_substr(:bindvar, '[^,]+', 1, level) is not null
);
As per #Marcin's answer you can't do this, however, there's a fair bit to add to that, as your query should actually work, i.e. run.
Simply put, you cannot use a bind variable for a table or column. Not only that, bind variables they are assumed to be a character, so if you want a number you have to use to_number(:b1) etc.
This is where your query falls down. As you're passing in a string Oracle assumes that your entire list is a single string. Thus you are effectively running:
select field1
from table1
where field2 = v_list
There is no reason why you can't do this a different way though. I'm going to assume you're dynamically creating v_list, which means that all you need to do is create this list differently. A series of or conditions is, purportedly :-), no different to using an in.
By purportedly, I mean never rely on something that's untested. Although Tom does say in the link that there may be performance constraints there's no guarantee that it wasn't quicker than using in to begin with. The best thing to do is to run the trace on your query and his and see what difference there is, if any.
SQL> set serveroutput on
SQL>
SQL> declare
2
3 l_string varchar2(32767);
4 l_count number;
5
6 begin
7
8 for xx in ( select rownum as rnum, a.*
9 from user_tables a
10 where rownum < 20 ) loop
11
12 if xx.rnum = 1 then
13 l_string := 'table_name = ''' || xx.table_name || '''';
14 else
15 l_string := l_string || ' or table_name = ''' || xx.table_name || '
''';
16 end if;
17
18 end loop;
19
20 execute immediate 'select count(*)
21 from user_tables
22 where ' || l_string
23 into l_count
24 ;
25
26 dbms_output.put_line('count is ' || l_count);
27
28 end;
29 /
count is 19
PL/SQL procedure successfully completed.