Oracle custom sort - sql

The query...
select distinct name from myTable
returns a bunch of values that start with the following character sequences...
ADL*
FG*
FH*
LAS*
TWUP*
Where '*' is the remainder of the string.
I want to do an order by that sorts in the following manner...
ADL*
LAS*
TWUP*
FG*
FH*
But then I also want to sort within each name in the standard order by fashion. So, an example, if I have the following values
LAS-21A
TWUP-1
FG999
FH3
ADL99999
ADL88888
ADL77777
LAS2
I want it to be sorted like this...
ADL77777
ADL88888
ADL99999
LAS2
TWUP-1
FG999
FH3
I initially thought I could accomplish this vias doing an order by decode(blah) with some like trickery inside of the decode but I've been unable to accomplish it. Any insights?

Goofy and verbose, but should work:
select name, case when substr (name, 1, 3) = 'ADL' then 1
when substr (name, 1, 3) = 'LAS' then 2
when substr (name, 1, 4) = 'TWUP' then 3
when substr (name, 1, 2) = 'FG' then 4
when substr (name, 1, 2) = 'FH' then 5
else 6
end SortOrder
from myTable
order by 2, 1;
Not sure if 6 is the correct place to sort the other items, but it is obvious how to fix that. At least it is clear what is going on, even if I have no idea why you are doing it this way.
EDIT: If these are the only values, you could change lines 4 and 5:
select name, case when substr (name, 1, 3) = 'ADL' then 1
when substr (name, 1, 3) = 'LAS' then 2
when substr (name, 1, 4) = 'TWUP' then 3
when substr (name, 1, 1) = 'F' then 4
else 6
end SortOrder
from myTable
order by 2, 1;
ANOTHER EDIT: And again, if these are the only values, you can simplify even more. Since the only one out of order is the F* series, you can force them to the end, and use the actual first letter for all the others. This is simpler, but relies too much on the exact values for my preference. On the other hand, it does remove many of the seemingly unnecessary calls to substr :
select name, case when substr (name, 1, 1) = 'F' then 'Z'
else name
end SortOrder
from myTable
order by 2, 1;

The problem is that your prefix contains a variable number of characters. This is a good time to deploy regular expressions (if you have 10g or higher).
SQL> select cola
2 from t34
3 order by decode( regexp_substr(cola, '[[:alpha:]]+')
4 , 'ADL' , 10
5 , 'LAS', 20
6 , 'TWUP', 30
7 , 'FG' , 40
8 , 'FH' , 50
9 , 60 )
10 , cola
11 /
COLA
----------
ADL77777
ADL88888
ADL99999
LAS-21A
LAS2
TWUP-1
FG999
FH3
8 rows selected.
SQL>
If earlier versions of Oracle we can use the OWA_PATTERN.AMATCH() function to the same effect:
SQL> select cola
2 from t34
3 order by decode( owa_pattern.amatch(cola, 1, '^[A-Z]+')
4 , 'ADL' , 10
5 , 'LAS', 20
6 , 'TWUP', 30
7 , 'FG' , 40
8 , 'FH' , 50
9 , 60 )
10 , cola
11 /
COLA
----------
ADL77777
ADL88888
ADL99999
FG999
FH3
LAS-21A
LAS2
TWUP-1
8 rows selected.
SQL>

Related

Oracle SQL: Merging multiple columns into 1 with conditions

