SQL Server 2016 - Temporal Table - how to identify the user
up vote
5
down vote
favorite
Is it possible to get the information about the user/connection that modified data that is in the historical table? I read about the audit scenario where I can use temporal tables and that it's possible to detect who has changed the data. But how can I do that?
sql-server sql-server-2016
|
show 1 more comment
up vote
5
down vote
favorite
Is it possible to get the information about the user/connection that modified data that is in the historical table? I read about the audit scenario where I can use temporal tables and that it's possible to detect who has changed the data. But how can I do that?
sql-server sql-server-2016
1
This isn't really for auditing. If you think about it, when a user modifies a row, the row in the base table is the one that user changed. So you couldn't capture the user name of the person who caused a history row to be created, they could be updating a row that I updated yesterday, and therefore the row that moves to the history table represents the changes I made yesterday. So you would need to have a column in the base table that gets updated using a trigger, but this will create two versions of the history row (one with the previous editor, and one with the new editor).
– Aaron Bertrand
Jan 12 '17 at 1:17
And no, you can't sneak around this with INSTEAD OF triggers. I tried it, and there was a loophole, but I reported it, and they fixed it.
– Aaron Bertrand
Jan 12 '17 at 1:20
Thx for the hints. I read this german/english msdn article msdn.microsoft.com/en-us/library/mt631669.aspx Data Audit "Use temporal system-versioning on tables that store critical information for which you need to keep track of what has changed, when and by whom, and to perform data forensics at any point in time." I use currently a solution with a usercolumn and cdc. I thought that i can replace this solution with a temporal table. But it looks like that i must wait for the next update.
– user1481065
Jan 12 '17 at 18:50
Yeah, that article does mention auditing data, but that isn't what I meant by auditing. Other than a hand-wavy "by whom," it doesn't mention anything explicit at all about auditing the user who made the change.
– Aaron Bertrand
Jan 12 '17 at 18:53
And the documentation has now been updated - notice that it no longer makes any kind of statement about "by whom."
– Aaron Bertrand
Jan 13 '17 at 19:00
|
show 1 more comment
up vote
5
down vote
favorite
up vote
5
down vote
favorite
Is it possible to get the information about the user/connection that modified data that is in the historical table? I read about the audit scenario where I can use temporal tables and that it's possible to detect who has changed the data. But how can I do that?
sql-server sql-server-2016
Is it possible to get the information about the user/connection that modified data that is in the historical table? I read about the audit scenario where I can use temporal tables and that it's possible to detect who has changed the data. But how can I do that?
sql-server sql-server-2016
sql-server sql-server-2016
edited Jun 7 '17 at 20:16
skeletank
2,09453764
2,09453764
asked Jan 11 '17 at 23:20
user1481065
2001113
2001113
1
This isn't really for auditing. If you think about it, when a user modifies a row, the row in the base table is the one that user changed. So you couldn't capture the user name of the person who caused a history row to be created, they could be updating a row that I updated yesterday, and therefore the row that moves to the history table represents the changes I made yesterday. So you would need to have a column in the base table that gets updated using a trigger, but this will create two versions of the history row (one with the previous editor, and one with the new editor).
– Aaron Bertrand
Jan 12 '17 at 1:17
And no, you can't sneak around this with INSTEAD OF triggers. I tried it, and there was a loophole, but I reported it, and they fixed it.
– Aaron Bertrand
Jan 12 '17 at 1:20
Thx for the hints. I read this german/english msdn article msdn.microsoft.com/en-us/library/mt631669.aspx Data Audit "Use temporal system-versioning on tables that store critical information for which you need to keep track of what has changed, when and by whom, and to perform data forensics at any point in time." I use currently a solution with a usercolumn and cdc. I thought that i can replace this solution with a temporal table. But it looks like that i must wait for the next update.
– user1481065
Jan 12 '17 at 18:50
Yeah, that article does mention auditing data, but that isn't what I meant by auditing. Other than a hand-wavy "by whom," it doesn't mention anything explicit at all about auditing the user who made the change.
– Aaron Bertrand
Jan 12 '17 at 18:53
And the documentation has now been updated - notice that it no longer makes any kind of statement about "by whom."
– Aaron Bertrand
Jan 13 '17 at 19:00
|
show 1 more comment
1
This isn't really for auditing. If you think about it, when a user modifies a row, the row in the base table is the one that user changed. So you couldn't capture the user name of the person who caused a history row to be created, they could be updating a row that I updated yesterday, and therefore the row that moves to the history table represents the changes I made yesterday. So you would need to have a column in the base table that gets updated using a trigger, but this will create two versions of the history row (one with the previous editor, and one with the new editor).
– Aaron Bertrand
Jan 12 '17 at 1:17
And no, you can't sneak around this with INSTEAD OF triggers. I tried it, and there was a loophole, but I reported it, and they fixed it.
– Aaron Bertrand
Jan 12 '17 at 1:20
Thx for the hints. I read this german/english msdn article msdn.microsoft.com/en-us/library/mt631669.aspx Data Audit "Use temporal system-versioning on tables that store critical information for which you need to keep track of what has changed, when and by whom, and to perform data forensics at any point in time." I use currently a solution with a usercolumn and cdc. I thought that i can replace this solution with a temporal table. But it looks like that i must wait for the next update.
– user1481065
Jan 12 '17 at 18:50
Yeah, that article does mention auditing data, but that isn't what I meant by auditing. Other than a hand-wavy "by whom," it doesn't mention anything explicit at all about auditing the user who made the change.
– Aaron Bertrand
Jan 12 '17 at 18:53
And the documentation has now been updated - notice that it no longer makes any kind of statement about "by whom."
– Aaron Bertrand
Jan 13 '17 at 19:00
1
1
This isn't really for auditing. If you think about it, when a user modifies a row, the row in the base table is the one that user changed. So you couldn't capture the user name of the person who caused a history row to be created, they could be updating a row that I updated yesterday, and therefore the row that moves to the history table represents the changes I made yesterday. So you would need to have a column in the base table that gets updated using a trigger, but this will create two versions of the history row (one with the previous editor, and one with the new editor).
– Aaron Bertrand
Jan 12 '17 at 1:17
This isn't really for auditing. If you think about it, when a user modifies a row, the row in the base table is the one that user changed. So you couldn't capture the user name of the person who caused a history row to be created, they could be updating a row that I updated yesterday, and therefore the row that moves to the history table represents the changes I made yesterday. So you would need to have a column in the base table that gets updated using a trigger, but this will create two versions of the history row (one with the previous editor, and one with the new editor).
– Aaron Bertrand
Jan 12 '17 at 1:17
And no, you can't sneak around this with INSTEAD OF triggers. I tried it, and there was a loophole, but I reported it, and they fixed it.
– Aaron Bertrand
Jan 12 '17 at 1:20
And no, you can't sneak around this with INSTEAD OF triggers. I tried it, and there was a loophole, but I reported it, and they fixed it.
– Aaron Bertrand
Jan 12 '17 at 1:20
Thx for the hints. I read this german/english msdn article msdn.microsoft.com/en-us/library/mt631669.aspx Data Audit "Use temporal system-versioning on tables that store critical information for which you need to keep track of what has changed, when and by whom, and to perform data forensics at any point in time." I use currently a solution with a usercolumn and cdc. I thought that i can replace this solution with a temporal table. But it looks like that i must wait for the next update.
– user1481065
Jan 12 '17 at 18:50
Thx for the hints. I read this german/english msdn article msdn.microsoft.com/en-us/library/mt631669.aspx Data Audit "Use temporal system-versioning on tables that store critical information for which you need to keep track of what has changed, when and by whom, and to perform data forensics at any point in time." I use currently a solution with a usercolumn and cdc. I thought that i can replace this solution with a temporal table. But it looks like that i must wait for the next update.
– user1481065
Jan 12 '17 at 18:50
Yeah, that article does mention auditing data, but that isn't what I meant by auditing. Other than a hand-wavy "by whom," it doesn't mention anything explicit at all about auditing the user who made the change.
– Aaron Bertrand
Jan 12 '17 at 18:53
Yeah, that article does mention auditing data, but that isn't what I meant by auditing. Other than a hand-wavy "by whom," it doesn't mention anything explicit at all about auditing the user who made the change.
– Aaron Bertrand
Jan 12 '17 at 18:53
And the documentation has now been updated - notice that it no longer makes any kind of statement about "by whom."
– Aaron Bertrand
Jan 13 '17 at 19:00
And the documentation has now been updated - notice that it no longer makes any kind of statement about "by whom."
– Aaron Bertrand
Jan 13 '17 at 19:00
|
show 1 more comment
4 Answers
4
active
oldest
votes
up vote
5
down vote
A seemingly watertight auditing solution, which gives the name of the logged-in user who made each change (and a great improvement on my previous answer on this page):
SELECT
e.EmployeeID, e.FirstName, e.Score,
COALESCE (eh.LoggedInUser, o.CreatedBy, e.CreatedBy) AS CreatedOrModifiedBy,
e.ValidFromUTC, e.ValidToUTC
FROM dbo.Employees FOR SYSTEM_TIME ALL AS e
LEFT JOIN dbo.EmployeeHistory AS eh -- history table
ON e.EmployeeID = eh.EmployeeID AND e.ValidFromUTC = eh.ValidToUTC
OUTER APPLY
(SELECT TOP 1 CreatedBy
FROM dbo.EmployeeHistory
WHERE EmployeeID = e.EmployeeID
ORDER BY ValidFromUTC ASC) AS o -- oldest history record
WHERE e.EmployeeID = 1
ORDER BY e.ValidFromUTC
- Does not use triggers or user defined functions
- Requires small changes to the table
NB: Note that SQL Server always uses UTC, not local time, for time stamps in temporal tables.
Explanation:
Two fields are added to the main and history tables:
- To record the name of the user who created the record - a normal SQL default:
CreatedBy NVARCHAR(128) NOT NULL DEFAULT (SUSER_SNAME())
- To record the name of the current logged in user at any time. A computed column:
LoggedInUser AS (SUSER_SNAME())
When a record is inserted into the main table, SQL Server does not insert anything into the history table. But the field CreatedBy
records who created the record, because of the default constraint. But if/when the record gets updated, SQL Server inserts a record into the associated history table. The key idea here is that the name of the logged-in user who made the change is recorded into the history table, i.e. the contents of field LoggedInUser
in the main table (which always contains the name of who is logged in to the connection) is saved into the field LoggedInUser
in the history table.
That's almost what we want, but not quite - it's one change behind. E.g. if user Dave inserted the record, but user Andrew made the first update, "Andrew" is recorded as the user name in the history table, next to the original contents of the record that Dave inserted. However, all the information is there - it just needs to be unravelled. Joining the system generated fields for ROW START and ROW END, we get the user who made the change (from the previous record in the history table). However, there's no record in the history table for the originally inserted version of the record. In that case we retrieve the CreatedBy
field.
This seems to provide a watertight auditing solution. Even if a user edits the field CreatedBy
, the edit will be recorded in the history table. For that reason, we recover the oldest value for CreatedBy
from the history table, instead of the current value from the main table.
Deleted records
The query above does not show who deleted records from the main table. This can be retrieved using the following (could be simplified?):
SELECT
d.EmployeeID, d.LoggedInUser AS DeletedBy,
d.CreatedBy, d.ValidFromUTC, d.ValidToUTC AS DeletedAtUTC
FROM
(SELECT EmployeeID FROM dbo.EmployeeHistory GROUP BY EmployeeID) AS eh -- list of IDs
OUTER APPLY
(SELECT TOP 1 * FROM dbo.EmployeeHistory
WHERE EmployeeID = eh.EmployeeID
ORDER BY ValidFromUTC DESC) AS d -- last history record, which may be for DELETE
LEFT JOIN
dbo.Employees AS e
ON eh.EmployeeID = e.EmployeeID
WHERE e.EmployeeID IS NULL -- record is no longer in main table
Sample table script
The above examples are based on the table script (history table is created by SQL Server):
CREATE TABLE dbo.Employees(
EmployeeID INT /*IDENTITY(1,1)*/ NOT NULL,
FirstName NVARCHAR(40) NOT NULL,
Score INTEGER NULL,
LoggedInUser AS (SUSER_SNAME()),
CreatedBy NVARCHAR(128) NOT NULL DEFAULT (SUSER_SNAME()),
ValidFromUTC DATETIME2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL DEFAULT SYSUTCDATETIME(),
ValidToUTC DATETIME2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL DEFAULT CAST('9999-12-31 23:59:59.9999999' AS DATETIME2),
CONSTRAINT PK_Employees PRIMARY KEY CLUSTERED (EmployeeID ASC),
PERIOD FOR SYSTEM_TIME (ValidFromUTC, ValidToUTC)
)
WITH (SYSTEM_VERSIONING = ON ( HISTORY_TABLE = dbo.EmployeeHistory ))
Edit: (2018/11/19) Added default constraints against the system_time fields, which is considered by some to be best practice, and helps if you're adding system-versioning to an existing table.
add a comment |
up vote
3
down vote
EDIT: See my much better answer elsewhere on this page
My solution does not need triggers. I have a computed column in the main table which always contains the logged in user, e.g.
CREATE TABLE dbo.Employees(
EmployeeID INT NOT NULL,
FirstName sysname NOT NULL,
ValidFrom DATETIME2(7) GENERATED ALWAYS AS ROW START NOT NULL,
ValidTo DATETIME2(7) GENERATED ALWAYS AS ROW END NOT NULL,
LoggedInUser AS (SUSER_SNAME()), --<<-- computed column
... etc.
The field LoggedInUser
always contains the name of the currently logged in user in every record, and is thus saved into the history table at the time any change was made to any record.
Of course, that's not very useful in the main table, as it doesn't show who made the last change for each record. But in the history table it gets frozen at the point the change was made, which is very useful, (although it records the user at the end of the period, not the start).
Note that as a computed column, LoggedInUser
must be nullable, and therefore the corresponding column in the history table must be as well.
Main (current) table:
History table:
Of course in the history table, it records who changed the record from that state, not to that state, i.e. the logged in user at the end of the validity period. It works for deletes as well, but the SQL Server temporal table system does not insert a record in the history table for inserts.
Any ideas about how to improve this would be welcome, e.g. how to record who made the change at the start of each validity period in the history table. I have an idea involving another calculated field in the main table, which uses a UDF to get the user who made the last change in the history table.
Edit: I found a lot of inspiration from @Aaron Bertrand's excellent article here, which uses a trigger.
Thx for sharing your solution, that was very usefull for me.
– user1481065
Feb 9 at 6:56
add a comment |
up vote
2
down vote
In the current implementation of temporal tables, it records only time based information and nothing else about the session that made the change. And don't read that statement as me having some sort of insider knowledge that that situation may change in the future; I don't know anything about it. If you need that information, you will need to record it in row. A classic approach for doing that is to use a trigger that fires on DML operations and maintains that value on behalf of the user.
add a comment |
up vote
0
down vote
Another option that I was thinking about to solve this issue is to have a LastModifiedBy field in your base temporal table that is filled when the row is saved or updated.
This would show who modified the table and thus created the history record. As Aaron mentioned above, you could do it in a trigger, but my thought is to have it decided before it gets to the insert/update and put the value in the LastModifiedBy field at the time the record is updated.
This would then also be in the history table each time the record is modified.
This would work if they can control all data access through stored procedures and/or have access to the application code that issues the updates. Many can't.
– Aaron Bertrand
Jan 12 '17 at 18:57
2
I made a (not yet production ready) solution using a LastModifiedBy field and utilizing the new SESSION_CONTEXT feature. I created a default constraint on the field with the valueSESSION_CONTEXT('UserID')
which I set while opening the connection. It works OK but can be overwritten if someone explicitly states the value.
– fran.tonkovic
Jan 24 '17 at 15:19
add a comment |
4 Answers
4
active
oldest
votes
4 Answers
4
active
oldest
votes
active
oldest
votes
active
oldest
votes
up vote
5
down vote
A seemingly watertight auditing solution, which gives the name of the logged-in user who made each change (and a great improvement on my previous answer on this page):
SELECT
e.EmployeeID, e.FirstName, e.Score,
COALESCE (eh.LoggedInUser, o.CreatedBy, e.CreatedBy) AS CreatedOrModifiedBy,
e.ValidFromUTC, e.ValidToUTC
FROM dbo.Employees FOR SYSTEM_TIME ALL AS e
LEFT JOIN dbo.EmployeeHistory AS eh -- history table
ON e.EmployeeID = eh.EmployeeID AND e.ValidFromUTC = eh.ValidToUTC
OUTER APPLY
(SELECT TOP 1 CreatedBy
FROM dbo.EmployeeHistory
WHERE EmployeeID = e.EmployeeID
ORDER BY ValidFromUTC ASC) AS o -- oldest history record
WHERE e.EmployeeID = 1
ORDER BY e.ValidFromUTC
- Does not use triggers or user defined functions
- Requires small changes to the table
NB: Note that SQL Server always uses UTC, not local time, for time stamps in temporal tables.
Explanation:
Two fields are added to the main and history tables:
- To record the name of the user who created the record - a normal SQL default:
CreatedBy NVARCHAR(128) NOT NULL DEFAULT (SUSER_SNAME())
- To record the name of the current logged in user at any time. A computed column:
LoggedInUser AS (SUSER_SNAME())
When a record is inserted into the main table, SQL Server does not insert anything into the history table. But the field CreatedBy
records who created the record, because of the default constraint. But if/when the record gets updated, SQL Server inserts a record into the associated history table. The key idea here is that the name of the logged-in user who made the change is recorded into the history table, i.e. the contents of field LoggedInUser
in the main table (which always contains the name of who is logged in to the connection) is saved into the field LoggedInUser
in the history table.
That's almost what we want, but not quite - it's one change behind. E.g. if user Dave inserted the record, but user Andrew made the first update, "Andrew" is recorded as the user name in the history table, next to the original contents of the record that Dave inserted. However, all the information is there - it just needs to be unravelled. Joining the system generated fields for ROW START and ROW END, we get the user who made the change (from the previous record in the history table). However, there's no record in the history table for the originally inserted version of the record. In that case we retrieve the CreatedBy
field.
This seems to provide a watertight auditing solution. Even if a user edits the field CreatedBy
, the edit will be recorded in the history table. For that reason, we recover the oldest value for CreatedBy
from the history table, instead of the current value from the main table.
Deleted records
The query above does not show who deleted records from the main table. This can be retrieved using the following (could be simplified?):
SELECT
d.EmployeeID, d.LoggedInUser AS DeletedBy,
d.CreatedBy, d.ValidFromUTC, d.ValidToUTC AS DeletedAtUTC
FROM
(SELECT EmployeeID FROM dbo.EmployeeHistory GROUP BY EmployeeID) AS eh -- list of IDs
OUTER APPLY
(SELECT TOP 1 * FROM dbo.EmployeeHistory
WHERE EmployeeID = eh.EmployeeID
ORDER BY ValidFromUTC DESC) AS d -- last history record, which may be for DELETE
LEFT JOIN
dbo.Employees AS e
ON eh.EmployeeID = e.EmployeeID
WHERE e.EmployeeID IS NULL -- record is no longer in main table
Sample table script
The above examples are based on the table script (history table is created by SQL Server):
CREATE TABLE dbo.Employees(
EmployeeID INT /*IDENTITY(1,1)*/ NOT NULL,
FirstName NVARCHAR(40) NOT NULL,
Score INTEGER NULL,
LoggedInUser AS (SUSER_SNAME()),
CreatedBy NVARCHAR(128) NOT NULL DEFAULT (SUSER_SNAME()),
ValidFromUTC DATETIME2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL DEFAULT SYSUTCDATETIME(),
ValidToUTC DATETIME2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL DEFAULT CAST('9999-12-31 23:59:59.9999999' AS DATETIME2),
CONSTRAINT PK_Employees PRIMARY KEY CLUSTERED (EmployeeID ASC),
PERIOD FOR SYSTEM_TIME (ValidFromUTC, ValidToUTC)
)
WITH (SYSTEM_VERSIONING = ON ( HISTORY_TABLE = dbo.EmployeeHistory ))
Edit: (2018/11/19) Added default constraints against the system_time fields, which is considered by some to be best practice, and helps if you're adding system-versioning to an existing table.
add a comment |
up vote
5
down vote
A seemingly watertight auditing solution, which gives the name of the logged-in user who made each change (and a great improvement on my previous answer on this page):
SELECT
e.EmployeeID, e.FirstName, e.Score,
COALESCE (eh.LoggedInUser, o.CreatedBy, e.CreatedBy) AS CreatedOrModifiedBy,
e.ValidFromUTC, e.ValidToUTC
FROM dbo.Employees FOR SYSTEM_TIME ALL AS e
LEFT JOIN dbo.EmployeeHistory AS eh -- history table
ON e.EmployeeID = eh.EmployeeID AND e.ValidFromUTC = eh.ValidToUTC
OUTER APPLY
(SELECT TOP 1 CreatedBy
FROM dbo.EmployeeHistory
WHERE EmployeeID = e.EmployeeID
ORDER BY ValidFromUTC ASC) AS o -- oldest history record
WHERE e.EmployeeID = 1
ORDER BY e.ValidFromUTC
- Does not use triggers or user defined functions
- Requires small changes to the table
NB: Note that SQL Server always uses UTC, not local time, for time stamps in temporal tables.
Explanation:
Two fields are added to the main and history tables:
- To record the name of the user who created the record - a normal SQL default:
CreatedBy NVARCHAR(128) NOT NULL DEFAULT (SUSER_SNAME())
- To record the name of the current logged in user at any time. A computed column:
LoggedInUser AS (SUSER_SNAME())
When a record is inserted into the main table, SQL Server does not insert anything into the history table. But the field CreatedBy
records who created the record, because of the default constraint. But if/when the record gets updated, SQL Server inserts a record into the associated history table. The key idea here is that the name of the logged-in user who made the change is recorded into the history table, i.e. the contents of field LoggedInUser
in the main table (which always contains the name of who is logged in to the connection) is saved into the field LoggedInUser
in the history table.
That's almost what we want, but not quite - it's one change behind. E.g. if user Dave inserted the record, but user Andrew made the first update, "Andrew" is recorded as the user name in the history table, next to the original contents of the record that Dave inserted. However, all the information is there - it just needs to be unravelled. Joining the system generated fields for ROW START and ROW END, we get the user who made the change (from the previous record in the history table). However, there's no record in the history table for the originally inserted version of the record. In that case we retrieve the CreatedBy
field.
This seems to provide a watertight auditing solution. Even if a user edits the field CreatedBy
, the edit will be recorded in the history table. For that reason, we recover the oldest value for CreatedBy
from the history table, instead of the current value from the main table.
Deleted records
The query above does not show who deleted records from the main table. This can be retrieved using the following (could be simplified?):
SELECT
d.EmployeeID, d.LoggedInUser AS DeletedBy,
d.CreatedBy, d.ValidFromUTC, d.ValidToUTC AS DeletedAtUTC
FROM
(SELECT EmployeeID FROM dbo.EmployeeHistory GROUP BY EmployeeID) AS eh -- list of IDs
OUTER APPLY
(SELECT TOP 1 * FROM dbo.EmployeeHistory
WHERE EmployeeID = eh.EmployeeID
ORDER BY ValidFromUTC DESC) AS d -- last history record, which may be for DELETE
LEFT JOIN
dbo.Employees AS e
ON eh.EmployeeID = e.EmployeeID
WHERE e.EmployeeID IS NULL -- record is no longer in main table
Sample table script
The above examples are based on the table script (history table is created by SQL Server):
CREATE TABLE dbo.Employees(
EmployeeID INT /*IDENTITY(1,1)*/ NOT NULL,
FirstName NVARCHAR(40) NOT NULL,
Score INTEGER NULL,
LoggedInUser AS (SUSER_SNAME()),
CreatedBy NVARCHAR(128) NOT NULL DEFAULT (SUSER_SNAME()),
ValidFromUTC DATETIME2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL DEFAULT SYSUTCDATETIME(),
ValidToUTC DATETIME2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL DEFAULT CAST('9999-12-31 23:59:59.9999999' AS DATETIME2),
CONSTRAINT PK_Employees PRIMARY KEY CLUSTERED (EmployeeID ASC),
PERIOD FOR SYSTEM_TIME (ValidFromUTC, ValidToUTC)
)
WITH (SYSTEM_VERSIONING = ON ( HISTORY_TABLE = dbo.EmployeeHistory ))
Edit: (2018/11/19) Added default constraints against the system_time fields, which is considered by some to be best practice, and helps if you're adding system-versioning to an existing table.
add a comment |
up vote
5
down vote
up vote
5
down vote
A seemingly watertight auditing solution, which gives the name of the logged-in user who made each change (and a great improvement on my previous answer on this page):
SELECT
e.EmployeeID, e.FirstName, e.Score,
COALESCE (eh.LoggedInUser, o.CreatedBy, e.CreatedBy) AS CreatedOrModifiedBy,
e.ValidFromUTC, e.ValidToUTC
FROM dbo.Employees FOR SYSTEM_TIME ALL AS e
LEFT JOIN dbo.EmployeeHistory AS eh -- history table
ON e.EmployeeID = eh.EmployeeID AND e.ValidFromUTC = eh.ValidToUTC
OUTER APPLY
(SELECT TOP 1 CreatedBy
FROM dbo.EmployeeHistory
WHERE EmployeeID = e.EmployeeID
ORDER BY ValidFromUTC ASC) AS o -- oldest history record
WHERE e.EmployeeID = 1
ORDER BY e.ValidFromUTC
- Does not use triggers or user defined functions
- Requires small changes to the table
NB: Note that SQL Server always uses UTC, not local time, for time stamps in temporal tables.
Explanation:
Two fields are added to the main and history tables:
- To record the name of the user who created the record - a normal SQL default:
CreatedBy NVARCHAR(128) NOT NULL DEFAULT (SUSER_SNAME())
- To record the name of the current logged in user at any time. A computed column:
LoggedInUser AS (SUSER_SNAME())
When a record is inserted into the main table, SQL Server does not insert anything into the history table. But the field CreatedBy
records who created the record, because of the default constraint. But if/when the record gets updated, SQL Server inserts a record into the associated history table. The key idea here is that the name of the logged-in user who made the change is recorded into the history table, i.e. the contents of field LoggedInUser
in the main table (which always contains the name of who is logged in to the connection) is saved into the field LoggedInUser
in the history table.
That's almost what we want, but not quite - it's one change behind. E.g. if user Dave inserted the record, but user Andrew made the first update, "Andrew" is recorded as the user name in the history table, next to the original contents of the record that Dave inserted. However, all the information is there - it just needs to be unravelled. Joining the system generated fields for ROW START and ROW END, we get the user who made the change (from the previous record in the history table). However, there's no record in the history table for the originally inserted version of the record. In that case we retrieve the CreatedBy
field.
This seems to provide a watertight auditing solution. Even if a user edits the field CreatedBy
, the edit will be recorded in the history table. For that reason, we recover the oldest value for CreatedBy
from the history table, instead of the current value from the main table.
Deleted records
The query above does not show who deleted records from the main table. This can be retrieved using the following (could be simplified?):
SELECT
d.EmployeeID, d.LoggedInUser AS DeletedBy,
d.CreatedBy, d.ValidFromUTC, d.ValidToUTC AS DeletedAtUTC
FROM
(SELECT EmployeeID FROM dbo.EmployeeHistory GROUP BY EmployeeID) AS eh -- list of IDs
OUTER APPLY
(SELECT TOP 1 * FROM dbo.EmployeeHistory
WHERE EmployeeID = eh.EmployeeID
ORDER BY ValidFromUTC DESC) AS d -- last history record, which may be for DELETE
LEFT JOIN
dbo.Employees AS e
ON eh.EmployeeID = e.EmployeeID
WHERE e.EmployeeID IS NULL -- record is no longer in main table
Sample table script
The above examples are based on the table script (history table is created by SQL Server):
CREATE TABLE dbo.Employees(
EmployeeID INT /*IDENTITY(1,1)*/ NOT NULL,
FirstName NVARCHAR(40) NOT NULL,
Score INTEGER NULL,
LoggedInUser AS (SUSER_SNAME()),
CreatedBy NVARCHAR(128) NOT NULL DEFAULT (SUSER_SNAME()),
ValidFromUTC DATETIME2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL DEFAULT SYSUTCDATETIME(),
ValidToUTC DATETIME2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL DEFAULT CAST('9999-12-31 23:59:59.9999999' AS DATETIME2),
CONSTRAINT PK_Employees PRIMARY KEY CLUSTERED (EmployeeID ASC),
PERIOD FOR SYSTEM_TIME (ValidFromUTC, ValidToUTC)
)
WITH (SYSTEM_VERSIONING = ON ( HISTORY_TABLE = dbo.EmployeeHistory ))
Edit: (2018/11/19) Added default constraints against the system_time fields, which is considered by some to be best practice, and helps if you're adding system-versioning to an existing table.
A seemingly watertight auditing solution, which gives the name of the logged-in user who made each change (and a great improvement on my previous answer on this page):
SELECT
e.EmployeeID, e.FirstName, e.Score,
COALESCE (eh.LoggedInUser, o.CreatedBy, e.CreatedBy) AS CreatedOrModifiedBy,
e.ValidFromUTC, e.ValidToUTC
FROM dbo.Employees FOR SYSTEM_TIME ALL AS e
LEFT JOIN dbo.EmployeeHistory AS eh -- history table
ON e.EmployeeID = eh.EmployeeID AND e.ValidFromUTC = eh.ValidToUTC
OUTER APPLY
(SELECT TOP 1 CreatedBy
FROM dbo.EmployeeHistory
WHERE EmployeeID = e.EmployeeID
ORDER BY ValidFromUTC ASC) AS o -- oldest history record
WHERE e.EmployeeID = 1
ORDER BY e.ValidFromUTC
- Does not use triggers or user defined functions
- Requires small changes to the table
NB: Note that SQL Server always uses UTC, not local time, for time stamps in temporal tables.
Explanation:
Two fields are added to the main and history tables:
- To record the name of the user who created the record - a normal SQL default:
CreatedBy NVARCHAR(128) NOT NULL DEFAULT (SUSER_SNAME())
- To record the name of the current logged in user at any time. A computed column:
LoggedInUser AS (SUSER_SNAME())
When a record is inserted into the main table, SQL Server does not insert anything into the history table. But the field CreatedBy
records who created the record, because of the default constraint. But if/when the record gets updated, SQL Server inserts a record into the associated history table. The key idea here is that the name of the logged-in user who made the change is recorded into the history table, i.e. the contents of field LoggedInUser
in the main table (which always contains the name of who is logged in to the connection) is saved into the field LoggedInUser
in the history table.
That's almost what we want, but not quite - it's one change behind. E.g. if user Dave inserted the record, but user Andrew made the first update, "Andrew" is recorded as the user name in the history table, next to the original contents of the record that Dave inserted. However, all the information is there - it just needs to be unravelled. Joining the system generated fields for ROW START and ROW END, we get the user who made the change (from the previous record in the history table). However, there's no record in the history table for the originally inserted version of the record. In that case we retrieve the CreatedBy
field.
This seems to provide a watertight auditing solution. Even if a user edits the field CreatedBy
, the edit will be recorded in the history table. For that reason, we recover the oldest value for CreatedBy
from the history table, instead of the current value from the main table.
Deleted records
The query above does not show who deleted records from the main table. This can be retrieved using the following (could be simplified?):
SELECT
d.EmployeeID, d.LoggedInUser AS DeletedBy,
d.CreatedBy, d.ValidFromUTC, d.ValidToUTC AS DeletedAtUTC
FROM
(SELECT EmployeeID FROM dbo.EmployeeHistory GROUP BY EmployeeID) AS eh -- list of IDs
OUTER APPLY
(SELECT TOP 1 * FROM dbo.EmployeeHistory
WHERE EmployeeID = eh.EmployeeID
ORDER BY ValidFromUTC DESC) AS d -- last history record, which may be for DELETE
LEFT JOIN
dbo.Employees AS e
ON eh.EmployeeID = e.EmployeeID
WHERE e.EmployeeID IS NULL -- record is no longer in main table
Sample table script
The above examples are based on the table script (history table is created by SQL Server):
CREATE TABLE dbo.Employees(
EmployeeID INT /*IDENTITY(1,1)*/ NOT NULL,
FirstName NVARCHAR(40) NOT NULL,
Score INTEGER NULL,
LoggedInUser AS (SUSER_SNAME()),
CreatedBy NVARCHAR(128) NOT NULL DEFAULT (SUSER_SNAME()),
ValidFromUTC DATETIME2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL DEFAULT SYSUTCDATETIME(),
ValidToUTC DATETIME2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL DEFAULT CAST('9999-12-31 23:59:59.9999999' AS DATETIME2),
CONSTRAINT PK_Employees PRIMARY KEY CLUSTERED (EmployeeID ASC),
PERIOD FOR SYSTEM_TIME (ValidFromUTC, ValidToUTC)
)
WITH (SYSTEM_VERSIONING = ON ( HISTORY_TABLE = dbo.EmployeeHistory ))
Edit: (2018/11/19) Added default constraints against the system_time fields, which is considered by some to be best practice, and helps if you're adding system-versioning to an existing table.
edited Nov 19 at 19:01
answered Mar 23 at 17:08
DaveBoltman
621617
621617
add a comment |
add a comment |
up vote
3
down vote
EDIT: See my much better answer elsewhere on this page
My solution does not need triggers. I have a computed column in the main table which always contains the logged in user, e.g.
CREATE TABLE dbo.Employees(
EmployeeID INT NOT NULL,
FirstName sysname NOT NULL,
ValidFrom DATETIME2(7) GENERATED ALWAYS AS ROW START NOT NULL,
ValidTo DATETIME2(7) GENERATED ALWAYS AS ROW END NOT NULL,
LoggedInUser AS (SUSER_SNAME()), --<<-- computed column
... etc.
The field LoggedInUser
always contains the name of the currently logged in user in every record, and is thus saved into the history table at the time any change was made to any record.
Of course, that's not very useful in the main table, as it doesn't show who made the last change for each record. But in the history table it gets frozen at the point the change was made, which is very useful, (although it records the user at the end of the period, not the start).
Note that as a computed column, LoggedInUser
must be nullable, and therefore the corresponding column in the history table must be as well.
Main (current) table:
History table:
Of course in the history table, it records who changed the record from that state, not to that state, i.e. the logged in user at the end of the validity period. It works for deletes as well, but the SQL Server temporal table system does not insert a record in the history table for inserts.
Any ideas about how to improve this would be welcome, e.g. how to record who made the change at the start of each validity period in the history table. I have an idea involving another calculated field in the main table, which uses a UDF to get the user who made the last change in the history table.
Edit: I found a lot of inspiration from @Aaron Bertrand's excellent article here, which uses a trigger.
Thx for sharing your solution, that was very usefull for me.
– user1481065
Feb 9 at 6:56
add a comment |
up vote
3
down vote
EDIT: See my much better answer elsewhere on this page
My solution does not need triggers. I have a computed column in the main table which always contains the logged in user, e.g.
CREATE TABLE dbo.Employees(
EmployeeID INT NOT NULL,
FirstName sysname NOT NULL,
ValidFrom DATETIME2(7) GENERATED ALWAYS AS ROW START NOT NULL,
ValidTo DATETIME2(7) GENERATED ALWAYS AS ROW END NOT NULL,
LoggedInUser AS (SUSER_SNAME()), --<<-- computed column
... etc.
The field LoggedInUser
always contains the name of the currently logged in user in every record, and is thus saved into the history table at the time any change was made to any record.
Of course, that's not very useful in the main table, as it doesn't show who made the last change for each record. But in the history table it gets frozen at the point the change was made, which is very useful, (although it records the user at the end of the period, not the start).
Note that as a computed column, LoggedInUser
must be nullable, and therefore the corresponding column in the history table must be as well.
Main (current) table:
History table:
Of course in the history table, it records who changed the record from that state, not to that state, i.e. the logged in user at the end of the validity period. It works for deletes as well, but the SQL Server temporal table system does not insert a record in the history table for inserts.
Any ideas about how to improve this would be welcome, e.g. how to record who made the change at the start of each validity period in the history table. I have an idea involving another calculated field in the main table, which uses a UDF to get the user who made the last change in the history table.
Edit: I found a lot of inspiration from @Aaron Bertrand's excellent article here, which uses a trigger.
Thx for sharing your solution, that was very usefull for me.
– user1481065
Feb 9 at 6:56
add a comment |
up vote
3
down vote
up vote
3
down vote
EDIT: See my much better answer elsewhere on this page
My solution does not need triggers. I have a computed column in the main table which always contains the logged in user, e.g.
CREATE TABLE dbo.Employees(
EmployeeID INT NOT NULL,
FirstName sysname NOT NULL,
ValidFrom DATETIME2(7) GENERATED ALWAYS AS ROW START NOT NULL,
ValidTo DATETIME2(7) GENERATED ALWAYS AS ROW END NOT NULL,
LoggedInUser AS (SUSER_SNAME()), --<<-- computed column
... etc.
The field LoggedInUser
always contains the name of the currently logged in user in every record, and is thus saved into the history table at the time any change was made to any record.
Of course, that's not very useful in the main table, as it doesn't show who made the last change for each record. But in the history table it gets frozen at the point the change was made, which is very useful, (although it records the user at the end of the period, not the start).
Note that as a computed column, LoggedInUser
must be nullable, and therefore the corresponding column in the history table must be as well.
Main (current) table:
History table:
Of course in the history table, it records who changed the record from that state, not to that state, i.e. the logged in user at the end of the validity period. It works for deletes as well, but the SQL Server temporal table system does not insert a record in the history table for inserts.
Any ideas about how to improve this would be welcome, e.g. how to record who made the change at the start of each validity period in the history table. I have an idea involving another calculated field in the main table, which uses a UDF to get the user who made the last change in the history table.
Edit: I found a lot of inspiration from @Aaron Bertrand's excellent article here, which uses a trigger.
EDIT: See my much better answer elsewhere on this page
My solution does not need triggers. I have a computed column in the main table which always contains the logged in user, e.g.
CREATE TABLE dbo.Employees(
EmployeeID INT NOT NULL,
FirstName sysname NOT NULL,
ValidFrom DATETIME2(7) GENERATED ALWAYS AS ROW START NOT NULL,
ValidTo DATETIME2(7) GENERATED ALWAYS AS ROW END NOT NULL,
LoggedInUser AS (SUSER_SNAME()), --<<-- computed column
... etc.
The field LoggedInUser
always contains the name of the currently logged in user in every record, and is thus saved into the history table at the time any change was made to any record.
Of course, that's not very useful in the main table, as it doesn't show who made the last change for each record. But in the history table it gets frozen at the point the change was made, which is very useful, (although it records the user at the end of the period, not the start).
Note that as a computed column, LoggedInUser
must be nullable, and therefore the corresponding column in the history table must be as well.
Main (current) table:
History table:
Of course in the history table, it records who changed the record from that state, not to that state, i.e. the logged in user at the end of the validity period. It works for deletes as well, but the SQL Server temporal table system does not insert a record in the history table for inserts.
Any ideas about how to improve this would be welcome, e.g. how to record who made the change at the start of each validity period in the history table. I have an idea involving another calculated field in the main table, which uses a UDF to get the user who made the last change in the history table.
Edit: I found a lot of inspiration from @Aaron Bertrand's excellent article here, which uses a trigger.
edited Mar 23 at 17:10
answered Feb 1 at 13:29
DaveBoltman
621617
621617
Thx for sharing your solution, that was very usefull for me.
– user1481065
Feb 9 at 6:56
add a comment |
Thx for sharing your solution, that was very usefull for me.
– user1481065
Feb 9 at 6:56
Thx for sharing your solution, that was very usefull for me.
– user1481065
Feb 9 at 6:56
Thx for sharing your solution, that was very usefull for me.
– user1481065
Feb 9 at 6:56
add a comment |
up vote
2
down vote
In the current implementation of temporal tables, it records only time based information and nothing else about the session that made the change. And don't read that statement as me having some sort of insider knowledge that that situation may change in the future; I don't know anything about it. If you need that information, you will need to record it in row. A classic approach for doing that is to use a trigger that fires on DML operations and maintains that value on behalf of the user.
add a comment |
up vote
2
down vote
In the current implementation of temporal tables, it records only time based information and nothing else about the session that made the change. And don't read that statement as me having some sort of insider knowledge that that situation may change in the future; I don't know anything about it. If you need that information, you will need to record it in row. A classic approach for doing that is to use a trigger that fires on DML operations and maintains that value on behalf of the user.
add a comment |
up vote
2
down vote
up vote
2
down vote
In the current implementation of temporal tables, it records only time based information and nothing else about the session that made the change. And don't read that statement as me having some sort of insider knowledge that that situation may change in the future; I don't know anything about it. If you need that information, you will need to record it in row. A classic approach for doing that is to use a trigger that fires on DML operations and maintains that value on behalf of the user.
In the current implementation of temporal tables, it records only time based information and nothing else about the session that made the change. And don't read that statement as me having some sort of insider knowledge that that situation may change in the future; I don't know anything about it. If you need that information, you will need to record it in row. A classic approach for doing that is to use a trigger that fires on DML operations and maintains that value on behalf of the user.
answered Jan 12 '17 at 0:29
Ben Thul
22k32445
22k32445
add a comment |
add a comment |
up vote
0
down vote
Another option that I was thinking about to solve this issue is to have a LastModifiedBy field in your base temporal table that is filled when the row is saved or updated.
This would show who modified the table and thus created the history record. As Aaron mentioned above, you could do it in a trigger, but my thought is to have it decided before it gets to the insert/update and put the value in the LastModifiedBy field at the time the record is updated.
This would then also be in the history table each time the record is modified.
This would work if they can control all data access through stored procedures and/or have access to the application code that issues the updates. Many can't.
– Aaron Bertrand
Jan 12 '17 at 18:57
2
I made a (not yet production ready) solution using a LastModifiedBy field and utilizing the new SESSION_CONTEXT feature. I created a default constraint on the field with the valueSESSION_CONTEXT('UserID')
which I set while opening the connection. It works OK but can be overwritten if someone explicitly states the value.
– fran.tonkovic
Jan 24 '17 at 15:19
add a comment |
up vote
0
down vote
Another option that I was thinking about to solve this issue is to have a LastModifiedBy field in your base temporal table that is filled when the row is saved or updated.
This would show who modified the table and thus created the history record. As Aaron mentioned above, you could do it in a trigger, but my thought is to have it decided before it gets to the insert/update and put the value in the LastModifiedBy field at the time the record is updated.
This would then also be in the history table each time the record is modified.
This would work if they can control all data access through stored procedures and/or have access to the application code that issues the updates. Many can't.
– Aaron Bertrand
Jan 12 '17 at 18:57
2
I made a (not yet production ready) solution using a LastModifiedBy field and utilizing the new SESSION_CONTEXT feature. I created a default constraint on the field with the valueSESSION_CONTEXT('UserID')
which I set while opening the connection. It works OK but can be overwritten if someone explicitly states the value.
– fran.tonkovic
Jan 24 '17 at 15:19
add a comment |
up vote
0
down vote
up vote
0
down vote
Another option that I was thinking about to solve this issue is to have a LastModifiedBy field in your base temporal table that is filled when the row is saved or updated.
This would show who modified the table and thus created the history record. As Aaron mentioned above, you could do it in a trigger, but my thought is to have it decided before it gets to the insert/update and put the value in the LastModifiedBy field at the time the record is updated.
This would then also be in the history table each time the record is modified.
Another option that I was thinking about to solve this issue is to have a LastModifiedBy field in your base temporal table that is filled when the row is saved or updated.
This would show who modified the table and thus created the history record. As Aaron mentioned above, you could do it in a trigger, but my thought is to have it decided before it gets to the insert/update and put the value in the LastModifiedBy field at the time the record is updated.
This would then also be in the history table each time the record is modified.
answered Jan 12 '17 at 17:19
xDJR1875
19014
19014
This would work if they can control all data access through stored procedures and/or have access to the application code that issues the updates. Many can't.
– Aaron Bertrand
Jan 12 '17 at 18:57
2
I made a (not yet production ready) solution using a LastModifiedBy field and utilizing the new SESSION_CONTEXT feature. I created a default constraint on the field with the valueSESSION_CONTEXT('UserID')
which I set while opening the connection. It works OK but can be overwritten if someone explicitly states the value.
– fran.tonkovic
Jan 24 '17 at 15:19
add a comment |
This would work if they can control all data access through stored procedures and/or have access to the application code that issues the updates. Many can't.
– Aaron Bertrand
Jan 12 '17 at 18:57
2
I made a (not yet production ready) solution using a LastModifiedBy field and utilizing the new SESSION_CONTEXT feature. I created a default constraint on the field with the valueSESSION_CONTEXT('UserID')
which I set while opening the connection. It works OK but can be overwritten if someone explicitly states the value.
– fran.tonkovic
Jan 24 '17 at 15:19
This would work if they can control all data access through stored procedures and/or have access to the application code that issues the updates. Many can't.
– Aaron Bertrand
Jan 12 '17 at 18:57
This would work if they can control all data access through stored procedures and/or have access to the application code that issues the updates. Many can't.
– Aaron Bertrand
Jan 12 '17 at 18:57
2
2
I made a (not yet production ready) solution using a LastModifiedBy field and utilizing the new SESSION_CONTEXT feature. I created a default constraint on the field with the value
SESSION_CONTEXT('UserID')
which I set while opening the connection. It works OK but can be overwritten if someone explicitly states the value.– fran.tonkovic
Jan 24 '17 at 15:19
I made a (not yet production ready) solution using a LastModifiedBy field and utilizing the new SESSION_CONTEXT feature. I created a default constraint on the field with the value
SESSION_CONTEXT('UserID')
which I set while opening the connection. It works OK but can be overwritten if someone explicitly states the value.– fran.tonkovic
Jan 24 '17 at 15:19
add a comment |
Thanks for contributing an answer to Stack Overflow!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Some of your past answers have not been well-received, and you're in danger of being blocked from answering.
Please pay close attention to the following guidance:
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f41602579%2fsql-server-2016-temporal-table-how-to-identify-the-user%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
1
This isn't really for auditing. If you think about it, when a user modifies a row, the row in the base table is the one that user changed. So you couldn't capture the user name of the person who caused a history row to be created, they could be updating a row that I updated yesterday, and therefore the row that moves to the history table represents the changes I made yesterday. So you would need to have a column in the base table that gets updated using a trigger, but this will create two versions of the history row (one with the previous editor, and one with the new editor).
– Aaron Bertrand
Jan 12 '17 at 1:17
And no, you can't sneak around this with INSTEAD OF triggers. I tried it, and there was a loophole, but I reported it, and they fixed it.
– Aaron Bertrand
Jan 12 '17 at 1:20
Thx for the hints. I read this german/english msdn article msdn.microsoft.com/en-us/library/mt631669.aspx Data Audit "Use temporal system-versioning on tables that store critical information for which you need to keep track of what has changed, when and by whom, and to perform data forensics at any point in time." I use currently a solution with a usercolumn and cdc. I thought that i can replace this solution with a temporal table. But it looks like that i must wait for the next update.
– user1481065
Jan 12 '17 at 18:50
Yeah, that article does mention auditing data, but that isn't what I meant by auditing. Other than a hand-wavy "by whom," it doesn't mention anything explicit at all about auditing the user who made the change.
– Aaron Bertrand
Jan 12 '17 at 18:53
And the documentation has now been updated - notice that it no longer makes any kind of statement about "by whom."
– Aaron Bertrand
Jan 13 '17 at 19:00