Oracle: Using Case Statement in Where Clause - sql

In Oracle 12 (and APEX) I am having problems with a CASE statement in a WHERE clause. The scenario is a master table, ORDER, and a PRODUCTS_BOUGHT table, so this is a one to many relationship. I have a report, with a filter on PRODUCTS_BOUGHT. The filter populates a bind variable/APEX page item called :P36_PRODUCT_LISTING. If the user selects a given product, I want the report to just show those orders which contain the given product. The filter contains the word 'All', which should not do any filtering, as well as each product we carry.
My SQL statement is
Select distinct
:P36_PRODUCT_LISTING,
LISTAGG(product_name, ', ') WITHIN GROUP (ORDER BY product_name) OVER (PARTITION BY B.SALES_ORDER_NUMBER) AS products,
...
from ORDER A, PRODUCTS_BOUGHT B
where
A.SALES_ORDER_NUMBER = B.SALES_ORDER_NUMBER
and
case when
:P36_PRODUCT_LISTING = 'All' then 1 = 1
else a.SALES_ORDER_NUMBER IN (SELECT SALES_ORDER_NUMBER from PRODUCTS_BOUGHT where PRODUCT_NAME = :P36_PRODUCT_LISTING) end
When I run the statement, the error I get is Missing Keyword.
What am I doing wrong?

Don't use case. Just use boolean logic:
where (:P36_PRODUCT_LISTING = 'All' or
a.SALES_ORDER_NUMBER IN (SELECT SALES_ORDER_NUMBER
from PRODUCTS_BOUGHT
where PRODUCT_NAME = :P36_PRODUCT_LISTING
)
)
The problem with the case (as you have written it) is that Oracle does not treat the value from a logical expression as a valid value. Some databases do, but not Oracle.
In addition:
Don't use commas in the from clause. Always use proper, explicit join syntax.
Use table aliases that are abbreviations for the table names. Much easier to read the queries and fix bugs.
select distinct should not be necessary here. You are doing an aggregation without a group by, so there is only one row anyway.
So:
Select :P36_PRODUCT_LISTING,
LISTAGG(b.product_name, ', ') WITHIN GROUP (ORDER BY b.product_name) OVER (PARTITION BY B.SALES_ORDER_NUMBER) AS products,
...
from ORDER o join
PRODUCTS_BOUGHT B
on p.SALES_ORDER_NUMBER = B.SALES_ORDER_NUMBER
where (:P36_PRODUCT_LISTING = 'All' or
o.SALES_ORDER_NUMBER IN (SELECT SALES_ORDER_NUMBER
from PRODUCTS_BOUGHT
where PRODUCT_NAME = :P36_PRODUCT_LISTING
)
);

Case is designed to return a value, not a statement..
Try OR instead
where :P36_PRODUCT_LISTING = 'All'
or (:P36_PRODUCT_LISTING <> 'All'
and a.SALES_ORDER_NUMBER IN (SELECT SALES_ORDER_NUMBER from PRODUCTS_BOUGHT where PRODUCT_NAME = :P36_PRODUCT_LISTING))

Related

Stacking my conditions in a CASE statement it's not returning all cases for each member

SELECT DISTINCT
Member_ID,
CASE
WHEN a.ASTHMA_MBR = 1 THEN 'ASTHMA'
WHEN a.COPD_MBR = 1 THEN 'COPD'
WHEN a.HYPERTENSION_MBR = 1 THEN 'HYPERTENSION'
END AS DX_FLAG
So a member may have more than one, but my statement is only returning one of them.
I'm using Teradata and trying to convert multiple columns of boolean data into one column. The statement is only returning one condition when members may have 2 or more. I tried using Select instead of Select Distinct and it made no difference.
This is a kind of UNPIVOT:
with base_data as
( -- select the columns you want to unpivot
select
member_id
,date_col
-- the aliases will be the final column value
,ASTHMA_MBR AS ASTHMA
,COPD_MBR AS COPD
,HYPERTENSION_MBR AS HYPERTENSION
from your_table
)
,unpvt as
(
select member_id, date_col, x, DX_FLAG
from base_data
-- now unpivot those columns into rows
UNPIVOT(x FOR DX_FLAG IN (ASTHMA, COPD, HYPERTENSION)
) dt
)
select member_id, DX_FLAG, date_col
from unpvt
-- only show rows where the condition is true
where x = 1

