Metaprogramming oracle sql select statement - sql

Let's imagine I have a query like the following:
SELECT
CASE WHEN ONE = 1 THEN 1 ELSE 0 END,
CASE WHEN JUST_ONE = 1 THEN 1 ELSE 0 END,
CASE WHEN ANOTHER_ONE = 1 THEN 1 ELSE 0 END,
CASE WHEN TWO = 1 THEN 1 ELSE 0 END,
CASE WHEN JUST_TWO = 1 THEN 1 ELSE 0 END,
CASE WHEN ANOTHER_TWO = 1 THEN 1 ELSE 0 END
-- 20 more things like that where changes only columns name
FROM
SOME_TABLE;
As you can see, the only difference between these two groups is that in the first one I use columns that have 'ONE' and in the second the ones that have 'TWO' and I have around 30 groups like this in my actual query so I wonder if there is a way to shorten it somehow?

Since they are different columns, you must explicitly mention them separately in the SELECT list. You cannot do it dynamically in pure SQL.
I would suggest, using a good text editor, it would hardly take a minute or two to write the entire SQL.
You could use DECODE which will have some less syntax instead of CASE expression which is verbose.
For example,
DECODE(ONE, 1, 1, 0) AS col1,
DECODE(JUST_ONE, 1, 1, 0) AS col2,
DECODE(ANOTHER_ONE, 1, 1, 0) AS col3,
DECODE(TWO, 1, 1, 0) AS col4,
DECODE(JUST_TWO, 1, 1, 0) AS col5,
DECODE(ANOTHER_TWO, 1, 1, 0) as col6
I would suggest to stick to SQL, and not use PL/SQL. They are not the same, they are different engines. PL --> Procedural Language.
But if you insist, then you could use a cursor for loop to loop through all the columns in [DBA|ALL|USER]_TAB_COLS. You could use a SYS_REFCURSOR to see the data. First you will have to build the dynamic SQL.

Below is an example of dynamically creating query. You can put this in e.g. cursor variable if you want more queries created.
select 'SELECT ' || listagg('CASE WHEN '||column_name||' = 1 THEN 1 ELSE 0 END ' || column_name,',') within group(order by column_name) || ' FROM YOUR_TABLE_NAME'
from cols
where data_type in ('NUMBER')
and table_name = 'YOUR_TABLE_NAME';
You can use COLS view to get all column names and their datatypes for all your tables.

Oracle SQL metaprogramming is possible by combining Oracle Data Cartridge with the ANY types.
Even using an existing package for metaprogramming, building a query inside a query is complicated and should be avoided when possible. The other answers are generally better, even though they may require an extra step and are not "pure" SQL.
If you really need to do everything in a single SQL statement, try my open source project, Method4. After installing it, create a sample schema:
create table some_table(
one number, just_one number, another_one number,
two number, just_two number, another_two number);
insert into some_table values(1,1,1,2,2,2);
Run this query that builds the real SELECT statement based on the data dictionary:
select * from table(method4.dynamic_query(
q'[
--Find columns that match pattern and aggregate into SELECT list.
select
'SELECT'||chr(10)||
rtrim(listagg(' CASE WHEN '||column_name||' = 1 THEN 1 ELSE 0 END '||column_name||',', chr(10))
within group (order by order_by1, order_by2), ',')||chr(10)||
'FROM some_table' sql_statement
from user_tab_columns
join
(
--Column names that might match the pattern [null|JUST_|ANOTHER]SPELLED_NUMBER.
select prefix||spelled_number possible_column_names
,order_by1, order_by2
from
(
--Numbers 1-10.
select upper(to_char(to_date(level, 'j'), 'jsp')) spelled_number
,level order_by1
from dual
--Increase this number up to the maximum possible number.
connect by level <= 10
)
cross join
(
--Possible prefixes.
select null prefix, 1 order_by2 from dual union all
select 'JUST_' prefix, 2 order_by2 from dual union all
select 'ANOTHER_' prefix, 3 order_by2 from dual
)
) column_names
on user_tab_columns.column_name = column_names.possible_column_names
where table_name = 'SOME_TABLE'
]'
));
The same query will return different columns based on the table:
ONE JUST_ONE ANOTHER_ONE TWO JUST_TWO ANOTHER_TWO
---------- ---------- ----------- ---------- ---------- -----------
1 1 1 0 0 0
That's some seriously complicated coding to avoid typing a few lines. This is the kind of solution that a manager would dream up when he first hears that hard-coding is always bad.
This literally answers the question about metaprogramming an Oracle SQL SELECT statement. There are a few rare cases where this approach is a life-saver. But 99.9% of the time it's better to do things the simple way even if it is a bit less automated.

