T-SQL Fixing Row Order / Sequence - sql

Overview:
I have a page in my application that allows a user to set the priority of a list of records. These records are essentially tasks or goals that the team will need to complete. The order they are given is determined by the user dragging and dropping them into place. Once the user is done and saves the changes, it loops over the data and updates the records in the database with their respective order.
Issue:
The problem I am now running into is on a separate page at which the user can delete a record that may have a priority assigned to it. Since the saving and calculation of a priority is done on another page, deleting a record causes gaps in the sequence.
Example:
Projects that are prioritized:
A (1)
B (2)
C (3)
D (4)
E (5)
F (6)
G (7)
Project gets deleted from another page / function:
A (1)
B (2)
C (3)
D (4)
F (6)
G (7)
Question:
I need to make some type of function I can run after a record is deleted to "repair/re sync" this number sequence. Essentially the single column priority needs to be updated and in the example, F would need to be updated to 5 and G updated to 6 in order to fix the sequence gap.
What I have tried:
Mostly researching a way to solve for this to be honest. I was thinking of putting all the records into a temp table with an Auto Increment number and using that to update the priority level but I feel like there is probably a more simple solution and wanted some opinions.

After you delete you simple need to update the rows that are greater than the priority just deleted. So if your column name is Priority and you delete the row where Priority = 5 you simply do something like this. Notice you don't need to use loops here.
Update YourTable
set Priority = Priority - 1
where Priority > 5

You could use triggers, which would trigger automatically when delete is done. You could always take values from deleted.
More info
SQL Server ON DELETE Trigger

As others have suggested, if you're only deleting one at a time, just update the numbers immediately after deletion. If you want to renumber the whole table at once (perhaps there have been multiple deletions), you can use ROW_NUMBER() and a CTE:
DECLARE #Project TABLE (
Name nvarchar(100)
, Priority int
);
-- Projects that are prioritized
INSERT #Project ( Name, Priority ) VALUES
('A', 1)
, ('B', 2)
, ('C', 3)
, ('D', 4)
, ('E', 5)
, ('F', 6)
, ('G', 7)
;
SELECT * FROM #Project;
-- Projects get deleted
DELETE #Project WHERE Name = 'E';
DELETE #Project WHERE Name = 'C';
SELECT * FROM #Project;
-- Renumber
WITH P AS
(
SELECT Name, Priority, ROW_NUMBER() OVER ( ORDER BY Priority ) AS NewPriority
FROM #Project
)
UPDATE P SET Priority = NewPriority
;
-- Ta da!
SELECT * FROM #Project;

Why not just set the Priority in the procedure that extracts the records from the database? that way you don't need to change the priority cause it's always computed on the fly when you read the rows. the only issue is that when you save the new sequence after a user changes the priority sequence, you delete all the records and add them back in in the correct sequence, with an auto-incrementing primary key (pk).
I don't know your sequence, but this example should give you the idea.
Select Record,
(Select count(*) From table
Where pk <= t.pk) Priority
From Table t

Related

Update table by Preference and reorder table accordingly

I have this app when you drag and drop rows in a list it reorders and SETS that row by preference number, 1-5 for example, 1 being most priority. So if I have 5 records in the list, I can then drag each row and when dropped it will reorder the list by preference. I can move row 5 to row 1, or row 2 to row 3, etc... This will update the preference number in the SQL table according where you drop.
This app is in real-time. When new rows are added to the table automatically, they have an initial preference of "0". This query will add the next number preference to the record, so if I have rows with preferences of 1-5, a new record comes in, then it's assigned a preference of 6:
with CTE as (
select Id, Preference, cp.maxpref, row_number() over(order by Id) rn
from [RadioQDB].[dbo].[Rad5]
cross apply (
select max(preference) maxpref
from [RadioQDB].[dbo].[Rad5] p
) cp
where preference = 0
)
update cte
set preference = maxpref + rn
where preference = 0
The issue I am having now is if a record is removed from the list during an update (not user drag and drop), let's say you have 1,2,3,4,5 records in the list. If during the table update, a record is removed automatically, let's say #2, then you end up with preferences 1,3,4,5. How can I move up everything and reorder the table accordingly by preference?
1 stays the same, 3 moves to 2, 4 moves to 3 and so forth.
Thank you.
To insert a new record with Preference=Max(Preference)+1 use the following query:
insert into Rad5 values(10,(select max(Preference)+1 from Rad5));
-- inserts a new record with id=10
To reorder the records according to the Preference after deleting a record try the following:
with cte as (
select id, Preference, row_number() over (order by Preference) as rn
from Rad5)
update cte set Preference=rn;
You can use Trigger on delete from your table to call the update query automatically whenever a record is deleted, if you want to do so use the following:
create trigger Rad5_Delete on Rad5
for delete
as
with cte as (
select id, Preference, row_number() over (order by Preference) as rn
from Rad5)
update cte set Preference=rn;
See a demo from db<>fiddle.

