ORACLE SQL group based on values in a reference table - sql

Customer table and Acct tables has global scope, they share and increment this value
Below is customer table, SEQ NO 1 is beginning of customer data, SEQ_NO 238 is beginning of another customer data
Another is account table, all accounts with their SEQ_NOs inside a boundary of customer get same group (I want to group those accounts to the same customer, so that I can use listAgg to concatenate account id.), for example, below from SEQ_NO 2 and NO 224 (inclusive) should be assigned to the same group.
Is there a SQL way to do that, The worst case I was thinking is to define oracle type, and using function do that.
Any help is appreciate.

If I understand your question correctly, you want to be able to assign rows in the account table to groups, one per customer, so that you can then aggregate based on these groups.
So, the question is how to identify to which customer each account belongs, based on the sequence boundaries given in the first table ("customer") and the specific account numbers in the second table ("account").
This can be done in plain SQL, and relatively easily. You need a join between the accounts table and a subquery based on the customers table. The subquery must show the first and the last sequence number allocated to each client; to do that, you can use the lead analytic function. A bit of care must be taken regarding the last customer, for whom there is no upper limit for the sequence numbers.
You didn't provide test data in a usable format, so I created sample data in the with clause below (which is not part of the query - it's just there as a placeholder for test data).
with
customer (cust_id, seq_no) as (
select 101, 1 from dual union all
select 102, 34 from dual union all
select 200, 58 from dual union all
select 130, 90 from dual
)
, account (acct_id, seq_no) as (
select 1003, 3 from dual union all
select 1005, 11 from dual union all
select 1007, 33 from dual union all
select 1008, 60 from dual union all
select 1103, 77 from dual union all
select 1140, 92 from dual union all
select 1145, 99 from dual
)
select c.cust_id,
listagg(a.acct_id, ',') within group (order by a.acct_id) as acct_list
from (
select cust_id, seq_no as lower_no,
lead(seq_no) over (order by seq_no) - 1 as upper_no
from customer
) c
left outer join account a
on a.seq_no between c.lower_no and nvl(c.upper_no, a.seq_no)
group by c.cust_id
order by c.cust_id
;
OUTPUT
CUST_ID ACCT_LIST
------- --------------------
101 1003,1005,1007
102
130 1140,1145
200 1008,1103

Related

Recursive ORDER BY

I have a USERS table which is a membership matrix like below. Table is unique on ID, and each ID belongs to at least one group, but could belong to all 3.
SELECT 1 AS ID, 0 AS IS_A, 0 AS IS_B, 1 AS IS_C FROM DUAL UNION ALL
SELECT 2,0,1,0 FROM DUAL UNION ALL
SELECT 3,0,1,1 FROM DUAL UNION ALL
SELECT 4,1,1,0 FROM DUAL UNION ALL
SELECT 5,1,1,0 FROM DUAL UNION ALL
SELECT 6,1,1,1 FROM DUAL UNION ALL
SELECT 7,0,1,1 FROM DUAL UNION ALL
SELECT 8,0,0,1 FROM DUAL UNION ALL
SELECT 9,1,0,0 FROM DUAL UNION ALL
SELECT 10,1,0,1 FROM DUAL UNION ALL
SELECT 11,0,0,1 FROM DUAL UNION ALL
SELECT 12,0,1,1 FROM DUAL
The final goal is to SELECT randomly a sample of at least 4 users from A, 3 from B and 5 from C (just an example) but with exactly 10 distinct IDs (otherwise the solution is trivial; just SELECT *).
The focus is less to determine if it's possible at all, but more to attempt a best effort to maximize memberships.
The output is expected to be unique on ID.
I can only think of a procedural way to achieve this:
Take the first ID with MAX(IS_A+IS_B+IS_C)
Check if the quotas are reached
If, for example, we already have 4 users from A, then we'll continue with the next ID with MAX(IS_B+IS_C), completely ignoring any further contributions from IS_A column
If we have already achieved all quotas, revert back to taking MAX(IS_A+IS_B+IS_C) to get "bonus" points
Stop upon reaching the overall maximum of 10
In essence, we prioritize and incrementally take the ID that has the most memberships in groups that have not reached the quota
However, I can't figure out how to do this in Oracle SQL since the ORDER BY would depend on not just the current row's values, but also recursively on whether the earlier rows have filled up the respective quotas.
I've tried ROWNUM, ROW_NUMBER(), SUM(IS_A) OVER (ORDER BY ...), RECURSIVE CTE but to no avail. Best I have is
WITH CTE AS (
SELECT ID, IS_A, IS_B, IS_C
, ROW_NUMBER() OVER (ORDER BY IS_A+IS_B+IS_C DESC) AS RN
FROM USERS
)
, CTE2 AS (
SELECT CTE.*
, GREATEST(4 - SUM(IS_A) OVER (ORDER BY RN), 0.001) AS QUOTA_A --clip negatives to 0.001
, GREATEST(3 - SUM(IS_B) OVER (ORDER BY RN), 0.001) AS QUOTA_B --so that when all quotas are exhausted,
, GREATEST(5 - SUM(IS_C) OVER (ORDER BY RN), 0.001) AS QUOTA_C --we still prioritize those that contribute most number of concurrent memberships
FROM CTE
)
SELECT ID FROM CTE2
ORDER BY QUOTA_A*IS_A + QUOTA_B*IS_B + QUOTA_C*IS_C DESC
FETCH NEXT 10 ROWS ONLY
but it does not work because QUOTA_A is computed based on ORDER BY RN instead of recursively.
Thanks in advance!

