Update DateUsed by trigger - sql-server

I'm trying to update a table with a trigger from another table. I thought this would be a very simple query but the query I first came up with does not work and I don't understand why.
CREATE TABLE [dbo].[Vehicle](
[id] [int] IDENTITY(1,1) NOT NULL,
[plate] [nvarchar](50) NOT NULL,
[name] [nvarchar](50) NOT NULL,
[dateUsed] [datetime] NULL
)
CREATE TABLE [dbo].[Transaction](
[id] [int] IDENTITY(1,1) NOT NULL,
[vehicleId] [int] NOT NULL,
[quantity] [float] NOT NULL,
[dateTransaction] [datetime] NOT NULL,
)
When a transaction is added, I wish to update the Vehicle table. If the added dateTransaction is later then dateUsed it should be updated so the dateUsed field always contains the latest date of that specific vehicle.
I would think that this trigger should do the trick.. but it does not:
UPDATE [Vehicle]
SET [dateUsed] =
CASE
WHEN [dateUsed] < [Transaction].[dateTransaction]
OR [dateUsed] IS NULL
THEN [Transaction].[dateTransaction]
ELSE [dateUsed]
END
FROM [Transaction]
WHERE [Vehicle].[id]=[Transaction].[vehicleId]
It looks good to me... It should go over all newly inserted records and update the dateUsed field. If the dateTransaction is newer, use that one.. if not.. use the current. But I seem to missing something because it's not updating to the latest date. It does match one of the transactions of that specific vehicle but not the latest one.
A query that does work:
UPDATE [Vehicle]
SET [dateUsed] = InsertedPartitioned.[dateTransaction]
FROM [Vehicle]
LEFT JOIN (
SELECT
[vehicleId],
[dateTransaction],
ROW_NUMBER() OVER(PARTITION BY [VehicleId] ORDER BY [dateTransaction] DESC) AS RC
FROM [Inserted]) AS InsertedPartitioned
ON InsertedPartitioned.RC=1
AND InsertedPartitioned.[vehicleId]=[Vehicle].[id]
WHERE InsertedPartitioned.[vehicleId] IS NOT NULL
AND ([Vehicle].[dateUsed] IS NULL
OR InsertedPartitioned.[dateTransaction] > [Vehicle].[dateUsed]);
So I have a working solution and it may even be for the better (haven't timed it with a large insert) but it bugs the hell out of my not knowing why the first it not working!
Can anyone 'enlighten me'?

why the first it not working
Because of a wonderful aspect of the Microsoft extension to UPDATE that uses a FROM clause:
Use caution when specifying the FROM clause to provide the criteria for the update operation. The results of an UPDATE statement are undefined if the statement includes a FROM clause that is not specified in such a way that only one value is available for each column occurrence that is updated, that is if the UPDATE statement is not deterministic.
(My emphasis).
That is, if more than one row from inserted matches the same row in Vehicle then it's undefined which row will be used to apply the update - and all computations within the SET clause are computed "as if" they're all evaluated in parallel - so it's not as if a second attempt to update the same row will observe the results on the first attempt - the current value of the DateUsed column that can be observed is always the original value.
In ANSI standard SQL, you'd have to write the UPDATE without using the FROM extension and would thus have to write a correlated subquery, something like:
UPDATE [Vehicle]
SET [dateUsed] = COALESCE((SELECT dateUsed FROM inserted i
WHERE i.VehicleId = Vehicle.Id and
(i.dateUsed > Vehicle.DateUsed or
Vehicle.DateUsed IS NULL),
dateUsed)
WHERE [id] IN (select [vehicleId] FROM inserted)
Which, under the same circumstances, would nicely give you an error about a subquery returning more than one value (for the one inside the COALESCE, not the one in the IN) and thus give you a clue to why it's not working.
But, undeniably, the FROM extension is useful - I just wish it triggered a warning for this kind of situation.

Related

How to increase job order finish days column value in SQL SERVER by 1 automatic every day based on condition?

I have a job orders system and when i create new job order i have the following columns order date , order status (open or close) , expected days column for example 5 days to finish this order and i have the actual days column this column actual days default value = 0 .
I need to create trigger to increment the value of column actual days by 1 daily every new day automatically if the job order open.
How can i do this trigger daily increment and update the value when new day start at 12 midnight ?
I am using SQL SERVER.
This is my table:
CREATE TABLE [Documents_followup](
[Doc_id] [int] IDENTITY(1,1) NOT NULL,
[Document_subject] [nvarchar](200) NULL,
[Date] [datetime] NULL,
[Document_number] [nvarchar](50) NULL,
[Doc_place_id] [int] NULL,
[Doc_expect_time] [int] NULL,
[Doc_finish_time] [int] NULL,
[DocStatus_id] [int] NULL,
[Notes] [nvarchar](100) NULL,
[user_id] [int] NULL)
You don't need a trigger, you can just add a computed column. I would just use the datediff function.
select
*,
actual_days = datediff(day,order_date,getdate())
from YourTable
Now this doesn't work for jobs only open but you aren't accounting for when the job is closed. I suppose that's what the actual_days is supposed to do... add to the order_date to figure out when it closed. Instead, I would get rid of the actual_days column and just add a close_date column that can be updated whenever the order_status changes from open to close. Then, you can use the same datediff logic.
This makes more sense than writing a job that updates your table everyday. Without something to trigger the TRIGGER the update wouldn't happen. Besides it would be slow to do this RBAR so if you really can't change the design, then just use a SQL Agent Job to do this in batch.
update yourtable
set actual_days = datediff(day,order_date,getdate())
where order_status = 'open
You'll schedule that to run everyday or whatever... but again, it wouldn't be accurate if a job closes after you run that until the next day's run.
The other option is to only update the actual_days once the order is closed. This would allow you to use a trigger, or just include it in your update statement For example, a boiler plate for closing the ticket
create proc usp_closeTicket (#order_no int)
as
update mytable
set
order_status = 'closed'
,actual_days = datediff(day,order_date,getdate())
where order_no = #order_no
Of course you need to use proper error handling and what not. Check our Erland's blog for that.
Edit, now that you added the DDL
Nothing is different than my suggestions above. Mainly I would use the UPDATE statement instead of a trigger which would change to this using your column names:
create proc usp_closeTicket (#order_no int)
as
update Documents_followup
set
DocStatus_id = 1 --or what ever signifies closed
,Doc_finish_time= datediff(day,[Date],getdate())
where Doc_id = #order_no

How to update a table if a column exists in SQL Server?

I have a table MyTable created by
CREATE TABLE MyTable
(
[ID] [bigint] IDENTITY(1,1) NOT NULL,
[Type] [int] NOT NULL,
[CreatedDate] [datetime] NOT NULL,
[ModifiedDate] [datetime] NOT NULL,
)
I want to check if a column exists in my table, and if it does, I want to copy the data to a different column, then drop the old column, like this:
IF (SELECT COLUMNPROPERTY(OBJECT_ID('MyTable'), 'Timestamp', 'Precision')) IS NOT NULL
BEGIN
UPDATE [dbo].[MyTable]
SET [CreatedDate] = [Timestamp]
ALTER TABLE [dbo].[MyTable]
DROP COLUMN [Timestamp]
END
GO
However, when I try to run this I get an error:
Invalid column name 'Timestamp'
How can I accomplish what I'm trying to do?
This is a compilation issue.
If the table doesn't exist when you compile the batch all works fine as the statements referencing the table are subject to deferred compile. However for a preexisting table you will hit this problem as it tries to compile all statements and balks at the non existent column.
You can push the code into a child batch so it is only compiled if that branch is hit.
IF (SELECT COLUMNPROPERTY(OBJECT_ID('MyTable'), 'Timestamp', 'Precision')) IS NOT NULL
BEGIN
EXEC('
UPDATE [dbo].[MyTable]
SET [CreatedDate] = [Timestamp]
ALTER TABLE [dbo].[MyTable]
DROP COLUMN [Timestamp]
')
END
GO
If you are just trying to rename the column
EXEC sys.sp_rename 'dbo.MyTable.[TimeStamp]' , 'CreatedDate', 'COLUMN'
Would be easier though (from a position where the CreatedDate column doesn't exist).
You have to first create the [Timestamp] column with an ALTER TABLE statement.
Then the rest should run.
EDIT based on comment (I know this info is duplicated elsewhere on SO, but I couldn't find it):
Ok, the IF condition in SQL Server unfortunately does not allow you to ignore code that does not parse. What is happening is that SQL Server is looking at your command, and parsing every statement to make sure it is valid.
When it does this, SQL Server isn't smart enough to figure out that the invalid statement (the UPDATE that requires the presence of [TimeStamp]) will not be reached if there is no [TimeStamp].
In other words, you can't write a SQL command that expects a column that doesn't exist EVEN IF you nest that command in an IF condition that won't be reached. SQL Server will parse the entire statement and not allow it to run BEFORE it tests the IF condition.
A commonly used Work arounds for this is Dynamic SQL, which SQL Server can't pre-parse, so it won't try.

SQL Server - Order Identity Fields in Table

I have a table with this structure:
CREATE TABLE [dbo].[cl](
[ID] [int] IDENTITY(1,1) NOT NULL,
[NIF] [numeric](9, 0) NOT NULL,
[Name] [varchar](80) NOT NULL,
[Address] [varchar](100) NULL,
[City] [varchar](40) NULL,
[State] [varchar](30) NULL,
[Country] [varchar](25) NULL,
Primary Key([ID],[NIF])
);
Imagine that this table has 3 records. Record 1, 2, 3...
When ever I delete Record number 2 the IDENTITY Field generates a Gap. The table then has Record 1 and Record 3. Its not correct!
Even if I use:
DBCC CHECKIDENT('cl', RESEED, 0)
It does not solve my problem becuase it will set the ID of the next inserted record to 1. And that's not correct either because the table will then have a multiple ID.
Does anyone has a clue about this?
No database is going to reseed or recalculate an auto-incremented field/identity to use values in between ids as in your example. This is impractical on many levels, but some examples may be:
Integrity - since a re-used id could mean records in other systems are referring to an old value when the new value is saved
Performance - trying to find the lowest gap for each value inserted
In MySQL, this is not really happening either (at least in InnoDB or MyISAM - are you using something different?). In InnoDB, the behavior is identical to SQL Server where the counter is managed outside of the table, so deleted values or rolled back transactions leave gaps between last value and next insert. In MyISAM, the value is calculated at time of insertion instead of managed through an external counter. This calculation is what is giving the perception of being recalcated - it's just never calculated until actually needed (MAX(Id) + 1). Even this won't insert inside gaps (like the id = 2 in your example).
Many people will argue if you need to use these gaps, then there is something that could be improved in your data model. You shouldn't ever need to worry about these gaps.
If you insist on using those gaps, your fastest method would be to log deletes in a separate table, then use an INSTEAD OF INSERT trigger to perform the inserts with your intended keys by first looking for records in these deletions table to re-use (then deleting them to prevent re-use) and then using the MAX(Id) + 1 for any additional rows to insert.
I guess what you want is something like this:
create table dbo.cl
(
SurrogateKey int identity(1, 1)
primary key
not null,
ID int not null,
NIF numeric(9, 0) not null,
Name varchar(80) not null,
Address varchar(100) null,
City varchar(40) null,
State varchar(30) null,
Country varchar(25) null,
unique (ID, NIF)
)
go
I added a surrogate key so you'll have the best of both worlds. Now you just need a trigger on the table to "adjust" the ID whenever some prior ID gets deleted:
create trigger tr_on_cl_for_auto_increment on dbo.cl
after delete, update
as
begin
update dbo.cl
set ID = d.New_ID
from dbo.cl as c
inner join (
select c2.SurrogateKey,
row_number() over (order by c2.SurrogateKey asc) as New_ID
from dbo.cl as c2
) as d
on c.SurrogateKey = d.SurrogateKey
end
go
Of course this solution also implies that you'll have to ensure (whenever you insert a new record) that you check for yourself which ID to insert next.

Computed Column that doesn't auto update

I have a computed column that is automatically creating a confirmation number by adding the current max ID to some Prefix. It works, but not exactly how I need it to work.
This is the function
ALTER FUNCTION [dbo].[SetEPNum](#IdNum INT)
RETURNS VARCHAR(255)
AS
BEGIN
return (select 'SomePrefix' + RIGHT('00000' + CAST(MAX(IdNum) AS VARCHAR(255)), 5)
FROM dbo.someTable
/*WHERE IdNum = #IdNum*/)
END
If I add WHERE IdNum = #IdNum to the select in the function, that gives the illusion of working, but in reality it is picking the max IdNUM from the one row where IDNum = #IdNum rather than actually picking the current max IDNUM from all IDNums. If I remove the where statement, the computed function simply sets every field to the max Id every time it changes.
This is the table
CREATE TABLE [dbo].[someTable](
[IdNum] [int] IDENTITY(1,1) NOT NULL,
[First_Name] [varchar](50) NOT NULL,
[Last_Name] [varchar](50) NOT NULL,
[EPNum] AS ([dbo].[SetEPNum]([IdNum]))
) ON [PRIMARY]
GO
SET ANSI_PADDING OFF
GO
This is the computed column
ALTER TABLE dbo.someTable
ADD EPNum AS dbo.SetEPnum(IdNum)
Is there any way to accomplish this? If not, is there an alternative solution?
If my understanding is correct, you try to get the max id of some table to appear next to each record at the time it was updated?
Right now you get the same max id next to all records.
That is because the max id is one and only one. You have provided no context.
It seems to me this is the job of a trigger or even your update statement. Why employ computed columns? The computed column gets recomputed every time you display the data.
If you absolutely need to go this way, you should employ some other field (e.g. modification date) and get the max id from those records that were updated before the current. It all depends though on the business logic of your application and what you try to achieve.

Using t-sql to select a dataset with duplicate values removed

I have a set of tables in SQL Server 2005 which contain timeseries data. There is hence a datetime field and a set of values.
CREATE TABLE [dbo].[raw_data](
[Time] [datetime] NULL,
[field1] [float] NULL,
[field2] [float] NULL,
[field3] [float] NULL
)
The datetime field is unfortunately not a unique key, and there appear to be a lot of datetime values with multiple (non-identical) entries - hence DISTINCT doesn't work.
I want to select data from these tables for insertion into a new, properly indexed table.
Hence I want a select query that will return a dataset with a single row entry for each Time. I am not concerned which set of values is selected for a given time, as long as one (and only one) is chosen.
There are a LOT of these tables, so I do not have time to find and manually purge duplicate values, so a standard HAVING COUNT(*)>1 query is not applicable. There are also too many duplicates to just ignore those time values altogether.
Any ideas? I was thinking of some kind of cursor based on PARTITION BY, but got stuck beyond that point.
You don't need a cursor:
SELECT tmp.*
FROM
(
SELECT *, ROW_NUMBER() OVER (PARTITION BY [Time] ORDER BY [Time]) AS RowNum
FROM raw_data
) AS tmp
WHERE tmp.RowNum = 1

Resources