How to count changes within each column and in SQL - 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

Related

plsql subtract columns in a same table

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

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;

Oracle check if any of multiple string exists in another table

I am newbie to Oracle. I have a requirement in which I need to fetch all the error codes from the comment field and then check it in another table to see the type of code. Depending on the type of code I have to give preference to particular type and then display that error code and type into a csv along with other columns. Below how the data is present in a column
TABLE 1 : COMMENTS_TABLE
id | comments
1 | Manually added (BPM001). Currency code does not exists(TECH23).
2 | Invalid counterparty (EXC001). Manually added (BPM002)
TABLE 2 : ERROR_CODES
id | error_code | error_type
1 | BPM001 | MAN
2 | EXC001 | EXC
3 | EXC002 | EXC
4 | BPM002 | MAN
I am able to get all error codes using REGEX_SUBSTR but not sure how to check it with other table and depending on type display only one. For eg. if the type is MAN only that error code should be returned in select clause.
I propose you to define a hierarchy of error_codes
within the FIRST function to search for the best fit.
SQL Fiddle
Query 1:
SELECT c.id,
MAX (
ERROR_CODE)
KEEP (DENSE_RANK FIRST
ORDER BY CASE ERROR_TYPE WHEN 'MAN' THEN 1 WHEN 'EXC' THEN 2 END)
AS ERROR_CODE,
MAX (
ERROR_TYPE)
KEEP (DENSE_RANK FIRST
ORDER BY CASE ERROR_TYPE WHEN 'MAN' THEN 1 WHEN 'EXC' THEN 2 END)
AS ERROR_TYPE
FROM ERROR_CODES e
JOIN COMMENTS_TABLE c ON c.COMMENTS LIKE '%' || e.ERROR_CODE || '%'
GROUP BY c.id
Results:
| ID | ERROR_CODE | ERROR_TYPE |
|----|------------|------------|
| 1 | BPM001 | MAN |
| 2 | BPM002 | MAN |
EDIT : You said in your comments
This is helpul, but I have multiple fields in select clause and adding
that in group by could be a problem
One option could be to use a WITH clause to define this result set and then join with other columns.
with res as
(
select ...
--query1
)
select t.other_columns, r.id, r.error_code ...
from other_table join res on ...
You may also use row_number() alternatively ( Which was actually my original answer. But I changed it to KEEP .. DENSE_RANK as it is efficient.
SELECT * FROM
( SELECT c.id
,ERROR_CODE
,ERROR_TYPE
--Other columns,
,row_number() OVER (
PARTITION BY c.id ORDER BY CASE error_type
WHEN 'MAN'
THEN 1
WHEN 'EXC'
THEN 2
ELSE 3
END
) AS rn
FROM ERROR_CODES e
INNER JOIN COMMENTS_TABLE c
ON c.COMMENTS LIKE '%' || e.ERROR_CODE || '%'
) WHERE rn = 1;
Fiddle
You can sort, prioritize and filter records with analytic functions.
with comments as(
select 1 as id
,'Manually added (BPM001). Currency code does not exists(TECH23).' as comments
from dual union all
select 2 as id
,'Invalid counterparty (EXC001). Manually added (BPM002)' as comments
from dual
)
,error_codes as(
select 1 as id, 'BPM001' as error_code, 'MAN' as error_type from dual union all
select 2 as id, 'EXC001' as error_code, 'EXC' as error_type from dual union all
select 3 as id, 'EXC002' as error_code, 'EXC' as error_type from dual union all
select 4 as id, 'BPM002' as error_code, 'MAN' as error_type from dual
)
-- Everything above this line is not part of the query. Just for generating test data
select *
from (select c.id as comment_id
,c.comments
,e.error_code
,row_number() over(
partition by c.id -- For each comment
order by case error_type when 'MAN' then 1 -- First prio
when 'EXC' then 2 -- Second prio
else 3 -- Everything else
end) as rn
from comments c
join error_codes e on(
e.error_code = regexp_substr(c.comments, e.error_code)
)
)
where rn = 1 -- Pick the highest priority code
/
If you could add a priority column to your error code (or even error_type) you could skip the case/when logic in the order by and simply replacing it with the priority column.

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 order the result based on the column values in sql server

I have a table with the following type:
Id Parent_id Code Name market
1 NULL 1ex name 1 3
2 1 2ex name 2 3
3 1 3ex name 3 3
4 Null 4ex name 4 1
5 null 5ex name 5 3
6 4 6ex name 6 3
I wanted to select code and name from the above table such that it is ordered in the following way:
based on the market where market id=3
Parent id
related child
others
ie. id 1 (Parent_id) should be displayed first followed by id 2 and 3 (Child id). The values in 'parent_id' are from the column 'id'.
I have built the following query so far and i am feeling little difficult to order the parent code and the related child codes.
select code,name from tbl_codes A
order by CASE WHEN(A.[Market] = 3) THEN 0 ELSE 1 END
Can someone please help me out.
Try this
SELECT code ,
name
FROM tbl_codes A
ORDER BY CASE WHEN ( A.[Market] = 3 ) THEN 0
ELSE 1
END ,
CASE WHEN ( ISNULL(parent_id,0) = 1 ) THEN 0
ELSE 1
END
A recursive CTE is the best way to construct a parent/child heirarchy as follows:
-- Set up test data
CREATE TABLE tbl_codes (id INT , Parent_id INT, Code VARCHAR(3), NAME VARCHAR(12), Market INT)
INSERT tbl_codes
SELECT 1, NULL, '1ex', 'name 1', 3 UNION ALL
SELECT 2, 1 , '2ex', 'name 2', 3 UNION ALL
SELECT 3, 1 , '3ex', 'name 3', 3 UNION ALL
SELECT 4, NULL , '4ex', 'name 4', 1 UNION ALL
SELECT 5, NULL , '5ex', 'name 5', 3 UNION ALL
SELECT 6, 4 , '6ex', 'name 6', 3
CREATE VIEW [dbo].[View_ParentChild]
AS
-- Use a recursive CTE to build a parent/child heirarchy
WITH
RecursiveCTE AS
(
SELECT
id,
name,
parent_id,
Code,
market,
sort = id
FROM
tbl_codes
WHERE
parent_id IS NULL
UNION ALL
SELECT
tbl_codes.id,
tbl_codes.name,
tbl_codes.parent_id,
tbl_codes.Code,
tbl_codes.market,
sort = tbl_codes.parent_id
FROM
tbl_codes
INNER JOIN RecursiveCTE
ON tbl_codes.parent_id = RecursiveCTE.id
WHERE
tbl_codes.parent_id IS NOT NULL
)
SELECT
Code,
NAME,
Market,
Sort
FROM
RecursiveCTE
GO
As per your request I have refactored the query as a VIEW.
To use the view:
SELECT
*
FROM
dbo.View_ParentChild AS vpc
ORDER BY
CASE WHEN ( Market = 3 ) THEN 0
ELSE 1
END,
sort
It gives the following result:
Code NAME Market Sort
---- ------ ------ ----
1ex name 1 3 1
2ex name 2 3 1
3ex name 3 3 1
6ex name 6 3 4
5ex name 5 3 5
4ex name 4 1 4
To learn more about recursive CTEs click here
And, as requested, is a new version of the view that does not use a recursive CTE
CREATE VIEW [dbo].[View_ParentChild_v2]
AS
SELECT
id,
Code,
market,
sort
FROM
(
SELECT
id,
name,
parent_id,
Code,
market,
sort = id
FROM
tbl_codes
WHERE
parent_id IS NULL
UNION ALL
SELECT
tbl_codes.id,
tbl_codes.name,
tbl_codes.parent_id,
tbl_codes.Code,
tbl_codes.market,
sort = tbl_codes.parent_id
FROM
tbl_codes
WHERE
tbl_codes.parent_id IS NOT NULL
) AS T
GO
Used as follows:
SELECT
*
FROM
View_ParentChild_v2
ORDER BY
CASE WHEN ( Market = 3 ) THEN 0
ELSE 1
END,
sort
nb: The first version, using a recursive CTE, could handle virtually unlimited levels of Parent/Child while version 2 only handles one level.
You can put condition in your columns. Try:
SELECT code ,
name ,
CASE WHEN ( A.[Market] = 3 ) THEN 0
ELSE 1
END AS marketOrder ,
CASE WHEN ( parent_id = 1 ) THEN 0
ELSE 1
END AS parentOrder
FROM tbl_codes A
ORDER BY parentOrder ,
marketOrder