Cumulative data along with the original data

I have two tables .
Input:
I have joined with the calendar table and bring the data till current.
I need a output .
I tried a query with UNION and Aggregation but I need to query two times and aggregate the same table . Since the table is very big .Is there a option to do different way
SELECT ID ,PERIOD,SUM(AMOUNTYTD) AMOUNTYTD,SUM(AMOUNT) AMOUNT
FROM (
SELECT ID ,b.PERIOD,SUM(AMOUNT) AMOUNTYTD,0 AMOUNT
FROM transaction a RIGHT OUTER JOIN CALENDAR b
ON b.PERIOD<=a.PERIOD
UNION ALL
SELECT ID ,PERIOD,0,SUM(AMOUNT)
FROM transaction
GROUP BY ID,PERIOD
)
GROUP BY ID,PERIOD
Showing the periodic amount side by side with the cumulative amount is easy - actually you only need to be able to create the correct table with the periodic amounts, the cumulative amounts are a simple application of analytic sum.
The key to joining the calendar table to the "input" data is to use a partitioned outer join - notice the partition by (id) clause in the join of the two tables. This causes the "inputs" data to be partitioned into separate sub-tables, one for each distinct id; the outer join to the calendar table is done separately for each such sub-table, and then the results are combined with a logical "union all".
with
input (id, period, amount) as (
select 1, 202010, 100 from dual union all
select 1, 202011, 50 from dual union all
select 2, 202011, 400 from dual
)
, calendar (period) as (
select 202010 from dual union all
select 202011 from dual union all
select 202012 from dual union all
select 202101 from dual
)
select id, period, amountytd, amount
from (
select i.id, period, i.amount,
sum(i.amount) over (partition by i.id order by period)
as amountytd
from calendar c left outer join input i partition by (id)
using (period)
)
where amountytd is not null
order by id, period
;
ID PERIOD AMOUNTYTD AMOUNT
--- ---------- ---------- ----------
1 202010 100 100
1 202011 150 50
1 202012 150
1 202101 150
2 202011 400 400
2 202012 400
2 202101 400
You have the query - I have joined with the calendar table and bring the data till current. let us assume it as your_query
You can use analytical function on it as follows:
Select t.*,
Case when lead(amountytd) over (partition by id order by period) = amountytd
then null
else amountytd
end as amount
From (your_query) t

Query dubious understanding

