Conditional delete in oracle using sql or plsql - sql

We have an oracle table x and we have values in it in a following manner
num1 dt cd
1 24-06-2017 3
1 24-06-2017 4
2 24-06-2017 1
2 24-06-2017 2
2 24-06-2017 4
2 25-06-2017 3
2 25-06-2017 4
Now I have to delete duplicate data from this table using num1,dt , if num1 and dt have any other value instead of 4 then it should exist in table and cd=4 should be deleted. Here in below condition 3 or any other value should exist but it should not be 4
1 24-06-2017 3
1 24-06-2017 4
Now here 4 should be deleted completely for 2 , 24/06/2017 and only 1 rows of each field should exist in the table ie :2 24/06/2017 1 and 2 24/06/2017 2 should exist.
2 24-06-2017 1
2 24-06-2017 2
2 24-06-2017 4
Now in third case if we dont have any other value except 4 for num1 and date then we should have only 1 row of 4 for these dates.
3 26-06-2017 4
3 26-06-2017 4
Please provide solution for this, help will be appreciated.

Oracle conveniently has rowid, which can really help with this. Here is an approach that uses rowid:
delete from t
where t.rowid <> (select max(rowid) keep (dense_rank first
order by (case when cd = 4 then 2 else 1 end)
)
from t
where t2.num1 = t.num1 and t2.date = t.date
);
This will keep exactly one row per num1/date comparison.
If you are using Oracle 12C+, then a more intuitive form of this query is:
delete from t
where t.rowid <> (select rowid
from t
where t2.num1 = t.num1 and t2.date = t.date
order by (case when cd = 4 then 2 else 1 end)
fetch first 1 row only
);
This doesn't work in earlier versions because fetching one row requires an additional subquery -- and that subquery cannot be correlated with the outer query.

If I get your question right it might work like this:
DELETE FROM table
WHERE (num1, dt, ct) IN (SELECT num1, dt, 4
FROM table
GROUP BY num1, dt
HAVING MAX(ct) != 4
OR MIN(ct) != 4)
OR ((num, dt, ct) IN (SELECT num1, dt, MAX(ct)
FROM table
GROUP BY num1, dt
HAVING MAX(ct) = 4
AND MIN(ct) = 4)
AND rowid NOT IN (SELECT MIN(rowid)
FROM table
GROUP BY num1, dt
HAVING MAX(ct) = 4
AND MIN(ct) = 4))

Related

How to append a count number in duplicate values in a column and update in SQL Server?

Currently my table looks like this; I want to add the count numbers with distinct InstanceId and duplicate values.
Id
InstanceId
Name
1
1
DiscoveryInstance
2
1
DiscoveryInstance
3
2
ETLInstance
4
3
DiscoveryInstance
5
3
DiscoveryInstance
6
2
ETLInstance
7
2
ETLInstance
I want the output to be like this:
Id
InstanceId
Name
1
1
DiscoveryInstance
2
1
DiscoveryInstance_Backup_1
3
2
ETLInstance
4
3
DiscoveryInstance
5
3
DiscoveryInstance_Backup_1
6
2
ETLInstance_Backup_1
7
2
ETLInstance_Backup_2
I don't want to update the first value and update should start with the next duplicate value in the column.
How to update this table to make this output possible in SQL Server query?
EDIT This solution addresses the ORIGINAL question and original output. This is no longer valid because you changed your desired output.
You could use rank() and concat in this manner:
with cte as (select id, name, rank() over (partition by name order by id) as name_rank
from my_table
)
select t.id,
case
when c.name_rank = 1 then t.name
else concat(t.name, '_Backup_', c.name_rank - 1)
end name
from my_table t
join cte c
on t.id = c.id
Output:
id
name
1
DiscoveryInstance
2
DiscoveryInstance_Backup_1
3
ETLInstance
4
DiscoveryInstance_Backup_2
5
DiscoveryInstance_Backup_3
6
ETLInstance_Backup_1
DB-fiddle found here. I see you updated the question after I posted this answer by adding another column, but that does not look important at the moment.
EDIT
This is an updated answer (thanks Guido) that would address your newly updated output:
with cte as (select id, name, rank() over (partition by name, instanceid order by id) as name_rank
from mytable
)
select t.id,
case
when c.name_rank = 1 then t.name
else concat(t.name, '_Backup_', c.name_rank - 1)
end name
from mytable t
join cte c
on t.id = c.id
Another option is using the row_number() like this
This solution uses your new column instanceid to get the correct data
select t.id,
case when rownumber > 1 then t.Name + '_Backup_' + convert(varchar(10), t.rownumber - 1)
else t.Name
end
from ( select t.id,
t.name,
row_number() over (partition by t.Name, t.instanceid order by t.id) as rownumber
from mytable t
) t
order by t.id
See this DBFiddle
output is
id
(No column name)
1
DiscoveryInstance
2
DiscoveryInstance_Backup_1
3
ETLInstance
4
DiscoveryInstance
5
DiscoveryInstance_Backup_1
6
ETLInstance_Backup_1
7
ETLInstance_Backup_2

