Override Max Function to allow strings SQL? - sql

Hello what I feel to be a simple question but cannot figure it out. I am trying to find the max number in relation to another column and group it, the issue that comes up is that one of the values is a string.
Name Value
Nate 0
Nate 1
Jeff 2
Nate 2
Nate 'Test'
For the data I actually want 'Test' to be equal to 1. However if I use the MAX() function here I will get:
Name Value
Nate 'Test'
Jeff 2
I can only think that maybe if I read 'Test' as 1 then use the max function (which I am not sure how to do) or possibly overload MAX() to my own definition somehow.
Thank you for any help you can give.

Storing mixed data in a string column is generally a bad idea.
You can convert a specific string to a fixed value with a case expression:
select max(case when value = 'Test' then '1' else value end) from ...
But you are still dealing with strings, so you probably want to convert them to numbers, to prevent '10' sorting before '2' for instance:
select max(to_number(case when value = 'Test' then '1' else value end)) from ...
or
select max(case when value = 'Test' then 1 else to_number(value) end) from ...
Using a CTE for your sample data:
-- CTE for dummy data
with your_table (name, value) as (
select 'Nate', '0' from dual
union all select 'Nate', '1' from dual
union all select 'Jeff', '2' from dual
union all select 'Nate', '2' from dual
union all select 'Nate', 'Test' from dual
)
-- actual query
select name,
max(case when value = 'Test' then 1 else to_number(value) end) as value
from your_table
group by name;
NAME VALUE
---- ----------
Nate 2
Jeff 2
But you have to cover all values that cannot be explicitly or implicitly converted to numbers.
If would be slightly easier if you wanted to ignore non-numeric values, or treat them all as the same fixed value, rather than mapping individual strings to their own numeric values. Then you could write a function that attempts to convert any string and if it gets any exception returns null (or some other fixed value).
From 12cR1 you can even do with with a PL/SQL declaration rather than a permanent standalone or packaged function, if this an occasional thing:
with
function hack_to_number(string varchar2) return number is
begin
return to_number(string);
exception
when others then
return 1;
end;
select name,
max(hack_to_number(value)) as value
from your_table
group by name;
NAME VALUE
---- ----------
Nate 2
Jeff 2
You'd probably be better off going back and redesigning the data model to prevent this kind of issue by using the correct data types.
As #DrYWit pointed out in a comment, from 12cR2 you don't even need to do that, as the to_number() function has this built in, if you call it explicitly:
select name,
max(to_number(value default 1 on conversion error)) as value
from your_table
group by name;

How about this regular expression "trick"?
SQL> with your_table (name, value) as (
2 select 'Nate', '0' from dual
3 union all select 'Nate', '1' from dual
4 union all select 'Jeff', '2' from dual
5 union all select 'Nate', '2' from dual
6 union all select 'Nate', 'Test' from dual
7 )
8 select name, max(to_number(value)) mv
9 from your_table
10 where regexp_like (value, '^\d+$')
11 group by name;
NAME MV
---- ----------
Nate 2
Jeff 2
SQL>

Related

Extracting substring in Oracle