The generative approach (in SQL limited with manuall post processing) could be
with src as (
select 'ONE' col_name from dual union all
select 'TWO' col_name from dual
)
select
'CASE WHEN '||col_name||' = 1 THEN 1 ELSE 0 END,'||chr(10)||
'CASE WHEN JUST_'||col_name||' = 1 THEN 1 ELSE 0 END,'||chr(10)||
'CASE WHEN ANOTHER_'||col_name||' = 1 THEN 1 ELSE 0 END,'||chr(10)
from src;
giving
CASE WHEN ONE = 1 THEN 1 ELSE 0 END,
CASE WHEN JUST_ONE = 1 THEN 1 ELSE 0 END,
CASE WHEN ANOTHER_ONE = 1 THEN 1 ELSE 0 END,
CASE WHEN TWO = 1 THEN 1 ELSE 0 END,
CASE WHEN JUST_TWO = 1 THEN 1 ELSE 0 END,
CASE WHEN ANOTHER_TWO = 1 THEN 1 ELSE 0 END,
Note that you provide the list of your column names and gets the expanded syntax.
Typically you need to remove the last delimiter and add the wrapping SELECT frame.

Related

SQL: See how many items in a list, return a hit in a table (using LIKE)

For the sake of argument, say I'm looking for first names in a table. And, the table is irregular - no pattern, some people have 2 names, some 3, some it's all run together, whatever.
SELECT
COUNT(*)
FROM
NAME_TABLE N
WHERE
--the list
N.NAME LIKE 'John%'
OR N.NAME LIKE 'Mich%'
OR N.NAME LIKE 'Rob%'
The above would give how many hits - maybe 70 or 70000, who knows. But, what I really want is a response from 0-3.
i.e., how many of my search terms, get a hit in the table.
I could just run the query and pull the entire table of hits, then
use Excel to get the answer.
Or if in a more typical programming language, I could run a loop
that has an x + 1 in it.
But is there a way to do this directly in an SQL query? Specifically T-SQL I guess...very specifically SQL Server 2008, but I'm kinda curious in general.
Your question is not very clear, at least for me, but my magic crystall ball tells me, that eventually you are looking for something like this:
USE master;
GO
SELECT SUM(CASE WHEN PATINDEX('sys%',[name])>0 THEN 1 ELSE 0 END) AS CountOfSys
,SUM(CASE WHEN PATINDEX('plan%',[name])>0 THEN 1 ELSE 0 END) AS CountOfPlan
,SUM(CASE WHEN PATINDEX('spt_%',[name])>0 THEN 1 ELSE 0 END) AS CountOfSpt
FROM sys.objects;
Another approach was to do a
SELECT 'sys%' AS Pattern, COUNT(*) AS CountPattern
FROM ... WHERE [name] LIKE 'sys%'
UNION ALL
SELECT 'plan%',COUNT(*)
...
UNION ALL
...
This would return a list of all your counts in tabular form.
The third chance was to place all your search patterns into a table and use this table in a CROSS JOIN (similar idea then the UNION approach, but more flexible and more generic):
USE master;
GO
DECLARE #tblPattern TABLE(Pattern VARCHAR(100));
INSERT INTO #tblPattern VALUES('sys%'),('plan%'),('spt_%');
SELECT p.Pattern
,SUM(CASE WHEN PATINDEX(p.Pattern,o.[name])>0 THEN 1 ELSE 0 END) AS CountPattern
FROM sys.objects AS o
CROSS JOIN #tblPattern AS p
GROUP BY p.Pattern
You can use a case statement for each name that resolves to 1 if the name exists in the table or 0 if it does not. Then just add them together and the result will be 0-3 i.e. the number of names that exist in the table.
select
case when exists
select 1 from name_table
where name like 'John%'
then 1 else 0 end
+
case when exists
select 1 from name_table
where name like 'Mich%'
then 1 else 0 end
+
case when exists
select 1 from name_table
where name like 'Rob%'
then 1 else 0 end