SQL: Insert a new unique row based off previous row

I'm working with Deltek Vision ERP software trying to create a custom solution to provide a list of deliverables for projects. I've created a custom grid in the Project area of Vision that has a table in SQL structured like this:
SELECT TOP (1000) [WBS1]
,[WBS2]
,[WBS3]
,[Seq]
,[CreateUser]
,[CreateDate]
,[ModUser]
,[ModDate]
,[CustDeliverables]
,[CustDueDate]
,[CustCompletionDate]
FROM [Vision_Prod].[dbo].[Projects_Deliverables]
The table has three user entered fields, which are the last three columns listed above.
What I'm trying to accomplish is have deliverables set at WBS2 level also roll up to WBS1, so basically what needs to happen is that any time a record is created with a value in WBS2 the record is duplicated but the duplicate has no value in WBS2.
I've setup a workflow in Vision so that when someone enters a deliverable into the grid on a phase it kicks off a stored procedure to accomplish this. The problem is the Seq field. This is a unique identifier the system is assigning when a record is created. When my stored procedure fires I'm getting an error that the sequence has to be included in the record.
This is the stored procedure I'm using:
INSERT INTO [Vision_Prod].[dbo].[Projects_Deliverables] (WBS1, WBS2, WBS3, Seq, CreateUser, ModUser, ModDate, CreateDate, CustDueDate, CustCompletionDate)
SELECT WBS1, '', '', (NEXT VALUE FOR Seq), CreateUser, ModUser, ModDate, CreateDate, CustDueDate, CustCompletionDate
FROM Projects_Deliverables
WHERE Projects_Deliverables.WBS2 IS NOT Null and Projects_Deliverables.WBS1 = #WBS1
If anyone can help me figure out how to get the system to assign a new sequence when a record is created in this way that would be most appreciated.
You could create a dummy table using VALUES to create the an extra row and then fiter out when not needed. Something (but not tested) like this:
SELECT TOP (1000)
[WBS1]
,CASE c WHEN 2 THEN NULL ELSE [WBS2] END AS [WBS2]
,[WBS3]
,[Seq]
,[CreateUser]
,[CreateDate]
,[ModUser]
,[ModDate]
,[CustDeliverables]
,[CustDueDate]
,[CustCompletionDate]
FROM [Vision_Prod].[dbo].[Projects_Deliverables]
CROSS JOIN (VALUES (1), (2)) t(c)
WHERE (WBS2 IS NULL AND c < 2)
OR WBS2 IS NOT NULL
This was the solution:
REPLACE(CAST(NEWID() AS VARCHAR(36)), '-', '')

Oracle sql: insert into by copying from two different tables

