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(*)