I have a table with this structure
ITEM_ID VARCHAR(255) NOT NULL PRIMARY KEY,
COST1 FLOAT NOT NULL,
COST2 FLOAT NOT NULL,
PRICE1 FLOAT NOT NULL,
PRICE2 FLOAT NOT NULL,
VALID_FROM DATE NOT NULL PRIMARY KEY,
VALID_UNTIL DATE NULL,
The default value for VALID_UNTIL is ‘2099-12-31’
I need to create a trigger in this table so that when I insert new records the most recent record in the table updates the value of VALID_UNTIL to be the same as the value of VALID_FROM of the record being inserted as long as ITEM_ID is the same. And when there is no record matching on the table I want it to just insert the new record.
And I want the trigger to apply for every new record individually from oldest to newest by the VALID_FROM column being inserted.
I already tried this query, but the trigger applies to all the records being inserted at the same time so the VALID_UNTIL result for every record is ‘2099-12-31’
CREATE TRIGGER trg_update_valid_until
ON cal.MA_COSTS
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
UPDATE t
SET t.VALID_UNTIL = i.VALID_FROM
FROM cal.MA_COSTS t
INNER JOIN inserted i
ON t.ITEM_ID= i.ITEM_ID
AND t.VALID_UNTIL = '2099-12-31'
AND t.VALID_FROM < i.VALID_FROM;
END;
Here is some sample data as well
INSERT INTO cal.MA_COSTS (ITEM_ID, COST1, COST2, PRICE1, PRICE2, VALID_FROM)
VALUES
(2008209, 4062.7, 4062.7, 5803.85, 6732.47, '2024-12-01'),
(2008209, 4119.57, 4119.57, 5885.09, 6826.7, '2024-12-02'),
(2008209, 4150.85, 4150.85, 5929.78, 6878.54, '2024-12-06');
Santiago C is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.
10
For a more robust solution, don’t store VALID_UNTIL
because VALID_FROM
is enough. When you need to obtain the prices for a given date just use the following query:
select top 1 *
from MA_COSTS
where ITEM_ID = @ItemId
and VALID_FROM <= @DateOfInterest
order by VALID_FROM desc;
There is no advantage in storing VALID_UNTIL
– it just makes things more complicated.
And ensure you change the datatype of your columns – DO NOT USE FLOAT to store money! Float is an inaccurate datatype intended for scientific data… you need a precise datatype, you don’t want to lose money due to the behaviour of float. Use something like decimal(18,2)
0
I like Dale K’s idea of eliminating the VALID_UNTIL
column and using the CROSS APPLY (SELECT TOP 1 ...)
pattern for looking up prices.
However, if that isn’t an option, the following should satisfy your needs.
CREATE TRIGGER trg_update_valid_until
ON MA_COSTS
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
UPDATE tPrior
SET VALID_UNTIL = i.VALID_FROM
FROM inserted i
CROSS APPLY (
-- Locate immediately prior cost row for this item
SELECT TOP 1 t.*
FROM MA_COSTS t
WHERE t.ITEM_ID = i.ITEM_ID
AND t.VALID_FROM < i.VALID_FROM
ORDER BY t.VALID_FROM DESC
) tPrior
WHERE tPrior.VALID_UNTIL = '2099-12-31'
--or perhaps: WHERE tPrior.VALID_UNTIL > i.VALID_FROM
END;
For each inserted row, the prior row is identified and updated as needed.
An index in MA_COSTS(ITEM_ID, VALID_FROM)
is needed to support direct lookup of the related rows using an index seek. If (ITEM_ID, VALID_FROM)
is the primary key, no separate index is needed.
If you might be inserting rows with VALID_FROM
values in the middle of the existing range, a second update may be needed to set the VALID_UNTIL
values for the newly added row(s).
...
UPDATE tCurrent
SET VALID_UNTIL = tNext.VALID_FROM
FROM inserted i
JOIN MA_COSTS tCurrent
ON tCurrent.ITEM_ID = i.ITEM_ID
AND tCurrent.VALID_FROM = i.VALID_FROM
CROSS APPLY (
-- Locate immediately following cost row for this item
SELECT TOP 1 t.*
FROM MA_COSTS t
WHERE t.ITEM_ID = i.ITEM_ID
AND t.VALID_FROM > i.VALID_FROM
ORDER BY t.VALID_FROM
) tNext
WHERE tCurrent.VALID_UNTIL > tNext.VALID_FROM
Note that the above does not handle updates or deletes that may require adjustments to other rows.
Sample results after multiple insert operations with rows presented in a mixed order:
ITEM_ID | COST1 | COST2 | PRICE1 | PRICE2 | VALID_FROM | VALID_UNTIL |
---|---|---|---|---|---|---|
2008209 | 4062.7 | 4062.7 | 5803.85 | 6732.47 | 2024-12-01 | 2024-12-02 |
2008209 | 4119.57 | 4119.57 | 5885.09 | 6826.7 | 2024-12-02 | 2024-12-03 |
2008209 | 3333.33 | 3333.33 | 3333.33 | 3333.33 | 2024-12-03 | 2024-12-04 |
2008209 | 4444.44 | 4444.44 | 4444.44 | 4444.44 | 2024-12-04 | 2024-12-05 |
2008209 | 5555.55 | 5555.55 | 5555.55 | 5555.55 | 2024-12-05 | 2024-12-06 |
2008209 | 4150.85 | 4150.85 | 5929.78 | 6878.54 | 2024-12-06 | 2024-12-07 |
2008209 | 7777.77 | 7777.77 | 7777.77 | 7777.77 | 2024-12-07 | 2024-12-08 |
2008209 | 8888.88 | 8888.88 | 8888.88 | 8888.88 | 2024-12-08 | 2024-12-09 |
2008209 | 9999.99 | 9999.99 | 9999.99 | 9999.99 | 2024-12-09 | 2099-12-31 |
See this db<>fiddle for a demo.
I have slightly corrected the description of the table. It’s more consistent with your text.
Mainly – set default value for column VALID_UNTIL.
create table ma_costs(
ITEM_ID VARCHAR(255) NOT NULL ,
COST1 FLOAT NOT NULL,
COST2 FLOAT NOT NULL,
PRICE1 FLOAT NOT NULL,
PRICE2 FLOAT NOT NULL,
VALID_FROM DATE NOT NULL ,
VALID_UNTIL DATE default '2099-12-31' NULL,
PRIMARY KEY(item_id,valid_from)
);
create index ix_ma_costs_item_id_valid_from on ma_costs(item_id,valid_from);
Your trigger works, when you insert 1 row in query. Then there is 1 row with the value valid_until=’2099-12-31′. JOIN is correct
ON t.ITEM_ID= i.ITEM_ID
AND t.VALID_UNTIL = '2099-12-31' AND t.VALID_FROM < i.VALID_FROM
When you insert batch, and default value for valid_until is ‘2099-12-31’ INNER JOIN in trigger can not work correctly. There only 1 row in t
match to all rows in i
.
See example:
For test. Insert 1 row without trigger. Default for valid_until is correct.
INSERT INTO MA_COSTS (ITEM_ID, COST1, COST2, PRICE1, PRICE2, VALID_FROM)
VALUES
(2008201, 4062.71, 4062.71, 5803.81, 6732.41, '2024-10-01');
select * from ma_costs;
ITEM_ID | COST1 | COST2 | PRICE1 | PRICE2 | VALID_FROM | VALID_UNTIL |
---|---|---|---|---|---|---|
2008201 | 4062.71 | 4062.71 | 5803.81 | 6732.41 | 2024-10-01 | 2099-12-31 |
Your trigger:
CREATE TRIGGER trg_update_valid_until
ON MA_COSTS
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
UPDATE t
SET t.VALID_UNTIL = i.VALID_FROM
FROM MA_COSTS t
INNER JOIN inserted i
ON t.ITEM_ID= i.ITEM_ID
AND t.VALID_UNTIL = '2099-12-31'
AND t.VALID_FROM < i.VALID_FROM;
END;
Insert rows one by one (item_id=2008201).
INSERT INTO MA_COSTS (ITEM_ID, COST1, COST2, PRICE1, PRICE2, VALID_FROM)
VALUES (2008201, 4062.7, 4062.7, 5803.85, 6732.47, '2024-12-01');
INSERT INTO MA_COSTS (ITEM_ID, COST1, COST2, PRICE1, PRICE2, VALID_FROM)
VALUES (2008201, 4119.57, 4119.57, 5885.09, 6826.7, '2024-12-02');
INSERT INTO MA_COSTS (ITEM_ID, COST1, COST2, PRICE1, PRICE2, VALID_FROM)
VALUES (2008201, 4150.85, 4150.85, 5929.78, 6878.54, '2024-12-06');
Output is correct
ITEM_ID | COST1 | COST2 | PRICE1 | PRICE2 | VALID_FROM | VALID_UNTIL |
---|---|---|---|---|---|---|
2008201 | 4062.71 | 4062.71 | 5803.81 | 6732.41 | 2024-10-01 | 2024-12-01 |
2008201 | 4062.7 | 4062.7 | 5803.85 | 6732.47 | 2024-12-01 | 2024-12-02 |
2008201 | 4119.57 | 4119.57 | 5885.09 | 6826.7 | 2024-12-02 | 2024-12-06 |
2008201 | 4150.85 | 4150.85 | 5929.78 | 6878.54 | 2024-12-06 | 2099-12-31 |
Insert several rows simultaneously (item_id=2008209)
INSERT INTO MA_COSTS (ITEM_ID, COST1, COST2, PRICE1, PRICE2, VALID_FROM)
VALUES
(2008209, 4062.7, 4062.7, 5803.85, 6732.47, '2024-11-01'),
(2008209, 4119.57, 4119.57, 5885.09, 6826.7, '2024-11-02'),
(2008209, 4150.85, 4150.85, 5929.78, 6878.54, '2024-11-06');
ITEM_ID | COST1 | COST2 | PRICE1 | PRICE2 | VALID_FROM | VALID_UNTIL |
---|---|---|---|---|---|---|
2008201 | 4062.71 | 4062.71 | 5803.81 | 6732.41 | 2024-10-01 | 2024-12-01 |
2008201 | 4062.7 | 4062.7 | 5803.85 | 6732.47 | 2024-12-01 | 2024-12-02 |
2008201 | 4119.57 | 4119.57 | 5885.09 | 6826.7 | 2024-12-02 | 2024-12-06 |
2008201 | 4150.85 | 4150.85 | 5929.78 | 6878.54 | 2024-12-06 | 2099-12-31 |
2008209 | 4062.7 | 4062.7 | 5803.85 | 6732.47 | 2024-11-01 | 2024-11-06 |
2008209 | 4119.57 | 4119.57 | 5885.09 | 6826.7 | 2024-11-02 | 2024-11-06 |
2008209 | 4150.85 | 4150.85 | 5929.78 | 6878.54 | 2024-11-06 | 2099-12-31 |
fiddle
My suggestion is
CREATE TRIGGER trg_update_valid_until
ON MA_COSTS
AFTER INSERT
AS
BEGIN
declare @nn bigint;
set @nn=next value for sq_trigger_work_count; -- for test
SET NOCOUNT ON;
UPDATE t
SET t.VALID_UNTIL = i.VALID_FROM
-- log operation to column testValue
,t.testValue=concat('(',i.valid_from,'-',i.valid_until
,':',t.valid_from,'-',t.valid_until
,':',(select count(*) from ma_costs)
,')' )
FROM MA_COSTS t
INNER JOIN inserted i
ON t.ITEM_ID= i.ITEM_ID
and t.valid_from=(select max(valid_from) from ma_costs t2
where t2.item_id=i.item_id and t2.valid_from<i.valid_from)
;
END;
Details see in example fiddle