Assume there are ten employee records and each contains a salary value of 100, except for one, which has a null value in the salary field....
SELECT SUM((AVG(LENGTH(NVL(SALARY,0)))))
FROM DUCK
GROUP BY SALARY;
First inner bracket NVL(SALARY, 0) -> the first 9 employee's salary is 100 and the last one is 0.
Second inner bracket LENGTH() -> the first 9 will be 3 and the last one is 0.
Third inner bracket AVG() calculates length of the salary of 10 employees which is ((3*9)+0)/10 = 2.7
So what does the last bracket do when the sum function computes a list of data. but the avg function computes out the data which leaves to a digit?
Your query is running like this:
null is considered as 0
length of the salary is calculated. For 100, it is 3 and for null salary (0), it is 1 (length of 0 is 1)
average of the salaries group by salary amount. So there will be two groups, 1st is 100 and 2nd is 0. The average will be 3 for the first group and 1 for the 2nd group.
the sum of all averages. That is 3+1 = 4
So for sample data mentioned in your case, it will be 4.
See this db<>fiddle to get an idea.
In my view the group by clause at the end is what's to be noted. Without the group by you are Not able to do a nested group by. Trying to do it results in
With sal as
(
Select 100 salary from dual union all
Select 200 from dual union all
Select 300 from dual union all
Select 10 from dual union all
Select 20 from dual union all
Select 40 from dual union all
Select 50 from dual union all
Select 50 from dual union all
Select null from dual union all
Select null from dual
)
Select SUM(AVG(LENGTH(coalesce(SALARY,0))))
--,AVG(LENGTH(coalesce(SALARY,0)))
from sal
ORA-00978: nested group function without GROUP BY
00978. 00000 - "nested group function without GROUP BY"
*Cause:
*Action:
Now when you add the group by , the sum is repeated for each grouped by salary. So in this case you are summing the value 2.1 9 times . The value 2.1 is derived by the AVG AVG(LENGTH(coalesce(SALARY,0))). NULL are counted as invididual records and not as one after being grouped hence 9 and not 8 records.
Hope this helps.
By the way what does it do in your application, the use case? just curious.

In oracle How can I Find out one/two Columns data which corresponding other columns have maximum value

