SQL delete on group by conditions - sql

A cursor is used, but it's slow and appears to be a big bottleneck in a SQL job. Basically, this is a cleanup effort to remove all but the top X accessories (ordered by sales rank) from a particular source that's previously grouped by a product id and account visibility.
The command is basically built in the each iteration of the cursor loop and exec'ed manually.
The vis column refers to multiple tenants that sort of acts like a bitmask e.g. two tenants could have the same product.
declare #prodid int
declare #cnt int
declare #vis bigint
declare #cmd varchar(600)
declare #clause varchar(600)
-- find records with more than X excess accessories
declare cur cursor for
select pa.prodid, 'cnt' = count(*), vis from [accessories] pa
group by prodid, vis
having count(*) > X -- e.g. 5
Sample output could look like
prodid cnt vis
123 6 128
234 8 260
345 10 512
In the case where X=5, the last 1 salesrank item for 123 would be removed, the last 3 for 234 and the last 5 for 345. Can this be done using a DELETE statement while including the groupings in some nested select?
open cur
fetch next from cur into #prodid, #cnt, #vis
while ##fetch_status = 0
begin
-- a clause that ends up looking like this:
-- 12345 and vis = 128 -- OR -- 23456 and vis is null
set #clause = convert(varchar(14), #prodid) + ' and vis ' + case
when #vis is null then ' is null '
else ' = ' + cast(#vis as varchar) end
-- delete all but the top X from source=2 and that match prodid and vis
set #cmd = 'delete from [accessories]
where source = 2 and prodid=' + #clause +
' and access_prodid in (select top ' + convert(varchar(5), #cnt - X) +
' access_prodid from [accessories] where prodid = '
+ #clause + ' and source = 2 order by salesrank)'
exec(#cmd)
fetch next from cur into #prodid, #cnt, #vis
end
close cur
deallocate cur

Try this:
WITH DupData AS
(
SELECT *,
ROW_NUMBER()
OVER(PARTITION BY pa.prodid, pa.vis ORDER BY salesrank) Position
FROM [accessories] pa
WHERE pa.source = 2
)
DELETE
FROM DupData
WHERE Position > 5

I would do this by using windows functions to identify the rows to be deleted:
with t as (select pa.*,
row_number() over (partition by prodid, vis order b salesrank) as sr
from [accessories] pa
)
delete from pa
from t
where pa.prodid = t.prodid and pa.vis = t.vis and pa.salesrank = t.salesrank
If there is a unique id in the pa table, then you can use that instead of the more complicated where statement. This assumes that salesrank is unique within each prodid/vis group.

Related

How to get TWO Previous record in MySQL using CURSOR

On my table, I have records that contain ','.
Id Name
1 Here is the result
2 of your examination.
3 ,
4 New Opening for the position of
5 PT Teacher, Science Lab.
6 ,
So in cursor If I found ',' then I want to merge the 2 rows value into the first one.
DECLARE #ID int
DECLARE #Name nvarchar(500)
DECLARE MergeCursor CURSOR FOR
select ID,NAME from TEST_TABLE
OPEN MergeCursor
FETCH NEXT FROM NarrationCursor into #ID,#NAME
WHILE (##FETCH_STATUS=0)
BEGIN
if(#Name = ',')
select * from TEST_TABLE where ID = (select max(ID) from TEST_TABLE where ID < #ID)
FETCH NEXT FROM NarrationCursor into #ID,#NAME
END
CLOSE MergeCursor
DEALLOCATE MergeCursor
IN cursor how can I get the PREVIOUS TWO-ROW And UPDATE the value in 1st row and DELETE THE 2nd and THIRD ROW. AS WELL AS UPDATE THE ID
In the End, I want to output
Id Name
1 Here is the result of your examination.
2 New Opening for the position of PT Teacher, Science Lab.
WITH
grouped AS
(
SELECT
SUM(CASE WHEN name=',' THEN 1 ELSE 0 END)
OVER (ORDER BY id)
AS group_id,
id,
name
FROM
TEST_TABLE
)
SELECT
group_id + 1 AS id,
STRING_AGG(name, ' ') WITHIN GROUP (ORDER BY id) AS name
FROM
grouped
WHERE
name <> ','
GROUP BY
group_id
ORDER BY
group_id

SQL: extract common string from by group

I have a table with a location and device name. I want to keep the part of the device name that matches within the group.
location_code | device_name | location
1 Building_1_in Building_1
1 Building_1_out Building_1
1 Building_1_gate Building_1
2 Drive 3 gate2 Drive 3
2 Drive 3 gate1 Drive 3
2 Drive 3 keypad Drive 3
I have location code and device name, but I'm trying to create the location column. I can't use a SUBSTRING function since the target strings are of different length, and I can't use a CHARINDEX function since there is no consistent delimiter. Also, there are too many location_code to write a CASE WHEN function.
Does anyone have any ideas?
If you group these records by location_code then
You can get required result by
Create a Function
Create FUNCTION dbo.getLocation
(
#location_code int
)
RETURNS varchar(max)
AS
BEGIN
declare #result varchar(max)
declare #device_name1 varchar(max)
,#device_name2 varchar(max)
,#iterator int
set #result = ''
select top 1 #device_name1 = device_name from TableName where location_code = #location_code order by device_name
select top 1 #device_name2 = device_name from TableName where location_code = #location_code order by device_name desc
set #iterator = 1
while(#iterator <= len(#device_name1) and #iterator <= len(#device_name2))
begin
if(SUBSTRING(#device_name1, #iterator, 1) = SUBSTRING(#device_name2, #iterator, 1))
begin
set #result = #result + SUBSTRING(#device_name1, #iterator, 1)
end
else
break
set #iterator = #iterator + 1
end
return #result
END
GO
and use it like
select t.location_code, t.device_name, l.location
from TableName t
inner join (select location_code, dbo.getLocation(location_code) as location from TableName group by location_code) l on l.location_code = t.location_code
The general idea:
You can sort the table ordering by location_code and device name. Then you can extract the common begining part of the first and the last for each location_code.

how to get results of function with datatype nvarchar

I have a database table like this
Id Code Amount Formula
-------------------------------------
1 A01 20.00
2 A08 0.00 dbo.ufn_Test(40)
3 A03 0.00 dbo.ufn_Test(60)
My Formula column is a string with name as a function in my database, how can I return the result into the Amount column?
My table has about 100000 rows so when I used while() it takes a lot of time.
I'm using SQL Server 2012
I've used dynamic SQL like this:
DECLARE #_j INT = 1
WHILE (#_j<=(SELECT MAX(Id) FROM #Ct_Lv))
BEGIN
SET #_CtLv = (SELECT Formula FROM #Ct_Lv WHERE Id = #_j)
DECLARE #sql NVARCHAR(MAX)
DECLARE #result NUMERIC(18, 2) = 0
SET #sql = N'set #result = N''''SELECT''' + #_CtLv
EXEC sp_executesql #sql, N'#result float output', #result out
UPDATE #Ct_Lv
SET Amount = #result
WHERE Id = #_j
SET #_j = #_j + 1
END
but my max #_j = 100000, I've run my code for 3 hours and it's still running
one thing, i would like know here is, does id attribute is identity column ?
2nd most important part is, you are declaring variables #sql and #result for each row and you are taking max at every row iterate, which might decrease the performance. I am not sure, how much faster the solution i have been given here, but you can try it once.
Set Nocount On;
Declare #_count Int
,#_j Int
,#_cnt Int
,#_dynamicSql Varchar(Max)
,#_formula Varchar(Max)
,#_row25Cnt Int
Select #_count = Count(1)
,#_j = 0
,#_cnt = 0
,#_dynamicSql = ''
,#_formula = ''
,#_row25Cnt = 1
From #Ct_Lv As ct With (Nolock)
While (#_cnt < #_count)
Begin
Select Top 1
#_j = ct.Id
,#_formula = ct.Formula
From #Ct_Lv As ct With (Nolock)
Where ct.Id > #_j
Order By ct.Id Asc
Select #_dynamicSql = 'Update ct Set ct.Amount = f.result From #Ct_Lv As ct Join ( Select ' + Cast(#_j As Varchar(20)) + ' As Id, [fuctionResultAttribute] As result From ' + #_formula + ' ) As f On ct.Id = f.Id; '
If (#_row25Cnt = 25)
Begin
Exec (#_dynamicSql)
Select #_dynamicSql = ''
,#_row25Cnt = 0
End
Else If ((#_cnt + 1) = #_count)
Begin
Exec (#_dynamicSql)
Select #_dynamicSql = ''
,#_row25Cnt = 0
End
Select #_cnt = #_cnt + 1
,#_row25Cnt = #_row25Cnt + 1
End
Here, what i have done so far is, I am looping Id by Id and generating dynamic sql for each 25 rows, once count is reach to 25, that dynamic sql will be executed which will update your amount. and again start generating dynamic sql for next 25 rows, and when count is about to end and there would be no 25 rows as end then dynamic sql will be executed when loop about to end in 'else if' condition.
above my solution will work only in that case when there would be only one formula in formula column for each row.
I just suggest one thing if Formula field calling the same function each time then better to store only parameter that you want to pass to the function, then you can easily process over huge data.
Else looping over huge data is not preferable way to perform any operation. So it's advisable to use some other trick over there in table structure and storing data.

SQL column sorting by value

Using T-SQL I'm creating a temp table grid. I need to reorder the columns based on the total of the column starting with the largest.
For example
---- DO MO BC NI SC
Total 22 44 53 57 24
Prod A 0 24 0 24 0
Prod B 0 0 0 20 7
Prod C 0 20 0 13 13
Would become:
---- NI BC MO SC DO
Total 57 53 44 24 22
Prod A 24 0 24 0 0
Prod B 20 0 0 7 0
Prod C 13 0 20 13 0
First of, ---- if a terrible column name but I could think of no better for this so I kept it.
You can build the query dynamically where you sort the columns when you build the query string.
declare #SQL nvarchar(max)
set #SQL = '
select [----]'+
(
select ', '+T2.N.value('local-name(.)', 'nvarchar(128)')
from (
select DO, MO, BC, NI, SC
from T
where [----] = 'Total'
for xml path(''), type
) as T1(X)
cross apply T1.X.nodes('*') as T2(N)
order by T2.N.value('.', 'int') desc
for xml path('')
)+'
from T'
exec (#SQL)
SQL Fiddle
Update
If you think the XML version of building the dynamic query is a bit complicated and unintuitive you can use this instead, totally void of XML stuff.
declare #SQL nvarchar(max)
declare #Col nvarchar(128)
declare C cursor local fast_forward for
select U.Col
from (
select DO, MO, BC, NI, SC
from T
where [----] = 'Total'
) as T
unpivot(Val for Col in (DO, MO, BC, NI, SC)) as U
order by U.Val desc
set #SQL = 'select [----]'
open C
fetch next from C into #Col
while ##FETCH_STATUS = 0
begin
set #SQL = #SQL + ',' + #Col
fetch next from C into #Col
end
close C
deallocate C
set #SQL = #SQL + ' from T'
exec (#SQL)
SQL Fiddle
If is is a temp table you could:
create it
calculate the column order
create another table with the correct order
insert into that table (in the correct order)
But I still have to ask why?
You do know you can order the columns in a select statement?
Do do know how to sum the columns?
select sum(col1), sum(col2)
from #temp
Unfortunately you cannot reorder columns in SQL.

FOR UPDATE cannot be specified on a READ ONLY cursor

I'm using a cursor to update a single field in a table, and I'm attempting to declare the cursor using an ORDER BY in the text of the select.
I've got the following example data:
testTable:
RecordGuid RecordID DupeParentID
---------- -------- ------------
[guid] A Y
[guid] A N
[guid] A N
[guid] A N
[guid] B Y
[guid] B N
[guid] B N
[guid] C Y
[guid] C N
[guid] C N
And script:
DECLARE #allcounter INT
SET #allcounter = 1
SELECT RecordID, count(*) as [NumberDupes]
INTO #RecordGroupCounts
FROM testTable
GROUP BY RecordID
DECLARE #temp VARCHAR(500)
DECLARE #current VARCHAR(500)
DECLARE c1 CURSOR
FOR
SELECT RecordID FROM testTable WHERE RecordID IN (SELECT RecordID FROM testTable WHERE DupeParentID = 'Y')
ORDER BY RecordID
FOR UPDATE OF RecordID
OPEN c1
FETCH NEXT FROM c1 INTO #current
FETCH NEXT FROM c1 INTO #current
WHILE ##fetch_status = 0
BEGIN
UPDATE testTable
SET RecordID = RecordID + '-' + cast(#allcounter AS VARCHAR)
WHERE CURRENT OF c1
IF (#allcounter + 1) = (SELECT [NumberDupes] FROM #RecordGroupCounts WHERE RecordID = #current)
BEGIN
FETCH NEXT FROM c1 INTO #current
SET #allcounter = 0
END
SET #allcounter = #allcounter + 1
FETCH NEXT FROM c1 INTO #current
END
CLOSE c1
DEALLOCATE c1
The desired output of all of this is:
RecordGuid RecordID DupeParentID
---------- -------- ------------
[guid] A Y
[guid] A-1 N
[guid] A-2 N
[guid] A-3 N
[guid] B Y
[guid] B-1 N
[guid] B-2 N
[guid] C Y
[guid] C-1 N
[guid] C-2 N
I'm working with SQL Server 2000 so I don't have ROW_NUMBER() available - I know the common way to do this is with loops, but I am by no means a DBA, and this currently works if I remove my ORDER BY RecordID in the cursor declaration.
With as small as my current test table is this seems to be working fine, but the reason I'm attempting to order this is that I'm fairly sure it'll break if the RecordIDs aren't in order (by RecordID ASC, DupeParentID DESC) and I intend to use this on a much larger set of records semi-regularly. Is there a way to define the order for a cursor that updates? Is the cursor ordered automatically somehow? If not, is there a simpler (or faster) way to write this for SQL Server 2000?
select recordid, max(recordguid)
from testable
where DupeParentID = 'N'
group by recordID
The above statement should return 1 row per recordID with the max(guID) for that recordID. heh, spell check keeps changing guid to guide. Now we can increment all of these with the 1.
update testtable
set recordID = recordID + '-1'
where recordguid in (select recordguid from ( select recordid, max(recordguid) recordguid
from testable
where DupeParentID = 'N'
group by recordID) a)
Get the logic here? What we're doing is taking the first duplicate ID using the max recordguid as an identifier for the 'first'...lil arbitrary, we could use min as well as long as it returns just one guID for each recordID. If you had some other logic as to which record was to be called the -1 vs the -2, you can include it here.
This will create all the recordID-1 (A-1,B-1) and leave the rest alone. Now you should be able to loop this and increment the -1 as needed...or you could just repeat run it manually incrementing the -1 yourself if this is a one time fix...your call there.
Let me know how it goes...the logic should work and will be quite quicker than going through each recordID at a time.
You can try the following. Although I do not expect performance to be to great, so I would not recomend this solution if you need to run the query on production on regular basis.
declare #counter int ,#continue tinyint
set #counter =1
set #continue = 1
while #continue = 1
Begin
update testtable
set testtable.recordid = testtable.recordid + ' - ' + CAST (#counter as nchar(6))
from
testtable
inner join
(
select MAX(cast(t1.guid as char(36))) as maxguid from testtable t1
inner join testtable t2 on t1.recordid = t2.recordid
where
t2.dupeparentid = 'y' and t1.dupeparentid = 'n'
group by t1.recordid
) t4
on testtable.guid = cast (t4.maxguid as uniqueidentifier)
if ##ROWCOUNT = 0
set #continue = 0
set #counter = #counter + 1
end