Oracle: Get column names of Top n values over multiple columns - oracle

My question is similar to
https://community.oracle.com/message/4418327
in my query i need the MAX value of 3 different columns.
example: column 1 = 10, column 2 = 20, column 3 = 30 > output should
be 30. i need this MAX value to sort the list by it.
However instead of the actually value I need the column name and ideally not just the max one but top 3 as example.
The desired output would then be
ID first second third
-------------------------------
1 column 3 column 2 column1

There is no built-in function for what you are asking. So user1578653's answer is good, straight-forward and fast. Another way would be to write a function with PL/SQL.
In case you want to use pure SQL, but want it easier to add columns to the comparision, then if you are satisfied with just two columns (id and column names string), you can do this:
select id, listagg(colname, ', ') within group (order by value desc) as columns
from
(
select id, 'column1' as colname, col1 as value from mytable
union all
select id, 'column2' as colname, col2 as value from mytable
union all
select id, 'column3' as colname, col3 as value from mytable
-- add more columns here, if you wish
)
group by id;
Be aware this is slower than user1578653's SQL statement, as the table is being read three times. (I also treat equal values differently here, same values lead to random order.)

You can use PIVOT/UNPIVOT for this.
Fisrt UNPIVOT your table and assign rank with values ordered in descending order.
create table sample(
id number,
col1 number,
col2 number,
col3 number
);
insert into sample values(1,10,20,30);
insert into sample values(2,10,20,15);
select id, col_name, val,
row_number() over (partition by id order by val desc) r
from sample
unpivot(val for col_name in (
col1 AS 'col1', col2 AS 'col2', col3 AS 'col3')
);
Output:
| ID | COL_NAME | VAL | R |
|----|----------|-----|---|
| 1 | col3 | 30 | 1 |
| 1 | col2 | 20 | 2 |
| 1 | col1 | 10 | 3 |
| 2 | col2 | 20 | 1 |
| 2 | col3 | 15 | 2 |
| 2 | col1 | 10 | 3 |
sqlfiddle.
Next PIVOT the col_name based on the rank column.
with x(id, col_name, val,r) as (
select id, col_name, val,
row_number() over (partition by id order by val desc)
from sample
unpivot(val for col_name in (
col1 AS 'col1', col2 AS 'col2', col3 AS 'col3')
)
)
select * from (
select id, col_name, r
from x
)
pivot(max(col_name) for r in (
1 as first, 2 as second, 3 as third)
);
Output:
| ID | FIRST | SECOND | THIRD |
|----|-------|--------|-------|
| 1 | col3 | col2 | col1 |
| 2 | col2 | col3 | col1 |
sqlfiddle.