I'm Using Oracle where,
I have a Table(FE_IMPORT_LC Table) with data from where i give in following few column with data
TRANSMIT_LC_NO LIAB_AMT_LCY REM_LC_AMT_LCY IMP_AMEND_NO
108615020048 10022000 10022112 00
108615020048 10022000 10022112 01
108615020048 10022000 10022112 02
108615020048 11692000 8351760 03
I want to find out Data of the Red Marked Rows, which IMP_AMEND_NO column value is maximum. That means I want to find out one/two Columns data which corresponding other columns have maximum value.
So, I already create following query:
SELECT l1.liab_amt_lcy
FROM fe_import_lc l1
WHERE l1.transmit_lc_no = '108615020048'
AND l1.imp_amend_no = (SELECT MAX(l2.imp_amend_no)
FROM fe_import_lc l2
WHERE l2.transmit_lc_no = l1.transmit_lc_no)
But I want more effective query for this, If any one know about it please...Please give answer/reply as early as possible.
Try;
select liab_amt_lcy
from (
SELECT l1.liab_amt_lcy, imp_amend_no
FROM fe_import_lc l1
WHERE l1.transmit_lc_no = '108615020048'
order by imp_amend_no desc
)
where rownum < 2
Try something like below, where l1 would be your FE_IMPORT_LC table. Better to create a view with the logic of l2 table given below and then select.
with l1(TRANSMIT_LC_NO, LIAB_AMT_LCY, REM_LC_AMT_LCY, IMP_AMEND_NO) as(
select 108615020048,10022000,10022112,00 from dual union
select 108615020048,10022000,10022112,01 from dual union
select 108615020048,10022000,10022112,02 from dual union
select 108615020048,10022000,10022112,03 from dual
), l2 as(
select l1.*,row_number() over (partition by TRANSMIT_LC_NO order by IMP_AMEND_NO desc) as rno from l1)
select TRANSMIT_LC_NO, LIAB_AMT_LCY,REM_LC_AMT_LCY,IMP_AMEND_NO from l2
where rno=1;
If 2 rows have same max(IMP_AMEND_NO ) and if you want both, use below query(instead of row_number, I am using rank here. Rest same.
with l1(TRANSMIT_LC_NO, LIAB_AMT_LCY, REM_LC_AMT_LCY, IMP_AMEND_NO) as(
select 108615020048,10022000,10022112,00 from dual union all
select 108615020048,10022000,10022112,01 from dual union all
select 108615020048,10022000,10022112,03 from dual union all
select 108615020048,10022000,10022112,03 from dual
), l2 as(
select l1.*,rank() over (partition by TRANSMIT_LC_NO order by IMP_AMEND_NO desc) as rno from l1)
select TRANSMIT_LC_NO, LIAB_AMT_LCY,REM_LC_AMT_LCY,IMP_AMEND_NO from l2
where rno=1;
Here you dont have to specify TRANSMIT_LC_NO explicitely. If you have many records, then also you can get only row corresponding to max(IMP_AMEND_NO). But if you want to use this is a PL/SQL block, then put the TRANSMIT_LC_NO in the where clause in the select query from FE_IMPORT_LC and proceed like below.
You can try this, I don't have environment currently to test syntax error. However, I think with little modification it should work fine
select * from
(
select TRANSMIT_LC_NO, LIAB_AMT_LCY, REM_LC_AMT_LCY, IMP_AMEND_NO,
row_number() over(partition by transmit_lc_no order by imp_amend_no desc) as MAX_ID
from fe_import_lc
)
t where t.MAX_ID=1
and T.TRANSMIT_LC_NO = '108615020048';

How to Correctly Sum Totals from a Table That Must be Joined to Another Table that Causes Duplicates

I have two tables like the following:
PAY_TABLE
EMPLID PAY
123 100
123 150
123 150
DEDUCTION_TABLE
EMPLID DEDUCTION
123 15
123 30
and I want a result like the following:
TOTAL_PAY
400
I would like to get that result with a fairly simple query and I feel like I'm missing an obvious way to do it, but I can't seem to figure out what is.
For instance, this query returns 800 because every row in the PAY_TABLE is being duplicated when joined to the DEDUCTION_TABLE:
SELECT SUM(PAY) AS TOTAL_PAY
FROM PAY_TABLE JOIN DEDUCTION_TABLE USING(EMPLID);
And this query returns 250 because the DISTINCT keyword causes the second 150 value in the PAY_TABLE to be ignored:
SELECT SUM(DISTINCT PAY) AS TOTAL_PAY
FROM PAY_TABLE JOIN DEDUCTION_TABLE USING(EMPLID);
There are probably several ways to do this, but I am looking for the simplest way to return a result of 400.
Here is some code to create the example tables to make it easier:
WITH
PAY_TABLE AS (
SELECT 123 AS EMPLID, 100 AS PAY FROM DUAL
UNION ALL
SELECT 123, 150 FROM DUAL
UNION ALL
SELECT 123, 150 FROM DUAL
),
DEDUCTION_TABLE AS (
SELECT 123 AS EMPLID, 15 AS DEDUCTION FROM DUAL
UNION ALL
SELECT 123, 30 FROM DUAL
)
It's unclear exactly what you need, since your example doesn't make use of the DEDUCTION_TABLE table, but I believe what you'll want is to aggregate before you JOIN:
;with pay AS (SELECT EmplID,SUM(PAY) AS Pay
FROM PAY_TABLE
GROUP BY EmplID
)
,ded AS (SELECT EmplID,SUM(DEDUCTION) AS Ded
FROM DEDUCTION_TABLE
GROUP BY EmplID
)
SELECT *
FROM pay
LEFT JOIN ded
ON pay.EmplID = ded.EmplID
Assuming you need the join to DEDUCTION_TABLE just to ensure that there is a deduction for the employee:
SELECT SUM(P.PAY) AS TOTAL_PAY
FROM PAY_TABLE P
WHERE EXISTS (SELECT NULL FROM DEDUCTION_TABLE D
WHERE D.EMPLID = P.EMPLID;