SQL can I select column name as value

Here is my table
I would like to select as below
The tch_col will get the column name which column value is not 0 and if it's all 0 give the value as 19.
Could we do this is in SQL?
pls check lane 97 here
the result should
The first answer maybe not works but still
Thank you
Using a UNION to collate multiple entries
SELECT tch_function,tch_part,9 AS tch_col FROM TCH WHERE tch_col_09 > 0
UNION
SELECT tch_function,tch_part,10 AS tch_col FROM TCH WHERE tch_col_10 > 0
UNION
SELECT tch_function,tch_part,11 AS tch_col FROM TCH WHERE tch_col_11 > 0
UNION
SELECT tch_function,tch_part,12 AS tch_col FROM TCH WHERE tch_col_12 > 0
UNION
SELECT tch_function,tch_part,13 AS tch_col FROM TCH WHERE tch_col_13 > 0
UNION
SELECT tch_function,tch_part,14 AS tch_col FROM TCH WHERE tch_col_14 > 0
UNION
SELECT tch_function,tch_part,15 AS tch_col FROM TCH WHERE tch_col_15 > 0
UNION
SELECT tch_function,tch_part,16 AS tch_col FROM TCH WHERE tch_col_16 > 0
UNION
SELECT tch_function,tch_part,17 AS tch_col FROM TCH WHERE tch_col_17 > 0
UNION
SELECT tch_function,tch_part,18 AS tch_col FROM TCH WHERE tch_col_18 > 0
UNION
SELECT tch_function,tch_part,19 AS tch_col FROM TCH
WHERE tch_col_09 = 0 AND tch_col_10 = 0 AND tch_col_11 = 0
AND tch_col_12 = 0 AND tch_col_13 = 0 AND tch_col_14 = 0
AND tch_col_15 = 0 AND tch_col_16 = 0 AND tch_col_17 = 0
AND tch_col_18 = 0
ORDER BY 1,2
From discussion in comments: this is for SQL Server; so using a view to create the 19 column followed by the UNPIVOT operator should simplify things.
Original answer for standard SQL follows for posterity:
You can get the result you describe (with most major SQL databases; if you're using something old or obscure this may not work).
But it's not quite as pretty as selecting the column name. The following SQL is dependent on which specific columns are in the table; see notes afterward.
If you need a record for every non-0 value, it's going to be quite bad.
SELECT tch_function, tch_part, 9
FROM your_table
WHERE tch_col_09 <> 0
UNION ALL
SELECT tch_function, tch_part, 10
FROM your_table
WHERE tch_col_10 <> 0
-- you can see where this is going, right?
-- union in another select for each column
The problem here is that standard SQL isn't good about creating an undetermined number of result rows from each input row, other than during a join.
Now if you can get your hands on a relation with a single column containing the numbers from 9 through 19, you could do a cross join with that and then filter out the rows you don't want (WHERE (your_table.tch_col_09<>0 AND ref.value=9) OR ... OR (your_table.tch_col_09=0 AND ... AND your_table.tch_col_18=0 AND ref.value=19)); but it's still cumbersome, isn't it? You could use a DB2 VALUES subquery, or a temp table, or whatever to get such a relation if you decide to go that route...
If you could assume that only one column has a non-0 value, or if you'd be happy with just the name of the column containing the first non-0 value, it'd be less ugly:
SELECT tch_function
, tch_part
, case when tch_col_09 <> 0 then 9
when tch_col_10 <> 0 then 10
when tch_col_11 <> 0 then 11
when tch_col_12 <> 0 then 12
when tch_col_13 <> 0 then 13
when tch_col_14 <> 0 then 14
when tch_col_15 <> 0 then 15
when tch_col_16 <> 0 then 16
when tch_col_17 <> 0 then 17
when tch_col_18 <> 0 then 18
else 19
end as tch_col
from your_table
The case structure will return the value for the first matching THEN clause, or 19 if none of the THEN clauses match.
But it sounds like that won't do.
Lastly - Any of these queries would have to change if the set of TCH_COL_n columns in the table were to change; at a minimum a good relational table structure should lead to a fairly static set of columns.
More to the point, if you could change your table structure or use a processing language other than SQL you'd be in better shape.

SQL, Select if or select case

I am not sure what I am trying is achievable or not!!
I am trying to write a SQL query will will do select statement based on user input.
so if user input = 1 then I want it to select from actual table.
if user input = 0 then I want it do select 0 or null from dual. (if this is possible).
so Here is Parameter which will used to get input from user. ?i_userkey:'':null?
if user input's 1 then it will change null to 1.
I want to write a query using this parameter. something like this.
below is the logic.
IF i_userkey = 1 then
select ID,Gender,Age from TableA
If i_userkey = 0 then
select 0 or null from dual.
is this possible?
How about this:
SELECT
CASE WHEN i_userkey = 1 THEN ID ELSE NULL END AS ID
CASE WHEN i_userkey = 1 THEN Gender ELSE NULL END AS Gender
CASE WHEN i_userkey = 1 THEN AGE ELSE NULL END AS Age
FROM TableA
This will at least give you a consistent three-column result set you can work with. Having the query return differing column counts is not going to work.
select ID,Gender,Age
from TableA
where i_userkey = 1
union all
select 0, 0, 0
from dual
where i_userkey = 0
You might have to adjust the datatypes in the dual-select to match TableA

Can we have multiple case then statements in sql

I am trying for something which requires case within case I just wanted to make sure if we can use multiple case then?I am running this on sql teradata
The code I am trying to use is as below
AND(
case when CHARACTER_LENGTH(drug.n)=0 then 0
when CHARACTER_LENGTH(drug.n)=1 then
(case when substring(drug.n from 1,1) in (''0'',''1'',''2'',''3'',''4'',''5'',''6'',''7'',''8'',''9'') then 1 else 0 end)
when CHARACTER_LENGTH(drug.n)=2 then
(case when substring(drug.n from 1,1) in (''0'',''1'',''2'',''3'',''4'',''5'',''6'',''7'',''8'',''9'') then 1 else 0 end *
case when substring(drug.n from 2,1) in (''0'',''1'',''2'',''3'',''4'',''5'',''6'',''7'',''8'',''9'') then 1 else 0 end )
when CHARACTER_LENGTH(drug.n)=3 then
(case when substring(drug.n from 1,1) in (''0'',''1'',''2'',''3'',''4'',''5'',''6'',''7'',''8'',''9'') then 1 else 0 end *
case when substring(drug.n from 2,1) in (''0'',''1'',''2'',''3'',''4'',''5'',''6'',''7'',''8'',''9'') then 1 else 0 end *
case when substring(drug.n from 3,1) in (''0'',''1'',''2'',''3'',''4'',''5'',''6'',''7'',''8'',''9'') then 1 else 0 end )=1
If somebody has better idea you can let me know. I cannot use isnumeric function.
Yes you can use nested CASE statements. No problems with that in Teradata
Okay -
To determine whether an arbitrary length string contains only numeric characters (or does not), you can use a recurive CTE.
Please note that I don't know whether or not your RDBMS actually supports recursive CTEs, but this is a potential solution. Also, I'm not sure of the performance implications - however, it does remove the multiple CASE effect (And why isn't that an actual numeric field, anyways?).
So... For a table that looks like this:
id ch
================
1 1234567890
2 asdg
This statement returns all rows that contain only numeric characters (of any length):
WITH splitstring (id, chard, start, orig) as (
SELECT id, SUBSTRING(ch, 1, 1), 1, ch
FROM chartable
UNION ALL
SELECT id, SUBSTRING(orig, start + 1, 1), start + 1, orig
FROM splitstring
WHERE LENGTH(orig) > start)
SELECT *
FROM chartest as a
WHERE NOT EXISTS (SELECT '1'
FROM splitstring as b
WHERE a.id = b.id
AND chard NOT BETWEEN '0' AND '9')
Without some of the larger context it's somewhat difficult to know exactly what you're trying to accomplish. However, this should be adaptable for your needs.
(As a side note, DB2 for the iSeries doesn't seem to support regex either...)

SQL Server query - loop question

I'm trying to create a query that would generate a cross-check table with about 40 custom columns that show Y or N. Right now I have
SELECT DISTINCT [Company],
[Option1],
[Option2],
[Option3],
CASE
WHEN [Table1].[ID1] IN (SELECT ID2 FROM Table2 WHERE Variable = 1 AND Bit = 1) THEN
'Y'
ELSE 'N'
END AS 'CustomColumn1:',
CASE
WHEN [Table1].[ID1] IN (SELECT ID2 FROM Table2 WHERE Variable = 2 AND Bit = 1) THEN
'Y'
ELSE 'N'
END AS 'CustomColumn1:',
CASE
WHEN [Table1].[ID1] IN (SELECT ID2 FROM Table2 WHERE Variable = 3 AND Bit = 1) THEN
'Y'
ELSE 'N'
END AS 'CustomColumn1:',
.............
-- REPEAT ANOTHER 40 times
FROM [Table1]
WHERE [Table1].[OtherCondition] = 'True'
ORDER BY [Company]
So my question is, how do I create a loop (while? for?) that will loop on variable and assign Y or N to the row based on the condition, rather than creating 40+ Case statements?
You couldn't use a loop, but you could create a stored procedure/function to perform the sub-select and case expression and call that 40 times.
Also, you could improve performance of the sub-select by changing it to
SELECT 1 FROM Table2 WHERE EXISTS [Table2].[ID2] = [Table1.ID1] AND Variable = 3 AND Bit = 1
A loop (that is, iterating through a cursor) works on rows, not columns. You will still have to have 40 expressions, one for each column, and the performance will be terrible.
Let SQL Server do its job. And do your bit by telling exactly what you need and creating proper indices. That is, replace
CASE WHEN [Table1].[ID1] IN (SELECT ID2 FROM Table2 WHERE Variable = 2 AND Bit = 1)
with
CASE WHEN EXISTS (SELECT 0 FROM Table2 WHERE ID2 = [Table1].[ID1] AND Variable = 2 AND Bit = 1)
If the output is so vastly different than the schema, there is a question as to whether the schema properly models the business requirements. That said, I would recommend just writing the SQL. You can simplify the SQL like so:
Select Company
, Option1, Option2, Option3
, Case When T2.Variable = 1 Then 'Y' Else 'N' End As CustomCol1
, Case When T2.Variable = 2 Then 'Y' Else 'N' End As CustomCol2
, Case When T2.Variable = 3 Then 'Y' Else 'N' End As CustomCol3
, Case When T2.Variable = 4 Then 'Y' Else 'N' End As CustomCol4
...
From Table1 As T1
Left Join Table2 As T2
On T2.ID2 = T1.ID
And T2.Bit = 1
Where T1.OtherCondition = 'True'
Group By T1.Company
Order By T1.Company
If you want to write something that can help you auto-gen those Case statements (and you are using SQL Server 2005+), you could do something like:
With Numbers As
(
Select 0 As Value
Union All
Select Value + 1
From Numbers
Where Value < 41
)
Select ', Case When T2.Variable = ' + Cast(N.Value As varchar(10)) + ' Then ''Y'' Else ''N'' End As CustomCol' + Cast(N.Value As varchar(10))
From Numbers As N
You would run the query and copy and paste the results into your procedure or code.
One way could have been to use Pivot statement, which is in MS SQL 2005+. But even in that you have to put 1 ... 40 hardcoded columns in pivot statement.
Other way i can think of is to create dynamic SQL, but it is not so much recommended, So what we can do is we can create a dynamic sql query by running a while loop on table and can create the big sql and then we can execute it by using sp_execute. So steps would be.
int #loopVar
SET #loopVar = 0
int #rowCount
varchar #SQL
SET #SQl = ''
Select #rowcount = Count(ID2) from Table2
WHILE(#loopVar <= #rowCount)
BEGIN
// create ur SQL here
END
sp_execute(#SQL)