is it possible to capture the SysEndTime temporal timestamp with an OUTPUT keyword?
For example:
DECLARE #MyTableVar TABLE (sysendtime datetime2);
DELETE
FROM dbo.someTable
OUTPUT DELETED.sysendtime INTO #MyTableVar
WHERE
colA = 'something';
SELECT sysendtime FROM #MyTableVar;
The last SELECT is returning 9999-12-31 23:59:59.000 which is the value before the DELETE exec'd.
I read that SysEndTime is set at the end of the transaction so it wouldn't be visible until all the calls in that blocked completed but that'd mean I'd have to do a secondary query on the someTable table using the "for system_time all" syntax and retrieving the max(SysEndTime) column to capture the most recent change and with that I wouldn't be guaranteed that it's the delete that as an UPDATE would set SysEndTime too.
No this is not possible through the OUTPUT clause because the value does not exist in a column in the system versioned table.
SQL Server uses the transaction start time - not end time so logically it shouldn't be inherently impossible. The execution plan calls systrandatetime() in the compute scalar to get the value to use when inserting to the history table.
This is an internal function though and not exposed to us. The following query run in the same transaction should likely return something close but this is datetime datatype so will get the legacy rounding behaviour to 1/300 of a seconds. I doubt that you will find any guarantees that this even uses the exact same underlying data source either.
SELECT transaction_begin_time
FROM sys.dm_tran_active_transactions
WHERE transaction_id = (SELECT transaction_id
FROM sys.dm_tran_current_transaction);
I would not suggest using the transaction DMVs directly. However as the row already exists in the history table when the DELETE statement finishes you can (inside the same transaction) just query the history table to get the highest sysendtime for the primary key of one of the deleted rows (the PK can be captured with OUTPUT if needed).
e.g. Setup
CREATE TABLE dbo.someTable
(
PK INT PRIMARY KEY,
ColA VARCHAR(15),
[ValidFrom] datetime2 (2) GENERATED ALWAYS AS ROW START ,
[ValidTo] datetime2 (2) GENERATED ALWAYS AS ROW END ,
PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)
)
WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.someTableHistory))
INSERT INTO dbo.someTable
(PK,
ColA)
VALUES (1,'something'),
(2,'something');
Then you could achieve this as below
DECLARE #DeletedPks TABLE
(
PK INT
)
BEGIN TRAN
DELETE FROM dbo.someTable
OUTPUT DELETED.PK
INTO #DeletedPks
WHERE colA = 'something';
SELECT MAX(ValidTo)
FROM dbo.someTableHistory
WHERE PK = (SELECT TOP 1 PK /*Doesn't matter which PK we choose*/
FROM #DeletedPks)
COMMIT
You can get ROW START but as you say not ROW END
create table dbo.someTable
(
--sysendtime datetime2 ,
ColA varchar(15),
[ValidFrom] datetime2 (2) GENERATED ALWAYS AS ROW START
, [ValidTo] datetime2 (2) GENERATED ALWAYS AS ROW END
, PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)
)
GO
✓
insert into someTable (ColA) values ( 'something')
GO
1 rows affected
DECLARE #MyTableVar TABLE (sysendtime datetime2);
DELETE FROM dbo.someTable
OUTPUT DELETED.ValidFrom INTO #MyTableVar
WHERE colA = 'something';
SELECT sysendtime FROM #MyTableVar;
GO
| sysendtime |
| :------------------ |
| 06/02/2019 20:52:54 |
DECLARE #MyTableVar TABLE (sysendtime datetime2);
DELETE FROM dbo.someTable
OUTPUT DELETED.ValidTo INTO #MyTableVar
WHERE colA = 'something';
SELECT sysendtime FROM #MyTableVar;
GO
| sysendtime |
| :------------------ |
| 31/12/9999 23:59:59 |
*db<>fiddle here**
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE; BEGIN TRANSACTION;
CREATE TABLE #SYSUTCTRANSBEGINTIME (PK int, ValidFrom datetime2(7) GENERATED ALWAYS AS ROW START, ValidTo datetime2(7) GENERATED ALWAYS AS ROW END, PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo));
WAITFOR DELAY '00:00:00.100';
SELECT SYSUTCDATETIME() AS SYSUTCDATETIME;
insert into #SYSUTCTRANSBEGINTIME (PK) values (0);
select ValidFrom as SYSUTCTRANSBEGINTIME from #SYSUTCTRANSBEGINTIME;
go
ROLLBACK;
Related
I have created the following temportal table
/*
ALTER TABLE [dbo].________Test SET ( SYSTEM_VERSIONING = OFF)
GO
DROP TABLE dbo.________Test
GO
DROP TABLE dbo.________TestHistory
GO
*/
CREATE TABLE dbo.________Test
(
DeptID INT NOT NULL PRIMARY KEY CLUSTERED
, DeptName VARCHAR(50) NOT NULL
, ManagerID INT NULL
, ParentDeptID INT NULL
, SysStartTime DATETIME2 GENERATED ALWAYS AS ROW START NOT NULL
, SysEndTime DATETIME2 GENERATED ALWAYS AS ROW END NOT NULL
, PERIOD FOR SYSTEM_TIME (SysStartTime, SysEndTime)
-- ) WITH (SYSTEM_VERSIONING = ON);
) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.________TestHistory));
Now I can insert, update and delete values.
But the values do only get historized on an update or a delete.
Is there a way to historize the insert-statements, too ?
I want to avoid having to write a trigger for that, if at all possible...
Having the data in two tables is extremely disadvantageous, because then i need to create a union first, and miss out on any field created afterwards, or I have potentially garbage columns if I use SELECT * and create a union-view, if the column-order gets altered, plus new columns would be missing, too.
Hmmm, those temporal tables seem to be really stupid(ly made)...
Apparently, they historize every potential change, even if there's no change in the data.
So I can do this:
CREATE TRIGGER TR_________Test_AfterInsert ON dbo.________Test
AFTER INSERT
AS
BEGIN
UPDATE dbo.________Test
SET DeptID = DeptID
WHERE DeptID IN
(
SELECT DeptID
FROM inserted
)
END
And the entries will swiftly be historized after insert.
On the other hand, this is nice, since then I don't have to manually synchronize the table-schema with the history-table-schema.
I have the following table:
==OFFICES==
OFFICE INTEGER NOT NULL,
CITY VARCHAR(15) NOT NULL,
REGION VARCHAR(15) NOT NULL,
DIR INTEGER,
OBJECTIVE MONEY,
SALES MONEY
I have to define a trigger named TRIGGER_EX3. Whenever a new row is inserted into OFFICES, it stores into a table named TCONTROL_OFFICES the following values:
==TCONTROL_OFFICES==
INSERTION_DATE DATETIME, --The day the office was inserted
OPERATION VARCHAR(15), --The name of the operation (inserted)
OFFICE INTEGER, --The id of the office (The value of OFFICE in OFFICES)
THE_USER VARCHAR(30) --Name of the user
My idea was to use a while loop (or for loop) to insert the first row of INSERTED into TCONTROL_OFFICES and then delete that same row from INSERTED and repeat until INSERTED was empty. However, it seems I can't modify INSERTED. Regardless, it still seemed like a clumsy approach. Is there any better way of doing this?
This is what I have so far:
USE EMPLOYEES
GO
IF(OBJECT_ID ('TRIGGER_EX3', 'TR') IS NOT NULL)
BEGIN
DROP TRIGGER TRIGGER_EX3;
END
GO
CREATE TRIGGER TRIGGER_EX3 ON OFFICES
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON
--CHECK IF THE TABLE EXISTS
IF EXISTS (SELECT *
FROM SYS.TABLES
WHERE NAME = 'TCONTROL_OFFICES') --IT DOES
BEGIN
PRINT 'IT EXISTS';
END
ELSE --IT DOESNT
BEGIN
CREATE TABLE TCONTROL_OFFICES
(INSERTION_DATE DATETIME,
OPERATION VARCHAR(15),
OFFICE INTEGER,
THE_USER VARCHAR(30));
END
WHILE((SELECT COUNT(*) FROM INSERTED) > 0)
BEGIN
INSERT INTO TCONTROL_OFFICES
VALUES(GETDATE(), 'INSERT', (SELECT OFFICE FROM INSERTED), SUSER_SNAME())
DELETE TOP (1) FROM INSERTED
END
END
Keep in mind that the insertion can be of 3 rows but it may also be of 100+ so I cannot write 1 by 1 every row I want to insert.
Simply do an INSERT ... SELECT. You don't need any loop at all.
INSERT INTO tcontrol_offices
(insertion_date,
operation,
office,
the_user)
SELECT getdate(),
'INSERT',
office,
suser_sname()
FROM inserted;
Using Azure SQL Server database. I have a few tables partitioned on a 90 day date boundary. We have a stored procedure to shift data to maintain the proper partition breakpoint/range. I'm using a small function to provide the proper date breakpoint for my queries so I don't have to constantly update all my views.
But just by virtue of using that function in my queries, partitioning is ignored. Do I have no choice but to put hard-coded values in my queries everywhere and constantly modify them?
Here is a sample that reproduces the problem.
Update: After changing the PartitionDate function below according to the marked answer, it was fine for a short time (partition elimination occurred). Then, queries started sucking again. When I ran simple queries filtered by the date function, partitions were no longer eliminated.
------------------------------- setup
-- Create functions PartitionDate and PartitionQueryDate
create function PartitionDate() returns date as
begin
return GETDATE() - 91 -- returns 1/4/2019 today
end
go
create function PartitionQueryDate() returns date as
begin
return GETDATE() - 90 -- returns 1/5/2019
end
go
-- Create partition func and scheme using above functions
CREATE PARTITION FUNCTION order_pf (smalldatetime) AS RANGE RIGHT FOR VALUES (dbo.PartitionDate())
CREATE PARTITION SCHEME order_ps AS PARTITION order_pf ALL TO ([PRIMARY])
-- Create Order (pk, OrderDate, Fk), Customer (pk) tables. Order is partitioned
create table Customer
(
id int primary key identity(1,1),
FirstName varchar(255) not null
)
create table [Order]
(
id int identity(1,1), OrderDate smalldatetime not null,
CustomerId int not null,
CONSTRAINT [FK_Orders_Customer] FOREIGN KEY ([CustomerId]) REFERENCES Customer([id])
) on order_ps(OrderDate);
-- Add in indexes to Order: only OrderDate on the partition func
CREATE CLUSTERED INDEX [Order_OrderDate] ON [Order]([OrderDate] ASC) ON [order_ps] ([OrderDate]);
CREATE NONCLUSTERED INDEX [FK_Order_Customer] ON [Order](CustomerId, OrderDate) ON [order_ps] ([OrderDate]) -- seems to work the same with or without the partition reference.
go
-- Add some data before and after the partition break
insert Customer values ('bob')
insert [Order] values('12-31-2018', SCOPE_IDENTITY())
insert Customer values ('hank')
insert [Order] values('1-6-2019', SCOPE_IDENTITY())
---------------------------- test
-- verify a row per partition:
SELECT $PARTITION.order_pf(OrderDate) as Partition_Number, COUNT(*) as Row_Count
FROM [Order]
GROUP BY $PARTITION.order_pf(OrderDate)
-- Simple queries with actual execution plan turned on. The queries are logically equivalent.
select COUNT(1) from [Order] where OrderDate > '1-5-2019' -- Index seek Order_OrderDate; actual partition count 1
select COUNT(1) from [Order] where OrderDate > dbo.PartitionQueryDate() -- Index seek Order_OrderDate; actual partition count 2
-- Cleanup
drop table if exists [Order]
drop table if exists Customer
drop partition scheme order_ps
drop partition function order_pf
drop function if exists PartitionDate
drop function if exists PartitionQueryDate
One workaround would be to assign the function result to a variable first.
declare #pqd smalldatetime = dbo.PartitionQueryDate();
select COUNT(1) from [Order] where OrderDate > #pqd
Another option would be to use an inline TVF
CREATE FUNCTION dbo.PartitionQueryDateTVF ()
RETURNS TABLE
AS
RETURN
(
SELECT CAST(CAST( GETDATE() - 90 AS DATE) AS SMALLDATETIME) AS Date
)
GO
SELECT COUNT(1) from [Order] where OrderDate > (SELECT Date FROM dbo.PartitionQueryDateTVF())
This may be something that is improved with inline scalar UDFs but I'm not in a position to test this at the moment
I'm trying to set the ValidFrom range for the current record in a temporal table. I'm doing this because I'm rebuilding history from another system (non SQL) into a data warehouse so the current version of records may be "as of" a date that's in the past. If I can't get this to work, my fall back is to add a row in the history table that fills in the gap but I'm thinking there's a way to get this to work. Maybe there are some ways with alter columns?
/******** CURRENT TIME=2018-03-10 15:32:26 *****/
CREATE TABLE TestHist(
ID int NOT NULL,
Name varchar(max),
--Temporal Stuff
ValidFrom datetime2(7) NOT NULL,
ValidTo datetime2(7) NOT NULL
)
GO
CREATE TABLE Test(
ID int IDENTITY(1,1) NOT NULL,
Name varchar(max),
--Temporal Stuff
ValidFrom datetime2(7) GENERATED ALWAYS AS ROW START NOT NULL,
ValidTo datetime2(7) GENERATED ALWAYS AS ROW END NOT NULL,
PRIMARY KEY CLUSTERED (ID ASC) ,
PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)
)
WITH( SYSTEM_VERSIONING = ON ( HISTORY_TABLE = dbo.TestHist ) )
GO
ALTER TABLE Test SET (SYSTEM_VERSIONING = OFF)
go
--THIS WORKS BUT SETS THE VALIDFROM TO CURRENT TIME
insert into Test(name) values ('fred')
--AND BTW, THIS IS HOW I LOAD THE HISTORY (THIS WORKS TOO)
insert into TestHist(ID,Name,ValidFrom,ValidTo) values (1,'joe',null,'1/1/18','1/15/18')
insert into TestHist(ID,Name,ValidFrom,ValidTo) values (1,'steve','fred','2/1/18','3/1/18')
But the problem is that it sets the current ValidFrom time arbitrarily to when you do your insert statement:
select * from test
ID Name ParentName ValidFrom ValidTo
1 fred NULL 2018-03-10 15:32:26.4403041 9999-12-31 23:59:59.9999999
And here's what I wish I could do:
--THIS DOESN'T WORK
insert into Test(name,ValidFrom,ValidTo) values ('fred','2/1/18','9999-12-31 23:59:59.997')
I get this error:
Msg 13536, Level 16, State 1, Line 38
Cannot insert an explicit value into a GENERATED ALWAYS column in table 'CodeAnalytics.dbo.Test'. Use INSERT with a column list to exclude the GENERATED ALWAYS column, or insert a DEFAULT into GENERATED ALWAYS column.
You cannot update ValidFrom on the temporal table. However, you can update ValidFrom on the history table that keeps track of changes. You create a record there by changing any value in the temporal table.
So you can do following steps:
Change anything in rows of your temporal table where you want to change the value of the ValidFrom column. This step creates a record in the history table for every changed record in the original table.
Set system versioning off for your temporal table.
Update ValidFrom in your history table.
Set system versioning back on for your temporal table.
Edit: oops. it's 2019 now. anyway, i needed to do this, so leaving here in case anyone else finds useful.
maybe something like this is what you're looking for?
CREATE TABLE TestHist(
ID int NOT NULL,
Name varchar(max),
--Temporal Stuff
ValidFrom datetime2(7) NOT NULL,
ValidTo datetime2(7) NOT NULL
)
GO
CREATE TABLE Test(
ID int IDENTITY(1,1) NOT NULL,
Name varchar(max),
--Temporal Stuff
ValidFrom datetime2(7) GENERATED ALWAYS AS ROW START NOT NULL,
ValidTo datetime2(7) GENERATED ALWAYS AS ROW END NOT NULL,
PRIMARY KEY CLUSTERED (ID ASC) ,
PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo)
)
WITH( SYSTEM_VERSIONING = ON ( HISTORY_TABLE = dbo.TestHist ) )
GO
ALTER TABLE Test SET (SYSTEM_VERSIONING = OFF);
insert into TestHist(ID,Name,ValidFrom,ValidTo) values (1,'steve','2/1/18','3/1/18')
insert into TestHist(ID,Name,ValidFrom,ValidTo) values (1,'joe','1/1/18','1/15/18')
SET IDENTITY_INSERT Test ON;
insert into Test(id, name) values (1, 'fred')
--after dropping period one can update validfrom on temporal table from max history
alter table Test DROP PERIOD FOR SYSTEM_TIME;
GO
update test set validfrom =(select max(validto) from TestHist where id=test.ID);
--now add period and turn system versioning back on
alter table test ADD PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo);
GO
alter table test set (system_versioning = on (HISTORY_TABLE=dbo.TestHist));
GO
--think this gives what you want
select * from test for system_time all
CREATE TABLE [schema].[table] (
[column1] int IDENTITY NOT NULL,
[column2] int NULL,
[column3] int NULL,
PRIMARY KEY CLUSTERED ([column1])
);
INSERT INTO schema.table (column2,column3) VALUES (1,1);
SELECT scope_identity();
it inserts TWO identical rows, and returns the primary key for the second inserted row.
It is probably a very basic reason, but google is not my friend on this one.
Please copy and paste verbatim
SET NOCOUNT ON;
USE tempdb;
CREATE TABLE dbo.[table] (
[column1] int IDENTITY NOT NULL,
[column2] int NULL,
[column3] int NULL,
PRIMARY KEY CLUSTERED ([column1])
);
INSERT INTO dbo.[table] (column2,column3) VALUES (1,1);
SELECT scope_identity();
SELECT * FROM dbo.[table]
You should get
---------------------------------------
1
column1 column2 column3
----------- ----------- -----------
1 1 1
When in doubt, always try on a clean new table in tempdb.
Other notes:
If you are running insert from ASP.Net, check whether you have CSS elements (background image link) that is blank, it causes a 2nd request to the same page
If you are running just a plain INSERT in SSMS or similar tool, check for triggers
To find triggers against a table using TSQL
select name, OBJECT_NAME(parent_object_id)
from sys.objects
where type='TR'
and OBJECT_NAME(parent_object_id) = 'table' -- or whatever the table name is
To view the text of a trigger (or any module) using TSQL
select definition
from sys.sql_modules
where object_id = object_id('tg_table') -- or whatever the trigger is named