BigQuery use the where clause to filter on a column that not always exists in the table

I need to create some kind of a uniform query for multiple tables. Some tables contain a certain column with a type. If this is the case, I need to apply filtering to it. I don't know how to do this.
I have for example two tables
table_customer_1
CustomerId, CustomerType
1, 1
2, 1
3, 2
Table_customer_2
Customerid
4
5
6
The query needs to be something like the one below and should work for both tables (the table name wil be replaced by the customer that uses the query):
With input1 as(
SELECT
(CASE WHEN exists(customerType) THEN customerType ELSE "0" END) as customerType, *
FROM table_customer_1)
SELECT * from input1
WHERE customerType != 2
Below is for BigQuery Standard SQL
#standardSQL
SELECT *
FROM `project.dataset.table` t
WHERE SAFE_CAST(IFNULL(JSON_EXTRACT_SCALAR(TO_JSON_STRING(t), '$.CustomerType'), '0') AS INT64) != 2
or as a simplification you can ignore casting to INT64 and use comparison to STRING
#standardSQL
SELECT *
FROM `project.dataset.table` t
WHERE IFNULL(JSON_EXTRACT_SCALAR(TO_JSON_STRING(t), '$.CustomerType'), '0') != '2'
above will work for whatever table you put instead of project.dataset.table: either project.dataset.table_customer_1 or project.dataset.table_customer_2 - so quite generic I think
I can think of no good reason for doing this. However, it is possible by playing with the scoping rules for subqueries:
SELECT t.*
FROM (SELECT t.*,
(SELECT customerType -- will choose from tt if available, otherwise x
FROM table_customer_1 tt
WHERE tt.Customerid = t.Customerid
) as customerType
FROM (SELECT t.* EXCEPT (Customerid)
FROM table_customer_1 t
) t CROSS JOIN
(SELECT 0 as customerType) x
) t
WHERE customerType <> 2

Avoiding aggregation when selecting values from tables

I have the following code which selects value from table2 when 'some string' occurs more than once in 1990
SELECT a.value, COUNT(*) AS test
FROM table1 c
JOIN table2 a
ON c.value2 = a.value_2
JOIN table3 o
ON c.value3 = o.value_3
AND o.value4 = 1990
WHERE c.string = 'Some string'
GROUP BY a.value
HAVING COUNT(*) > 1
This works fine but I am attempting to write a query that produces a similar result without using aggregation. I just need to select values with more then 1 c.string and select those rather than counting and selecting the count as well. I thought about searching for pairs of 'some string' occurring in 1990 for a value but am unsure of how to execute this. Pointing me in the right direction would be appreciated! Struggling to find any documentation referencing this. Thank you!
Use window function ROW_NUMBER() to assign a sequence number within the rows of each table2.value. And use window function FIRST_VALUE() to get the largest row number for each table2.value. Use DISTINCT to remove the duplicates:
select distinct value, first_value(rn) over ( order by rn desc) as count
from
(
SELECT a.value , row_number() over (partition by a.value order by null) rn
FROM table1 c
JOIN table2 a
ON c.value2 = a.value_2
JOIN table3 o
ON c.value3 = o.value_3
AND o.value4 = 1990
WHERE c.string = 'Some string' ) t
where rn > 1;
To check for duplicates, you can use 'WHERE EXISTS', as a starting point. You could start by reading this:
https://www.w3schools.com/sql/sql_exists.asp
This will give you quite a long, cumbersome piece of code compared to using aggregation. But I expect that's the point of the task - to show how useful aggregation is.

"ORDER BY items must appear in the select list if SELECT DISTINCT is specified"