Not sure if there is a better way of doing this, but here's a possible solution using the CASE statement. Here's the table structure I've tested it on:
CREATE TABLE MAX_COL(
"ID" INT,
COLUMN_1 INT,
COLUMN_2 INT,
COLUMN_3 INT
);
And here's the SQL I used:
SELECT
"ID",
CASE
WHEN COLUMN_1 > COLUMN_2 AND COLUMN_1 > COLUMN_3 THEN 'COLUMN_1'
WHEN COLUMN_2 > COLUMN_1 AND COLUMN_2 > COLUMN_3 THEN 'COLUMN_2'
WHEN COLUMN_3 > COLUMN_2 AND COLUMN_3 > COLUMN_1 THEN 'COLUMN_3'
ELSE 'NONE'
END AS "FIRST",
CASE
WHEN COLUMN_1 > COLUMN_2 AND COLUMN_1 < COLUMN_3 THEN 'COLUMN_1'
WHEN COLUMN_2 > COLUMN_1 AND COLUMN_2 < COLUMN_3 THEN 'COLUMN_2'
WHEN COLUMN_3 > COLUMN_2 AND COLUMN_3 < COLUMN_1 THEN 'COLUMN_3'
ELSE 'NONE'
END AS "SECOND",
CASE
WHEN COLUMN_1 < COLUMN_2 AND COLUMN_1 < COLUMN_3 THEN 'COLUMN_1'
WHEN COLUMN_2 < COLUMN_1 AND COLUMN_2 < COLUMN_3 THEN 'COLUMN_2'
WHEN COLUMN_3 < COLUMN_2 AND COLUMN_3 < COLUMN_1 THEN 'COLUMN_3'
ELSE 'NONE'
END AS "THIRD"
FROM
MAX_COL;
Please note that you will need to deal with situations where one or more columns have the same value. In this case I am just returning 'NONE' if this occurs, but you may want to do something else.
UPDATE
You could also use PLSQL functions to achieve this, which may be easier to use for many columns. Here's an example:
CREATE OR REPLACE
FUNCTION COLUMN_POSITION (IDVAL INT, POSITION INT) RETURN VARCHAR2 AS
TYPE COLUMN_VALUE_TYPE IS TABLE OF NUMBER INDEX BY VARCHAR2(64);
COLS COLUMN_VALUE_TYPE;
COL_KEY VARCHAR2(64);
QUERY_STR VARCHAR2(1000 CHAR) := 'SELECT COL FROM(SELECT T.*, DENSE_RANK() OVER (ORDER BY VAL DESC) AS RANK FROM (';
COL_NAME VARCHAR2(64);
BEGIN
COLS('COLUMN_1') := NULL;
COLS('COLUMN_2') := NULL;
COLS('COLUMN_3') := NULL;
--ADD MORE COLUMN NAMES HERE...
COL_KEY := COLS.FIRST;
LOOP
EXIT WHEN COL_KEY IS NULL;
QUERY_STR := QUERY_STR || 'SELECT ' || COL_KEY || ' AS VAL, '''|| COL_KEY ||''' AS COL FROM MAX_COL WHERE ID = '|| IDVAL ||' UNION ALL ';
COL_KEY := COLS.NEXT(COL_KEY);
END LOOP;
QUERY_STR := SUBSTR(QUERY_STR, 0, LENGTH(QUERY_STR) - 11);
QUERY_STR := QUERY_STR || ') T ) WHERE RANK = ' || POSITION;
EXECUTE IMMEDIATE QUERY_STR INTO COL_NAME;
RETURN COL_NAME;
END;
This example has only 3 columns but you could easily add more. You can then use this in a query like this:
SELECT
MAX_COL.*,
COLUMN_POSITION(ID, 1) AS "FIRST",
COLUMN_POSITION(ID, 2) AS "SECOND",
COLUMN_POSITION(ID, 3) AS "THIRD"
FROM MAX_COL

SELECT
(CASE
WHEN A > B AND A > C THEN 'A'
WHEN B > A AND B > C THEN 'B'
WHEN C > B AND C > A THEN 'C'
END) AS "MaxValue_Column_Name",
greatest(A,B,C) AS Max_Value FROM max_search ;
--table name =max_search and column_name are A,B and C

Related

Need a query to seperate char values and number values from table in oracle [closed]

Closed. This question needs details or clarity. It is not currently accepting answers.
Want to improve this question? Add details and clarify the problem by editing this post.
Closed 3 years ago.
Improve this question
I have table TAB1 which is having one column COL1. as shown below.
TAB1
COL1
123
Xyz
CM
44
I need single query which will give following output.
Ccol | Ncol
Xyz | 123
CM | 45
From Oracle 12 you can define a function in a sub-query factoring clause and this can easily determine whether a value is numeric:
Oracle Setup:
CREATE TABLE table_name (COL1) AS
SELECT '123' FROM DUAL UNION ALL
SELECT 'Xyz' FROM DUAL UNION ALL
SELECT 'CM' FROM DUAL UNION ALL
SELECT '44' FROM DUAL UNION ALL
SELECT '1E3' FROM DUAL UNION ALL
SELECT '-1.2' FROM DUAL
Query:
WITH
FUNCTION isNumeric( value VARCHAR2 ) RETURN NUMBER
IS
n NUMBER;
BEGIN
n := TO_NUMBER( value );
RETURN 1;
EXCEPTION
WHEN OTHERS THEN
RETURN 0;
END;
SELECT Ccol,
TO_NUMBER( Ncol ) AS Ncol
FROM (
SELECT col1,
isNumeric( col1 ) AS isNumber,
ROW_NUMBER() OVER ( PARTITION BY isNumeric( col1 ) ORDER BY ROWNUM ) AS rn
FROM table_name
)
PIVOT ( MAX( Col1 ) FOR isNumber IN ( 0 AS Ccol, 1 AS Ncol ) )
ORDER BY rn
Output:
CCOL | NCOL
:--- | ---:
Xyz | 123
CM | 44
null | 1000
null | -1.2
db<>fiddle here
In earlier versions you can use CREATE FUNCTION rather than defining it in the query.
You can try this query:
WITH TAB1(COL1) AS
(
SELECT '123' FROM DUAL UNION ALL
SELECT 'Xyz' FROM DUAL UNION ALL
SELECT 'CM' FROM DUAL UNION ALL
SELECT '44' FROM DUAL
)
-- Actual query starts from here
, CTE AS (SELECT
COL1,
NUMERIC,
ROW_NUMBER() OVER(
PARTITION BY NUMERIC
ORDER BY LENGTH(COL1) DESC -- here I considered that Xyz and 123 both have length 3 and are related and same for CM and 44
) AS RN
FROM
(
SELECT
COL1,
CASE
WHEN REGEXP_LIKE ( COL1,
'^[[:digit:]]+$' ) THEN 'NUMBER'
ELSE 'NOT NUMBER'
END AS NUMERIC
FROM
TAB1
))
SELECT
C.COL1 AS "Ccol",
N.COL1 AS "Ncol"
FROM
CTE N
FULL OUTER JOIN CTE C ON ( N.RN = C.RN )
WHERE
N.NUMERIC = 'NUMBER'
AND C.NUMERIC = 'NOT NUMBER';
Output:
Cco Nco
--- ---
Xyz 123
CM 44
db<>fiddle demo
Cheers!!

REGEXP to capture values delimited by a set of delimiters

My column value looks something like below: [Just an example i created]
{BASICINFOxxxFyyy100x} {CONTACTxxx12345yyy20202x}
It can contain 0 or more blocks of data... I have created the below query to split the blocks
with x as
(select
'{BASICINFOxxxFyyy100x}{CONTACTxxx12345yyy20202x}' a from dual)
select REGEXP_SUBSTR(a,'({.*?x})',1,rownum,null,1)
from x
connect by rownum <= REGEXP_COUNT(a,'x}')
However I would like to further split the output into 3 columns like below:
ColumnA | ColumnB | ColumnC
------------------------------
BASICINFO | F |100
CONTACT | 12345 |20202
The delimiters are always standard. I failed to create a pretty query which gives me the desired output.
Thanks in advance.
SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE your_table ( str ) AS
SELECT '{BASICINFOxxxFyyy100x}{CONTACTxxx12345yyy20202x}' from dual
/
Query 1:
select REGEXP_SUBSTR(
t.str,
'\{([^}]*?)xxx([^}]*?)yyy([^}]*?)x\}',
1,
l.COLUMN_VALUE,
NULL,
1
) AS col1,
REGEXP_SUBSTR(
str,
'\{([^}]*?)xxx([^}]*?)yyy([^}]*?)x\}',
1,
l.COLUMN_VALUE,
NULL,
2
) AS col2,
REGEXP_SUBSTR(
str,
'\{([^}]*?)xxx([^}]*?)yyy([^}]*?)x\}',
1,
l.COLUMN_VALUE,
NULL,
3
) AS col3
FROM your_table t
CROSS JOIN
TABLE(
CAST(
MULTISET(
SELECT LEVEL
FROM DUAL
CONNECT BY LEVEL <= REGEXP_COUNT( t.str,'\{([^}]*?)xxx([^}]*?)yyy([^}]*?)x\}')
) AS SYS.ODCINUMBERLIST
)
) l
Results:
| COL1 | COL2 | COL3 |
|-----------|-------|-------|
| BASICINFO | F | 100 |
| CONTACT | 12345 | 20202 |
Note:
Your query:
select REGEXP_SUBSTR(a,'({.*?x})',1,rownum,null,1)
from x
connect by rownum <= REGEXP_COUNT(a,'x}')
Will not work when you have multiple rows of input - In the CONNECT BY clause, the hierarchical query has nothing to restrict it connecting Row1-Level2 to Row1-Level1 or to Row2-Level1 so it will connect it to both and as the depth of the hierarchies gets greater it will create exponentially more duplicate copies of the output rows. There are hacks you can use to stop this but it is much more efficient to put the row generator into a correlated sub-query which can then be CROSS JOINed back to the original table (it is correlated so it won't join to the wrong rows) if you are going to use hierarchical queries.
Better yet would be to fix your data structure so you are not storing multiple values in delimited strings.
SQL> with x as
2 (select '{BASICINFOxxxFyyy100x}{CONTACTxxx12345yyy20202x}' a from dual
3 ),
4 y as (
5 select REGEXP_SUBSTR(a,'({.*?x})',1,rownum,null,1) c1
6 from x
7 connect by rownum <= REGEXP_COUNT(a,'x}')
8 )
9 select
10 substr(c1,2,instr(c1,'xxx')-2) z1,
11 substr(c1,instr(c1,'xxx')+3,instr(c1,'yyy')-instr(c1,'xxx')-3) z2,
12 rtrim(substr(c1,instr(c1,'yyy')+3),'x}') z3
13 from y;
Z1 Z2 Z3
--------------- --------------- ---------------
BASICINFO F 100
CONTACT 12345 20202
Here is another solution, which is derived from the place you left. Your query had already resulted into splitting of a row to 2 row. Below will make it in 3 columns:
WITH x
AS (SELECT '{BASICINFOxxxFyyy100x}{CONTACTxxx12345yyy20202x}' a
FROM DUAL),
-- Your query result here
tbl
AS ( SELECT REGEXP_SUBSTR (a,
'({.*?x})',
1,
ROWNUM,
NULL,
1)
Col
FROM x
CONNECT BY ROWNUM <= REGEXP_COUNT (a, 'x}'))
--- Actual Query
SELECT col,
REGEXP_SUBSTR (col,
'(.*?{)([^x]+)',
1,
1,
'',
2)
AS COL1,
REGEXP_SUBSTR (REGEXP_SUBSTR (col,
'(.*?)([^x]+)',
1,
2,
'',
2),
'[^y]+',
1,
1)
AS COL2,
REGEXP_SUBSTR (REGEXP_SUBSTR (col,
'[^y]+x',
1,
2),
'[^x]+',
1,
1)
AS COL3
FROM tbl;
Output:
SQL> /
COL COL1 COL2 COL3
------------------------------------------------ ------------------------------------------------ ------------------------------------------------ ------------------------------------------------
{BASICINFOxxxFyyy100x} BASICINFO F 100
{CONTACTxxx12345yyy20202x} CONTACT 12345 20202

Oracle query by column1 where column2 is the same

I have a table like this in Oracle 9i DB:
+------+------+
| Col1 | Col2 |
+------+------+
| 1 | a |
| 2 | a |
| 3 | a |
| 4 | b |
| 5 | b |
+------+------+
Col1 is the primary key, Col2 is indexed.
I input col1 as condition for my query and I want to get col1 where col2 is the same as my input.
For example I query for 1 and the result should be 1,2,3.
I know I can use self join for this, I would like to know if there is a better way to do this.
I'd call this a semi-join: does it satisfy your 'no self joins' requirement?:
SELECT *
FROM YourTable
WHERE Col2 IN ( SELECT t2.Col2
FROM YourTable t2
WHERE t2.Col1 = 1 );
I'd be inclined to avoid the t2 range variable like this:
WITH YourTableSearched
AS ( SELECT Col2
FROM YourTable
WHERE Col1 = 1 )
SELECT *
FROM YourTable
WHERE Col2 IN ( SELECT Col2
FROM YourTableSearched );
but TNH I would probably do this:
WITH YourTableSearched
AS ( SELECT Col2
FROM YourTable
WHERE Col1 = 1 )
SELECT *
FROM YourTable
NATURAL JOIN YourTableSearched;
It's possible. Whether it's better (i.e. more performant) than using a self-join, particularly if there is an index on col1, col2, is anyone's guess.
Assuming col1 is unique, you could do:
SELECT col1
FROM (SELECT col1,
col2,
MAX(CASE WHEN col1 = :p_col1_value THEN col2 END) OVER () col2_comparison
FROM your_table)
WHERE col2 = col2_comparison;
And with :p_col1_value = 1:
COL1
----------
1
2
3
And with :p_col1_value = 5:
COL1
----------
4
5

Oracle Order by array index

I have a collection which is a table of numbers containing primary keys and I want to sort a select statement - containing those keys - by the index of the number collection.
For example:
TYPE "NUMBERCOLLECTION" AS TABLE OF NUMBER;
...
myNumberCollection numberCollection := numberCollection(45, 7799, 2187);
...
select nr
, columnA
, columnB
from myTable
where myTable.nr member of myNumberCollection
order by index of myNumberCollection
will result in
NR COLUMNA COLUMNB
-----------------------
45 xyz abc
7799 xyz abc
2187 xyz abc
SELECT nr,
columnA,
columnB
FROM myTable m
INNER JOIN
( SELECT ROWNUM AS id, COLUMN_VALUE FROM TABLE( myNumberCollection ) ) c
ON ( m.nr = c.COLUMN_VALUE )
ORDER BY c.id;

Select rows with same id without nulls and one row with Null if multiple nulls are present

I want to get only rows having a value and some other value than NULL for a particular username column.
If both rows have null for that particular username then it should show Null only once in output. If there are more than two rows for same username with null and some other value then display the value only not null.
Below is example sample and output. How it can be done using sql query?
Table:
Col1 | Col2
-------------------------
a | abc
a | bc
b | null
b | null
c | der
c | null
Output:
Col1 | Col2
-------------------------
a | abc
a | bc
b | null
c | der
Outlining the idea, there might be some syntax errors, don't have access to oracle.
SELECT * FROM
( SELECT DISTINCT USERNAME FROM <TABLE> ) USERS
LEFT OUTER JOIN
( SELECT USERNAME, COL2 FROM <TABLE> WHERE COL2 IS NOT NULL) USERS_COL2
ON
USRES.USERNAME = USERS_COL2.USERNAME
you use listagg () or stragg ()
drop table test;
create table test (
col1 varchar2(10),
col2 varchar2(10)
);
insert into test values ( 'a','abc');
insert into test values ( 'a','abc');
insert into test values ( 'b',null);
insert into test values ( 'b',null);
insert into test values ( 'c','der');
insert into test values ( 'c',null);
commit;
select col1,
listagg (col2,',') within group (order by col1) col2
from test
group by col1;
COL1 COL2
---------- -----------
a abc,abc
b
c der
select col1, stragg (col2)
from test
group by col1;
select col1, col2, count(*)
from omc.test
group by col1,col2;
you can remove count(*)

Resources