OracleSQL CASE Statement on Number in string - sql

I have a table called TEST_TABLE with 1 column called COLUMN1. This table has 2 records:
V.WEEKLY_2020_15
V.WEEKLY_2020_16
I'm trying to write a CASE statement that maps these records to different Periods. e.g.
SELECT
CASE WHEN COLUMN1='V.WEEK_2020_ **MAXIMUM NUMBER** ' THEN 'CURRENT PERIOD'
ELSE 'HISTORICAL PERIOD 1' END
FROM TEST_TABLE
I'm not sure what is the best way to do this though. I need to get the number from the end of the string, and then compare it to the other numbers in the table. Once it finds one number that is higher or lower it can stop the search as there will always only be 2 numbers in this table.

You can get the number from the end of the string with a regular expression. This one gets the 3rd group of characters which don't include an underscore.
select column1, regexp_substr(column1,'[^_]+',1,3) from test_table;
Alternately you could get the 2nd group of numbers with regexp_substr(column1,'[0-9]+',1,2). The best regexp will depend on your knowledge of the possible string values. If you know the number will always be the last 2 characters, you could do substr(column1, -2)
And if you want to identify rows which have the highest/lowest/etc value, adding a column which applies an window/analytical function is a common pattern.
-- sample data
with test_table as (select 'V.WEEKLY_2020_15' as column1 from dual
union select 'V.WEEKLY_2020_16' from dual)
-- query
SELECT column1, regexp_substr(column1,'[^_]+',1,3) as regex, max_number,
CASE WHEN COLUMN1=max_number THEN 'CURRENT PERIOD'
ELSE 'HISTORICAL PERIOD 1' END as period
FROM (select test_table.*,
max(column1) over (order by regexp_substr(column1,'[^_]+',1,3) desc) as max_number
from test_table) T;
Usually the data will be more complicated then you're showing - for example, you might have 2 periods in the table for each primary key, and then you'll want to partition your window function.
-- sample data
with test_table as (select 1 as pk, 'V.WEEKLY_2020_15' as column1 from dual
union select 1, 'V.WEEKLY_2020_16' from dual
union select 2, 'V.WEEKLY_2021_1' from dual
union select 2, 'V.WEEKLY_2021_200' from dual)
-- query
SELECT pk, column1, regexp_substr(column1,'[^_]+',1,3) as regex,
CASE WHEN COLUMN1=max_number THEN 'CURRENT PERIOD'
ELSE 'HISTORICAL PERIOD 1' END as period
FROM (select test_table.*,
max(column1) over (partition by pk order by regexp_substr(column1,'[^_]+',1,3) desc) as max_number
from test_table) T;
Output:
PK
COLUMN1
REGEX
PERIOD
1
V.WEEKLY_2020_16
16
CURRENT PERIOD
1
V.WEEKLY_2020_15
15
HISTORICAL PERIOD 1
2
V.WEEKLY_2021_200
200
CURRENT PERIOD
2
V.WEEKLY_2021_1
1
HISTORICAL PERIOD 1

Related

Oracle Pivot rows to columns pattern matching

I want to rearrange the rows to columns (in tbl2 below) to count the number of occurrences of EXEN for the EXEN col, and any code starting with MPA for the MPACODE column.
SELECT *
FROM (select code from tbl2 where pidm='4062161')
PIVOT (count(*) FOR (code) IN ('EXEN' AS EXEN, 'MPA%' AS MPACODE));
tbl2:
Desired output:
Actual output:
You must perform an intermediate step to transform all MPA%to MPAsee subquery dt2
with dt as (
select 'EXEN' code from dual union all
select 'MPA'||rownum from dual connect by level <= 10),
dt2 as (
select
case when code like 'MPA%' then 'MPA' else code end as code
from dt)
select *
from dt2
pivot (
count(*) for
(code) IN ('EXEN' AS EXEN, 'MPA' AS MPACODE));
EXEN MPACODE
---------- ----------
1 10
PIVOT perform an equal comparison (not LIKE), so this is not valid: 'MPA%' AS MPACODE and the reason why the query fails.
for example:
select
count(case when code='EXEN' then 1 end) exen,
count(case when code like 'MPA%' then 1 end) mpacode
from tbl2 where pidm='4062161';

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!

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

Select where record does not exists

