SQL History table and triggers - sql

I have a requirement to keep a history of changes made to a specific table when there is an UPDATE called, but only care about specific columns.
So, I have created a History table:
CREATE TABLE [dbo].[SourceTable_History](
[SourceTable_HistoryID] [int] IDENTITY(1,1) NOT NULL,
[SourceTableID] [int] NOT NULL,
[EventDate] [date] NOT NULL,
[EventUser] [date] NOT NULL,
[ChangedColumn] VARCHAR(50) NOT NULL,
[PreviousValue] VARCHAR(100) NULL,
[NewValue] VARCHAR(100) NULL
CONSTRAINT pk_SourceTable_History PRIMARY KEY ([SourceTable_HistoryID]),
CONSTRAINT fk_SourceTable_HistoryID_History_Source FOREIGN KEY ([SourceTableID]) REFERENCES SourceTable (SourceTableId)
)
Abd my plan is to create an Update trigger on the SourceTable. The business only cares about changes to certain columns, so, in psudo code, I was planning to do something like
If source.Start <> new.start
Insert into history (PrimaryKey, EventDate, EventUser, ColumnName, OldValue, NewValue)
(PK, GETDATYE(), updateuser, "StartDate", old.value, new.value)
And there would be a block like that per column we want history on.
We're NOT allowed to use CDC, so we have to roll our own, and this is my plan so far.
Does this seem a suitable plan?
There are 7 tables we need to monitor, with a column count of between 2 and 5 columns per table.
I just need to work out how to get a trigger to first comapr the before and after values of a specific columnm and then write a new row.
I thought it was something as simple as:
CREATE TRIGGER tr_PersonInCareSupportNeeds_History
ON PersonInCareSupportNeeds
FOR UPDATE
AS
BEGIN
IF(inserted.StartDate <> deleted.StartDate)
BEGIN
INSERT INTO [dbo].[PersonInCareSupportNeeds_History]
([PersonInCareSupportNeedsID], [EventDate], [EventUser], [ChangedColumn], [PreviousValue], [NewValue])
VALUES
(inserted.[PersonInCareSupportNeedsID], GETDATE(), [LastUpdateUser], 'StartDate', deleted.[StartDate], deleted.[StartDate])
END
END

We have trigger based auditing system and we basically created it by analyzing how third party tool for generating audit triggers ApexSQL Audit creates triggers and manages storage and developed our own system based on that.
I think your solution is generally ok but that you need to think about modifying storage a bit and plan for scaling.
What if business decides to keep track of all columns in all tables? What if they decide to track inserts and deletes as well? Will your solution be able to accommodate this?
Storage: Use two tables to hold your data. One table for holding all info about transactions (when, who, application name, table name, schema name, affected rows, etc… ). And another table to hold the actual data (before and after values, primary key, etc..).
Triggers: We ended up with a template for insert, update and delete triggers and very simple C# app where we enter tables and columns so application outputs DDL. This saved us a lot of time.

Depending on your requirments, I think history tables should mirror the table you want to capture, plus the extra audit details (who, when, why).
That can make it easier to use the same existing logic (sql, data classes, screens etc) to view historical data.
With your design getting the data in will be ok, but how easy will it be to pull the data out in a usable format?

