plsql subtract columns in a same table - sql

I have a simple student table.
name | amount | vdate
Josh | 15 | 01.01.2020
Steve | 25 | 05.04.2008
Josh | 40 | 01.01.2022
What I want to do is subtract Josh value from each other.
I wrote this query but it is not working
select name , sum(b.amount-a.amount) diff from
select name,amount from student a where name = 'Josh' and vdate='01.01.2020'
union all
select name,amount from student b where name = 'Josh' and vdate = '01.01.2022')
group by name
Expected Result is:
name | diff
Josh | 25
Steve| 25

you can try this code,
select
fname,
abs(sum(amount2)) amount
from
(
WITH
student(fname,amount,vdate) AS (
SELECT 'Josh' ,15, to_date('01102017','ddmmyyyy') from dual
UNION ALL SELECT 'Steve',25, to_date('01102017','ddmmyyyy') from dual
UNION ALL SELECT 'Josh' ,40 ,to_date('01102019','ddmmyyyy')from dual
)
select
h.fname,
h.amount,
decode((ROW_NUMBER() OVER(PARTITION BY fname order by vdate desc)),1,amount,amount* -1) amount2
from student h
)
group by
fname
;
I assume that you get the greater amount value of the person and substract other values, you can select the bigger date instead by modifying the order by clause in the partition window i. e.
decode((ROW_NUMBER() OVER(PARTITION BY fname order by vdate desc)),1,amount,amount * -1) amount2