I am trying out my hands on oracle 11g. I have a requirement such that I want to fetch those id from list which does not exists in table.
For example:
SELECT * FROM STOCK
where item_id in ('1','2'); // Return those records where result is null
I mean if item_id '1' is not present in db then the query should return me 1.
How can I achieve this?
You need to store the values in some sort of "table". Then you can use left join or not exists or something similar:
with ids as (
select 1 as id from dual union all
select 2 from dual
)
select ids.id
from ids
where not exists (select 1 from stock s where s.item_id = ids.id);
You can use a LEFT JOIN to an in-line table that contains the values to be searched:
SELECT t1.val
FROM (
SELECT '1' val UNION ALL SELECT '2'
) t1
LEFT JOIN STOCK t2 ON t1.val = t2.item_id
WHERE t2.item_id IS NULL
First create the list of possible IDs (e.g. 0 to 99 in below query). You can use a recursive cte for this. Then select these IDs and remove the IDs already present in the table from the result:
with possible_ids(id) as
(
select 0 as id from dual
union all
select id + 1 as id from possible_ids where id < 99
)
select id from possible_ids
minus
select item_id from stock;
A primary concern of the OP seems to be a terse notation of the query, notably the set of values to test for. The straightforwwrd recommendation would be to retrieve these values by another query or to generate them as a union of queries from the dual table (see the other answers for this).
The following alternative solution allows for a verbatim specification of the test values under the following conditions:
There is a character that does not occur in any of the test values provided ( in the example that will be - )
The number of values to test stays well below 2000 (to be precise, the list of values plus separators must be written as a varchar2 literal, which imposes the length limit ). However, this should not be an actual concern - If the test involves lists of hundreds of ids, these lists should definitely be retrieved froma table/view.
Caveat
Whether this method is worth the hassle ( not to mention potential performance impacts ) is questionable, imho.
Solution
The test values will be provided as a single varchar2 literal with - separating the values which is as terse as the specification as a list argument to the IN operator. The string starts and ends with -.
'-1-2-3-156-489-4654648-'
The number of items is computed as follows:
select cond, regexp_count ( cond, '[-]' ) - 1 cnt_items from (select '-1-2-3-156-489-4654648-' cond from dual)
A list of integers up to the number of items starting with 1 can be generated using the LEVEL pseudocolumn from hierarchical queries:
select level from dual connect by level < 42;
The n-th integer from that list will serve to extract the n-th value from the string (exemplified for the 4th value) :
select substr ( cond, instr(cond,'-', 1, 4 )+1, instr(cond,'-', 1, 4+1 ) - instr(cond,'-', 1, 4 ) - 1 ) si from (select cond, regexp_count ( cond, '[-]' ) - 1 cnt_items from (select '-1-2-3-156-489-4654648-' cond from dual) );
The non-existent stock ids are generated by subtracting the set of stock ids from the set of values. Putting it all together:
select substr ( cond, instr(cond,'-',1,level )+1, instr(cond,'-',1,level+1 ) - instr(cond,'-',1,level ) - 1 ) si
from (
select cond
, regexp_count ( cond, '[-]' ) - 1 cnt_items
from (
select '-1-2-3-156-489-4654648-' cond from dual
)
)
connect by level <= cnt_items + 1
minus
select item_id from stock
;

How do I sort a VARCHAR column in SQL server that contains numbers?