Well I think that your idea is not so bad. Actually, I have the similar system in production. I will not give you my complete code (with acynchronious history saving), but I could give you some guidelines.
The main idea is to turn your data from relational model into Entity-Attribute-Value model. Also we want our triggers to be as much general as we can, that means - do not write column names explicitly. This could be done by different ways, but the most general I know in SQL Server is to use FOR XML and then select from xml:
declare #Data xml
select #Data = (select * from Test for xml raw('Data'))
select
T.C.value('../#ID', 'bigint') as ID,
T.C.value('local-name(.)', 'nvarchar(128)') as Name,
T.C.value('.', 'nvarchar(max)') as Value
from #Data.nodes('Data/#*') as T(C)
SQL FIDDLE EXAMPLE
To get different rows of two tables, you could use EXCEPT:
select * from Test1 except select * from Test2
union all
select * from Test2 except select * from Test1
SQL FIDDLE EXAMPLE
and, finally, your trigger could be something like this:
create trigger utr_Test_History on Test
after update
as
begin
declare #Data_Inserted xml, #Data_Deleted xml
select #Data_Inserted =
(
select *
from (select * from inserted except select * from deleted) as a
for xml raw('Data')
)
select #Data_Deleted =
(
select *
from (select * from deleted except select * from inserted) as a
for xml raw('Data')
)
;with CTE_Inserted as (
select
T.C.value('../#ID', 'bigint') as ID,
T.C.value('local-name(.)', 'nvarchar(128)') as Name,
T.C.value('.', 'nvarchar(max)') as Value
from #Data_Inserted.nodes('Data/#*') as T(C)
), CTE_Deleted as (
select
T.C.value('../#ID', 'bigint') as ID,
T.C.value('local-name(.)', 'nvarchar(128)') as Name,
T.C.value('.', 'nvarchar(max)') as Value
from #Data_Deleted.nodes('Data/#*') as T(C)
)
insert into History (Table_Name, Record_ID, Event_Date, Event_User, Column_Name, Value_Old, Value_New)
select 'Test', isnull(I.ID, D.ID), getdate(), system_user, isnull(D.Name, I.Name), D.Value, I.Value
from CTE_Inserted as I
full outer join CTE_Deleted as D on D.ID = I.ID and D.Name = I.Name
where
not
(
I.Value is null and D.Value is null or
I.Value is not null and D.Value is not null and I.Value = D.Value
)
end
SQL FIDDLE EXAMPLE

Related

How can I auto increment the primary key in SQL Server 2016 merge insert without sequences?