I'm building a Query for a db oriented webapp, and running into an issue with SELECT DISTINCT and ORDER BY. I want the first item shown to be determined by a variable (showing which option a user previously selected), then the rest to be sorted normally. My ORDER BY worked fine before I added the DISTINCT option to the select (needed to eliminate duplicates). The columns being sorted appear in my SELECT, so I'm not sure why it won't accept it.
WITH COURSE2 AS
(
SELECT DISTINCT(SUBSTRING(section_table.crs_cde, 1, 10)) AS CRSID, yr_cde, trm_Cde FROM section_table
)
SELECT DISTINCT crs_cde, crs_title
FROM course_table
LEFT JOIN COURSE2
ON crs_cde = CRSID
WHERE yr_cde = #currentyear#
AND trm_cde = #currentterm#
ORDER BY <cfif isDefined("FORM.ndd")>(case crs_cde when '#FORM.ndd#' then 0 else 1 end),</cfif> crs_cde ASC
Appreciate any and all help :)
The columns being sorted appear in my SELECT
Not quite. Just using one of the same columns in a CASE statement does not count. The ORDER BY reference has to match the SELECT list - exactly. One alternative is move the CASE into the SELECT list, as a new column. Then sort by the column alias:
SELECT DISTINCT crs_cde
, crs_title
, CASE crs_cde WHEN 'some value' THEN 0 ELSE 1 END AS SortOrder
FROM ...
ORDER BY SortOrder, crs_cde
Couple of other comments about the query:
WHERE yr_cde = #currentyear#
AND trm_cde = #currentterm#
That will cause the query to be converted into an INNER JOIN. If you really need an outer join, move those filters into the CTE statement.
Be sure to wrap ALL variable parameters in cfqueryparam. Using raw variables in queries puts the database at risk for sql injection.
For clarity and readability, consider adding table aliases and using them to prefix all of the columns in the join query.
Scope all variables, ie use FORM.someField instead of just someField
Putting it all together, something like this (not tested)
WITH COURSE2 AS
(
SELECT DISTINCT(SUBSTRING(crs_cde, 1, 10)) AS CRSID
FROM section_table
WHERE yr_cde = <cfqueryparam value="#FORM.currentYear#" cfsqltype="cf_sql_integer">
AND trm_cde = <cfqueryparam value="#FORM.currentTerm#" cfsqltype="cf_sql_integer">
)
SELECT DISTINCT ct.crs_cde
, ct.crs_title
<cfif structKeyExists(FORM, "ndd")>
, CASE ct.crs_cde
WHEN <cfqueryparam value="#FORM.ndd#" cfsqltype="cf_sql_varchar"> THEN 0
ELSE 1
END AS SortOrder
</cfif>
FROM course_table ct LEFT JOIN COURSE2 c2 ON c2.CRSID = ct.crs_cde
ORDER BY
<cfif structKeyExists(FORM, "ndd")>SortOrder,</cfif>
crs_cde ASC

SQL: Return "true" if list of records exists?