Although it's much more complex than I'm about to explain, I'll try to only stick to the relevant bits of what I want to accomplish. Our data model is quite complex, and the terms are also a bit confusing. We basically have a Request, and this request can have an active Request_Status (which has an Enum_Value to indicate it's current status), as well as previous Request_Statuses that aren't relevant anymore (to preserve history). A Person is linked to this Request, but the Values that are entered are linked to the current Request_Status.
So here are those tables, and the relevant columns:
Persons:
person_id
unique_code
Some other values
Requests:
request_id
fk_person_id
year
Some other values
Enum_Values:
enum_value_id
value
Some other values
Request_Statuses:
request_status_id
fk_request_id
fk_enum_value_id
created_date
Some other values
Values:
value_id
fk_request_status_id
Some other values
I have: A list of Person.unique_codes.
I want to achieve two things:
For each Person.unique_code I want to get the Request of the year 2017, and then create a new Request_Status with fk_enum_value_id set to 4, linked to this existing Request.
Create copies of the Values that were linked to the previously active Request_Status, and set their fk_request_status_id to the currently active Request_Status (the records I've created in step 1).
I've been able to do step 1 myself with a monstrous query (but it works..)
Here is the monstrous query for step 1:
Some things to note:
- There will only be a single Request of a given year.
- There can be more than one Request_Statuses for a given Request, so finding the active is the one with the highest created_date.
- p.unique_code IN ('12345','67890') is privatized and reduced code. In reality I have about 500 person.unique_codes.
- SELECT rs1.fk_request_id, 4 /*, some other irrelevant values */ FROM Request_Statuses rs1 LEFT JOIN Request_Statuses rs2 ON (rs1.fk_request_id = rs2.fk_request_id AND rs1.created_date < rs2.created_date) WHERE rs2.created_date IS NULL is copied from this SO answer for the question "Retrieving the last record in each group". I've used the windowing function at the top before, but it wasn't really suitable for sub-queries in combination with Oracle SQL, so I've used the (probably slightly slower) original method that was posted in 2009, which does work as intended.
INSERT_INTO Request_Statuses (fk_request_id, fk_enum_value_id /*, some other irrelevant values */)
(SELECT rs1.fk_request_id, 4 /*, some other irrelevant values */ FROM Request_Statuses rs1
LEFT JOIN Request_Statuses rs2 ON (rs1.fk_request_id = rs2.fk_request_id AND rs1.created_date < rs2.created_date)
WHERE rs2.created_date IS NULL AND rs1.fk_request_id IN (SELECT r.request_id FROM Requests r
WHERE r.fk_person_id IN (SELECT p.person_id FROM Persons p
WHERE p.unique_code IN ('12345','67890')) AND r.year = 2017));
And I'm currently working on step 2.
I currently have this:
INSERT INTO Values (fk_request_status_id /* some other irrelevant values */)
(SELECT /*TODO: Get request_status_id created in step 1*/, /* some other irrelevant values */
FROM Values v1 WHERE v1.fk_request_status_id IN (SELECT rs.status_id FROM Request_Statuses rs
WHERE rs.fk_request_id IN (SELECT r.request_id FROM Requests r
WHERE r.fk_person_id IN (SELECT p.person_id FROM Persons p
WHERE p.bsn IN ('12345','67890')) AND r.year = 2017) AND (SELECT COUNT(*) FROM Values v2
WHERE v2.fk_request_status_id = rs.status_id) > 0));
All I need is to get the request_status_id of the Request_Statuses I've created in step 1, based on the same person.unique_code, and insert it at the TODO..
I've also been thinking about using a default value for now, and then update just the fk_request_status_id with a third (monstrous) query. Unfortunately, the fk_request_status_id in combination with a second column in the Values table form an unique constraint, and fk_request_status_id cannot be empty, so I can't just insert any value here to update later.. Maybe I should remove the constraints temporarily, and add them later again after the query..
PS: Performance isn't that important. I've only got around 500-750 person.unique_codes for which I have to create one new Request_Status each (and zero to about 50 Values that are potentially linked to the previous active Request_Status). It should work in under 4 hours, though. ;)

SQL Server custom record sort in table, allowing to delete records