You can try this (I don't know what sense it makes ...):
Count the number of rows found until now per fname ("name" is a reserved word and I don't use it). And if the row number obtained this way is odd, then use the negative amount, else the positive amount.
Finally, run a sum over these positive/negative rows.
WITH
indata(fname,amount) AS (
SELECT 'Josh' ,15
UNION ALL SELECT 'Steve',25
UNION ALL SELECT 'Josh' ,40
)
,
alternate AS (
SELECT
fname
, CASE ROW_NUMBER() OVER(PARTITION BY fname) % 2
WHEN 1 THEN amount * -1 -- when odd then negative
ELSE amount -- else positive
END AS amount
FROM indata
)
SELECT
fname
, ABS(SUM(amount)) AS amount -- absolute value
FROM alternate
GROUP BY fname;
-- out fname | amount
-- out -------+--------
-- out Josh | 25
-- out Steve | 25

Related

How to count changes within each column and in SQL

This is how the table is looking like:
id
city
address
steps
date
1
null
null
a
2021-11-01
1
NY
null
b
2021-11-04
1
Chicago
null
c
2021-11-05
2
SF
33, ABC colony
x
2021-12-01
2
SF
33, ABC colony
y
2021-12-04
2
SF
44, Kang Street
z
2021-12-05
3
Austin
null
i
2022-01-01
3
Austin
12, Bridgetown
j
2022-01-04
3
Austin
null
k
2022-01-05
What I want is total count of times that for any 'id' there was an update in fields city and address only but excluding null. We dont care about the column steps and any updates there.
For id = 1, the city was changed from null to NY to Chicago. However, the address remained null, but the given the dates I count it as 2. Changing from null to NY is not supposed to be counted as an update.
For id = 2, the city was never changed it was always SF. But, there is a change in address but only once and thus we count the update as 2 again.
For id = 3, the city was never changed but the address changed from null to an address back to null. We don't count the first null because the customer may not have the info but if he/she changes it back to null that has to be counted. Here also update count will be 2.
I am expecting the results as:
id
change_count
1
2
2
2
3
2
Can I know how to do this via sql? The major problem is to not count "null" as I rank the id in ascending order of when the record came but count when it is changed back to "null" is where I am mainly confused.
Any help is appreciated. I am working on it and if I get the SQL finalized, I will share it here too.
Can this work for you?
WITH
-- your input, do not use in query ...
indata(id,city,addr,steps,dt) AS (
SELECT 1,NULL ,NULL ,'a',DATE '2021-11-01'
UNION ALL SELECT 1,'NY' ,NULL ,'b',DATE '2021-11-04'
UNION ALL SELECT 1,'Chicago',NULL ,'c',DATE '2021-11-05'
UNION ALL SELECT 2,'SF' ,'33, ABC colony' ,'x',DATE '2021-12-01'
UNION ALL SELECT 2,'SF' ,'33, ABC colony' ,'y',DATE '2021-12-04'
UNION ALL SELECT 2,'SF' ,'44, Kang Street','z',DATE '2021-12-05'
UNION ALL SELECT 3,'Austin' ,NULL ,'i',DATE '2022-01-01'
UNION ALL SELECT 3,'Austin' ,'12, Bridgetown' ,'j',DATE '2022-01-04'
UNION ALL SELECT 3,'Austin' ,NULL ,'k',DATE '2022-01-05'
)
-- end of your input
-- real query starts here, replace following comma with "WITH" ...
,
olap AS (
SELECT
id
-- a NULL is not COUNTed DISTINCT, but an empty string is
, CASE WHEN city IS NULL AND LAG(city) OVER w IS NOT NULL THEN '' ELSE city END AS city
, CASE WHEN addr IS NULL AND LAG(addr) OVER w IS NOT NULL THEN '' ELSE addr END AS addr
FROM indata
WINDOW w AS (PARTITION BY id ORDER BY dt)
)
SELECT
id
, GREATEST(COUNT(DISTINCT city),COUNT(DISTINCT addr)) AS changecount
FROM olap
GROUP BY 1
ORDER BY 1
;
-- out id | changecount
-- out ----+-------------
-- out 1 | 2
-- out 2 | 2
-- out 3 | 2
I tired using combination of window-function lag and coalesce method and I finally got the answer but if someone has a better solution, do suggest. :)
My sql:
with cte1 as(
select *,
row_number over(partition by id order by date) as rn
from main_table),
cte2 as (
select * from cte1 where (rn =1 and city <> null or address <> null)),
cte3 as (
SELECT id,
case when coalesce(city,'-1')=COALESCE(lag(city,1) over(partition by id order by date), city,'-1') then 0 else 1 end as cityChange,
case when coalesce(address,'-1')=COALESCE(lag(address,1) over(partition by id order by date), address,'-1') then 0 else 1 end as addressChange
from cte2)
select id,
sum(cityChange) as cityChangeCount,
sum(addressChange) as addressChangeCount
from cte3
group by id

Return rows that do not contain a subset of the same rows

Let's say I have a table called activitiesthat has 2 columns: nameActivityand zipCode.
How do I write a query that returns all activities a for which there are no other activities b that:
have the same zipCode
that share the same nameActivity as another activity c. With other words, for this condition, we need to check that de nameActivity of activity b is unique.
Example:
nameActivity zipCode
kayaking 1000
bouldering 1000
museum 2000
paintball 2000
museum 3000
karting 4000
Following all the conditions above this should result in:
nameActivity zipCode
paintball 2000
museum 3000
karting 4000
For the first part of the condition :
SELECT (array_agg(nameActivity))[1]
, zipCode
FROM activities
GROUP BY zipCode
HAVING count(*) = 1
result :
array_agg zipcode
karting 4000
museum 3000
(paintball, 2000) is excluded from the result because another nameActivity (museum) exists for the same zipCode.
The second part of the condition is unclear : what means "b share the same nameActivity as another activity c" ???
well, this was a hard one. I think I got it right with some tinkering, hope it helps.
with data (nameActivity, zipCode)as (
Select 'kayaking', 1000 from dual union all
Select 'bouldering', 1000 from dual union all
Select 'museum', 2000 from dual union all
Select 'paintball', 2000 from dual union all
Select 'museum', 3000 from dual union all
Select 'karting', 4000 from dual )
,duplicatesname as(
select nameActivity,zipCode
from(
select d1.nameActivity,max(zipCode) zipCode, count(*) cnt
from data d1
group by d1.nameActivity)
where cnt >1)
,withoutduplicatename as(
select nameActivity,zipCode
from (
select d1.nameActivity,d1.zipCode,d2.nameActivity na2,d2.zipCode zi2
from data d1
left join duplicatesname d2 on d1.nameActivity = d2.nameActivity
and d1.zipCode != d2.zipCode
) where na2 is null)
,duplicatZipCode as(
select zipCode
from(
select zipCode , count(*) cnt
from withoutduplicatename d1
group by d1.zipCode)
where cnt >1)
select nameActivity,zipCode
from withoutduplicatename d1
where d1.zipCode not in (select zipCode from duplicatZipCode)
order by zipCode
I got near.
But kayaking can't be ignored.
create table activities (
nameActivity varchar(30),
zipCode int
)
insert into activities
(nameActivity, zipCode) values
('kayaking', 1000)
, ('bouldering', 1000)
, ('museum', 2000)
, ('paintball', 2000)
, ('museum', 3000)
, ('karting', 4000)
SELECT nameActivity, zipCode
FROM
(
SELECT nameActivity, zipCode
, ROW_NUMBER() OVER (PARTITION BY zipCode) rn
FROM activities a
WHERE NOT EXISTS (
select 1
from activities a2
group by a2.zipCode
having count(*) = 1
and a2.zipCode != a.zipCode
and max(a2.nameActivity) = a.nameActivity
)
) q
WHERE rn = 1
nameactivity | zipcode
:----------- | ------:
kayaking | 1000
paintball | 2000
museum | 3000
karting | 4000
db<>fiddle here

How to return all records from table A , if any one of the column has a specific value in oracle sql?

Below is the sample data
If I pass lot name as a parameter, I want to return employees who has greater than 0 records in The specific Lot . Not just the one record but all the records of that employee.
Table A
Empid lotname itemcount
1 A 1
1 B 1
2 B 0
3 B 1
3 C 0
Parameter - B
Result :
Empid lotname itemcount
1 A 1
1 B 1
3 B 1
3 C 0
Because employee 3 and 1 has count in B lot. All the employee lot details should be returned.
select data.* from A data,
(select Empid,count(lotname)
from A
group by Empid
having count(lotname)>1) MulLotEmp
where data.lotname='B'
and data.Empid=MulLotEmp.Empid;
Check if this query solves your problem. In this I created a inner table first for your first requirement that emp with multiple lot, then I mapped this table with actual table with condition of input lot name.
If I understand correctly, you want all "1" and then only "0" if there is no "1".
One method is:
select a.*
from a
where itemcount = 1 or
not exists (select 1 from a a2 where a2.empid = a.empid and a2.itemcount = 1);
In Oracle, you can use the MAX analytic function:
SELECT Empid,
lotname,
itemcount
FROM (
SELECT t.*,
MAX( itemcount ) OVER ( PARTITION BY Empid ) AS max_itemcount
FROM table_name t
)
WHERE max_itemcount = 1;
So, for you sample data:
CREATE TABLE table_name ( Empid, lotname, itemcount ) AS
SELECT 1, 'A', 1 FROM DUAL UNION ALL
SELECT 1, 'B', 1 FROM DUAL UNION ALL
SELECT 2, 'B', 0 FROM DUAL UNION ALL
SELECT 3, 'B', 1 FROM DUAL UNION ALL
SELECT 3, 'C', 0 FROM DUAL;
This outputs:
EMPID | LOTNAME | ITEMCOUNT
----: | :------ | --------:
1 | A | 1
1 | B | 1
3 | B | 1
3 | C | 0
db<>fiddle here
The analytic function
sum(case when LOTNAME = 'B' /* parameter */ then ITEMCOUNT end) over (partition by EMPID) as lot_itemcnt
calculates for each customer the total number of items with the selected lot.
Feel free to use it as a bind variable, e.g.
sum(case when LOTNAME = ? /* parameter */ then ITEMCOUNT end) over (partition by EMPID) as lot_itemcnt
The whole query is than as follows
with cust as (
select
EMPID, LOTNAME, ITEMCOUNT,
sum(case when LOTNAME = 'B' /* parameter */ then ITEMCOUNT end) over (partition by EMPID) as lot_itemcnt
from tab)
select
EMPID, LOTNAME, ITEMCOUNT
from cust
where lot_itemcnt >= 1;

Select except where different in SQL

I need a bit of help with a SQL query.
Imagine I've got the following table
id | date | price
1 | 1999-01-01 | 10
2 | 1999-01-01 | 10
3 | 2000-02-02 | 15
4 | 2011-03-03 | 15
5 | 2011-04-04 | 16
6 | 2011-04-04 | 20
7 | 2017-08-15 | 20
What I need is all dates where only one price is present.
In this example I need to get rid of row 5 and 6 (because there is two difference prices for the same date) and either 1 or 2(because they're duplicate).
How do I do that?
select date,
count(distinct price) as prices -- included to test
from MyTable
group by date
having count(distinct price) = 1 -- distinct for the duplicate pricing
The following should work with any DBMS
SELECT id, date, price
FROM TheTable o
WHERE NOT EXISTS (
SELECT *
FROM TheTable i
WHERE i.date = o.date
AND (
i.price <> o.price
OR (i.price = o.price AND i.id < o.id)
)
)
;
JohnHC answer is more readable and delivers the information the OP asked for ("[...] I need all the dates [...]").
My answer, though less readable at first, is more general (allows for more complexes tie-breaking criteria) and also is capable of returning the full row (with id and price, not just date).
;WITH CTE_1(ID ,DATE,PRICE)
AS
(
SELECT 1 , '1999-01-01',10 UNION ALL
SELECT 2 , '1999-01-01',10 UNION ALL
SELECT 3 , '2000-02-02',15 UNION ALL
SELECT 4 , '2011-03-03',15 UNION ALL
SELECT 5 , '2011-04-04',16 UNION ALL
SELECT 6 , '2011-04-04',20 UNION ALL
SELECT 7 , '2017-08-15',20
)
,CTE2
AS
(
SELECT A.*
FROM CTE_1 A
INNER JOIN
CTE_1 B
ON A.DATE=B.DATE AND A.PRICE!=B.PRICE
)
SELECT * FROM CTE_1 WHERE ID NOT IN (SELECT ID FROM CTE2)

How to apply pivot to result of query

There is my current query:
SELECT Name, Code, Today
, Account || Currency as Accounts
FROM (
SELECT
b.description AS Name
, b.contragentidentifycode AS Code
, c.systemday AS Today
, b.accountno AS Account
, b.currencysname AS Currency
FROM vAACCOUNT b, currentdaysetting c
WHERE b.contragentid = 412
AND b.accountno LIKE '26%'
)
it gives me such result:
Name | Code | Today | Accounts
---------------------------------------
name1 | code1 | 07.09.2016 | acc1+curr1
name1 | code1 | 07.09.2016 | acc2+curr1
name1 | code1 | 07.09.2016 | acc1+curr2
name1 | code1 | 07.09.2016 | acc2+curr2
name1 | code1 | 07.09.2016 | acc1+curr3
name1 | code1 | 07.09.2016 | acc2+curr3
name1 | code1 | 07.09.2016 | acc1+curr4
name1 | code1 | 07.09.2016 | acc2+curr4
I need convert this view to:
Name | Code | Today | someName1 | someName2 | someName3 | someName4 | someName5 | someName6 | someName7 | someName8
-------------------------------------------------------------------------------------------------------------------------------------------
name1 | code1 | 07.09.2016 | acc1+curr1 | acc2+curr1 | acc1+curr2 | acc2+curr2 | acc1+curr3 | acc2+curr3 | acc1+curr4 | acc2+curr4
I guess that most probably for this I have to use the keyword "Pivot". But all my attempts to do so - have failed. I can not to project what I see in the examples, to my table. Please help.
For number of columns I can add such "id" column:
SELECT id, Name, Code, Today
, Account || Currency as Accounts
FROM (
SELECT
row_number() over (ORDER BY b.id) AS id
, b.description AS Name
...
In my scenario:
numbers of accounts may be different;
name, code and data - one per query;
combination of accaunt+currency are unique;
result should be in one line;
total number of lines in result of query, cannot be more then 10 (in my example 8)
Per my comment above, I don't think PIVOT works for you. The answer from #RoundFour works, but requires that you know, and code for, all possible values for Account || Currency. This suggests there will never be new values for these items - I find that unlikely.
The following will allow you to switch the shape of your data. It makes no assumptions about the values in your data, but it does assume a limit on the number of possible combinations - I have coded for eight.
WITH account_data (name,code,today,account)
AS
(
SELECT 'name1','code1',TO_DATE('07.09.2016','DD.MM.YYYY'),'acc1+curr1' FROM dual UNION ALL
SELECT 'name1','code1',TO_DATE('07.09.2016','DD.MM.YYYY'),'acc2+curr1' FROM dual UNION ALL
SELECT 'name1','code1',TO_DATE('07.09.2016','DD.MM.YYYY'),'acc1+curr2' FROM dual UNION ALL
SELECT 'name1','code1',TO_DATE('07.09.2016','DD.MM.YYYY'),'acc2+curr2' FROM dual UNION ALL
SELECT 'name1','code1',TO_DATE('07.09.2016','DD.MM.YYYY'),'acc1+curr3' FROM dual UNION ALL
SELECT 'name1','code1',TO_DATE('07.09.2016','DD.MM.YYYY'),'acc2+curr3' FROM dual UNION ALL
SELECT 'name1','code1',TO_DATE('07.09.2016','DD.MM.YYYY'),'acc1+curr4' FROM dual UNION ALL
SELECT 'name1','code1',TO_DATE('07.09.2016','DD.MM.YYYY'),'acc2+curr4' FROM dual UNION ALL
SELECT 'name2','code1',TO_DATE('07.09.2016','DD.MM.YYYY'),'acc1+curr1' FROM dual UNION ALL
SELECT 'name2','code1',TO_DATE('07.09.2016','DD.MM.YYYY'),'acc2+curr1' FROM dual UNION ALL
SELECT 'name2','code1',TO_DATE('07.09.2016','DD.MM.YYYY'),'acc1+curr2' FROM dual UNION ALL
SELECT 'name3','code1',TO_DATE('07.09.2016','DD.MM.YYYY'),'acc2+curr2' FROM dual
)
SELECT
name
,code
,today
,MAX(account1)
,MAX(account2)
,MAX(account3)
,MAX(account4)
,MAX(account5)
,MAX(account6)
,MAX(account7)
,MAX(account8)
FROM
(SELECT
name
,code
,today
,CASE
WHEN rn = 1 THEN account
END account1
,CASE
WHEN rn = 2 THEN account
END account2
,CASE
WHEN rn = 3 THEN account
END account3
,CASE
WHEN rn = 4 THEN account
END account4
,CASE
WHEN rn = 5 THEN account
END account5
,CASE
WHEN rn = 6 THEN account
END account6
,CASE
WHEN rn = 7 THEN account
END account7
,CASE
WHEN rn = 8 THEN account
END account8
FROM
(SELECT
name
,code
,today
,account
,ROW_NUMBER() OVER (PARTITION BY name ORDER BY account) rn
FROM
account_data
)
)
GROUP BY
name
,code
,today
;
UPDATE >>>>>>>>>
The WITH... clause above is just because I don't have your tables and data in my system. I've rewritten my answer using your query as a guide - please note I have not been able to test this ...
SELECT
name
,code
,today
,MAX(account1)
,MAX(account2)
,MAX(account3)
,MAX(account4)
,MAX(account5)
,MAX(account6)
,MAX(account7)
,MAX(account8)
FROM
(SELECT
name
,code
,today
,CASE
WHEN rn = 1 THEN account
END account1
,CASE
WHEN rn = 2 THEN account
END account2
,CASE
WHEN rn = 3 THEN account
END account3
,CASE
WHEN rn = 4 THEN account
END account4
,CASE
WHEN rn = 5 THEN account
END account5
,CASE
WHEN rn = 6 THEN account
END account6
,CASE
WHEN rn = 7 THEN account
END account7
,CASE
WHEN rn = 8 THEN account
END account8
FROM
(SELECT
b.description AS Name
,b.contragentidentifycode AS Code
,c.systemday AS Today
,b.accountno AS Account
,b.currencysname AS Currency
,b.accountno || b.currencysname AS Accounts
,ROW_NUMBER() OVER (PARTITION BY b.description ORDER BY b.accountno) rn
FROM vAACCOUNT b, currentdaysetting c
WHERE b.contragentid = 412
AND b.accountno LIKE '26%'
)
)
GROUP BY
name
,code
,today
;
If you know all the account+currency combinations you can use this pivot (I only implemented 3 of them here):
select *
from (
<your-query> )
pivot (
min(accounts) as accounts FOR (accounts) in ('acc1+curr1' as a, 'acc2+curr1' as b, 'acc1+curr2' c)
);
There is my pivot solution:
SELECT *
FROM (
SELECT id, Name, Code, Today, Account || Currency as Accounts
FROM (
SELECT
row_number() over (ORDER BY b.id) AS id
, b.description AS Name
, b.contragentidentifycode AS Code
, c.systemday AS Today
, b.accountno AS Account
, b.currencysname AS Currency
FROM vAACCOUNT b, currentdaysetting c
WHERE b.contragentid = 412
AND b.accountno LIKE '26%'
)
)
pivot (
MIN(Accounts)
FOR ID IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
) pvt