(SQL) Per ID, starting from the first row, return all successive rows with a value N greater than the prior returned row

I have the following example dataset:
ID
Value
Row index (for reference purposes only, does not need to exist in final output)
a
4
1
a
7
2
a
12
3
a
12
4
a
13
5
b
1
6
b
2
7
b
3
8
b
4
9
b
5
10
I would like to write a SQL script that returns the next row which has a Value of N or more than the previously returned row starting from the first row per ID and ordered ascending by [Value]. An example of the final table for N = 3 should look like the following:
ID
Value
Row index
a
4
1
a
7
2
a
12
3
b
1
6
b
4
9
Can this script be written in a vectorised manner? Or must a loop be utilised? Any advice would be greatly appreciated. Thanks!
SQL tables represent unordered sets. There is no definition of "previous" value, unless you have a column that specifies the ordering. With such a column, you can use lag():
select t.*
from (select t.*,
lag(value) over (partition by id order by <ordering column>) as prev_value
from t
) t
where prev_value is null or prev_value <= value - 3;
EDIT:
I think I misunderstood what you want to do. You seem to want to start with the first row for each id. Then get the next row that is 3 or higher in value. Then hold onto that value and get the next that is 3 or higher than that. And so on.
You can do this in SQL using a recursive CTE:
with ts as (
select distinct t.id, t.value, dense_rank() over (partition by id order by value) as seqnum
from t
),
cte as (
select id, value, value as grp_value, 1 as within_seqnum, seqnum
from ts
where seqnum = 1
union all
select ts.id, ts.value,
(case when ts.value >= cte.grp_value + 3 then ts.value else cte.grp_value end),
(case when ts.value >= cte.grp_value + 3 then 1 else cte.within_seqnum + 1 end),
ts.seqnum
from cte join
ts
on ts.id = cte.id and ts.seqnum = cte.seqnum + 1
)
select *
from cte
where within_seqnum = 1
order by id, value;
Here is a db<>fiddle.

SQL group numbers that are 'close' together using a threshold value

