Aggregate and concatenate strings without duplication - sql

I want to aggregate strings and concatenate them. This is example of what I am using
SELECT deptno, LISTAGG(ename, ',') WITHIN GROUP (ORDER BY ename) AS employees
FROM emp
GROUP BY deptno;
DEPTNO EMPLOYEES
---------- --------------------------------------------------
10 CLARK, CLARK,KING,MILLER, MILLER
20 ADAMS, ADAMS, ADAMS, FORD,JONES,SCOTT,SMITH
30 ALLEN,BLAKE,JAMES,MARTIN,MARTIN,TURNER,WARD
3 rows selected.
But I want results without duplication in concatenated strings.
Desired results:
DEPTNO EMPLOYEES
---------- --------------------------------------------------
10 CLARK,KING,MILLER
20 SMITH,FORD,ADAMS,SCOTT,JONES
30 ALLEN,BLAKE,MARTIN,TURNER,JAMES,WARD
3 rows selected.
EDIT: so in example below there are some "Employeer names with comma". It is using the proposed answer with regular expression:
with emp as (
select 10 as deptno, 'clark,1' as ename from dual
union all
select 10, 'clark,1' from dual
union all
select 10, 'clark,1' from dual
union all
select 10, 'bob' from dual
union all
select 10, 'bob' from dual
union all
select 10, 'don' from dual
union all
select 20, 'tim,2' from dual
union all
select 20, 'tim,2' from dual
union all
select 20, 'tim,2' from dual
union all
select 20, 'jack' from dual
union all
select 20, 'mark' from dual
)
SELECT e.deptno, --LISTAGG(e.ename, ',') WITHIN GROUP (ORDER BY e.ename) AS employees,
RTRIM(REGEXP_REPLACE(listagg (e.ename, ',')
WITHIN GROUP (ORDER BY e.ename),
'([^,]+)(,\1)+', '\1'),
',') AS employees
FROM emp e
GROUP BY e.deptno;
The results of this query are not correct:

Replace
FROM emp
with
FROM (SELECT DISTINCT deptno, ename FROM emp)
(From a non-Oracle user, but it should work)

You can use REGEX:
SELECT deptno,
rtrim( regexp_replace( (Listagg(ename,',') within GROUP (ORDER BY ename) OVER ()), '([^-]*)(-\1)+($|-)', '\1\3'), '-') within GROUP (ORDER BY ename) AS employees
FROM emp
GROUP BY deptno;
Here's a method using crossjoin
SELECT DISTINCT deptno,
b.enames
FROM emp a
cross join (SELECT Listagg(ename, '-')
within GROUP (ORDER BY ename) enames
FROM (SELECT DISTINCT enam ename
FROM emp)) b
Let me know if they work

Related

Sum of multiple union select showing every result and total

My SQL shows the count of records in multiple tables
SELECT 'S_MH_DSC_Abandoned' Table_N ,COUNT(*) Count FROM S_MH_DSC_Abandoned
UNION SELECT 'S_MH_DSC_ExclAbandoned' Table_N, COUNT(*) Count FROM S_MH_DSC_ExclAbandoned
UNION SELECT 'S_MH_Private_All' Table_N, COUNT(*) Count FROM S_MH_Private_All
I want to show this plus the total of the values in the last column as above.
I have tried
Select 'Sum Total' Table_N, sum(a) as Total from
(
SELECT 'S_MH_DSC_Abandoned',COUNT(*) a FROM S_MH_DSC_Abandoned
UNION SELECT 'S_MH_DSC_ExclAbandoned', COUNT(*) a FROM S_MH_DSC_ExclAbandoned
UNION SELECT 'S_MH_Private_All', COUNT(*) a FROM S_MH_Private_All
)
But only get the total without the other values.
How can I get both the working counts and the total?
This is using QGIS flavour of SQL - https://sqlite.org/lang.html
I don't use SQLite.
However, reviewing its documentation, it looks as if it doesn't know ROLLUP (which comes handy in such a case):
SQL> with temp (job, sumsal) as
2 (select 'Clerk', sum(sal) from emp where job = 'CLERK' union all
3 select 'Mgr' , sum(sal) from emp where job = 'MANAGER' union all
4 select 'Sale' , sum(sal) from emp where job = 'SALESMAN'
5 )
6 select job, sum(sumsal) sumsal
7 from temp
8 group by rollup (job);
JOB SUMSAL
----- ----------
Clerk 4150
Mgr 8275
Sale 5600
18025
SQL>
Therefore, simulate it with a union:
SQL> with temp (job, sumsal) as
2 (select 'Clerk', sum(sal) from emp where job = 'CLERK' union all
3 select 'Mgr' , sum(sal) from emp where job = 'MANAGER' union all
4 select 'Sale' , sum(sal) from emp where job = 'SALESMAN'
5 )
6 select job, sumsal
7 from temp
8 union
9 select 'Total', sum(sumsal)
10 from temp;
JOB SUMSAL
----- ----------
Clerk 4150
Mgr 8275
Sale 5600
Total 18025
SQL>
Using your query:
with temp (tname, cnt) as
(SELECT 'S_MH_DSC_Abandoned' , COUNT(*) FROM S_MH_DSC_Abandoned union all
SELECT 'S_MH_DSC_ExclAbandoned', COUNT(*) FROM S_MH_DSC_ExclAbandoned union all
SELECT 'S_MH_Private_All' , COUNT(*) FROM S_MH_Private_All
)
select tname, cnt
from temp
union
select 'Total', sum(cnt)
from temp;