An alternative title might be:
Check for existence of multiple rows?
Using a combination of SQL and C# I want a method to return true if all products in a list exist in a table. If it can be done all in SQL that would be preferable. I have written a method that returns whether a single productID exists using the following SQL:
SELECT productID FROM Products WHERE ProductID = #productID
If this returns a row, then the c# method returns true, false otherwise.
Now I'm wondering if I have a list of product IDs (not a huge list mind you, normally under 20). How can I write a query that will return a row if all the product id's exist and no row if one or more product id's does not exist?
(Maybe something involving "IN" like:
SELECT * FROM Products WHERE ProductID IN ('1', '10', '100', 'ABC'))
EDIT:
How the result is expressed is not important to me. Whether the query returns a 1 or 0, an empty resultset or a non-empty one, true or false doesn't matter. I'd prefer the answer that is 1) easy to read and understand and 2) performant
I was envisioning concatenating the list of product id's with the SQL. Obviously this opens the code up to SQL injection (the product id's are actually varchar. in this case the chance is slim but still want to avoid that possibility). So if there is a way around this that would be better. Using SQL Server 2005.
Product ID's are varchar
Here's how I usually do it:
Just replace your query with this statement SELECT * FROM table WHERE 1
SELECT
CASE WHEN EXISTS
(
SELECT * FROM table WHERE 1
)
THEN 'TRUE'
ELSE 'FALSE'
END
Given your updated question, these are the simplest forms:
If ProductID is unique you want
SELECT COUNT(*) FROM Products WHERE ProductID IN (1, 10, 100)
and then check that result against 3, the number of products you're querying (this last part can be done in SQL, but it may be easier to do it in C# unless you're doing even more in SQL).
If ProductID is not unique it is
SELECT COUNT(DISTINCT ProductID) FROM Products WHERE ProductID IN (1, 10, 100)
When the question was thought to require returning rows when all ProductIds are present and none otherwise:
SELECT ProductId FROM Products WHERE ProductID IN (1, 10, 100) AND ((SELECT COUNT(*) FROM Products WHERE ProductID IN (1, 10, 100))=3)
or
SELECT ProductId FROM Products WHERE ProductID IN (1, 10, 100) AND ((SELECT COUNT(DISTINCT ProductID) FROM Products WHERE ProductID IN (1, 10, 100))=3)
if you actually intend to do something with the results. Otherwise the simple SELECT 1 WHERE (SELECT ...)=3 will do as other answers have stated or implied.
#Mark Hurd, thanks for pointing out the error.
this will work (if you are using Postgresql, Sql Server 2008):
create table products
(
product_id int not null
);
insert into products values(1),(2),(10),(100);
SELECT
CASE
WHEN EXISTS(
SELECT 1
FROM (values(1),(10),(100)) as x(id)
WHERE x.id NOT IN (select product_id from products))
THEN 0 --'NOT ALL'
ELSE 1 -- 'ALL'
END
If you are using MySQL, make a temporary memory table(then populate 1,10,100 there):
create table product_memory(product_id int) engine=MEMORY;
insert into product_memory values(1),(10),(100);
SELECT
CASE
WHEN EXISTS(
SELECT 1
FROM product_memory
WHERE product_memory.id NOT IN (select product_id from products))
THEN 0 -- 'NOT ALL'
ELSE 1 -- 'ALL'
END
On your C# code:
bool isAllExist = (int)(new SqlCommand(queryHere).ExecuteScalar()) == 1;
[EDIT]
How can I write a query that will
return a row if all the product id's
exist and no row if one or more
product id's does not exist?
Regarding, returning a row(singular) if all rows exists, and no row to be returned if one or more product id does not exists:
MySql:
SELECT 1
WHERE
NOT EXISTS(
SELECT 1
FROM product_memory
WHERE product_memory.id NOT IN (select product_id from products) )
Posgresql, Sql Server 2008:
SELECT 1
WHERE
NOT EXISTS(
SELECT 1
FROM (values(1),(10),(100)) as x(id)
WHERE x.id NOT IN (select product_id from products) )
Then on your C# code:
var da = new SqlDataAdapter(queryhere, connectionhere);
var dt = new DataTable();
da.Fill(dt);
if (dt.Rows.Count > 0)
return true;
else
return false;
Or just make the condition shorter:
return dt.Rows.Count > 0;
Assuming you're using SQL Server, the boolean type doesn't exist, but the bit type does, which can hold only 0 or 1 where 0 represents False, and 1 represents True.
I would go this way:
select 1
from Products
where ProductId IN (1, 10, 100)
Here, a null or no row will be returned (if no row exists).
Or even:
select case when EXISTS (
select 1
from Products
where ProductId IN (1, 10, 100)
) then 1 else 0 end as [ProductExists]
Here, either of the scalar values 1 or 0 will always be returned (if no row exists).
DECLARE #values TABLE (ProductId int)
INSERT #values (1)
INSERT #values (10)
INSERT #values (100)
SELECT CASE WHEN (SELECT COUNT(*) FROM #values v) =
(SELECT COUNT(*) FROM Products p WHERE p.ProductId IN
(SELECT v.ProductId FROM #values v))
THEN CAST(1 AS bit)
ELSE CAST(0 AS bit)
END [AreAllFound]
I know this is old but I think this will help anyone else who comes looking...
SELECT CAST(COUNT(ProductID) AS bit) AS [EXISTS] FROM Products WHERE(ProductID = #ProductID)
This will ALWAYS return TRUE if exists and FALSE if it doesn't (as opposed to no row).
You can use a SELECT CASE statement like so:
select case when EXISTS (
select 1
from <table>
where <condition>
) then TRUE else FALSE end
It returns TRUE when your query in the parents exists.
For PostgreSQL:
SELECT COUNT(*) = 1 FROM (
SELECT 1 FROM $table WHERE $condition LIMIT 1
) AS t
// not familiar with C#, but C#'s equivalent of PHP's:
$count = count($productIds); // where $productIds is the array you also use in IN (...)
SELECT IF ((SELECT COUNT(*) FROM Products WHERE ProductID IN (1, 10, 100)) = $count, 1, 0)
If the IN clause is a parameter (either to SP or hot-built SQL), then this can always be done:
SELECT (SELECT COUNT(1)
FROM product_a
WHERE product_id IN (1, 8, 100)
) = (number of commas in product_id as constant)
If the IN clause is a table, then this can always be done:
SELECT (SELECT COUNT(*)
FROM product_a
WHERE product_id IN (SELECT Products
FROM #WorkTable)
) = (SELECT COUNT(*)
FROM #WorkTable)
If the IN clause is complex then either spool it into a table or write it twice.
If you have the IDs stored in a temp table (which can be done by some C# function or simple SQL) then the problem becomes easy and doable in SQL.
select "all exist"
where (select case when count(distinct t.id) = (select count(distinct id) from #products) then "true" else "false" end
from ProductTable t, #products p
where t.id = p.id) = "true"
This will return "all exists" when all the products in #products exist in the target table (ProductTable) and will not return a row if the above is not true.
If you are not willing to write to a temp table, then you need to feed in some parameter for the number of products you are attempting to find, and replace the temp table with an 'in'; clause so the subquery looks like this:
SELECT "All Exist"
WHERE(
SELECT case when count(distinct t.id) = #ProductCount then "true" else "false"
FROM ProductTable t
WHERE t.id in (1,100,10,20) -- example IDs
) = "true"
If you are using SQL Server 2008, I would create a stored procedure which takes a table-valued parameter. The query should then be of a particularly simple form:
CREATE PROCEDURE usp_CheckAll
(#param dbo.ProductTableType READONLY)
AS
BEGIN
SELECT CAST(1 AS bit) AS Result
WHERE (SELECT COUNT(DISTINCT ProductID) FROM #param)
= (SELECT COUNT(DISTINCT p.ProductID) FROM #param AS p
INNER JOIN Products
ON p.ProductID = Products.ProductID)
END
I changed this to return a row, as you seem to require. There are other ways to do this with a WHERE NOT EXISTS (LEFT JOIN in here WHERE rhs IS NULL):
CREATE PROCEDURE usp_CheckAll
(#param dbo.ProductTableType READONLY)
AS
BEGIN
SELECT CAST(1 AS bit) AS Result
WHERE NOT EXISTS (
SELECT * FROM #param AS p
LEFT JOIN Products
ON p.ProductID = Products.ProductID
WHERE Products.ProductID IS NULL
)
END
Your c# will have to do just a bit of work (counting the number of IDs passed in), but try this:
select (select count(*) from players where productid in (1, 10, 100, 1000)) = 4
Edit:
4 can definitely be parameterized, as can the list of integers.
If you're not generating the SQL from string input by the user, you don't need to worry about attacks. If you are, you just have to make sure you only get integers. For example, if you were taking in the string "1, 2, 3, 4", you'd do something like
String.Join(",", input.Split(",").Select(s => Int32.Parse(s).ToString()))
That will throw if you get the wrong thing. Then just set that as a parameter.
Also, be sure be sure to special case if items.Count == 0, since your DB will choke if you send it where ParameterID in ().
Where is this list of products that you're trying to determine the existence of? If that list exists within another table you could do this
declare #are_equal bit
declare #products int
SELECT #products =
count(pl.id)
FROM ProductList pl
JOIN Products p
ON pl.productId = p.productId
select #are_equal = #products == select count(id) from ProductList
Edit:
Then do ALL the work in C#. Cache the actual list of products in your application somewhere, and do a LINQ query.
var compareProducts = new List<Product>(){p1,p2,p3,p4,p5};
var found = From p in GetAllProducts()
Join cp in compareProducts on cp.Id equals p.Id
select p;
return compareProducts.Count == found.Count;
This prevents constructing SQL queries by hand, and keeps all your application logic in the application.
This may be too simple, but I always use:
SELECT COUNT(*)>0 FROM `table` WHERE condition;
Example:
SELECT iif(count(id)=0,'false','true') FROM table WHERE id = 19