Incremental count column based on another column contents - sql

I need to populate a column with the running count based on another column contents. The table is like this:
count seq_num
1 123-456-789
1 123-456-780
1 123-456-990
2 123-456-789
2 123-456-990
So, as the seq_num column changes, the counter resets to '1' and as the column repeats, the counter increments by 1.
This is using SQL2000, and the seq_num field is varchar.
Any ideas?

If you're inserting, you can use a subquery:
insert into
table (count, seq_num)
values
((select count(*)+1 from table where seq_num = #seq)
,#seq)
Otherwise, you'll need to have a date on there or some way of telling it how to determine what was first:
update table
set count =
(select count(*)+1 from table t2
where t2.seq_num = table.seq_num
and t2.insertdate < table.insertdate)

if you need to be able to continue updating this in the future, you might try this. It's a few steps but would fix it AND set it up for future use. (probably need to check my syntax - I mess with ORacle more now, so I may have mixed up some things - but the logic should work.)
first, create a table to contain the current counter level per sequence:
Create newTable (counter int, sequence varchar)
then, fill it with data like this:
insert into newTable
(select distinct 0 as Counter, sequence
from table)
This will put each sequence number in the table one time and the counter for each will be set at 0.
Then, create an update proc with TWO update statements and a bit of extra logic:
Create procedere counterUpdater(#sequence varchar) as
Declare l_counter as int;
select l_counter = counter
from newTable
where sequence = #sequence
--assuming you have a primary key in the table.
Declare #id int;
Select top 1 #id = id from table
where sequence = #sequence
and counter is null;
--update the table needing edits.
update table
set counter = l_counter + 1
where id = #id
--update the new table so you can keep track of which
--counter you are on
update newTable
set counter = l_counter + 1
where id = #id
Then run a proc to execute this proc for each record in your table.
Now you should have a "newTable" filled with the currently used counter for each record in the table. Set up your insert proc so that anytime a new record is created, if it is a sequence not already in the newTable, you add it with a count of 1 and you put a count of 1 in the main table. If the sequence DOES already exist, use the logic above (increment the count already in use the "newTable" and place that count as the counter value in the newTable and the mainTable.
Basically, this method decided to use memory in place of querying the existing table. It will become most beneficial if you have a large table with lots of repeated sequence numbers. If your sequence numbers only happen two or three times, you probably want to do a query instead when you update and then later insert:
First, to update:
--find out the counter value
Declare l_counter int
select l_counter = max(counter)
from table where sequence = #sequence
update table
set counter = l_counter + 1
where id = (select top 1 id from table where sequence = #sequence
and counter is null)
then run that for each record.
Then, when inserting new records:
Declare l_counter int
select l_counter = max(counter)
from table where sequence = #sequence
IsNull(l_counter, 0)
Insert into table
(counter, sequence) values (l_counter + 1, #sequence)
Again, I'm positive I've mixed-and-matched my syntaxes here, but the concepts should work. OF course, it's a "one at a time" approach instead of set based, so it might be a little inefficient, but it will work.

Related

Enumerate the multiple rows in a multi-update Trigger

I have something like the table below:
CREATE TABLE updates (
id INT PRIMARY KEY IDENTITY (1, 1),
name VARCHAR (50) NOT NULL,
updated DATETIME
);
And I'm updating it like so:
INSERT INTO updates (name, updated)
VALUES
('fred', '2020-11-11),
('fred', '2020-11-11'),
...
('bert', '2020-11-11');
I need to write an after update Trigger and enumerate all the name(s) that were added and add each one to another table but can't work out how enumerate each one.
EDIT: - thanks to those who pointed me in the right direction, I know very little SQL.
What I need to do is something like this
foreach name in inserted
look it up in another table and
retrieve a count of the updates a 'name' has done
add 1 to the count
and update it back into the other table
I can't get to my laptop at the moment, but presumably I can do something like:
BEGIN
SET #count = (SELECT UCount from OTHERTAB WHERE name = ins.name)
SET #count = #count + 1
UPDATE OTHERTAB SET UCount = #count WHERE name = ins.name
SELECT ins.name
FROM inserted ins;
END
and that would work for each name in the update?
Obviously I'll have to read up on set based SQL processing.
Thanks all for the help and pointers.
Based on your edits you would do something like the following... set based is a mindset, so you don't need to compute the count in advance (in fact you can't). It's not clear whether you are counting in the same table or another table - but I'm sure you can work it out.
Points:
Use the Inserted table to determine what rows to update
Use a sub-query to calculate the new value if its a second table, taking into account the possibility of null
If you are really using the same table, then this should work
BEGIN
UPDATE OTHERTAB SET
UCount = COALESCE(UCount,0) + 1
WHERE [name] in (
SELECT I.[name]
FROM Inserted I
);
END;
If however you are using a second table then this should work:
BEGIN
UPDATE OTHERTAB SET
UCount = COALESCE((SELECT UCount+1 from OTHERTAB T2 WHERE T2.[name] = OTHERTAB.[name]),0)
WHERE [name] in (
SELECT I.[name]
FROM Inserted I
);
END;
Using inserted and set-based approach(no need for loop):
CREATE TRIGGER trg
ON updates
AFTER INSERT
AS
BEGIN
INSERT INTO tab2(name)
SELECT name
FROM inserted;
END

Query to update row value in next blank column

I have a table called StencilStorage and it has fields that look like this.
What I am trying to accomplish is creating a stored procedure that will update the table and set PCB_ID equal to some irrelevant number wherever the next available slot is, sorting the table from low->high by UNIQID. I have the following so far. The next available slot is labeled as 0 as shown in row 5 in the table above.
CREATE PROCEDURE sp_insert_to_storage
(
#PCB_ID integer
)
AS
BEGIN
UPDATE Tooling.StencilStorage
SET PCB_ID = #PCB_ID
WHERE
GO;
Any help is very much appreciated.
CREATE PROCEDURE sp_insert_to_storage
(
#PCB_ID integer
)
AS
BEGIN
UPDATE Tooling.StencilStorage
SET PCB_ID = #PCB_ID
WHERE UNIQID = (SELECT TOP 1 UNIQID FROM Tooling.StencilStorage WHERE PCB_ID = 0 ORDER BY UNIQID)
GO;

using MERGE for incremental insert

I have a scenario where one column of the target table needs to be auto incremented . I do not have identity enabled on this column. So i need to pick the last number and add 1 to it , each time an insert is done.
http://sqlfiddle.com/#!6/61eb4/5
A similar scenario is given in the fiddle link. I do not want the productid of ProductChanges table to be inserted. Instead, i need the last id to be picked and i need it to be incremented and inserted for each new row
Code to get this working
DECLARE #intctr int
SELECT #intctr = MAX(productid)+1 from products
DECLARE #strQry varchar(200)
SET #strQry =
'CREATE SEQUENCE dbo.seq_key_prd
START WITH ' +convert( varchar(12),#intctr) +' INCREMENT BY 1 ;'
print #strQry
exec( #strQry)
alter table Products
add default next value for seq_key_prd
for ProductId;
GO
--Merge statement for data sync
MERGE Products USING ProductChanges ON (Products.Productid = ProductChanges.Productid)
WHEN MATCHED AND Products.VendorlD =0 THEN DELETE
WHEN NOT MATCHED by target then insert (productid,Productname,VendorlD)
values(default,productname,VendorlD)
WHEN MATCHED THEN UPDATE SET
Products.ProductName = ProductChanges.ProductName ,
Products.VendorlD = ProductChanges.VendorlD;
1)create sequence and set to target table.
example
CREATE SEQUENCE table_seq
MINVALUE 1
START WITH 1
INCREMENT BY 1
CACHE 20;
2)create trigger for that sequence,to set the table
CREATE OR REPLACE TRIGGER my_trigger
BEFORE INSERT
ON myTable
FOR EACH ROW
WHEN (new.id is null)
DECLARE
v_id qname.qname_id%TYPE;
BEGIN
SELECT table_seq.nextval INTO v_id FROM DUAL;
:new.qname_id := v_id;
END my_trigger;

SQL insert statement from another table loop

I am trying to take values from a table and insert them into another table. However, there is one database column that needs to increase by 1 value each time. This value though is not an identity insert column, the value comes from another table. There is another db column that acts as a counter. I wrote a couple of things but it just isnt helping:
(121 documents)
declare #count int;
set #count=0
while #count<=121
begin
insert into cabinet..DOCUMENT_NAMES_ROBBY (TAG,TAGORDER,ACTIVE,QUEUE,REASON,FORM,DIRECT_VIEW,GLOBAL,FU_SIGN,
SIGN_X,SIGN_Y,SIGN_W,SIGN_H,ANNOTATE,doctype_id,CODE,DOC_TYPE,SET_ID,SUSPEND_DELAY,Text_Editing,Restrict_Viewing,Viewing_Expire,
Viewing_Period,DocHdrLength,DocFtrLength,DocRuleId,Outbound,SigQueue,SigReason) select TAG,TAGORDER,ACTIVE,QUEUE,REASON,FORM,
DIRECT_VIEW,GLOBAL,FU_SIGN,
SIGN_X,SIGN_Y,SIGN_W,SIGN_H,ANNOTATE,(select nextid from cabinet..Wfe_NextValue where Name='documents')+1, CODE,DOC_TYPE,'2',SUSPEND_DELAY,Text_Editing,Restrict_Viewing,Viewing_Expire,
Viewing_Period,DocHdrLength,DocFtrLength,DocRuleId,Outbound,SigQueue,SigReason from cabinet..document_names where SET_ID ='1'
update cabinet..Wfe_NextValue set NextID=NextID+1 where Name='documents'
set #count=#count+1
end
That database column is the doctype_id. Above obviously comes out wrong and puts like 14,000 rows in the table. Basically I want to take every single entry from document_names and put it in document_names_robby...except the doctype_id column should take the value from wfe_nextvalue +1, while at the same time increasing that number in that table by 1 BEFORE inserting the next document name into document_Names_Robby. Any help is appreciated
Many popular databases support sequences. For the sequence, there is a function nextval that returns the sequence value and increments the sequence counter and currval that returns the latest previously returned value, also you can set the initial value and increment. Sequences are thread safe when storing counters in the table column is not.
Rewrite your code using sequences.
Assuming that you are using SQL Server database. Use IDENTITY function
SELECT *, IDENTITY(int, 1,1) AS IDCol FROM Cabinet.DocumentNames INTO #Tab1 WHERE Set_Id = '1';
insert into cabinet..DOCUMENT_NAMES_ROBBY (TAG,TAGORDER,ACTIVE,QUEUE,REASON,FORM,DIRECT_VIEW,GLOBAL,FU_SIGN,
SIGN_X,SIGN_Y,SIGN_W,SIGN_H,ANNOTATE,doctype_id,CODE,DOC_TYPE,SET_ID,SUSPEND_DELAY,Text_Editing,Restrict_Viewing,Viewing_Expire,
Viewing_Period,DocHdrLength,DocFtrLength,DocRuleId,Outbound,SigQueue,SigReason)
SELECT * FROM #Tab1;
DROP TABLE #Tab1;
declare #count int;
set #count=0
declare #nextId int;
select #nextId= nextid from cabinet..Wfe_NextValue where Name='documents'
while #count<=121
begin
insert into cabinet..DOCUMENT_NAMES_ROBBY (TAG,TAGORDER,ACTIVE,QUEUE,REASON,FORM,DIRECT_VIEW,GLOBAL,FU_SIGN,
SIGN_X,SIGN_Y,SIGN_W,SIGN_H,ANNOTATE,doctype_id,CODE,DOC_TYPE,SET_ID,SUSPEND_DELAY,Text_Edi ting,Restrict_Viewing,Viewing_Expire,
Viewing_Period,DocHdrLength,DocFtrLength,DocRuleId,Outbound,SigQueue,SigReason) select TAG,TAGORDER,ACTIVE,QUEUE,REASON,FORM,
DIRECT_VIEW,GLOBAL,FU_SIGN,
SIGN_X,SIGN_Y,SIGN_W,SIGN_H,ANNOTATE,(select nextid from cabinet..Wfe_NextValue where Name='documents')+1, CODE,DOC_TYPE,'2',SUSPEND_DELAY,Text_Editing,Restrict_Viewing,Viewing_Expire,
Viewing_Period,DocHdrLength,DocFtrLength,DocRuleId,Outbound,SigQueue,SigReason from cabinet..document_names where SET_ID ='1'
set #count=#count+1
end
update cabinet..Wfe_NextValue set NextID=NextID+121 where Name='documents'

Weird trigger problem when I do an INSERT into a table

I've got a trigger attached to a table.
ALTER TRIGGER [dbo].[UpdateUniqueSubjectAfterInsertUpdate]
ON [dbo].[Contents]
AFTER INSERT,UPDATE
AS
BEGIN
-- Grab the Id of the row just inserted/updated
DECLARE #Id INT
SELECT #Id = Id
FROM INSERTED
END
Every time a new entry is inserted or modified, I wish to update a single field (in this table). For the sake of this question, imagine i'm updating a LastModifiedOn (datetime) field.
Ok, so what i've got is a batch insert thingy..
INSERT INTO [dbo].[Contents]
SELECT Id, a, b, c, d, YouDontKnowMe
FROM [dbo].[CrapTable]
Now all the rows are correctly inserted. The LastModifiedOn field defaults to null. So all the entries for this are null -- EXCEPT the first row.
Does this mean that the trigger is NOT called for each row that is inserted into the table, but once AFTER the insert query is finished, ie. ALL the rows are inserted? Which mean, the INSERTED table (in the trigger) has not one, but 'n' number of rows?!
If so .. er.. :( Would that mean i would need a cursor in this trigger? (if i need to do some unique logic to each single row, which i do currently).
?
UPDATE
I'll add the full trigger code, to see if it's possible to do it without a cursor.
BEGIN
SET NOCOUNT ON
DECLARE #ContentId INTEGER,
#ContentTypeId TINYINT,
#UniqueSubject NVARCHAR(200),
#NumberFound INTEGER
-- Grab the Id. Also, convert the subject to a (first pass, untested)
-- unique subject.
-- NOTE: ToUriCleanText just replaces bad uri chars with a ''.
-- eg. an '#' -> ''
SELECT #ContentId = ContentId, #ContentTypeId = ContentTypeId,
#UniqueSubject = [dbo].[ToUriCleanText]([Subject])
FROM INSERTED
-- Find out how many items we have, for these two keys.
SELECT #NumberFound = COUNT(ContentId)
FROM [dbo].[Contents]
WHERE ContentId = #ContentId
AND UniqueSubject = #UniqueSubject
-- If we have at least one identical subject, then we need to make it
-- unique by appending the current found number.
-- Eg. The first instance has no number.
-- Second instance has subject + '1',
-- Third instance has subject + '2', etc...
IF #NumberFound > 0
SET #UniqueSubject = #UniqueSubject + CAST(#NumberFound AS NVARCHAR(10))
-- Now save this change.
UPDATE [dbo].[Contents]
SET UniqueSubject = #UniqueSubject
WHERE ContentId = #ContentId
END
Why not change the trigger to deal with multiple rows?
No cursor or loops needed: it's the whole point of SQL ...
UPDATE
dbo.SomeTable
SET
LastModifiedOn = GETDATE()
WHERE
EXIST (SELECT * FROM INSERTED I WHERE I.[ID] = dbo.SomeTable.[ID]
Edit: Something like...
INSERT #ATableVariable
(ContentId, ContentTypeId, UniqueSubject)
SELECT
ContentId, ContentTypeId, [dbo].[ToUriCleanText]([Subject])
FROM
INSERTED
UPDATE
[dbo].[Contents]
SET
UniqueSubject + CAST(NumberFound AS NVARCHAR(10))
FROM
--Your original COUNT feels wrong and/or trivial
--Do you expect 0, 1 or many rows.
--Edit2: I assume 0 or 1 because of original WHERE so COUNT(*) will suffice
-- .. although, this implies an EXISTS could be used but let's keep it closer to OP post
(
SELECT ContentId, UniqueSubject, COUNT(*) AS NumberFound
FROM #ATableVariable
GROUP BY ContentId, UniqueSubject
HAVING COUNT(*) > 0
) foo
JOIN
[dbo].[Contents] C ON C.ContentId = foo.ContentId AND C.UniqueSubject = foo.UniqueSubject
Edit 2: and again with RANKING
UPDATE
C
SET
UniqueSubject + CAST(foo.Ranking - 1 AS NVARCHAR(10))
FROM
(
SELECT
ContentId, --not needed? UniqueSubject,
ROW_NUMBER() OVER (PARTITION BY ContentId ORDER BY UniqueSubject) AS Ranking
FROM
#ATableVariable
) foo
JOIN
dbo.Contents C ON C.ContentId = foo.ContentId
/* not needed? AND C.UniqueSubject = foo.UniqueSubject */
WHERE
foo.Ranking > 1
The trigger will be run only once for an INSERT INTO query. The INSERTED table will contain multiple rows.
Ok folks, I think I figure it out myself. Inspired by the previous answers and comments, I've done the following. (Can you folks have a quick look over to see if i've over-enginered this baby?)
.1. Created an Index'd View, representing the 'Subject' field, which needs to be cleaned. This is the field that has to be unique .. but before we can make it unique, we need to group by it.
-- Create the view.
CREATE VIEW ContentsCleanSubjectView with SCHEMABINDING AS
SELECT ContentId, ContentTypeId,
[dbo].[ToUriCleanText]([Subject]) AS CleanedSubject
FROM [dbo].[Contents]
GO
-- Index the view with three index's. Custered PK and a non-clustered,
-- which is where most of the joins will be done against.
-- Last one is because the execution plan reakons i was missing statistics
-- against one of the fields, so i added that index and the stats got gen'd.
CREATE UNIQUE CLUSTERED INDEX PK_ContentsCleanSubjectView ON
ContentsCleanSubjectView(ContentId)
CREATE NONCLUSTERED INDEX IX_BlahBlahSnipSnip_A ON
ContentsCleanSubjectView(ContentTypeId, CleanedSubject)
CREATE INDEX IX_BlahBlahSnipSnip_B ON
ContentsCleanSubjectView(CleanedSubject)
.2. Create the trigger code which now
a) grabs all the items 'changed' (nothing new/hard about that)
b) orders all the inserted rows, row numbered with partitioning by a clean subject
c) update the single row we're upto in the main update clause.
here's the code...
ALTER TRIGGER [dbo].[UpdateUniqueSubjectAfterInsertUpdate]
ON [dbo].[Contents]
AFTER INSERT,UPDATE
AS
BEGIN
SET NOCOUNT ON
DECLARE #InsertRows TABLE (ContentId INTEGER PRIMARY KEY,
ContentTypeId TINYINT,
CleanedSubject NVARCHAR(300))
DECLARE #UniqueSubjectRows TABLE (ContentId INTEGER PRIMARY KEY,
UniqueSubject NVARCHAR(350))
DECLARE #UniqueSubjectRows TABLE (ContentId INTEGER PRIMARY KEY,
UniqueSubject NVARCHAR(350))
-- Grab all the records that have been updated/inserted.
INSERT INTO #InsertRows(ContentId, ContentTypeId, CleanedSubject)
SELECT ContentId, ContentTypeId, [dbo].[ToUriCleanText]([Subject])
FROM INSERTED
-- Determine the correct unique subject by using ROW_NUMBER partitioning.
INSERT INTO #UniqueSubjectRows
SELECT SubResult.ContentId, UniqueSubject = CASE SubResult.RowNumber
WHEN 1 THEN SubResult.CleanedSubject
ELSE SubResult.CleanedSubject + CAST(SubResult.RowNumber - 1 AS NVARCHAR(5)) END
FROM (
-- Order all the cleaned subjects, partitioned by the cleaned subject.
SELECT a.ContentId, a.CleanedSubject, ROW_NUMBER() OVER (PARTITION BY a.CleanedSubject ORDER BY a.ContentId) AS RowNumber
FROM ContentsCleanSubjectView a
INNER JOIN #InsertRows b ON a.ContentTypeId = b.ContentTypeId AND a.CleanedSubject = b.CleanedSubject
GROUP BY a.contentId, a.cleanedSubject
) SubResult
INNER JOIN [dbo].[Contents] c ON c.ContentId = SubResult.ContentId
INNER JOIN #InsertRows d ON c.ContentId = d.ContentId
-- Now update all the effected rows.
UPDATE a
SET a.UniqueSubject = b.UniqueSubject
FROM [dbo].[Contents] a INNER JOIN #UniqueSubjectRows b ON a.ContentId = b.ContentId
END
Now, the subquery correctly returns all the cleaned subjects, partitioned correctly and numbered correctly. I never new about the 'PARTITION' command, so that trick was the big answer here :)
Then i just join'd the subquery with the row that is being updated in the parent query. The row number is correct, so now i just do a case. if this is the first time the cleaned subject exists (eg. row_number = 1), don't modify it. otherwise, append the row_number minus one. This means the 2nd instance of the same subject, the unique subject will be => cleansubject + '1'.
The reason why i believe i need to have an index'd view is because if i have two very similar subjects, that when you have stripped out (ie. cleaned) all the bad chars (which i've determined are bad) .. it's possible that the two clean subjects are the same. As such, I need to do all my joins on a cleanedSubject, instead of a subject. Now, for the massive amount of rows I have, this is crap for performance when i don't have the view. :)
So .. is this over engineered?
Edit 1:
Refactored trigger code so it's waay more performant.