I want to query difference between first and second highest salary

I already got the second-highest salary.
I would like to display: employee name, salary, department name, and the difference between the highest and second-highest salary.
Here is my current code.
select max(SAL) SecondHighestSalary
from EMP A, DEPT B
where SAL < (
select max(A.SAL)
from EMP A, DEPT B
where A.DEPTNO = B.DEPTNO and B.LOC = 'New York'
);
Thanks
Use the DENSE_RANK (if you want highest two unique salary values) or ROW_NUMBER (if you want the salary of the two highest earning people regardless of ties) analytic functions and then use conditional aggregation:
SELECT MAX( CASE WHEN sal_rank = 1 THEN sal END )
- MAX( CASE WHEN sal_rank = 2 THEN sal END ) AS sal_difference_ignore_ties,
MAX( CASE WHEN sal_rownum = 1 THEN sal END )
- MAX( CASE WHEN sal_rownum = 2 THEN sal END ) AS sal_difference_with_ties
FROM (
SELECT e.deptno,
e.sal,
DENSE_RANK() OVER ( ORDER BY e.sal DESC ) AS sal_rank,
ROW_NUMBER() OVER ( ORDER BY e.sal DESC ) AS sal_rownum
FROM emp e
INNER JOIN dept d
ON ( e.deptno = d.deptno )
WHERE d.loc = 'New York'
);
Which, for the sample data:
CREATE TABLE emp ( deptno, sal ) AS
SELECT 1, 2000 FROM DUAL UNION ALL
SELECT 1, 2000 FROM DUAL UNION ALL
SELECT 1, 1000 FROM DUAL UNION ALL
SELECT 1, 500 FROM DUAL UNION ALL
SELECT 1, 200 FROM DUAL UNION ALL
SELECT 2, 2000 FROM DUAL UNION ALL
SELECT 2, 1500 FROM DUAL UNION ALL
SELECT 2, 200 FROM DUAL UNION ALL
SELECT 2, 700 FROM DUAL;
CREATE TABLE dept ( deptno, loc ) AS
SELECT 1, 'New York' FROM DUAL UNION ALL
SELECT 2, 'Beijing' FROM DUAL;
Outputs:
SAL_DIFFERENCE_IGNORE_TIES | SAL_DIFFERENCE_WITH_TIES
-------------------------: | -----------------------:
1000 | 0
Or, if you want all the departments:
SELECT deptno,
MAX( CASE WHEN sal_rank = 1 THEN sal END )
- MAX( CASE WHEN sal_rank = 2 THEN sal END ) AS sal_difference_ignore_ties,
MAX( CASE WHEN sal_rownum = 1 THEN sal END )
- MAX( CASE WHEN sal_rownum = 2 THEN sal END ) AS sal_difference_with_ties
FROM (
SELECT e.deptno,
e.sal,
DENSE_RANK() OVER ( PARTITION BY e.deptno ORDER BY e.sal DESC ) AS sal_rank,
ROW_NUMBER() OVER ( PARTITION BY e.deptno ORDER BY e.sal DESC ) AS sal_rownum
FROM emp e
INNER JOIN dept d
ON ( e.deptno = d.deptno )
)
GROUP BY deptno;SELECT deptno,
MAX( CASE WHEN sal_rank = 1 THEN sal END )
- MAX( CASE WHEN sal_rank = 2 THEN sal END ) AS sal_difference_ignore_ties,
MAX( CASE WHEN sal_rownum = 1 THEN sal END )
- MAX( CASE WHEN sal_rownum = 2 THEN sal END ) AS sal_difference_with_ties
FROM (
SELECT e.deptno,
e.sal,
DENSE_RANK() OVER ( PARTITION BY e.deptno ORDER BY e.sal DESC ) AS sal_rank,
ROW_NUMBER() OVER ( PARTITION BY e.deptno ORDER BY e.sal DESC ) AS sal_rownum
FROM emp e
INNER JOIN dept d
ON ( e.deptno = d.deptno )
)
GROUP BY deptno;
Which outputs:
DEPTNO | SAL_DIFFERENCE_IGNORE_TIES | SAL_DIFFERENCE_WITH_TIES
-----: | -------------------------: | -----------------------:
1 | 1000 | 0
2 | 500 | 500
db<>fiddle here
How about this?
Based on Scott's EMP table:
SQL> select ename, sal
2 from emp
3 order by sal desc;
ENAME SAL
---------- ----------
KING 5001 --> highest salary
FORD 3000 --> Ford and Scott share the
SCOTT 3000 --> 2nd highest salary
JONES 2975
BLAKE 2850
CLARK 2450
ALLEN 1600
TURNER 1500
MILLER 1300
WARD 1250
MARTIN 1250
ADAMS 1100
SMITH 1000
JAMES 950
14 rows selected.
Then you'd rank them and - using LISTAGG (to display all employees that share the same highest or second highest salary) - get the final result:
SQL> with temp as
2 (select e.ename,
3 e.sal,
4 dense_rank() over (order by e.sal desc) rnk,
5 d.dname
6 from emp e join dept d on d.deptno = e.deptno
7 )
8 select listagg(case when rnk = 1 then ename ||' - '|| dname || ' ($' || sal ||')' end, ', ')
9 within group (order by ename) highest,
10 --
11 listagg(case when rnk = 2 then ename ||' - '|| dname || ' ($' || sal ||')' end, ', ')
12 within group (order by ename) second_highest,
13 --
14 max(case when rnk = 1 then sal end) -
15 max(case when rnk = 2 then sal end) diff
16 from temp t
17 where t.rnk in (1, 2);
HIGHEST SECOND_HIGHEST DIFF
------------------------------ -------------------------------------------------- ----------
KING - ACCOUNTING ($5001) FORD - RESEARCH ($3000), SCOTT - RESEARCH ($3000) 2001
SQL>

