SQL making column with % of total - sql

I am making a table with amount of transactions from other banks.
First row will be the total one. First column with absolute numbers, second with % of the total amount and third, last column, will be with name of the senders bank.
eg:
TRN
%
BANK
8000
100%
ALL
4000
50%
BANK1
2000
25%
BANK2
2000
25%
BANK3
I have trouble getting the % column as in my script the data are groupped already.
SELECT COUNT(*)
,'100%' as %,
,'ALL' as BANK
FROM table A
UNION ALL
SELECT COUNT(*)
,**IDK**
,BANK_ID BANK
FROM TABLE2 B
GROUP BY A.BANK_ID
ORDER BY 1 DESC
I know that there should be possibility with the over partition, but I can't figure out how.
Thanks for any tips!

Hi I think this should do the trick.
You can use the over partition with specifying anything in the brackets.
SELECT COUNT(*)
,'100%' as '%',
,'ALL' as BANK
UNION ALL
SELECT COUNT(*)
,cast(count(*)*100/count(*) over () as varchar) +'%'
,BANK_ID BANK
FROM TABLE2 B
GROUP BY B.BANK_ID
ORDER BY 1 DESC

Sample data:
create table demo (trn number, bank_id varchar2(10));
insert all
into demo values (1000, 'BANK1')
into demo values (1000, 'BANK1')
into demo values (1000, 'BANK1')
into demo values (1000, 'BANK1')
into demo values (1000, 'BANK2')
into demo values (1000, 'BANK2')
into demo values (1000, 'BANK3')
into demo values (1000, 'BANK3')
select * from dual;
Query:
select sum(trn)
, 100 * ratio_to_report(sum(trn)) over () * 2 as percent
, nvl(bank_id,'ALL') as bank
from demo
group by rollup (bank_id);
SUM(TRN) PERCENT BANK
---------- ---------- ----------
4000 50 BANK1
2000 25 BANK2
2000 25 BANK3
8000 100 ALL
group by rollup() generates a total row.
ratio_to_report() returns a value between 0 and 1, so I multiply by 100 to present it as a percentage.
The * 2 is because ratio_to_report() includes the whole column including the rollup total, so every value is halved.
I've assumed bank_id cannot be null and so a null value in the report must be the total row. If you need to distinguish between the total row and other null values, you can use grouping(bank_id) in a case expression, and it will return 1 for the total row and 0 for the rest. (You could also use this in an order by clause if you want to display the total row first.)