I have a VARCHAR column in a SQL Server 2000 database that can contain either letters or numbers. It depends on how the application is configured on the front-end for the customer.
When it does contain numbers, I want it to be sorted numerically, e.g. as "1", "2", "10" instead of "1", "10", "2". Fields containing just letters, or letters and numbers (such as 'A1') can be sorted alphabetically as normal. For example, this would be an acceptable sort order.
1
2
10
A
B
B1
What is the best way to achieve this?
One possible solution is to pad the numeric values with a character in front so that all are of the same string length.
Here is an example using that approach:
select MyColumn
from MyTable
order by
case IsNumeric(MyColumn)
when 1 then Replicate('0', 100 - Len(MyColumn)) + MyColumn
else MyColumn
end
The 100 should be replaced with the actual length of that column.
There are a few possible ways to do this.
One would be
SELECT
...
ORDER BY
CASE
WHEN ISNUMERIC(value) = 1 THEN CONVERT(INT, value)
ELSE 9999999 -- or something huge
END,
value
the first part of the ORDER BY converts everything to an int (with a huge value for non-numerics, to sort last) then the last part takes care of alphabetics.
Note that the performance of this query is probably at least moderately ghastly on large amounts of data.
select
Field1, Field2...
from
Table1
order by
isnumeric(Field1) desc,
case when isnumeric(Field1) = 1 then cast(Field1 as int) else null end,
Field1
This will return values in the order you gave in your question.
Performance won't be too great with all that casting going on, so another approach is to add another column to the table in which you store an integer copy of the data and then sort by that first and then the column in question. This will obviously require some changes to the logic that inserts or updates data in the table, to populate both columns. Either that, or put a trigger on the table to populate the second column whenever data is inserted or updated.
SELECT *, CONVERT(int, your_column) AS your_column_int
FROM your_table
ORDER BY your_column_int
OR
SELECT *, CAST(your_column AS int) AS your_column_int
FROM your_table
ORDER BY your_column_int
Both are fairly portable I think.
you can always convert your varchar-column to bigint as integer might be too short...
select cast([yourvarchar] as BIGINT)
but you should always care for alpha characters
where ISNUMERIC([yourvarchar] +'e0') = 1
the +'e0' comes from http://blogs.lessthandot.com/index.php/DataMgmt/DataDesign/isnumeric-isint-isnumber
this would lead to your statement
SELECT
*
FROM
Table
ORDER BY
ISNUMERIC([yourvarchar] +'e0') DESC
, LEN([yourvarchar]) ASC
the first sorting column will put numeric on top.
the second sorts by length, so 10 will preceed 0001 (which is stupid?!)
this leads to the second version:
SELECT
*
FROM
Table
ORDER BY
ISNUMERIC([yourvarchar] +'e0') DESC
, RIGHT('00000000000000000000'+[yourvarchar], 20) ASC
the second column now gets right padded with '0', so natural sorting puts integers with leading zeros (0,01,10,0100...) in correct order (correct!) - but all alphas would be enhanced with '0'-chars (performance)
so third version:
SELECT
*
FROM
Table
ORDER BY
ISNUMERIC([yourvarchar] +'e0') DESC
, CASE WHEN ISNUMERIC([yourvarchar] +'e0') = 1
THEN RIGHT('00000000000000000000' + [yourvarchar], 20) ASC
ELSE LTRIM(RTRIM([yourvarchar]))
END ASC
now numbers first get padded with '0'-chars (of course, the length 20 could be enhanced) - which sorts numbers right - and alphas only get trimmed
I solved it in a very simple way writing this in the "order" part
ORDER BY (
sr.codice +0
)
ASC
This seems to work very well, in fact I had the following sorting:
16079 Customer X
016082 Customer Y
16413 Customer Z
So the 0 in front of 16082 is considered correctly.
This seems to work:
select your_column
from your_table
order by
case when isnumeric(your_column) = 1 then your_column else 999999999 end,
your_column
This query is helpful for you. In this query, a column has data type varchar is arranged by good order.For example- In this column data are:- G1,G34,G10,G3. So, after running this query, you see the results: - G1,G10,G3,G34.
SELECT *,
(CASE WHEN ISNUMERIC(column_name) = 1 THEN 0 ELSE 1 END) IsNum
FROM table_name
ORDER BY IsNum, LEN(column_name), column_name;
This may help you, I have tried this when i got the same issue.
SELECT *
FROM tab
ORDER BY IIF(TRY_CAST(val AS INT) IS NULL, 1, 0),TRY_CAST(val AS INT);
The easiest and efficient way to get the job done is using TRY_CAST
SELECT my_column
FROM my_table
WHERE <condition>
ORDER BY TRY_CAST(my_column AS NUMERIC) DESC
This will sort all numbers in descending order and push down all non numeric values
SELECT FIELD FROM TABLE
ORDER BY
isnumeric(FIELD) desc,
CASE ISNUMERIC(test)
WHEN 1 THEN CAST(CAST(test AS MONEY) AS INT)
ELSE NULL
END,
FIELD
As per this link you need to cast to MONEY then INT to avoid ordering '$' as a number.
SELECT *,
ROW_NUMBER()OVER(ORDER BY CASE WHEN ISNUMERIC (ID)=1 THEN CONVERT(NUMERIC(20,2),SUBSTRING(Id, PATINDEX('%[0-9]%', Id), LEN(Id)))END DESC)Rn ---- numerical
FROM
(
SELECT '1'Id UNION ALL
SELECT '25.20' Id UNION ALL
SELECT 'A115' Id UNION ALL
SELECT '2541' Id UNION ALL
SELECT '571.50' Id UNION ALL
SELECT '67' Id UNION ALL
SELECT 'B48' Id UNION ALL
SELECT '500' Id UNION ALL
SELECT '147.54' Id UNION ALL
SELECT 'A-100' Id
)A
ORDER BY
CASE WHEN ISNUMERIC (ID)=0 /* alphabetical sort */
THEN CASE WHEN PATINDEX('%[0-9]%', Id)=0
THEN LEFT(Id,PATINDEX('%[0-9]%',Id))
ELSE LEFT(Id,PATINDEX('%[0-9]%',Id)-1)
END
END DESC