Write a query to display departno and no of employee which departno have max employee.?

Write a query to display departno and no of employee which departno have max employee.?
Here I tried out following query:
select deptno, count(*) as no_of_emp
from emp
group by deptno
order by no_of_emp;
but I was getting as
Deptno no_of_emp
30 6
20 4
10 4
But I just need first row not all. Is it possible to display only first record in oracle sql?
You may use ROWNUM
select * from
(
select deptno, count(*) as no_of_emp
from emp
group by deptno
order by no_of_emp desc
) where rownum = 1;
Or in 12c and above, FETCH..FIRST
select deptno, count(*) as no_of_emp
from emp
group by deptno
order by no_of_emp desc fetch first 1 ROWS ONLY
As an alternative you might use max(count(*)) over (order by ...) analytic function with descending count option :
with emp( empno,ename,deptno ) as
(
select 7839,'KING',10 from dual union all
select 7698,'BLAKE',30 from dual union all
select 7782,'CLARK',10 from dual union all
select 7566,'JONES',20 from dual union all
select 7788,'SCOTT',20 from dual union all
select 7902,'FORD',20 from dual union all
select 7369,'SMITH',20 from dual union all
select 7499,'ALLEN',30 from dual union all
select 7521,'WARD',30 from dual union all
select 7654,'MARTIN',30 from dual union all
select 7844,'TURNER',30 from dual union all
select 7876,'ADAMS',20 from dual union all
select 7900,'JAMES',30 from dual union all
select 7934,'MILLER',10 from dual
)
select deptno, no_of_emp
from
(
select deptno, count(*) as no_of_emp,
max(count(*)) over (order by count(*) desc) as max_populated
from emp
group by deptno
order by no_of_emp
)
where max_populated = no_of_emp;
DEPTNO NO_OF_EMP
------ ---------
30 6
Rextester Demo
Although what you are trying to achieve is can be done by other sql queries changing yo ur query as below will work:
SELECT * from (select deptno, count(*) as no_of_emp
from emp
group by deptno
order by no_of_emp desc) where rownum<=1
;
other query is as follows:
select deptno, count(*) as no_of_emp
from emp
group by deptno
having count(*)=(select max(count(*)) as no_of_emp
from emp
group by deptno)
order by no_of_emp desc;