Let's say I have three rows with value as
1 121/2808B|:6081
2 OD308B|:6081_1:
3 008312100001200|:6081_1
I want to display value only until B but want to exclude everything after B. So as you can see in above data:
from 121/2808B|:6081 I want only 121/2808B
from OD308B|:6081_1: only OD308B
from 008312100001200|:6081_1 only 008312100001200.
Thanks for the Help.
Try this: regexp_substr('<Your_string>','[^B]+')
SELECT
REGEXP_SUBSTR('121/2808B|:6081', '[^B]+')
FROM
DUAL;
REGEXP_S
--------
121/2808
SELECT
REGEXP_SUBSTR('OD308B|:6081_1:', '[^B]+')
FROM
DUAL;
REGEX
-----
OD308
SELECT
REGEXP_SUBSTR('008312100001200.', '[^B]+')
FROM
DUAL;
REGEXP_SUBSTR('0
----------------
008312100001200.
db<>fiddle demo
Cheers!!
You could try using SUBSTR() and INSTR()
select SUBSTR('121/2808B|:6081',1,INSTR('121/2808B|:6081','B', 1, 1) -1)
from DUAL
I think you forgot to mention that you wanted to use | as a field separator, but I deduced this from the expected result from the third string. As such the following should give you what you want:
WITH cteData AS (SELECT 1 AS ID, '121/2808B|:6081' AS STRING FROM DUAL UNION ALL
SELECT 2, 'OD308B|:6081_1:' FROM DUAL UNION ALL
SELECT 3, '008312100001200|:6081_1' FROM DUAL)
SELECT ID, STRING, SUBSTR(STRING, 1, CASE
WHEN INSTR(STRING, 'B') = 0 THEN INSTR(STRING, '|')-1
ELSE INSTR(STRING, 'B')-1
END) AS UP_TO_B
FROM cteData;
dbfiddle here
Assuming Bob Jarvis is correct in the assumption that "|" is also a delimiter (as seems likely) try:
-- define test data
with test as
( select '121/2808B|:6081' stg from dual union all
select 'OD308B|:6081_1:' from dual union all
select '008312100001200|:6081_1' from dual
)
-- execute extract
select regexp_substr(stg , '[^B|]+') val
from test ;

Sort a value list that contains letters and also numbers in a specific order

I have a problem in SQL Oracle, I'm trying to create a view that contains values with letters and numbers and I want to sort them in a specific order.
Here is my query:
create or replace view table1_val (val, msg_text) as
select
val, msg_text
from
table_val
where
val in ('L1','L2','L3','L4','L5','L6','L7','L8','L9','L10','L11','L12','L13','L14','G1','G2','G3','G4')
order by lpad(val, 3);
The values are displayed like this:
G1,G2,G3,G4,L1,L2,L3,L4,L5,L6,L7,L8,L9,L10,L11,L12,L13
The thing is that I want to display the L values first and then the G values like in the where condition. The 'val' column is VARCHAR2(3 CHAR). The msg_text column is irrelevant. Can someone help me with that? I use Oracle 12C.
You must interpret the second part of the val column as a number
order by
case when val like 'L%' then 0 else 1 end,
to_number(substr(val,2))
This work fine for your current data, but may fail in future if a new record is added with non-numeric structure.
More conservative (and more hard to write), but safe would be to used a decode for all the current keys, ordering unknown keys on the last position (id = 18 in the example):
order by
decode(
'L1',1,
'L2',2,
'L3',3,
'L4',4,
'L5',5,
'L6',6,
'L7',7,
'L8',8,
'L9',9,
'L10',10,
'L11',11,
'L12',12,
'L13',13,
'G1',14,
'G2',15,
'G3',16,
'G4',17,18)
You can't do anything based on the order of the WHERE condition
But you can use a CASE on the ORDER BY
ORDER BY CASE
WHEN SUBSTR(val, 1, 1) = 'L' THEN 1
WHEN SUBSTR(val, 1, 1) = 'G' THEN 2
ELSE 3
END,
TO_NUMBER (SUBSTR(val, 2, 10));
Another option to consider might be using regular expressions, such as
SQL> with table1_val (val) as
2 (select 'L1' from dual union all
3 select 'L26' from dual union all
4 select 'L3' from dual union all
5 select 'L21' from dual union all
6 select 'L11' from dual union all
7 select 'L4' from dual union all
8 select 'G88' from dual union all
9 select 'G10' from dual union all
10 select 'G2' from dual
11 )
12 select val
13 from table1_val
14 order by regexp_substr(val, '^[[:alpha:]]+') desc,
15 to_number(regexp_substr(val, '\d+$'));
VAL
---
L1
L3
L4
L11
L21
L26
G2
G10
G88
9 rows selected.
SQL>

Oracle - Extract numbers for comparison from varchar2 column

I have encountered a following problem while solving a task:
In Oracle database I have got a table ENTITY_INFO that is fairly simple in structure. It contains 3 columns:
ENTITY_ID (VARCHAR2) - PK of the entity in database
NAME (VARCHAR2) - name of the information, i.e. "location", "cost", "last encounter"
VALUE (VARCHAR2) - a value of the information, i.e. "assets/music", "1500", "1.1.2000"
Currently, I need to filter out entities that have its "cost" < 1000.
A naive approach via
SELECT ENTITY_ID FROM ENTITY_INFO WHERE NAME = 'cost' AND TO_NUMBER(VALUE)<1000
does not work, because column VALUE contains values that are not number.
But all column values that match the filter NAME = 'cost' are numbers, so the case I need to do is valid.
I found Select string as number on Oracle topic, but the information inside prove not useful to solving this problem.
Due to nature of ENTITY_INFO and a state of project, the change of datamodel is not viable solution too.
Thanks for any hints.
You could make the conversion to a number conditional:
SELECT ENTITY_ID
FROM ENTITY_INFO
WHERE NAME = 'cost'
AND TO_NUMBER(CASE WHEN NAME = 'cost' THEN VALUE ELSE NULL END) < 1000
Alternate approach leveraging WITH clause, on the presumption that all the records with name are numbers
In the tab1 part, use the filter condition and query from tab1 with TO_NUMBER
WITH tab1
AS (SELECT entity_id, name, VALUE
FROM entity_info
WHERE name = 'cost')
SELECT *
FROM tab1
WHERE TO_NUMBER (VALUE) < 1000
Having numbers and characters in one column is an accident waiting to happen. Adding another column to distinguish numeric and non-numeric is not an option, I would reckon to have a constraint to deter entering non-numeric if name is cost
In my compiler, I see no problem with your code (or this equivalence of it's):
SELECT ENTITY_ID
FROM ENTITY_INFO
WHERE NAME = 'cost'
AND VALUE < 1000
Example with data samples:
with ENTITY_INFO as (
select 1 as ENTITY_ID, 'cost' as name, '2000' as value from dual
union all
select 2 as ENTITY_ID, 'cost' as name, '900' as value from dual
union all
select 3 as ENTITY_ID, 'cost' as name, '3000' as value from dual
union all
select 4 as ENTITY_ID, 'cost' as name, '2500' as value from dual
union all
select 5 as ENTITY_ID, 'cost' as name, '700' as value from dual
union all
select 6 as ENTITY_ID, 'frf' as name, '250sasd0' as value from dual
union all
select 7 as ENTITY_ID, 'corfrst' as name, '70fa0' as value from dual
)
SELECT ENTITY_ID
FROM ENTITY_INFO
WHERE NAME = 'cost'
AND VALUE < 1000
Result:
ENTITY_ID
2
5
Alternatively, you can use the subquery that would assure that all of the resulting column values from it would be number-like strings:
SELECT ENTITY_ID
FROM (SELECT ENTITY_ID,
VALUE
FROM ENTITY_INFO
WHERE NAME = 'cost' )
WHERE TO_NUMBER(VALUE)<1000
I hope I helped!

Is there a concept which is the 'opposite' of SQL NULL?

Is there a concept (with an implementation - in Oracle SQL for starters) which behaves like a 'universal' matcher ?
What I mean is; I know NULL is not equal to anything - including NULL.
Which is why you have to be careful to 'IS NULL' rather than '=NULL' in SQL expressions.
I also know it is useful to use the NVL (in Oracle) function to detect a NULL and replace it with something in the output.
However: what you replace the NULL with using NVL has to match the datatype of the underlying column; otherwise you'll (rightly) get an error.
An example:
I have a table with a NULLABLE column 'name' of type VARCHAR2; and this contains a NULL row.
I can fetch out the NULL and replace it with an NVL like this:
SELECT NVL(name, 'NullyMcNullFace’) from my_table;
Great.
But if the column happens to a NUMBER (say 'age'), then I have to change my NVL:
SELECT NVL(age, 32) from my_table;
Also great.
Now if the column happens to be a DATE (say 'somedate'), then I have to change my NVL again:
SELECT NVL(somedate, sysdate) from my_table;
What I'm getting at here : is that in order to deal with NULLs you have to replace with a specific something ; and that specific something has to 'fit' the data-type.
So is there a construct/concept of (for want of a better word) like 'ANY' here.
Where 'ANY' would fit into a column of any datatype (like NULL), but (unlike NULL and unlike all other specific values) would match ANYTHING (including NULL - ? probably urghhh dunno).
So that I could do:
SELECT NVL(whatever_column, ANY) from my_table;
I think the answer is probably no; and probably 'go away, NULLs are bad enough - never mind this monster you have half-thought of'.
No, there's no "universal acceptor" value in SQL that is equal to everything.
What you can do is raise the NVL into your comparison. Like if you're trying to do a JOIN:
SELECT ...
FROM my_table AS m
JOIN other_table AS o ON o.name = NVL(m.name, o.name)
So if m.name is NULL, then the join will compare o.name to o.name, which is of course always true.
For other uses of NULL, you might have to use another technique that suits the situation.
Adressing the question in the comment on Bill Karwin's answer:
I want to output a 1 if the NEW and OLD value differ and a 0 if they are the same. But (for my purposes) I want to also return 0 for two NULLS.
select
Case When (:New = :Old) or
(:New is NULL and :Old is NULL) then 0
Else
1
End
from dual
In a WHERE CLAUSE you can put a condition like this,
WHERE column1 LIKE NVL(any_column_or_param, '%')
Perhaps DECODE() would suit your purpose here?
WITH t1 AS (SELECT 1 ID, NULL val FROM dual UNION ALL
SELECT 2 ID, NULL val FROM dual UNION ALL
SELECT 3 ID, 1 val FROM dual UNION ALL
SELECT 4 ID, 2 val FROM dual UNION ALL
SELECT 5 ID, 5 val FROM dual),
t2 AS (SELECT 1 ID, NULL val FROM dual UNION ALL
SELECT 2 ID, 3 val FROM dual UNION ALL
SELECT 3 ID, 1 val FROM dual UNION ALL
SELECT 4 ID, 4 val FROM dual UNION ALL
SELECT 6 ID, 5 val FROM dual)
SELECT t1.id t1_id,
t1.val t1_val,
t2.id t2_id,
t2.val t2_val,
DECODE(t1.val, t2.val, 0, 1) different_vals
FROM t1
FULL OUTER JOIN t2 ON t1.id = t2.id
ORDER BY COALESCE(t1.id, t2.id);
T1_ID T1_VAL T2_ID T2_VAL DIFFERENT_VALS
---------- ---------- ---------- ---------- --------------
1 1 0
2 2 3 1
3 1 3 1 0
4 2 4 4 1
5 5 1
6 5 1

Oracle SQL Min in Select Clause

Can some one please help me in writing a sql query that should do a oracle min function based on the following conditions.
For eg for column values
0,0,0,0 then output should be 0
0,null,0,null then output should be o
0,2,4,5,6 then output should be 2 (Note that we are excluding Zero here)
0,2,null,4,5 then output should be 2 (same here we are excluding zero)
null,null,null, null then output should be null.
I wrote query already that satisfies all the above cases but failing for last case when all the column values are null. Instead of returning null it is returning 0. Can some one modify the below query to fit for the last case as well?
select NVL(MIN(NULLIF(columnname,0)),0) from tablename;
Please also keep in mind that the query should be runnable in oracle as well as hsqldb as we are using hsql db for running junits.
If all 4 cases satisfied by your query then just a case will solve your problem.
SELECT CASE WHEN MIN(COLUMNNAME) IS NULL THEN NULL ELSE NVL(MIN(NULLIF(COLUMNNAME,0)),0) END FROM TABLENAME;
Note:- assuming all the cases satisfied by your query except 5th.
I will show below an input table with two columns, ID and VAL, to illustrate the various possibilities. You want a single result per ID (or even for the entire table), so this must be a job for GROUP BY and some aggregate function. You want to distinguish between three types of values: Greater than zero, zero, and null (in this order); you want to pick the "highest priority group" that exists for each ID (in this order of priority), and for that priority group only, you want to pick the min value. This is exactly what the aggregate FIRST/LAST function does. To order by the three "classes" of values, we use a CASE expression in the ORDER BY clause of the aggregate LAST function.
The WITH clause below is not part of the solution - I only include it to create test data (in your real life situation, use your actual table and column names and remove the entire WITH clause).
with
inputs ( id, val ) as (
select 1, 0 from dual union all
select 1, 0 from dual union all
select 1, 0 from dual union all
select 2, 0 from dual union all
select 2, null from dual union all
select 2, 0 from dual union all
select 3, 0 from dual union all
select 3, 2 from dual union all
select 3, 5 from dual union all
select 4, 0 from dual union all
select 4, 3 from dual union all
select 4, null from dual union all
select 5, null from dual union all
select 5, null from dual
)
select id,
min(val) keep (dense_rank last order by case when val > 0 then 2
when val = 0 then 1
else 0
end
) as min_val
from inputs
group by id
order by id
;
ID MIN_VAL
---------- ----------
1 0
2 0
3 2
4 3
5