SQL Server table with custom sort has columns: ID (PK, auto-increment), OrderNumber, Col1, Col2..
By default an insert trigger copies value from ID to OrderNumber as suggested here.
Using some visual interface, user can sort records by incrementing or decrementing OrderNumber values.
However, how to deal with records being deleted in the meantime?
Example:
Say you add records with PK ID: 1,2,3,4,5 - OrderNumber receives same values. Then you delete records with ID=4,ID=5. Next record will have ID=6 and OrderNumber will receive the same value. Having a span of 2 missing OrderNumbers would force user to decrement record with ID=6 like 3 times to change it's order (i.e. 3x button pressed).
Alternatively, one could insert select count(*) from table into OrderNumber, but it would allow to have several similar values in table, when some old rows are deleted.
If one doesn't delete records, but only "deactivate" them, they're still included in sort order, just invisible for user. At the moment, solution in Java is needed, but I think the issue is language-independent.
Is there a better approach at this?
I would simply modify the script that switches the OrderNumber values so it does it correctly without relying on their being without gaps.
I don't know what arguments your script accepts and how it uses them, but the one that I've eventually come up with accept the ID of the item to move and the number of positions to move by (a negative value would mean "toward the lower OrderNumber values", and a positive one would imply the opposite direction).
The idea is as follows:
Look up the specified item's OrderNumber.
Rank all the items starting from OrderNumber in the direction determined by the second argument. The specified item thus receives the ranking of 1.
Pick the items with rankings from 1 to the one that is the absolute value of the second argument plus one. (I.e. the last item is the one where the specified item is being moved to.)
Join the resulting set with itself so that every row is joined with the next one and the last row is joined with the first one and thus use one set of rows to update the other.
This is the query that implements the above, with comments explaining some tricky parts:
Edited: fixed an issue with incorrect reordering
/* these are the arguments of the query */
DECLARE #ID int, #JumpBy int;
SET #ID = ...
SET #JumpBy = ...
DECLARE #OrderNumber int;
/* Step #1: Get OrderNumber of the specified item */
SELECT #OrderNumber = OrderNumber FROM atable WHERE ID = #ID;
WITH ranked AS (
/* Step #2: rank rows including the specified item and those that are sorted
either before or after it (depending on the value of #JumpBy */
SELECT
*,
rnk = ROW_NUMBER() OVER (
ORDER BY OrderNumber * SIGN(#JumpBy)
/* this little "* SIGN(#JumpBy)" trick ensures that the
top-ranked item will always be the one specified by #ID:
* if we are selecting rows where OrderNumber >= #OrderNumber,
the order will be by OrderNumber and #OrderNumber will be
the smallest item (thus #1);
* if we are selecting rows where OrderNumber <= #OrderNumber,
the order becomes by -OrderNumber and #OrderNumber again
becomes the top ranked item, because its negative counterpart,
-#OrderNumber, will again be the smallest one
*/
)
FROM atable
WHERE OrderNumber >= #OrderNumber AND #JumpBy > 0
OR OrderNumber <= #OrderNumber AND #JumpBy < 0
),
affected AS (
/* Step #3: select only rows that need be affected */
SELECT *
FROM ranked
WHERE rnk BETWEEN 1 AND ABS(#JumpBy) + 1
)
/* Step #4: self-join and update */
UPDATE old
SET OrderNumber = new.OrderNumber
FROM affected old
INNER JOIN affected new ON old.rnk = new.rnk % (ABS(#JumpBy) + 1) + 1
/* if old.rnk = 1, the corresponding new.rnk is N,
because 1 = N MOD N + 1 (N is ABS(#JumpBy)+1),
for old.rnk = 2 the matching new.rnk is 1: 2 = 1 MOD N + 1,
for 3, it's 2 etc.
this condition could alternatively be written like this:
new.rnk = (old.rnk + ABS(#JumpBy) - 1) % (ABS(#JumpBy) + 1) + 1
*/
Note: this assumes SQL Server 2005 or later version.
One known issue with this solution is that it will not "move" rows correctly if the specified ID cannot be moved exactly by the specified number of positions (for instance, if you want to move the topmost row up by any number of positions, or the second row by two or more positions etc.).
Ok - if I'm not mistaken, you want to defragment your OrderNumber.
What if you use ROW_NUMBER() for this ?
Example:
;WITH calc_cte AS (
SELECT
ID
, OrderNumber
, RowNo = ROW_NUMBER() OVER (ORDER BY ID)
FROM
dbo.Order
)
UPDATE
c
SET
OrderNumber = c.RowNo
FROM
calc_cte c
WHERE EXISTS (SELECT * FROM inserted i WHERE c.ID = i.ID)
Didn't want to reply my own question, but I believe I have found a solution.
Insert query:
INSERT INTO table (OrderNumber, col1, col2)
VALUES ((select count(*)+1 from table),val1,val2)
Delete trigger:
CREATE TRIGGER Cleanup_After_Delete ON table
AFTER DELETE AS
BEGIN
WITH rowtable AS (SELECT [ID], OrderNumber, rownum = ROW_NUMBER()
OVER (ORDER BY OrderNumber ASC) FROM table)
UPDATE rt SET OrderNumber = rt.rownum FROM rowtable rt
WHERE OrderNumber >= (SELECT OrderNumber FROM deleted)
END
The trigger fires up after every delete and corrects all OrderNumbers above the deleted one (no gaps). This means that I can simply change the order of 2 records by switching their OrderNumbers.
This is a working solution for my problem, however this one is also very good one, perhaps more useful for others.

shuffle values in an integer column so they are always unique and sequential

I have a table I'd like to sort with a "priority" column. This column needs to be reordered when the priority of a record is changed or records are removed. Think of it as an array. The values will be modified in a UI so I want them to remain whole numbers and represent the true position within the larger recordset. The priority column won't have NULLs.
id priority
1 2
2 1
3 4
4 3
Now say I change the priority of id 4 to 2 or I insert or delete a row how do I get all priorities to reshuffle so there are no gaps or duplicates and the highest possible priority is always the number of rows?
The table has a "date_modified" field which is accurate to the second and updated on insert/update so if needed it is possible to know which record was modified last (to break a tie when 2 records have the same priority)
Assuming you have 8.4 you can use window functions.
UPDATE test_priority
SET priority = sub.new_priority
FROM (
SELECT user_id, id, priority, rank() OVER (ORDER BY priority, date_modified) new_priority
FROM test_priority
WHERE user_id = $1
) sub
WHERE test_priority.user_id = sub.user_id
AND test_priority.id = sub.id
AND test_priority.priority <> sub.new_priority
Deleting a row:
UPDATE tbl SET priority = priority - 1
WHERE priority > the_priority_of_what_you_deleted
Inserting a row (do this before the insert):
UPDATE tbl SET priority = priority + 1
WHERE priority >= the_priority_about_to_be_inserted
You can put this kind of logic into INSERT and/or DELETE triggers, if you want.