Getting values as rows with its corresponding values using oracle

I have data in table as follows
DEPTNO ENAME SALARY
------ ---------- ------
Developer SENIOR 100000
Developer JUNIOR 100000
Tester SENIOR 200000
Tester JUNIOR 100000
Architect SENIOR 300000
Architect JUNIOR 300000
I need to show them like
Occupation Senior Sal Junior Sal
------ ---------- ------
Developer SENIOR JUNIOR
Developer 100000 100000
Tester SENIOR JUNIOR
Tester 200000 100000
Architect SENIOR JUNIOR
Architect 300000 300000
Am stuck with this how to achieve this
Use
DECODE
ROW_NUMBER
UNION ALL
SELECT deptno,
senior_sal,
junior_sal,
row_number() over(partition BY deptno order by senior_sal DESC) rn
FROM
(SELECT A.deptno, A.salary senior_sal, b.salary junior_sal
FROM
(SELECT deptno, TO_CHAR(salary) salary FROM your_table WHERE ename = 'SENIOR'
) A,
(SELECT deptno, TO_CHAR(salary) salary FROM your_table WHERE ename = 'JUNIOR'
) b
WHERE A.deptno = b.deptno
UNION ALL
SELECT A.deptno, A.salary senior_sal, b.salary junior_sal
FROM
(SELECT deptno, DECODE(ename, 'SENIOR', 'SENIOR') salary FROM your_table
WHERE ename = 'SENIOR'
) A,
(SELECT deptno, DECODE(ename, 'JUNIOR', 'JUNIOR') salary FROM your_table
WHERE ename = 'JUNIOR'
) b
WHERE A.deptno = b.deptno
);
Working demo
In SQL*Plus:
SQL> column rn noprint
SQL> WITH sample_data AS(
2 SELECT 'Developer' DEPTNO, 'SENIOR' ENAME, 100000 SALARY FROM dual UNION ALL
3 SELECT 'Developer' DEPTNO, 'JUNIOR' ENAME, 100000 SALARY FROM dual UNION ALL
4 SELECT 'Tester' DEPTNO, 'SENIOR' ENAME, 200000 SALARY FROM dual UNION ALL
5 SELECT 'Tester' DEPTNO, 'JUNIOR' ENAME, 100000 SALARY FROM dual UNION ALL
6 SELECT 'Architect' DEPTNO, 'SENIOR' ENAME, 300000 SALARY FROM dual UNION ALL
7 SELECT 'Architect' DEPTNO, 'JUNIOR' ENAME, 300000 SALARY FROM dual
8 )
9 -- end of sample_data mimicking real table
10 SELECT deptno,
11 senior_sal,
12 junior_sal,
13 row_number() over(partition BY deptno order by senior_sal DESC) rn
14 FROM
15 (SELECT A.deptno, A.salary senior_sal, b.salary junior_sal
16 FROM
17 (SELECT deptno, TO_CHAR(salary) salary FROM sample_data WHERE ename = 'SENIOR'
18 ) A,
19 (SELECT deptno, TO_CHAR(salary) salary FROM sample_data WHERE ename = 'JUNIOR'
20 ) b
21 WHERE A.deptno = b.deptno
22 UNION ALL
23 SELECT A.deptno, A.salary senior_sal, b.salary junior_sal
24 FROM
25 (SELECT deptno, DECODE(ename, 'SENIOR', 'SENIOR') salary FROM sample_data
26 WHERE ename = 'SENIOR'
27 ) A,
28 (SELECT deptno, DECODE(ename, 'JUNIOR', 'JUNIOR') salary FROM sample_data
29 WHERE ename = 'JUNIOR'
30 ) b
31 WHERE A.deptno = b.deptno
32 );
Output:
DEPTNO SENIOR_SAL JUNIOR_SAL
--------------- --------------- ---------------
Architect SENIOR JUNIOR
Architect 300000 300000
Developer SENIOR JUNIOR
Developer 100000 100000
Tester SENIOR JUNIOR
Tester 200000 100000
6 rows selected.
SQL>
You can use LISTAGG.
WITH sample_data AS(
SELECT 'Developer' DEPTNO, 'SENIOR' ENAME, 100000 SALARY FROM dual UNION ALL
SELECT 'Developer' DEPTNO, 'JUNIOR' ENAME, 100000 SALARY FROM dual UNION ALL
SELECT 'Tester' DEPTNO, 'SENIOR' ENAME, 200000 SALARY FROM dual UNION ALL
SELECT 'Tester' DEPTNO, 'JUNIOR' ENAME, 100000 SALARY FROM dual UNION ALL
SELECT 'Architect' DEPTNO, 'SENIOR' ENAME, 300000 SALARY FROM dual UNION ALL
SELECT 'Architect' DEPTNO, 'JUNIOR' ENAME, 300000 SALARY FROM dual
)
SELECT occupation, SUBSTR(name_sal,1,INSTR(name_sal,',')-1) "Senior Sal", SUBSTR(name_sal,INSTR(name_sal,',')+1) "Junior Sal"
FROM
(
SELECT DEPTNO as occupation,LISTAGG (ename, ',') WITHIN GROUP (ORDER BY ename DESC) name_sal
FROM sample_data
GROUP BY DEPTNO
UNION
SELECT deptno as occupation,LISTAGG (salary, ',') WITHIN GROUP (ORDER BY ename DESC)
FROM sample_data
GROUP BY deptno
)
ORDER BY 1, 2 DESC