Consider the table:
id value
1 2
2 4
3 6
4 9
5 10
6 12
7 19
8 20
9 22
I want to group them by a threshold value so that I can find values that are 'close' together.
To do this I want another column that groups these numbers together. For this example use 2 as the
threshold. The result should be like this. It does not matter what is used as the group label, just
as long as it makes it easy to query later.
id value group_label
1 2 A
2 4 A
3 6 A
4 9 B
5 10 B
6 12 B
7 19 C
8 20 C
9 22 C
I couldn't get the version using lag() to work but here's a mysql query using variables
select id, value,
(case
when (value - #value) > 2
then #groupLabel := #groupLabel + 1
else #groupLabel
end) groupLabel, #value := value
from data cross join (
select #value := -1, #groupLabel := 0
) t1
order by value
SQLFiddle
Update
Here's a query using lag
select t1.id, t1.value, count(t2.id)
from data t1 left join (
select id, value,
case when
(value - lag(value) over (order by value)) > 2
then 1 else 0
end groupLabel
from data
) t2 on t2.groupLabel = 1
and t2.id <= t1.id
group by t1.id, t1.value
order by t1.value
SQLFiddle

SQL server: Compare rows

I need a query which can do the following operation.
I have a table with 2 columns
ID Values
1 1
1 2
1 3
1 4
2 2
2 5
2 6
if you see for ID 1 I have 1,2,3 and 4 as values and for ID 2 I have 2, 5 and 6.
I want to write a query which return the following
1(-)
4(-)
5(+)
6(+)
mean 1 and 4 are deleted and 5 and 6 are added by comparing the two ids.
Is it possible? Please let me know
Thanks
This will give you 1 & 4:
select a.values
from my_table a
where not exists (
select * from my_table b where b.values = a.values and b.ID = 2)
and a.ID = 1
and this will give you 5 & 6:
select a.values
from my_table a
where not exists (
select * from my_table b where b.values = a.values and b.ID = 1)
and a.ID = 2
Something like:
(
SELECT T.Value FROM dbo.Table T WHERE T.ID = 1
EXCEPT
SELECT T.Value FROM dbo.Table T WHERE T.ID = 2
)
UNION
(
SELECT T.Value FROM dbo.Table T WHERE T.ID = 2
EXCEPT
SELECT T.Value FROM dbo.Table T WHERE T.ID = 1
)
That will get you the list of values that are associated with 1 but not 2, and 2 but not 1. You could easily multiply the values from one of those subqueries by -1 to differentiate them, or run them as two separate queries.

What is the SQL for 'next' and 'previous' in a table?

I have a table of items, each of which has a date associated with it. If I have the date associated with one item, how do I query the database with SQL to get the 'previous' and 'subsequent' items in the table?
It is not possible to simply add (or subtract) a value, as the dates do not have a regular gap between them.
One possible application would be 'previous/next' links in a photo album or blog web application, where the underlying data is in a SQL table.
I think there are two possible cases:
Firstly where each date is unique:
Sample data:
1,3,8,19,67,45
What query (or queries) would give 3 and 19 when supplied 8 as the parameter? (or the rows 3,8,19). Note that there are not always three rows to be returned - at the ends of the sequence one would be missing.
Secondly, if there is a separate unique key to order the elements by, what is the query to return the set 'surrounding' a date? The order expected is by date then key.
Sample data:
(key:date) 1:1,2:3,3:8,4:8,5:19,10:19,11:67,15:45,16:8
What query for '8' returns the set:
2:3,3:8,4:8,16:8,5:19
or what query generates the table:
key date prev-key next-key
1 1 null 2
2 3 1 3
3 8 2 4
4 8 3 16
5 19 16 10
10 19 5 11
11 67 10 15
15 45 11 null
16 8 4 5
The table order is not important - just the next-key and prev-key fields.
Both TheSoftwareJedi and Cade Roux have solutions that work for the data sets I posted last night. For the second question, both seem to fail for this dataset:
(key:date) 1:1,2:3,3:8,4:8,5:19,10:19,11:67,15:45,16:8
The order expected is by date then key, so one expected result might be:
2:3,3:8,4:8,16:8,5:19
and another:
key date prev-key next-key
1 1 null 2
2 3 1 3
3 8 2 4
4 8 3 16
5 19 16 10
10 19 5 11
11 67 10 15
15 45 11 null
16 8 4 5
The table order is not important - just the next-key and prev-key fields.
Select max(element) From Data Where Element < 8
Union
Select min(element) From Data Where Element > 8
But generally it is more usefull to think of sql for set oriented operations rather than iterative operation.
Self-joins.
For the table:
/*
CREATE TABLE [dbo].[stackoverflow_203302](
[val] [int] NOT NULL
) ON [PRIMARY]
*/
With parameter #val
SELECT cur.val, MAX(prv.val) AS prv_val, MIN(nxt.val) AS nxt_val
FROM stackoverflow_203302 AS cur
LEFT JOIN stackoverflow_203302 AS prv
ON cur.val > prv.val
LEFT JOIN stackoverflow_203302 AS nxt
ON cur.val < nxt.val
WHERE cur.val = #val
GROUP BY cur.val
You could make this a stored procedure with output parameters or just join this as a correlated subquery to the data you are pulling.
Without the parameter, for your data the result would be:
val prv_val nxt_val
----------- ----------- -----------
1 NULL 3
3 1 8
8 3 19
19 8 45
45 19 67
67 45 NULL
For the modified example, you use this as a correlated subquery:
/*
CREATE TABLE [dbo].[stackoverflow_203302](
[ky] [int] NOT NULL,
[val] [int] NOT NULL,
CONSTRAINT [PK_stackoverflow_203302] PRIMARY KEY CLUSTERED (
[ky] ASC
)
)
*/
SELECT cur.ky AS cur_ky
,cur.val AS cur_val
,prv.ky AS prv_ky
,prv.val AS prv_val
,nxt.ky AS nxt_ky
,nxt.val as nxt_val
FROM (
SELECT cur.ky, MAX(prv.ky) AS prv_ky, MIN(nxt.ky) AS nxt_ky
FROM stackoverflow_203302 AS cur
LEFT JOIN stackoverflow_203302 AS prv
ON cur.ky > prv.ky
LEFT JOIN stackoverflow_203302 AS nxt
ON cur.ky < nxt.ky
GROUP BY cur.ky
) AS ordering
INNER JOIN stackoverflow_203302 as cur
ON cur.ky = ordering.ky
LEFT JOIN stackoverflow_203302 as prv
ON prv.ky = ordering.prv_ky
LEFT JOIN stackoverflow_203302 as nxt
ON nxt.ky = ordering.nxt_ky
With the output as expected:
cur_ky cur_val prv_ky prv_val nxt_ky nxt_val
----------- ----------- ----------- ----------- ----------- -----------
1 1 NULL NULL 2 3
2 3 1 1 3 8
3 8 2 3 4 19
4 19 3 8 5 67
5 67 4 19 6 45
6 45 5 67 NULL NULL
In SQL Server, I prefer to make the subquery a Common table Expression. This makes the code seem more linear, less nested and easier to follow if there are a lot of nestings (also, less repetition is required on some re-joins).
Firstly, this should work (the ORDER BY is important):
select min(a)
from theTable
where a > 8
select max(a)
from theTable
where a < 8
For the second question that I begged you to ask...:
select *
from theTable
where date = 8
union all
select *
from theTable
where key = (select min(key)
from theTable
where key > (select max(key)
from theTable
where date = 8)
)
union all
select *
from theTable
where key = (select max(key)
from theTable
where key < (select min(key)
from theTable
where date = 8)
)
order by key
SELECT 'next' AS direction, MIN(date_field) AS date_key
FROM table_name
WHERE date_field > current_date
GROUP BY 1 -- necessity for group by varies from DBMS to DBMS in this context
UNION
SELECT 'prev' AS direction, MAX(date_field) AS date_key
FROM table_name
WHERE date_field < current_date
GROUP BY 1
ORDER BY 1 DESC;
Produces:
direction date_key
--------- --------
prev 3
next 19
My own attempt at the set solution, based on TheSoftwareJedi.
First question:
select date from test where date = 8
union all
select max(date) from test where date < 8
union all
select min(date) from test where date > 8
order by date;
Second question:
While debugging this, I used the data set:
(key:date) 1:1,2:3,3:8,4:8,5:19,10:19,11:67,15:45,16:8,17:3,18:1
to give this result:
select * from test2 where date = 8
union all
select * from (select * from test2
where date = (select max(date) from test2
where date < 8))
where key = (select max(key) from test2
where date = (select max(date) from test2
where date < 8))
union all
select * from (select * from test2
where date = (select min(date) from test2
where date > 8))
where key = (select min(key) from test2
where date = (select min(date) from test2
where date > 8))
order by date,key;
In both cases the final order by clause is strictly speaking optional.
If your RDBMS supports LAG and LEAD, this is straightforward (Oracle, PostgreSQL, SQL Server 2012)
These allow to choose the row either side of any given row in a single query
Try this...
SELECT TOP 3 * FROM YourTable
WHERE Col >= (SELECT MAX(Col) FROM YourTable b WHERE Col < #Parameter)
ORDER BY Col