I am not overly familiar with Oracle syntax so the below may need some adjustments, but it should be portable enough. This is in MS SQL. Apologies for not being able to provide you a solution in the direct syntax you need. If someone is more familiar, please feel free to edit.
DECLARE #totalCount BIGINT = (SELECT Count(*) FROM table);
SELECT
#TotalCount AS [Count],
FORMAT(1, 'P') AS [%],
'ALL' AS [BANK]
--removed table here as the only derived value is the count and we already have it in #totalCount
UNION ALL
SELECT
COUNT(*) AS [Count],
FORMAT(CAST(Count(*) AS DECIMAL) / #TotalCount,'P') AS [%],
BANK_ID AS [BANK]
FROM [tableName]
GROUP BY [tableName].BANK_ID
--ORDER BY 1

I got to this solution and it works thankfully,
with alll as
(
SELECT
COUNT(*) trn
,'100%' AS prcnt
,'ALL' AS BANK_ID
FROM table
)
, bank as
(
SELECT distinct
count(1) over (partition by BANK_ID) cnt_B
,to_char(round(count(1) over (partition by BANK_ID)/count(*) over (partition by 1),3)*100) || '%' as prcnt
,BANK_ID
FROM table
)
select * from alll
UNION ALL
select * from bank

The MODEL clause could be used for this:
-- S a m p l e d a t a
WITH
tbl (BANK_ID, TRN) AS
(
Select 'Bank 1', 500 From Dual Union All
Select 'Bank 1', 3500 From Dual Union All
Select 'Bank 2', 1200 From Dual Union All
Select 'Bank 2', 800 From Dual Union All
Select 'Bank 3', 2000 From Dual
)
-- M a i n S Q L
SELECT TRN, PCT, BANK_ID
FROM ( SELECT BANK_ID "BANK_ID", Sum(TRN) "TRN"
FROM tbl
GROUP BY BANK_ID
ORDER BY BANK_ID
)
MODEL Dimension By(BANK_ID )
Measures(TRN, 0 as PCT)
RULES
( TRN['ALL'] = Sum(TRN)[BANK_ID != 'ALL'],
TRN[ANY] = Sum(TRN)[CV()],
PCT['ALL'] = 100,
PCT[ANY] = Sum(TRN)[CV()] * 100 / Sum(TRN)[BANK_ID != 'ALL']
)
ORDER BY BANK_ID
-- R e s u l t
TRN PCT BANK_ID
---------- ---------- -------
8000 100 ALL
4000 50 Bank 1
2000 25 Bank 2
2000 25 Bank 3

Related

SQL Select value from other table based on column value as treshold

I have a SQLite query which returns a user name and how much a user spent (done by SELECT SUM() from the different table).
Name
Spent
Adam
700
Mike
400
Steve
100
I have another table which contains discount amount with corresponding treshold:
Treshold
Discount
200
5
400
10
600
15
I need to find what discount each user has (if it does at all). So results would look like this:
Name
Spent
Discount
Total
Adam
700
15
595
Mike
400
10
360
Steve
100
0
100
You need a LEFT join of your query to the 2nd table and aggregation:
SELECT t1.name, t1.Spent,
COALESCE(MAX(t2.Discount), 0) Discount,
t1.Spent * (1 - 0.01 * COALESCE(MAX(t2.Discount), 0)) Total
FROM (SELECT name, SUM(Spent) Spent FROM table1 GROUP BY name) t1
LEFT JOIN table2 t2 ON t2.Treshold <= t1.Spent
GROUP BY t1.name;
See the demo.
I am in a hurry. Sorry.
with a as (
select name, sum(spent) spe
from test1
group by name)
select a.name
, a.spe
, max(tres)
, max(disc)
, spe -spe * (0 || '.' || disc) total
from test2, a
where tres <= a.spe
DEMO

Alternative: Sql - SELECT rows until the sum of a row is a certain value

My question is very similar to my previous one posted here:
Sql - SELECT rows until the sum of a row is a certain value
To sum it up, I need to return the rows, until a certain sum is reached, but the difference this time, is that, I need to find the best fit for this sum, I mean, It doesn't have to be sequential. For example:
Let's say I have 5 unpaid receipts from customer 1:
Receipt_id: 1 | Amount: 110€
Receipt_id: 2 | Amount: 110€
Receipt_id: 3 | Amount: 130€
Receipt_id: 4 | Amount: 110€
Receipt_id: 5 | Amount: 190€
So, customer 1 ought to pay me 220€.
Now I need to select the receipts, until this 220€ sum is met and it might be in a straight order, like (receipt 1 + receipt 2) or not in a specific order, like (receipt 1 + receipt 4), any of these situations would be suitable.
I am using SQL Server 2016.
Any additional questions, feel free to ask.
Thanks in advance for all your help.
This query should solve it.
It is a quite dangerous query (containing a recursive CTE), so please be careful!
You can find some documentation here: https://www.essentialsql.com/recursive-ctes-explained/
WITH the_data as (
SELECT *
FROM (
VALUES (1, 1, 110),(1, 2,110),(1, 3,130),(1, 4,110),(1, 5,190),
(2, 1, 10),(2, 2,20),(2, 3,200),(2, 4,190)
) t (user_id, receipt_id, amount)
), permutation /* recursive used here */ as (
SELECT
user_id,
amount as sum_amount,
CAST(receipt_id as varchar(max)) as visited_receipt_id,
receipt_id as max_receipt_id,
1 as i
FROM the_data
WHERE amount > 0 -- remove empty amount
UNION ALL
SELECT
the_data.user_id,
sum_amount + amount as sum_amount,
CAST(concat(visited_receipt_id, ',', CAST(receipt_id as varchar))as varchar(max)) as visited_receipt_id,
receipt_id as max_receipt_id ,
i + 1
FROM the_data
JOIN permutation
ON the_data.user_id = permutation.user_id
WHERE i < 1000 -- max 1000 loops, means any permutation with less than 1000 different receipts
and receipt_id > max_receipt_id -- in order that sum in komutatif , we can check the sum in any unique order ( here we take the order of the reciept_id in fact we do not produce any duplicates )
-- AND sum_amount + amount <= 220 -- ignore everything that is bigger than the expected value (optional)
)
SELECT *
FROM permutation
WHERE sum_amount = 220
in order to select only one combination per user_id, replace the last three lines of the previous query by
SELECT *
FROM (
SELECT *, row_number() OVER (partition by user_id order by random() ) as r
FROM permutation
WHERE sum_amount = 220
) as t
WHERE r = 1
IF your target is to sum only 2 receipts in order to reach your value, this could be a solution:
DECLARE #TARGET INT = 220 --SET YOUR TARGET
, #DIFF INT
, #FIRSTVAL INT
SET #FIRSTVAL = (
SELECT TOP 1 AMOUNT
FROM myRECEIPTS
ORDER BY RECEIPT_ID ASC
)
SELECT TOP 1 *
FROM myRECEIPTS
WHERE AMOUNT = #TARGET - #FIRSTVAL
ORDER BY RECEIPT_ID ASC
this code will do it:
declare #sum1 int
declare #numrows int
set #numrows= 1
set #sum1 =0
while (#sum1 < 10)
begin
select top (#numrows) #sum1=sum(sum1) from receipts
set #numrows +=1
end
select top(#numrows) * from receipts

Paging in SQL with no break on primary key

I am new to Stackoverflow. Please forgive me if I have made some mistake.
I have following data in SQL.
Row No. Customer No. Customer Name
------------------------------------------------------------
1 1234 ABCD
2 1234 ABCD
3 1234 ABCD
4 6789 WXYZ
5 6789 WXYZ
6 3456 OPQR
7 4567 JKLM
I need to page the above data with following constraint
Page size : 4 records
If customer no. is splitting between two pages then new customer no. should go on the next page.
output desired:
Paging with 4 records each page
1st Page
Row No. Customer No. Customer Name
------------------------------------------------------------
1 1234 ABCD
2 1234 ABCD
3 1234 ABCD
2nd Page
Row No. Customer No. Customer Name
------------------------------------------------------------
4 6789 WXYZ
5 6789 WXYZ
6 3456 OPQR
7 4567 JKLM
Please help.
You can read page size +1 records using any limiting, then if the last record and the record before the last has the same Customer Name just filter out the customer using HAVING.
Could be tricky and depends on DB you use and could be easily done on presentation layer
Declare #PageNumber INT = 1, #PageSize INT = 4
;with cte1(RowNo,CustomerNo,CustomerName)
AS
(
Select 1,1234,'ABCD' union all
Select 2,1234,'ABCD' union all
Select 3,1234,'ABCD' union all
Select 4,6789,'WXYZ' union all
Select 5,6789,'WXYZ' union all
Select 6,3456,'OPQR' union all
Select 7,4567,'JKLM'
)
select * from cte1 order by RowNo
OFFSET #PageSize * (#PageNumber - 1) ROWS
FETCH NEXT #PageSize ROWS ONLY
This can be done and not to complicated using recursive CTE.
For sample data, I have used query from other answer
WITH cte1(RowNo,CustomerNo,CustomerName)
AS
(
SELECT 1,1234,'ABCD' UNION ALL
SELECT 2,1234,'ABCD' UNION ALL
SELECT 3,1234,'ABCD' UNION ALL
SELECT 4,6789,'WXYZ' UNION ALL
SELECT 5,6789,'WXYZ' UNION ALL
SELECT 6,3456,'OPQR' UNION ALL
SELECT 7,4567,'JKLM'
)
SELECT * INTO #table FROM cte1
I'l add one more row with reapaing user to split this to 3rd page
INSERT #table (RowNo, CustomerNo, CustomerName) VALUES (8,3456,'OPQR')
And here is the solution. I've put some comments to explain parts
WITH CTE_Source AS
(
--I use this to add RN column simply beacuse I can't trust that rowNo column will have no gaps
SELECT *
, ROW_NUMBER() OVER (ORDER BY RowNo) RN
FROM #table t
)
, CTE_R AS
(
--First part is select of first row
SELECT s.RowNo
, s.CustomerNo
, s.CustomerName
, s.RN
, 1 AS Grp --this is a current group of rows
, 1 AS Cnt --counter of how many rows group have
FROM CTE_Source s WHERE RN = 1
UNION ALL
--subsequent select is for next row
SELECT
s.RowNo
, s.CustomerNo
, s.CustomerName
, s.RN
-- increase group when different customer
, CASE WHEN s.CustomerNo = r.CustomerNo THEN r.Grp ELSE r.Grp+Cnt END
-- increase counter when same customer
, CASE WHEN s.CustomerNo = r.CustomerNo THEN r.Cnt + 1 ELSE 1 END
FROM CTE_R r
INNER JOIN CTE_Source s ON s.RN = r.Rn + 1
)
, CTE_Paging AS
(
SELECT *
, CEILING((r.Grp + r.Cnt) / 4.) AS Page -- replace 4 with your page size
FROM CTE_R r
)
SELECT * FROM CTE_Paging --Just add WHERE Page = if you want specific page
OPTION (MAXRECURSION 0) -- for unlimited recursion if you have more than 100 rows

Joining onto a table that doesn't have ranges, but requires ranges

Trying to find the best way to write this SQL statement.
I have a customer table that has the internal credit score of that customer. Then i have another table with definitions of that credit score. I would like to join these tables together, but the second table doesn't have any way to link it easily.
The score of the customer is an integer between 1-999, and the definition table has these columns:
Score
Description
And these rows:
60 LOW
99 MED
999 HIGH
So basically if a customer has a score between 1 and 60 they are low, 61-99 they are med, and 100-999 they are high.
I can't really INNER JOIN these, because it would only join them IF the score was 60, 99, or 999, and that would exclude anyone else with those scores.
I don't want to do a case statement with the static numbers, because our scores may change in the future and I don't want to have to update my initial query when/if they do. I also cannot create any tables or functions to do this- I need to create a SQL statement to do it for me.
EDIT:
A coworker said this would work, but its a little crazy. I'm thinking there has to be a better way:
SELECT
internal_credit_score
(
SELECT
credit_score_short_desc
FROM
cf_internal_credit_score
WHERE
internal_credit_score = (
SELECT
max(credit.internal_credit_score)
FROM
cf_internal_credit_score credit
WHERE
cs.internal_credit_score <= credit.internal_credit_score
AND credit.internal_credit_score <= (
SELECT
min(credit2.internal_credit_score)
FROM
cf_internal_credit_score credit2
WHERE
cs.internal_credit_score <= credit2.internal_credit_score
)
)
)
FROM
customer_statements cs
try this, change your table to contain the range of the scores:
ScoreTable
-------------
LowScore int
HighScore int
ScoreDescription string
data values
LowScore HighScore ScoreDescription
-------- --------- ----------------
1 60 Low
61 99 Med
100 999 High
query:
Select
.... , Score.ScoreDescription
FROM YourTable
INNER JOIN Score ON YourTable.Score>=Score.LowScore
AND YourTable.Score<=Score.HighScore
WHERE ...
Assuming you table is named CreditTable, this is what you want:
select * from
(
select Description, Score
from CreditTable
where Score > 80 /*client's credit*/
order by Score
)
where rownum = 1
Also, make sure your high score reference value is 1000, even though client's highest score possible is 999.
Update
The above SQL gives you the credit record for a given value. If you want to join with, say, Clients table, you'd do something like this:
select
c.Name,
c.Score,
(select Description from
(select Description from CreditTable where Score > c.Score order by Score)
where rownum = 1)
from clients c
I know this is a sub-select that executed for each returning row, but then again, CreditTable is ridiculously small and there will be no significant performance loss because of the the sub-select usage.
You can use analytic functions to convert the data in your score description table to ranges (I assume that you meant that 100-999 should map to 'HIGH', not 99-999).
SQL> ed
Wrote file afiedt.buf
1 with x as (
2 select 60 score, 'Low' description from dual union all
3 select 99, 'Med' from dual union all
4 select 999, 'High' from dual
5 )
6 select description,
7 nvl(lag(score) over (order by score),0) + 1 low_range,
8 score high_range
9* from x
SQL> /
DESC LOW_RANGE HIGH_RANGE
---- ---------- ----------
Low 1 60
Med 61 99
High 100 999
You can then join this to your CUSTOMER table with something like
SELECT c.*,
sd.*
FROM customer c,
(select description,
nvl(lag(score) over (order by score),0) + 1 low_range,
score high_range
from score_description) sd
WHERE c.credit_score BETWEEN sd.low_range AND sd.high_range

SQL Server Rounding Issue Looking for Explanation

I've solved this issue but I'm just wondering why this works the way it does. I have a temporary table I am selecting from and am looking to display a a name, the number of records that match this name, and the percentage for that name of the total records. This is the way I originally had it:
SELECT name, number,
CASE WHEN number = 0 THEN 0 ELSE
convert(Numeric(10,2), number / CONVERT(decimal(5,2),SUM(number)) * 100)
END as "Percentage of Total"
FROM #names
group by name, number
The results I received were:
name number Percentage of Total
------------------------- ----------- ---------------------------------------
Test 1 0 0.00
Test 2 22 100.00
Test 3 28 100.00
When I change the query to this, the results are correct:
declare #total decimal(5,2)
select #total = SUM(number) FROM #names
SELECT name, number, convert(Numeric(10,2), number/ #total * 100) as "Percentage of Total"
FROM #names
group by name, number
Correct Results:
name number Percentage of Total
------------------------- ----------- ---------------------------------------
Test 1 22 44.00
Test 2 0 0.00
Test 3 28 56.00
Can someone explain what is going on, I would like to understand this better. Thanks!
Jon
You first query groups by number.
Since you don't have duplicates of numbers, number / SUM(number) is equivalent to the 1 / COUNT (except when the number is 0).
You second query does not group by number, it calculates total sum.
Use this instead:
SELECT name, number * 100.0 / SUM(number) OVER ()
FROM #names
When used with OVER clause, SUM becomes the analytical function rather than the aggregate one.
It does not shrink several records into one: instead, it returns the total value along with each record:
-- This is an aggregate function. It shrinks all records into one record and returns the total sum
WITH q (name, number) AS
(
SELECT 'test1', 0
UNION ALL
SELECT 'test2', 22
UNION ALL
SELECT 'test3', 28
)
SELECT SUM(number)
FROM q
--
50
-- This is an analytical function. It calcuates the total sum as well but does not shrink the records.
WITH q (name, number) AS
(
SELECT 'test1', 0
UNION ALL
SELECT 'test2', 22
UNION ALL
SELECT 'test3', 28
)
SELECT SUM(number) OVER ()
FROM q
--
50
50
50