Oracle hierarchical query select records along path

Suppose the company hierarchy is like this:
King
-> John
-> Jack
-> Chris
-> Sean
-> April
-> Jerry
-> Tom
Given an ancestor e.g. King, and a subordinate, e.g. Chris, is it possible to select all records along the path /King/John/Jack/Chris in one query? i.e. the query will return 4 records - King, John, Jack and Chris?
Table structure:
Employee_Name Employee_ID Manager_ID
Manager_ID references to Employee_ID
I am not sure what your table structure is. In case you store it as paths then the below should work. The query supports multiple records with Chris. It will select all of them.
with test as
(
select 1 id, '/King/John/Jack/Chris' str from dual union all
select 2 id, '/King/John/Jack/April' str from dual union all
select 3 id, '/King/John/Jack/Sean' str from dual union all
select 4 id, '/King/Jerry/Tom' str from dual
)
select id,
str,
regexp_substr (str, '[^/]+', 1, rn) split,
rn
from test
cross
join (select rownum rn
from (select max (length (regexp_replace (str, '[^/]+'))) + 1 mx
from test
)
connect by level <= mx
) A
where regexp_substr (str, '[^/]+', 1, rn) is not null
and instr(str, 'Chris') > 0
order by id, rn
;
Here is an example in SQL Fiddle
Example in SQL Fiddle
The trick is cross join that creates multiple rows for each row in main table.
ID STR SPLIT RN
1 /King/John/Jack/Chris King 1
1 /King/John/Jack/Chris John 2
1 /King/John/Jack/Chris Jack 3
1 /King/John/Jack/Chris Chris 4
with t as
(
select 'King' as Employee_Name, 1 as Employee_ID, -1 as Manager_ID from dual union all
select 'John' as Employee_Name, 2 as Employee_ID, 1 as Manager_ID from dual union all
select 'Jack' as Employee_Name, 3 as Employee_ID, 2 as Manager_ID from dual union all
select 'Chris' as Employee_Name, 4 as Employee_ID, 3 as Manager_ID from dual union all
select 'Sean' as Employee_Name, 5 as Employee_ID, 3 as Manager_ID from dual union all
select 'April' as Employee_Name, 6 as Employee_ID, 2 as Manager_ID from dual union all
select 'Jerry' as Employee_Name, 7 as Employee_ID, 1 as Manager_ID from dual union all
select 'Tom' as Employee_Name, 8 as Employee_ID, 7 as Manager_ID from dual
)
select Employee_Name
from t
connect by prior Manager_ID = Employee_ID
start with Employee_Name = 'Chris'
order by level desc