I am new to SQL and don't really have a lot of experience. I need help on this where I have Table A and I want to write a SQL query to generate the result. Any help would be greatly appreciated! Thanks!
Table A
Name
Capacity A
Capacity B
Capacity C
Plant 1
10
20
Plant 2
10
Result Table
Name
Type
Capacity
Plant 1
A,C
10,20
Plant 2
B
10
I know listagg function might be able to combine few columns into one, but is there anyway for me to generate the additional column 'Type' where its smart enough to know which column I am taking my value from? Preferably without creating any additional views/table.
Use NVL2 (or CASE) and concatenate the columns and trim any excess trailing commas:
SELECT Name,
RTRIM(
NVL2(CapacityA,'A,',NULL)
||NVL2(CapacityB,'B,',NULL)
||NVL2(CapacityC,'C',NULL),
','
) AS type,
RTRIM(
NVL2(CapacityA,CapacityA||',',NULL)
||NVL2(CapacityB,CapacityB||',',NULL)
||NVL2(CapacityC,CapacityC,NULL),
','
) AS capacity
FROM table_name;
Which, for the sample data:
CREATE TABLE table_name (name, capacitya, capacityb, capacityc) AS
SELECT 'Plant1', 10, NULL, 20 FROM DUAL UNION ALL
SELECT 'Plant2', NULL, 10, NULL FROM DUAL;
Outputs:
NAME
TYPE
CAPACITY
Plant1
A,C
10,20
Plant2
B
10
db<>fiddle here
Here's one option:
sample data in lines #1 - 4
temp CTE simply - conditionally - concatenates types and capacities
final query (line #17)
removes double separators (commas) (regexp)
removes superfluous leading/trailing commas (trim)
SQL> with test (name, capa, capb, capc) as
2 (select 'Plant1', 10, null, 20 from dual union all
3 select 'Plant2', null, 10, null from dual
4 ),
5 temp as
6 (select name,
7 --
8 case when capa is not null then 'A' end ||','||
9 case when capb is not null then 'B' end ||','||
10 case when capc is not null then 'C' end as type,
11 --
12 case when capa is not null then capa end ||','||
13 case when capb is not null then capb end ||','||
14 case when capc is not null then capc end as capacity
15 from test
16 )
17 select name,
18 trim(both ',' from regexp_replace(type , ',+', ',')) as type,
19 trim(both ',' from regexp_replace(capacity, ',+', ',')) as capacity
20 from temp;
NAME TYPE CAPACITY
------ ---------- ----------
Plant1 A,C 10,20
Plant2 B 10
SQL>

How to union a hardcoded row after each grouped result

After every group / row i want to insert a hardcoded dummy row with a bunch of 'xxxx' to act a separator.
I would like to use oracle sql to do this query. i can execute it using a loop but i don't want to use plsql.
As the others suggest, it is best to do it on the front end.
However, if you have a burning need to be done as a query, here is how.
Here I did not use the rownum function as you have already done. I assume, your data is returned by a query, and you can replace my table with your query.
I made few more assumptions, as you have data with row numbers in it.
[I am not sure what do you mean by not PL/SQL]
Select Case When MOD(rownm, 2) = 0 then ' '
Else to_char((rownm + 1) / 2) End as rownm,
name, total, column1
From
(
select (rownm * 2 - 1) rownm,name, to_char(total) total ,column1 from t
union
SELECT (rownm * 2) rownm,'XXX' name, 'XXX' total, 'The row act .... ' column1 FROM t
) Q
Order by Q.rownm;
and here is the fiddle
Since you're already grouping the data, it might be easier to use GROUPING SETS instead of a UNION.
Grouping sets let you group by multiple sets of columns, including the same set twice to duplicate rows. Then the GROUP_ID function can be used to determine when the fake values should be used. This code will be a bit smaller than a UNION approach, and should be faster since it doesn't need to reference the table multiple times.
select
case when group_id() = 0 then name else '' end name,
case when group_id() = 0 then sum(some_value) else null end total,
case when group_id() = 1 then 'this rows...' else '' end column1
from
(
select 'jack' name, 22 some_value from dual union all
select 'jack' name, 1 some_value from dual union all
select 'john' name, 44 some_value from dual union all
select 'john' name, 1 some_value from dual union all
select 'harry' name, 1 some_value from dual union all
select 'harry' name, 1 some_value from dual
) raw_data
group by grouping sets (name, name)
order by raw_data.name, group_id();
You can use row generator technique (using CONNECT BY) and then use CASE..WHEN as follows:
SQL> SELECT CASE WHEN L.LVL = 1 THEN T.ROWNM END AS ROWNM,
2 CASE WHEN L.LVL = 1 THEN T.NAME
3 ELSE 'XXX' END AS NAME,
4 CASE WHEN L.LVL = 1 THEN TO_CHAR(T.TOTAL)
5 ELSE 'XXX' END AS TOTAL,
6 CASE WHEN L.LVL = 1 THEN T.COLUMN1
7 ELSE 'This row act as separator..' END AS COLUMN1
8 FROM T CROSS JOIN (
9 SELECT LEVEL AS LVL FROM DUAL CONNECT BY LEVEL <= 2
10 ) L ORDER BY T.ROWNM, L.LVL;
ROWNM NAME TOTAL COLUMN1
---------- ---------- ----- ---------------------------
1 Jack 23
XXX XXX This row act as separator..
2 John 45
XXX XXX This row act as separator..
3 harry 2
XXX XXX This row act as separator..
4 roy 45
XXX XXX This row act as separator..
5 Jacob 26
XXX XXX This row act as separator..
10 rows selected.
SQL>

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>

Using Regex_substr in Oracle to select string up to the last occurence of a space within \n characters length

We have an issue where a column in our Oracle database has a longer character length than a field in another system.
Therefore I am trying to use case statements along with substr in order to split strings that are more than 40 characters in length. My case statements so far do what I want them to do in the fact that it leaves the first 40 characters of a string in column_a and then puts the remainder of the string in column_b.
However, the problem that I have is that by just using substr, the strings are being split midway through words.
So I was wondering if anybody knew of a couple of regular expressions that I could use with regex_substr that will -
select a string UP TO the last space within 40 characters - for
column_a
select a string AFTER the last space within 40 characters - for
column_b
These are the case statements that I have so far with substr:
CASE WHEN Length(column_a) > 10 THEN SubStr(column_a, 0, 40) END AS column_a,
CASE WHEN Length(column_a) > 40 THEN SubStr(addressnum, 41) END AS column_b
I am not familiar with regular expressions at all and so any help would be very much appreciated!
I've solved with instr/substr:
select substr(column_a,1,instr(substr(column_a,1,40), ' ', -1 )) column1,
substr(column_a,instr(substr(column_a,1,40), ' ', -1 )+1, 40) column2
from table1
A very similar problem was posted today on OTN. https://community.oracle.com/message/13928697#13928697
I posted a general solution, which will cover the problem proposed here as well. It may come in handy if there are similar needs in the future.
For the problem posted here on SO, the row_lengths table will have only one row, with r_id = 1 and r_len = 40. For demo purposes I am showing below an input_strings different from what I used on OTN.
Setup:
create table input_strings (str_id number, txt varchar2(500));
insert into input_strings values (1,
'One Hundred Sixty-Nine Thousand Eight Hundred Seventy-Four Dollars And Nine Cents');
insert into input_strings values (2, null);
insert into input_strings values (3, 'Mathguy rules');
create table row_lengths (r_id number, r_len number);
insert into row_lengths values (1, 40);
commit;
select * from input_strings;
STR_ID TXT
------- ---------------------------------------------------------------------------------
1 One Hundred Sixty-Nine Thousand Eight Hundred Seventy-Four Dollars And Nine Cents
2
3 Mathguy rules
3 rows selected
select * from row_lengths;
R_ID R_LEN
------- ----------
1 40
1 row selected.
Query and output: (NOTE: I include token length to verify that the first token is no more than 40 characters. OP did not answer if the SECOND token can be more than 40 characters; if it can't, one can add rows to the row_lengths table, perhaps with r_len = 40 for every row.)
with
r ( r_id, r_len ) as (
select r_id , r_len from row_lengths union all
select max(r_id) + 1, 4000 from row_lengths union all
select max(r_id) + 2, null from row_lengths
),
b (str_id, str, r_id, token, prev_pos, new_pos) as (
select str_id, txt || ' ', -1, null, null, 0
from input_strings
union all
select b.str_id, b.str, b.r_id + 1,
substr(str, prev_pos + 1, new_pos - prev_pos - 1),
b.new_pos,
new_pos + instr(substr(b.str, b.new_pos + 1, r.r_len + 1) , ' ', -1)
from b join r
on b.r_id + 2 = r.r_id
)
select str_id, r_id, token, nvl(length(token), 0) as len
from b
where r_id > 0
order by str_id, r_id;
STR_ID R_ID TOKEN LEN
------- ------- ------------------------------------------------ -------
1 1 One Hundred Sixty-Nine Thousand Eight 37
1 2 Hundred Seventy-Four Dollars And Nine Cents 43
2 1 0
2 2 0
3 1 Mathguy rules 13
3 2 0
6 rows selected.

Regexp_substr expression

I have problem with my REGEXP expression which I want to loop and every iteration deletes text after slash. My expression looks like this now
REGEXP_SUBSTR('L1161148/1/10', '.*(/)')
I'm getting L1161148/1/ instead of L1161148/1
You said you wanted to loop.
CAVEAT: Both of these solutions assume there are no NULL list elements (all slashes have a value in between them).
SQL> with tbl(data) as (
select 'L1161148/1/10' from dual
)
select level, nvl(substr(data, 1, instr(data, '/', 1, level)-1), data) formatted
from tbl
connect by level <= regexp_count(data, '/') + 1 -- Loop # of delimiters +1 times
order by level desc;
LEVEL FORMATTED
---------- -------------
3 L1161148/1/10
2 L1161148/1
1 L1161148
SQL>
EDIT: To handle multiple rows:
SQL> with tbl(rownbr, col1) as (
select 1, 'L1161148/1/10/2/34/5/6' from dual
union
select 2, 'ALKDFJV1161148/123/456/789/1/2/3' from dual
)
SELECT rownbr, column_value substring_nbr,
nvl(substr(col1, 1, instr(col1, '/', 1, column_value)-1), col1) formatted
FROM tbl,
TABLE(
CAST(
MULTISET(SELECT LEVEL
FROM dual
CONNECT BY LEVEL <= REGEXP_COUNT(col1, '/')+1
) AS sys.OdciNumberList
)
)
order by rownbr, substring_nbr desc
;
ROWNBR SUBSTRING_NBR FORMATTED
---------- ------------- --------------------------------
1 7 L1161148/1/10/2/34/5/6
1 6 L1161148/1/10/2/34/5
1 5 L1161148/1/10/2/34
1 4 L1161148/1/10/2
1 3 L1161148/1/10
1 2 L1161148/1
1 1 L1161148
2 7 ALKDFJV1161148/123/456/789/1/2/3
2 6 ALKDFJV1161148/123/456/789/1/2
2 5 ALKDFJV1161148/123/456/789/1
2 4 ALKDFJV1161148/123/456/789
2 3 ALKDFJV1161148/123/456
2 2 ALKDFJV1161148/123
2 1 ALKDFJV1161148
14 rows selected.
SQL>
You can try removing the string after the last slash:
select regexp_replace('L1161148/1/10', '/([^/]*)$', '') from dual
You are trying to go as far as the last / and then "look back" and retain what was before it. With regular expressions you can do that with a subexpression, like this:
select regexp_substr('L1161148/1/10', '(.*)/.*', 1, 1, null, 1) from dual;
Here, as usual, the first argument "1" means where to start the search, the second "1" means which matching substring to choose, "null" means no special matching modifiers (like case-insensitive matching and such - not needed here), and the last "1" means return the first subexpression - the first thing in parentheses in the "match pattern."
However, regular expressions should only be used when you can't do it with the standard substr and instr (and translate) functions. Here the job is quite easy:
instr(text_string, '/', -1)
will give you the position of the LAST / in text_string (the -1 means find the last occurrence, instead of the first: count from the end of the string). So the whole thing can be written as:
select substr('L1161148/1/10', 1, instr('L1161148/1/10', '/', -1) - 1) from dual;
Edit: In the spirit of Gary_W's solution, here is a generalization to several strings and stripping successive layers from each input string; still not using regular expressions (resulting in slightly faster performance) and using a recursive CTE, available since Oracle version 11; I believe Gary's solution works only from Oracle 12c on.
Query: (I changed Gary's second input string a bit, to make sure the query works properly)
with tbl(item_id, input_str) as (
select 1, 'L1161148/1/10/2/34/5/6' from dual union all
select 2, 'ALKD/FJV11/61148/123/456/789/1/2/3' from dual
),
r (item_id, proc_string, stage) as (
select item_id, input_str, 0 from tbl
union all
select item_id, substr(proc_string, 1, instr(proc_string, '/', -1) - 1), stage + 1
from r
where instr(proc_string, '/') > 0
)
select * from r
order by item_id, stage;
Output:
ITEM_ID PROC_STRING STAGE
---------- ---------------------------------------- ----------
1 L1161148/1/10/2/34/5/6 0
1 L1161148/1/10/2/34/5 1
1 L1161148/1/10/2/34 2
1 L1161148/1/10/2 3
1 L1161148/1/10 4
1 L1161148/1 5
1 L1161148 6
2 ALKD/FJV11/61148/123/456/789/1/2/3 0
2 ALKD/FJV11/61148/123/456/789/1/2 1
2 ALKD/FJV11/61148/123/456/789/1 2
2 ALKD/FJV11/61148/123/456/789 3
2 ALKD/FJV11/61148/123/456 4
2 ALKD/FJV11/61148/123 5
2 ALKD/FJV11/61148 6
2 ALKD/FJV11 7
2 ALKD 8