Combine CASE and BETWEEN in WHERE clause - sql

I'm trying to execute what I presumed to be a simple enough query, however I'm encountering syntax errors and could use some guidance. Essentially I have a function with a text input parameter $1. This parameter can take on 7 different values controlled by the front end of an application.
Depending on the value of this parameter, I need to return a subset of a larger table between a certain range of one of the integer columns returned. To be slightly clearer, the returning query within the function looks something like:
SELECT val_1, val_2, val_3
FROM schema_name.table_name
WHERE val_3 BETWEEN
CASE WHEN $1 = 'a' THEN 0 AND 1
WHEN $1 = 'b' THEN 2 AND 7
.
.
.
WHEN $1 = 'g' THEN 50 AND 100
END;
However, I'm getting a typically vague error:
ERROR: syntax error at end of input
LINE 42: END;
^
********** Error **********
ERROR: syntax error at end of input
SQL state: 42601
Character: 1133
The ranges set out in the case statement all differ, with no discernible pattern between them. I could of course write an IF block with 7 ELSE statements essentially writing the same select statement 7 times, but I'm guessing the above can't be too far off. I've never had the need to use a CASE statement in a WHERE clause in this way before. Any hints in the right direction would be greatly appreciated.

A CASE expression can only return a single value (boolean in my fixed version), not conditional code like you tried:
SELECT val_1, val_2, val_3
FROM schema_name.table_name
WHERE CASE $1
WHEN 'a' THEN val_3 BETWEEN 0 AND 1
WHEN 'b' THEN val_3 BETWEEN 2 AND 7
.
.
.
WHEN 'g' THEN val_3 BETWEEN 50 AND 100
END;
Using the "simple" form of CASE to make it slightly shorter and faster.
Alternatively, just use boolean logic only:
...
WHERE (
$1 = 'a' AND val_3 BETWEEN 0 AND 1
OR $1 = 'b' AND val_3 BETWEEN 2 AND 7
.
.
.
OR $1 = 'g' AND val_3 BETWEEN 50 AND 100
)
Parentheses are not strictly needed since operator precedence works in our favor anyway. AND binds before OR and BETWEEN binds before both. But you'll need those parentheses if you add another WHERE condition with AND.
Performance?
For both variants, Postgres 12 (didn't test for earlier versions) is smart enough to still use an index on (val_3) or not, depending on actual column statistics. Even for prepared statements.
If that function is called a lot,
and you work with prepared statements (directly or indirectly),
and performance is critical,
and some of your ranges benefit from an index while others don't
then you might still fork (at least) two distinct queries in order to work with generic, saved query plans for either case to get absolute best performance.

A CASE expression returns 1 scalar value and not 2 or an interval of values.
What you can do is use it as a query cross joined to the table:
SELECT t.val_1, t.val_2, t.val_3
FROM schema_name.table_name t
CROSS JOIN (
SELECT CASE $1
WHEN 'a' THEN 0
WHEN 'b' THEN 1
.
END val
) c
WHERE t.val_3 BETWEEN 2 * c.val AND 2 * c.val + 1
Edit.
You can use a CTE which returns all the possible values of $1 and the intervals for each value:
WITH cte(val, min, max) AS (
VALUES ('a', 0, 1), ('b', 2, 7), .........
)
SELECT t.val_1, t.val_2, t.val_3
FROM table_name t
CROSS JOIN (SELECT * FROM cte WHERE val = $1) c
WHERE t.val_3 BETWEEN c.min AND c.max

Related

Equivalent of SELECT 0 as something but for strings. Is it SELECT '' as something?

What is the equivalent of:
SELECT 0 as foo;
but for strings, is it:
SELECT '' as bar;
?
For more context, this is for a UNION ALL query
[edit]
SELECT NULL is what I was looking for.
In the top query I was doing: SELECT COUNT(DISTINCT(bar)),
But empty strings counted as a distinct char, NULL doesn't.
If you want an "equivalent" of SELECT 0 in string form, then it would be SELECT '0'::int.
If you are looking for a non-null string to represent 'no data,' SELECT '' AS something would suffice
When you call UNION ALL, the expectation is that the columns would align and the data sets would be concatenated along their column orders. Since you want to UNION ALL an integer and char, you will get an error:
edb=# select 0 as something union all select '' as something;
ERROR: invalid input syntax for type integer: ""
LINE 1: select 0 as something union all select '' as something;
^
Therefore, in order to UNION ALL, you'll want to cast your 0 as a char:
edb=# select 0::char as something union all select '' as something;
something
-----------
0
(2 rows)
Caveat
I'm not sure if this is really what you want to do, but if you're looking to UNION ALL the two sets, that's how you'd do it. However, there's a chance this would open up a can of worms -- is '' going to be considered equivalent to 0? And what will you do with non-zero values? Will you still cast those integers into strings? I think you'll need to think through those implications and try to find a way to sanitize your data.
In general, it might be better to work with NULL values if your design allows for it

Oracle "Select Level from dual" does not work as expected with to_number result

Why does
select *
from (
SELECT LEVEL as VAL
FROM DUAL
CONNECT BY LEVEL <= 1000
ORDER BY LEVEL
) n
left outer join (select to_number(trim(alphanumeric_column)) as nr from my_table
where NOT regexp_like (trim(alphanumeric_column),'[^[:digit:]]')) d
on n.VAL = d.nr
where d.nr is null
and n.VAL >= 100
throw a ORA-01722 invalid number (reason is the last row, n.VAL), whereas the similar version with numeric columns im my_table works fine:
select *
from (
SELECT LEVEL as VAL
FROM DUAL
CONNECT BY LEVEL <= 1000
ORDER BY LEVEL
) n
left outer join (select numeric_column as nr from my_table) d
on n.VAL = d.nr
where d.nr is null
and n.VAL >= 100
given that numeric_column is of type number and alphanumeric_column of type nvarchar_2. Note that the upper example works fine without the numerical comparison (n.VAL >= 100).
Does anybody know?
This problem was driving me crazy. I narrowed the problem to a simpler query
SELECT *
FROM (SELECT TO_NUMBER(TRIM (alphanumeric_column)) AS nr
FROM my_table
WHERE NOT REGEXP_LIKE (TRIM (alphanumeric_column), '[^[:digit:]]')) d
WHERE d.nr > 1
With alphanumeric_colum values of ('100','200','XXXX'); Running the above statement gave the "invalid number" error. I then made a slight change to the query to use the CAST function instead of TO_NUMBER:
SELECT *
FROM (SELECT CAST (TRIM (alphanumeric_column) AS NUMBER) AS nr
FROM my_table
WHERE NOT REGEXP_LIKE (TRIM (alphanumeric_column), '[^[:digit:]]')) d
WHERE d.nr > 1
And this correctly returned - 100, 200. I would think that those functions would be similar in behavior. It almost appears as though oracle is trying to evaluate the d.nr > 1 constraint before the view is constructed, which makes no sense. If anyone can shed light on why this is happening, I would be grateful. See SQLFiddle example
UPDATE: I did some more digging, because I don't like not knowing why something just works. I ran EXPLAIN PLAN on both queries and got some interesting results.
For the query that failed, the predicate information looks like this:
1 - filter(TO_NUMBER(TRIM("ALPHANUMERIC_COLUMN"))>1 AND NOT
REGEXP_LIKE (TRIM("ALPHANUMERIC_COLUMN"),'[^[:digit:]]'))
You will notice that the TO_NUMBER function is called first in the AND condition, then
the regexp to exclude alpha values. I am thinking oracle maybe does a short-circuit evaluation with the AND condition, and since it is executing TO_NUMBER first, it fails.
However, when we use the CAST function, the evaluation order is swapped, and the
regexp exclusion is evaluated first. Since for the alpha values, it is false, then the
second part of the AND clause is not evaluated, and the query works.
1 - filter( NOT REGEXP_LIKE (TRIM("ALPHANUMERIC_COLUMN"),'[^[:digit:]
]') AND CAST(TRIM("ALPHANUMERIC_COLUMN") AS NUMBER)>1)
Oracle can be strange sometimes.
I believe when it comes to the Predicate (where) clause, Oracle can/will reorder the entire plan as it sees fit. So with regard to the predicate, it will short-circuit (as OldProgrammer noted) the evaluation however it wants, and you wont be able to guarantee the exact order it occurs.
In your current SQL, you are depending on the predicate to remove non numbers. One option would be to not use "WHERE NOT regexp_like ..." and instead use regexp_substr with coalesce. For example:
create table t_tab2
(
col varchar2(10)
);
create index t_tab2_idx on t_tab2(col);
insert into t_tab2
select level from dual
connect by level <= 100;
insert into t_tab2 values ('123ABC456');
commit;
-- select values > 95 (96->100 exclude non numbers)
select d.* from
(
select COALESCE(TO_NUMBER(REGEXP_SUBSTR(trim(col), '^\d+$')), 0) as nr
from t_tab2
) d
where d.nr > 95;
This should run without throwing invalid number error. Note that the coalesce will return the number 0 for any non numbers coming from the data, you may want to change that based on your needs and data.

SQL 'CASE WHEN x' vs. 'CASE x WHEN' with greater-than condition?

This is a questions about the two ways to use the SELECT CASE in MS SQL [CASE WHEN X = Y] and [CASE X WHEN Y]
I am trying to define buckets for a field based on its values. I would need to use ranges, so it is necessary to use the "<" or ">" identifiers.
As a simple example, I know it works like this:
SELECT CASE WHEN x < 0 THEN 'a' WHEN X > 100 THEN 'b' ELSE 'c' END
Now I have to write a lot of these, there will be more than 3 buckets and the field names are quite long, so this becomes very difficult to keep clean and easy to follow. I was hoping to use the other way of the select command but to me it looks like I can only use it with equals:
SELECT CASE X WHEN 0 then 'y' ELSE 'z' END
But how can I use this form to specify range conditions just as above? Something like:
SELECT CASE X WHEN < 0 THEN 'a' WHEN > 100 THEN 'b' ELSE "c" END
This one does not work.
Thank You!
As an alternative approach, remember that it is possible to do math on the value that is the input to the "simple" CASE statement. I often use ROUND for this purpose, like this:
SELECT
CASE ROUND(X, -2, 1)
WHEN 0 THEN 'b' -- 0-99
WHEN 100 THEN 'c' -- 100-199
ELSE 'a' -- 200+
END
Since your example includes both positive and negative open-ended ranges, this approach may not work for you.
Still another approach: if you are only thinking about readability in the SELECT statement, you could write a scalar-valued function to hide all the messiness:
CREATE FUNCTION dbo.ufn_SortValuesIntoBuckets (#inputValue INT) RETURNS VARCHAR(10) AS
BEGIN
DECLARE #outputValue VARCHAR(10);
SELECT #outputValue =
CASE WHEN #inputValue < 0 THEN 'a'
WHEN #inputValue BETWEEN 0 AND 100 THEN 'b'
WHEN #inputValue > 100 THEN 'c'
END;
RETURN #outputValue;
END;
GO
So now your SELECT statement is just:
SELECT dbo.ufn_SortValuesIntoBuckets(X);
One final consideration: I have often found, during benchmark testing, that the "searched" form (which you are trying to avoid) actually has better performance than the "simple" form, depending how many CASEs you have. So if performance is a consideration, it might be worth your while to do a little benchmarking before you change things around too much.
There is no such "third form" of the CASE - only the searched and the simple cases are supported *.
You need to use the searched kind (i.e. with separate conditions) even though the variable to which you apply the condition is always the same.
If you are looking to avoid repetition in your SQL when X represents a complex expression, use WITH clause or a nested query to assign a name to the expression that you are selecting.
* The official name of your first example is "searched CASE expression"; your second example is called the "simple CASE expression".
It won't look so bad using BETWEENs:
SELECT CASE
WHEN X < 0 THEN 'a'
WHEN X BETWEEN 0 AND 100 THEN 'b'
WHEN X BETWEEN 100 AND 200 THEN 'c'
ELSE 'd' END
That is actually very interesting question. It resembles C# 9.0 relational pattern
SELECT CASE X WHEN < 0 THEN 'a'
WHEN > 100 THEN 'b'
ELSE 'c'
END
C# 9.0
DeliveryTruck t when t.GrossWeightClass switch
{
> 5000 => 10.00m + 5.00m,
< 3000 => 10.00m - 2.00m,
_ => 10.00m,
},
SQL CASE expression as mentioned in previous answers has two forms: simple and searched.
But SQL standard defines also
SQL:2003 Extended CASE expression(F262). This feature is barely adopted by major vendors.
case — Conditional Expressions by Markus Winand
The so-called extended case accepts a comparison operator right after when and thus lifts the limitation that simple case always uses equals (=) comparisons. The following example uses the less than operator (<) to map values into intervals. It also relies on the precedence of when clauses: the first true condition wins.
CASE x WHEN < 0 THEN '< 0'
WHEN < 10 THEN '[0, 10['
WHEN < 100 THEN '[10, 100['
ELSE '>100'
END

Oracle: How can I get a value 'TRUE' or 'FALSE' comparing two NUMBERS in a query?

I want to compare two numbers. Let's take i.e. 1 and 2.
I've tried to write the following query but it simply doesn't work as expected (Toad says: ORA-00923: FROM keyword not found where expected):
SELECT 1 > 2 from dual
The DECODE is something like a Switch case, so how can I get the result of an expression evalutation (i.e. a number comparison) putting it in the select list?
I have found a solution using a functions instead of an expression in the SELECT LIST: i.e.
select DECODE(SIGN(actual - target)
, -1, 'NO Bonus for you'
, 0,'Just made it'
, 1, 'Congrats, you are a winner')
from some_table
Is there a more elegant way?
Also how do I compare two dates?
There is no boolean types in sql (at least in oracle).
you can use case:
SELECT CASE when 1 > 2 THEN 1 ELSE 0 END FROM dual
But your solution (decode) is also good, read here
The SIGN() function is indeed probably the best way of classifying (in)equality that may be of interest to you if you want to test a > b, a = b and a < b, and it will accept date-date or numeric-numeric as an argument.
I'd use a Case statement by preference, rather than a decode.
Select
case sign(actual-target)
when -1 then ...
when 0 then ...
when 1 then ...
end
SELECT (CASE
WHEN (SIGN(actual - target) > 0 ) THEN
'NO Bonus for you'
ELSE
'Just made it' END)
FROM dual
you can compare two dates with sql
METHOD (1):
SELECT TO_DATE('01/01/2012') - TO_DATE('01/01/2012')
FROM DUAL--gives zero
METHOD (2):
SELECT CASE
when MONTHS_BETWEEN('01/01/2012','01/01/2010') > 0
THEN 'FIRST IS GREATER'
ELSE 'SECOND IS GREATER OR EQUAL' END
FROM dual
sorry i cant format the code the formatting toolbar disappeared !
do any one know why?

Finding rows that don't contain numeric data in Oracle

I am trying to locate some problematic records in a very large Oracle table. The column should contain all numeric data even though it is a varchar2 column. I need to find the records which don't contain numeric data (The to_number(col_name) function throws an error when I try to call it on this column).
I was thinking you could use a regexp_like condition and use the regular expression to find any non-numerics. I hope this might help?!
SELECT * FROM table_with_column_to_search WHERE REGEXP_LIKE(varchar_col_with_non_numerics, '[^0-9]+');
To get an indicator:
DECODE( TRANSLATE(your_number,' 0123456789',' ')
e.g.
SQL> select DECODE( TRANSLATE('12345zzz_not_numberee',' 0123456789',' '), NULL, 'number','contains char')
2 from dual
3 /
"contains char"
and
SQL> select DECODE( TRANSLATE('12345',' 0123456789',' '), NULL, 'number','contains char')
2 from dual
3 /
"number"
and
SQL> select DECODE( TRANSLATE('123405',' 0123456789',' '), NULL, 'number','contains char')
2 from dual
3 /
"number"
Oracle 11g has regular expressions so you could use this to get the actual number:
SQL> SELECT colA
2 FROM t1
3 WHERE REGEXP_LIKE(colA, '[[:digit:]]');
COL1
----------
47845
48543
12
...
If there is a non-numeric value like '23g' it will just be ignored.
In contrast to SGB's answer, I prefer doing the regexp defining the actual format of my data and negating that. This allows me to define values like $DDD,DDD,DDD.DD
In the OPs simple scenario, it would look like
SELECT *
FROM table_with_column_to_search
WHERE NOT REGEXP_LIKE(varchar_col_with_non_numerics, '^[0-9]+$');
which finds all non-positive integers. If you wau accept negatiuve integers also, it's an easy change, just add an optional leading minus.
SELECT *
FROM table_with_column_to_search
WHERE NOT REGEXP_LIKE(varchar_col_with_non_numerics, '^-?[0-9]+$');
accepting floating points...
SELECT *
FROM table_with_column_to_search
WHERE NOT REGEXP_LIKE(varchar_col_with_non_numerics, '^-?[0-9]+(\.[0-9]+)?$');
Same goes further with any format. Basically, you will generally already have the formats to validate input data, so when you will desire to find data that does not match that format ... it's simpler to negate that format than come up with another one; which in case of SGB's approach would be a bit tricky to do if you want more than just positive integers.
Use this
SELECT *
FROM TableToSearch
WHERE NOT REGEXP_LIKE(ColumnToSearch, '^-?[0-9]+(\.[0-9]+)?$');
After doing some testing, i came up with this solution, let me know in case it helps.
Add this below 2 conditions in your query and it will find the records which don't contain numeric data
and REGEXP_LIKE(<column_name>, '\D') -- this selects non numeric data
and not REGEXP_LIKE(column_name,'^[-]{1}\d{1}') -- this filters out negative(-) values
Starting with Oracle 12.2 the function to_number has an option ON CONVERSION ERROR clause, that can catch the exception and provide default value.
This can be used for the test of number values. Simple set NULL when the conversion fails and filer all not NULL values.
Example
with num as (
select '123' vc_col from dual union all
select '1,23' from dual union all
select 'RV12P2000' from dual union all
select null from dual)
select
vc_col
from num
where /* filter numbers */
vc_col is not null and
to_number(vc_col DEFAULT NULL ON CONVERSION ERROR) is not null
;
VC_COL
---------
123
1,23
From http://www.dba-oracle.com/t_isnumeric.htm
LENGTH(TRIM(TRANSLATE(, ' +-.0123456789', ' '))) is null
If there is anything left in the string after the TRIM it must be non-numeric characters.
I've found this useful:
select translate('your string','_0123456789','_') from dual
If the result is NULL, it's numeric (ignoring floating point numbers.)
However, I'm a bit baffled why the underscore is needed. Without it the following also returns null:
select translate('s123','0123456789', '') from dual
There is also one of my favorite tricks - not perfect if the string contains stuff like "*" or "#":
SELECT 'is a number' FROM dual WHERE UPPER('123') = LOWER('123')
After doing some testing, building upon the suggestions in the previous answers, there seem to be two usable solutions.
Method 1 is fastest, but less powerful in terms of matching more complex patterns.
Method 2 is more flexible, but slower.
Method 1 - fastest
I've tested this method on a table with 1 million rows.
It seems to be 3.8 times faster than the regex solutions.
The 0-replacement solves the issue that 0 is mapped to a space, and does not seem to slow down the query.
SELECT *
FROM <table>
WHERE TRANSLATE(replace(<char_column>,'0',''),'0123456789',' ') IS NOT NULL;
Method 2 - slower, but more flexible
I've compared the speed of putting the negation inside or outside the regex statement. Both are equally slower than the translate-solution. As a result, #ciuly's approach seems most sensible when using regex.
SELECT *
FROM <table>
WHERE NOT REGEXP_LIKE(<char_column>, '^[0-9]+$');
You can use this one check:
create or replace function to_n(c varchar2) return number is
begin return to_number(c);
exception when others then return -123456;
end;
select id, n from t where to_n(n) = -123456;
I tray order by with problematic column and i find rows with column.
SELECT
D.UNIT_CODE,
D.CUATM,
D.CAPITOL,
D.RIND,
D.COL1 AS COL1
FROM
VW_DATA_ALL_GC D
WHERE
(D.PERIOADA IN (:pPERIOADA)) AND
(D.FORM = 62)
AND D.COL1 IS NOT NULL
-- AND REGEXP_LIKE (D.COL1, '\[\[:alpha:\]\]')
-- AND REGEXP_LIKE(D.COL1, '\[\[:digit:\]\]')
--AND REGEXP_LIKE(TO_CHAR(D.COL1), '\[^0-9\]+')
GROUP BY
D.UNIT_CODE,
D.CUATM,
D.CAPITOL,
D.RIND ,
D.COL1
ORDER BY
D.COL1