I am writing a query to import data from one table to a new table. I need to insert records that do not exist in the new table, and update records that do exist. I am trying to use a MERGE "upsert" method.
I have some unique problems due to the client's database and application structure. The table I am inserting into has a Unique ID field that increments by 1 for each new row, but the table does not do the auto incrementating; the insert statement needs to pull the highest ID in the target table and add 1 for the new record.
From my research, I can't figure out how to do that with MERGE. I do not database permissions to create a sequence. I have tried a lot of things, but currently my query looks like:
MERGE
dbo.targetTable as target
USING
dbo.sourceTable AS source
ON
target.account_no = source.account_ID
WHEN NOT MATCHED THEN
INSERT (
ID,
FIELD1,
FIELD2,
FIELD3
) VALUES (
(SELECT MAX(ID) + 1 FROM dbo.targetTable),
'field1',
'field2',
'field3'
)
The problem I am then running into with this code is that it appears to only run the select statement for the new ID once. That is, if the highest ID in the target table was 10, it would insert every new record with ID 11. That won't work as I'm getting a
Violation of PRIMARY KEY constraint. Cannot insert duplicate key in object error. I've been doing a ton of googling and trying different things but haven't been able to figure this one out. Any help is appreciated, thank you.
EDIT: For clarification, the unique ID column does not auto-populate. If I do not insert a value for the ID column, I get
Cannot insert the value NULL into column 'ID', table 'dbo.targetTable'; column does not allow nulls. UPDATE fails.
And again, as I mentioned originally I do not have permissions to create sequences. It just throws an error and says I do not have permission to do that.
I agree that changing the ID column to auto-increment automatically would be perfect, but I do not have the capability to modify the table like that either.
If you don't need the IDs to be consecutive, you can add the last available ID to a ROW_NUMBER() to generate new, non-repeated IDs.
BEGIN TRANSACTION
DECLARE #NextAvailableID INT = (SELECT ISNULL(MAX(ID), 0) FROM dbo.targetTable WITH (TABLOCKX))
;WITH SourceWithNewIDs AS
(
SELECT
S.*,
NewID = #NextAvailableID + ROW_NUMBER() OVER (ORDER BY S.account_ID)
FROM
dbo.sourceTable AS S
)
MERGE
dbo.targetTable as target
USING
SourceWithNewIDs AS source
ON
target.account_no = source.account_ID
WHEN NOT MATCHED THEN
INSERT (
ID,
FIELD1,
FIELD2,
FIELD3
) VALUES (
NewID,
'field1',
'field2',
'field3'
)
COMMIT
Keep in mind that this example is missing the proper error handling with rollback and the lock used to retrieve the max ID will block all other operations until commited or rollbacked.
If you need the new rows to have consecutive IDs then you can use this same approach with a regular INSERT (with WHERE NOT EXISTS...) instead of a MERGE (will have to write the UPDATE separately).
This is just a different way without using a Merge. Permissions aren't required for temp tables, so I would use one to hold the account numbers that need to be inserted, with an identity field to help with traversal. A while loop can traverse the identity, inserting the values with respect to the source table's account_no- into the target table. Since the insert is done in a loop, the MAX function should grab the target table's MAX(account_no) correctly on each loop.
DECLARE #tempTable TABLE (pkindex int IDENTITY(1,1) PRIMARY KEY, account_no int)
DECLARE #current int = 1
,#endcount int = 0
--account_no's that should be inserted
INSERT INTO #tempTable(account_no)
SELECT account_no
FROM sourceTable
WHERE account_no NOT IN (SELECT account_no FROM targetTable)
SET #endcount = (SELECT COUNT(*) FROM #tempTable)
--looping condition, should select the MAX(ID) with each subsequent loop
WHILE (#endcount > 0) AND (#current <= #endcount)
BEGIN
INSERT INTO dbo.targetTable(ID, FIELD1, FIELD2, FIELD3)
SELECT (SELECT MAX(T2.ID) + 1 FROM dbo.targetTable T2) AS MAXID
,S.field1
,S.field2
,S.field3
FROM #tempTable T INNER JOIN sourceTable S ON T.account_no = S.account_no
WHERE T.pkindex = #current --traversing temp table by its identity
SET #current += 1
END

Performance issues with UNION of large tables

I have seven large tables, that can be storing between 100 to 1 million rows at any time. I'll call them LargeTable1, LargeTable2, LargeTable3, LargeTable4...LargeTable7. These tables are mostly static: there are no updates nor new inserts. They change only once every two weeks or once a month, when they are truncated and a new batch of registers are inserted in each.
All these tables have three fields in common: Headquarter, Country and File. Headquarter and Country are numbers in the format '000', though in two of these tables they are parsed as int due to some other system necessities.
I have another, much smaller table called Headquarters with the information of each headquarter. This table has very few entries. At most 1000, actually.
Now, I need to create a stored procedure that returns all those headquarters that appear in the large tables but are either absent in the Headquarters table or have been deleted (this table is deleted logically: it has a DeletionDate field to check this).
This is the query I've tried:
CREATE PROCEDURE deletedHeadquarters
AS
BEGIN
DECLARE #headquartersFiles TABLE
(
hq int,
countryFile varchar(MAX)
);
SET NOCOUNT ON
INSERT INTO #headquartersFiles
SELECT headquarter, CONCAT(country, ' (', file, ')')
FROM
(
SELECT DISTINCT CONVERT(int, headquarter) as headquarter,
CONVERT(int, country) as country,
file
FROM LargeTable1
UNION
SELECT DISTINCT headquarter,
country,
file
FROM LargeTable2
UNION
SELECT DISTINCT headquarter,
country,
file
FROM LargeTable3
UNION
SELECT DISTINCT headquarter,
country,
file
FROM LargeTable4
UNION
SELECT DISTINCT headquarter,
country,
file
FROM LargeTable5
UNION
SELECT DISTINCT headquarter,
country,
file
FROM LargeTable6
UNION
SELECT DISTINCT headquarter,
country,
file
FROM LargeTable7
) TC
SELECT RIGHT('000' + CAST(st.headquarter AS VARCHAR(3)), 3) as headquarter,
MAX(s.deletionDate) as deletionDate,
STUFF
(
(SELECT DISTINCT ', ' + st2.countryFile
FROM #headquartersFiles st2
WHERE st2.headquarter = st.headquarter
FOR XML PATH('')),
1,
1,
''
) countryFile
FROM #headquartersFiles as st
LEFT JOIN headquarters s ON CONVERT(int, s.headquarter) = st.headquarter
WHERE s.headquarter IS NULL
OR s.deletionDate IS NOT NULL
GROUP BY st.headquarter
END
This sp's performance isn't good enough for our application. It currently takes around 50 seconds to complete, with the following total rows for each table (just to give you an idea about the sizes):
LargeTable1: 1516666 rows
LargeTable2: 645740 rows
LargeTable3: 1950121 rows
LargeTable4: 779336 rows
LargeTable5: 1100999 rows
LargeTable6: 16499 rows
LargeTable7: 24454 rows
What can I do to improve performance? I've tried to do the following, with no much difference:
Inserting into the local table by batches, excluding those headquarters I've already inserted and then updating the countryFile field for those that are repeated
Creating a view for that UNION query
Creating indexes for the LargeTables for the headquarter field
I've also thought about inserting these missing headquarters in a permanent table after the LargeTables change, but the Headquarters table can change more often, and I would like not having to change its module to keep these things tidy and updated. But if it's the best possible alternative, I'd go for it.
Thanks
Take this filter
LEFT JOIN headquarters s ON CONVERT(int, s.headquarter) = st.headquarter
WHERE s.headquarter IS NULL
OR s.deletionDate IS NOT NULL
And add it to each individual query in the union and insert into #headquartersFiles
It might seem like this makes a lot more filters but it will actually speed stuff up because you are filtering before you start processing as a union.
Also take out all your DISTINCT, it probably won't speed it up but it seems silly because you are doing a UNION and not a UNION all.
Do the filtering at each step. But first, modify the headquarters table so it has the right type for what you need . . . along with an index:
alter table headquarters add headquarter_int as (cast(headquarter as int));
create index idx_headquarters_int on headquarters(headquarters_int);
SELECT DISTINCT headquarter, country, file
FROM LargeTable5 lt5
WHERE NOT EXISTS (SELECT 1
FROM headquarters s
WHERE s.headquarter_int = lt5.headquarter and s.deletiondate is not null
);
Then, you want an index on LargeTable5(headquarter, country, file).
This should take less than 5 seconds to run. If so, then construct the full query, being sure that the types in the correlated subquery match and that you have the right index on the full table. Use union to remove duplicates between the tables.
I'd try doing the filtering with each individual table first. You just need to account for the fact that a headquarter might appear in one table, but not another. You can do this like so:
SELECT
headquarter
FROM
(
SELECT DISTINCT
headquarter,
'table1' AS large_table
FROM
LargeTable1 LT
LEFT OUTER JOIN Headquarters HQ ON HQ.headquarter = LT.headquarter
WHERE
HQ.headquarter IS NULL OR
HQ.deletion_date IS NOT NULL
UNION ALL
SELECT DISTINCT
headquarter,
'table2' AS large_table
FROM
LargeTable2 LT
LEFT OUTER JOIN Headquarters HQ ON HQ.headquarter = LT.headquarter
WHERE
HQ.headquarter IS NULL OR
HQ.deletion_date IS NOT NULL
UNION ALL
...
) SQ
GROUP BY headquarter
HAVING COUNT(*) = 5
That would make sure that it's missing from all five tables.
Table variables have horrible performance because sql server does not generate statistics for them. Instead of a table variable, try using a temp table instead, and if headquarter + country + file is unique in the temp table, add a unique constraint (which will create a clustered index) in the temp table definition. You can set indexes on a temp table after creating it, but for various reasons SQL Server may ignore it.
Edit: as it turns out, you can in fact create indexes on table variables, even non-unique in 2014+.
Secondly, try not to use functions in your joins or where clauses - doing so often causes performance problems.
The real answer is to create separate INSERT statements for each table with the caveat that data to be inserted does not exist in the destination table.

SQL Trigger to record UPDATE into Audit Table

I have a table with columns businessname, sortcode and accountnumber all populated, name, nationality and DOB all currently unpopulated. I need to create a trigger to shoot every update to an audit table when any of the null fields are updated so if I change just the name I'll get a timestamp, userid, the field changed, the old value and the new value.. If I changed all 3 null fields I'd like to send 3 rows to the audit table.
Can someone give me a pointer on the logic of this please?
In a very rudimentary format for testing I've got
CREATE TRIGGER RM_UPDATE_TRIGGER ON RM_BASE
ON UPDATE
AS
INSERT INTO RM_AUDITLOG
SELECT CURRENT_TIMESTAMP, SRT_CD
FROM RM_BASE
but this is sending all the current rows across after an UPDATE to any of them, I only want the row that has been affected. I'm not sure if I should be building more tables to join together to get the final answer or using the INSERT/DELETE tables.. I've seen an audit table in this format in previous roles so I know it works but can't figure it out!
Thanks
Yes, you need to use the INSERTED and/or DELETED pseudo-tables as those contain only the rows that have been modified. As in:
CREATE TRIGGER RM_UPDATE_TRIGGER ON RM_BASE
ON UPDATE
AS
INSERT INTO RM_AUDITLOG
SELECT CURRENT_TIMESTAMP, SRT_CD
FROM INSERTED
The INSERTED table has the "new" or "current" version of each row while the DELETED table has the "old" version that has been replaced via the UPDATE operation. This is the case for all versions of SQL Server, at least going back as far as SQL Server 2000.
In order to track the change itself (both "old" and "new" values), then you need to JOIN those two pseudo-tables, as in:
INSERT INTO RM_AUDITLOG
SELECT CURRENT_TIMESTAMP, ins.SRT_CD AS [SRT_CD_new], del.SRT_CD AS [SRT_CD_old]
FROM INSERTED ins
INNER JOIN DELETED del
ON del.PKfield = ins.PKfield
This is the basic operation for capturing changes (unless, of course, you use Change Data Capture) in a DML trigger.
If you want to unpivot this data such that each set of "old" and "new" columns becomes a row, that should be easily adaptable from the above. In that case, you could also add WHERE ISNULL(ins.column, '~~~~') <> ISNULL(del.column, '~~~~') COLLATE Latin1_General_BIN to avoid capturing fields that have not changed. The COLLATE ensures case-sensitive / accent-sensitive / etc comparisons.
Of course, unpivoting makes it really hard to reconstruct the entire row as you are then required to keep the full history forever. You would need to start with the base values for all fields and apply the changes incrementally. The typical audit scenario is to just to capture the row that has fields for both old and new for each source field (like I have already shown). If your audit table looks more like:
PKfield, DateModified, businessname_old, businessname_new, sortcode_old, sortcode_new
then you can write a query to identify which fields actually changed by comparing each set (given that more than 1 field can change in the same UPDATE operation), something like:
SELECT PKfield,
DateModified,
CASE
WHEN ISNULL(businessname_old, '~~~~') <> ISNULL(businessname_new, '~~~~')
COLLATE Latin1_General_BIN THEN 'BusinessName ' ELSE ''
END +
CASE
WHEN ISNULL(sortcode_old, '~~~~') <> ISNULL(sortcode_new, '~~~~')
COLLATE Latin1_General_BIN THEN 'SortCode ' ELSE ''
END AS [FieldsChanged]
FROM AuditTable
ORDER BY DateModified DESC;
BUT, if you really want to unpivot the data to have one row per actual changed field, then the following structure should work:
;WITH ins AS
(
SELECT PKfield, FieldName, Value
FROM (
SELECT PKfield, businessname, sortcode, accountnumber, name,
nationality, DOB
FROM INSERTED
) cols
UNPIVOT (Value FOR FieldName IN
(businessname, sortcode, accountnumber, name, nationality, DOB)
) colvals
), del AS
(
SELECT PKfield, FieldName, Value
FROM (
SELECT PKfield, businessname, sortcode, accountnumber, name,
nationality, DOB
FROM DELETED
) cols
UNPIVOT (Value FOR FieldName IN
(businessname, sortcode, accountnumber, name, nationality, DOB)
) colvals
)
INSERT INTO AuditTable (PKfield, DateModified, FieldName, OldValue, NewValue)
SELECT ins.PKfield, CURRENT_TIMESTAMP, ins.FieldName, del.Value, ins.Value
FROM ins
INNER JOIN del
ON del.PKfield = ins.PKfield
AND del.FieldName = ins.FieldName
WHERE ISNULL(del.Value, '~~~~') <>
ISNULL(ins.Value, '~~~~') COLLATE Latin1_General_BIN;
You might need to add a CONVERT(VARCHAR(1000), field) to the WHERE condition if DOB is a DATE or DATETIME field, or if SRT_CD is an INT or other type of number field:
WHERE ISNULL(CONVERT(VARCHAR(1000), del.Value), '~~~~') <>
ISNULL(CONVERT(VARCHAR(1000), ins.Value), '~~~~')
COLLATE Latin1_General_BIN;

Select with Many Discrete Values Possible in Where Clause

Given a table like
CREATE TABLE [dbo].[Article](
[Id] [int] NOT NULL,
[CategoryId] [int] NOT NULL,
[Text] [nchar](10) NOT NULL)
users are allowed to select one or more categories for which they would like to view data. Typically they will select 1-20 categories. To accommodate that, I generate parameterized queries similar to:
SELECT * FROM Article
WHERE CategoryId IN (#c1, #c2, #c3, #c4, #c5)
However, in some rare use cases a user can legitimately select hundreds of categories. This lead me to discover a limitation of Linq-to-Entities, which I worked around by forming ranges of category codes. Unfortunately this only pushes off the issue, as there are limits to the size of a query that can be passed to SQL Server.
I would like to refactor this query to avoid any hard limits. My first thought was to create a temporary table containing the requested categories, and performing an inner join against that temporary table in lieu of the IN(...) clause. However, I understand that temporary tables can be quite slow.
Is there a more elegant and/or better performing solution to this problem?
Your first instinct is correct, thopugh you might find a table-valued variable sufficient in place of a temp table. Don't worry about the performance in a case like this; it won't be significant. An index could always be created onthe temp table if needed, but that seems unlikley. Is there an index on the CategoryId field?
EDIT:
Oops. I missed the Linq part.
Here is a alternate syntax that may be worth a try (for performance reasons, not for string length reasons)
Select * from dbo.Article art where exists ( select null from ( select 0 as MyV union all select 2 as MyV union all select 3 as MyV ) as derived1 where derived1.MyV = art.CategoryId )
......................
This is how I handle it.
Sometimes my variable table is changed to a #temp table. I test the 2 different scenarios for performance.
You can pass as many or as few values via xml.
DECLARE #input XML = '<root>
<category myvalue="1" />
<category myvalue="2" />
<category myvalue="3" />
</root>'
declare #holder table ( CatID int )
Insert into #holder (CatID)
SELECT
myvalue = MyXmlTable.value('(#myvalue)', 'int')
FROM
#input.nodes('/root/category') AS Tbl(MyXmlTable)
select * from #holder
SELECT * FROM Article art
where exists (select null from #holder hold where hold.CatID = art.CategoryId
Bigger write up here:
http://www.sqlservercentral.com/articles/Stored+Procedures/thezerotonparameterproblem/2283/

T-SQL Insert into table without having to specify every column

In our db there is a table that has a little over 80 columns. It has a primary key and Identity insert is turned on. I'm looking for a way to insert into this table every column EXCEPT the primary key column from an identical table in a different DB.
Is this possible?
You can do this quite easily actually:
-- Select everything into temp table
Select * Into
#tmpBigTable
From [YourBigTable]
-- Drop the Primary Key Column from the temp table
Alter Table #tmpBigTable Drop Column [PrimaryKeyColumn]
-- Insert that into your other big table
Insert Into [YourOtherBigTable]
Select * From #tmpBigTable
-- Drop the temp table you created
Drop Table #tmpBigTable
Provided you have Identity Insert On in "YourOtherBigTable" and columns are absolutely identical you will be okay.
You could query Information_Schema to get a list of all the columns and programatically generate the column names for your query. If you're doing this all in t-sql it would be cumbersome, but it could be done. If you're using some other client language, like C# to do the operation, it would be a little less cumbersome.
No, that's not possible. You could be tempted to use
INSERT INTO MyLargeTable SELECT * FROM OtherTable
But that would not work, because your identity column would be included in the *.
You could use
SET IDENTITY_INSERT MyLargeTable ON
INSERT INTO MyLargeTable SELECT * FROM OtherTable
SET IDENTITY_INSERT MyLargeTable OFF
first you enable inserting identity values, than you copy the records, then you enable the identity column again.
But this won't work neither. SQL server won't accept the * in this case. You have to explicitly include the Id in the script, like :
SET IDENTITY_INSERT MyLargeTable ON
INSERT INTO MyLargeTable (Id, co1, col2, ...., col80) SELECT Id, co1, col2, ...., col80 FROM OtherTable
SET IDENTITY_INSERT MyLargeTable OFF
So we're back from where we started.
The easiest way is to right click the table in Management Studio, let it generate the INSERT and SELECT scripts, and edit them a little to let them work together.
CREATE TABLE Tests
(
TestID int IDENTITY PRIMARY KEY,
A int,
B int,
C int
)
INSERT INTO dbo.Tests
VALUES (1,2,3)
SELECT * FROM Tests
This works in SQL2012
Why not just create a VIEW of the original data, removing the unwanted fields?
Then 'Select * into' your hearts desire.
Localized control within a single view
No need to modify SPROC
Add/change/delete fields easy
No need to query meta-data
No temporary tables
Really, honestly it takes ten seconds or less to pull all of the columns over from the object browser and then delete the identity column from the list. It is a bad idea to use select * for anything but quick ad hoc query.
In answer to a related question (SELECT * EXCEPT), I point out the truly relational language Tutorial D allows projection to be expressed in terms of the attributes to be removed instead of the ones to be kept e.g.
my_relvar { ALL BUT description }
However its INSERT syntax requires tuple value constructors to include attribute name / value pairs e.g.
INSERT P
RELATION
{
TUPLE { PNO PNO ( 'P1' ) , PNAME CHARACTER ( 'Nut' ) },
TUPLE { PNO PNO ( 'P2' ) , PNAME CHARACTER ( 'Bolt' ) }
};
Of course, using this syntax there is no column ordering (because it is truly relational!) e.g. this is semantically equivalent:
INSERT P
RELATION
{
TUPLE { PNO PNO ( 'P1' ) , PNAME CHARACTER ( 'Nut' ) },
TUPLE { PNAME CHARACTER ( 'Bolt' ) , PNO PNO ( 'P2' ) }
};
The alternative would be to rely fully on attribute ordering, which SQL does partially e.g. this is a close SQL equivalent to the the above:
INSERT INTO P ( PNO , PNAME )
VALUES
( PNO ( 'P1' ) , CAST ( 'Nut' AS VARCHAR ( 20 ) ) ) ,
( PNO ( 'P2' ) , CAST ( 'Bolt' AS VARCHAR ( 20 ) ) );
Once the commalist of columns has been specified the VALUES row constructors have the maintain this order, which is not ideal. But at least the order is specified: your proposal would rely on some default order which may be possibly non-deterministic.