SQL Syntax errors with commas - sql

Complete Newb trying learn.
I want to have Table "Sales" populate Table "Finance" when the "OrderDate" is updated/entered however I am having trouble inserting multiple columns.
CREATE TRIGGER SalesOrderDateTrigger
ON [dbo].[Sales]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
DECLARE #OrderDate Date
SELECT #OrderDate = INSERTED.OrderDate FROM INSERTED
IF #OrderDate > 0
BEGIN
INSERT INTO Finance
(Quote, Customer, Project_Name, [Value],
POC_Name_#1, POC_Number_#1, POC_Email_#1,
POC_Name_#2, POC_Number_#2, POC_Email_#2,
Comment, [DA Link])
SELECT (INSERTED.Quote, INSERTED.Customer, INSERTED.Project_Name,
INSERTED.[Value],
INSERTED.POC_Name_#1, INSERTED.POC_Number_#1, INSERTED.POC_Email_#1,
INSERTED.POC_Name_#2, INSERTED.POC_Number_#2, INSERTED.POC_Email_#2,
INSERTED.Comment, INSERTED.[DA Link])
FROM INSERTED
END
END
Thank you for reading.

There are a few problems with your approach..
You're not always looking at this in a "block of data" kind of way. Sure you may only insert one row at a time, but it's actually possible to insert multiple rows into a table at the same time. When this occurs, the inserted pseudotable will have more than one row, hence you cannot select a sole order date from it, into a single variable #orderdate. Avoid any kind of "row by row" thinking with sqlserver triggers; the design intention of sqlserver server-side programming is that it is always set based; anything you write in a trigger should handle anything from zero to millions of rows all at once, not a row at a time
I'm not really sure what the line where you compare the date with 0, is trying to achieve
You subsequently run an insert that effectively inserts data that is in the Order table, into the Finance table. No new or different data is calculated, or brought in from elsewhere, which leads me to believe that order and finance share a large number of columns, having identical data. In terms of database design, this is a poor normalisation strategy; if the finance table replicates the orders table, it probably shouldn't exist. Indeed if your business logic is that an entry in the Orders table with a non null OrderDate realises an entry in the finance table, really Finance could actually just be a VIEW based off of the simple query
SELECT blahblah FROM orders WHERE orderdate IS NOT NULL

Related

How do I write a trigger that limits that a person from the bankcustomer table can only have 3 accounts in the account table?

I have a bankcustomer table which looks like this:
create table bankcustomer
(
cpr char(10) primary key,
name varchar(30) not null
)
And an account table which looks like this:
create table account
(
accountnr int identity(1001,1) primary key,
accountowner char(10) foreign key references bankcustomer,
created date not null,
balance decimal(14,2) not null
)
I want to write a trigger in SQL Server that limits a bankcustomer so that a bankcustomer can have no more than 3 accounts in the account table.
I create an account for a specific bank customer by inserting a value in the accountowner column in the account table that matches a cpr from the bankcustomer table (when inserting a record into the account table).
I have this code so far:
create trigger mytrigger4
on account
for insert
as
if exists (select count(*)
from inserted
join bankcustomer on inserted.accountowner = bankcustomer.cpr
where cpr = inserted.accountowner
having count(*) > 3)
begin
rollback tran
raiserror('A customer must have a maximum of 3 accounts', 16, 1)
end
go
The problem is that I can keep creating accounts (insert records in the account table) for a customer even though the customer already has 3 accounts. Which means the code in the trigger does not work at all.
Any help would be appreciated!
Let's think about your code. First, it is apparent that you test using single row inserts only. And that probably carries over into your sql code generally. That's bad, because an insert (or update or delete or merge) can affect any number of rows. While you can expect that the majority of inserts from an application are likely to be single rows, there are always situations that affect multiple rows. And that is an assumption that should always be in your mind when writing sql code generally - and triggers specifically.
Your test is based on exists. That is testing for the existence of rows generate by the query inside the exists clause. So - look carefully at your query. First, you count but do not group. Therefore, you are counting all the rows generated by the query. This is incorrect because of the assumption mentioned earlier. But let's ignore that for the moment and examine the select statement alone.
Your select statement joins inserted to the parent bankcustomer. The join is correct but why is there a where clause? And let's sidetrack into best practices. Always - ALWAYS - give each table a useful alias and use that alias when referencing columns. Why? Because this makes it easier for others to read and understand your query. BTW - a useful alias is not a single character. Yep - writing code can be a little work.
Let's continue. Your query counts all rows in the resultset. If you insert a single row, what is the result of your join? We know that an account is associated with a single bankcustomer. So when you insert a single row into account, the join will produce A SINGLE ROW. And counting that single row resultset will always produce a single row with the value of - tadah - 1. Now there is a way to cause your trigger generate an error. Insert 4 or more rows with a single statement. The error will probably not be accurate, but it will kill the transaction and display a message.
So you see that your logic is flawed. You need to count rows in the actual table (account), not inserted. But not all the rows - because that would be inefficient. You just need to consider all rows that "share" the accountowner values found in inserted. Note the plural "values". This is where your single row assumption fails. So, how to do that? Here is one way to write your trigger. Note that the first query is included to let you "see" what the count query is producing - this is for debugging only. Production triggers should never return a resultset in any fashion.
alter trigger mytrigger4
on account
for insert
as begin
select cust.cpr, count(*)
from bankcustomer as cust join account as acc on cust.cpr = acc.accountowner
where exists (select * from inserted as ins where ins.accountowner = cust.cpr)
group by cust.cpr;
if exists (select cust.cpr, count(*)
from bankcustomer as cust join account as acc on cust.cpr = acc.accountowner
where exists (select * from inserted as ins where ins.accountowner = cust.cpr)
group by cust.cpr
having count(*) > 3)
begin
rollback tran
raiserror('A customer must have a maximum of 3 accounts', 16, 1)
end
end;
go
I'll leave it to you to actually test is thoroughly - which includes the use of insert statements that insert multiple rows. And, of course, you will vary the test data to include customers that have no accounts, less than 3 accounts, exactly 3 accounts, and more than 3 accounts (because sometimes things happen and extra accounts get added despite your best efforts).

How should I reliably mark the most recent row in SQL Server table?

The existing design for this program is that all changes are written to a changelog table with a timestamp. In order to obtain the current state of an item's attribute we JOIN onto the changelog table and take the row having the most recent timestamp.
This is a messy way to keep track of current values, but we cannot readily change this changelog setup at this time.
I intend to slightly modify the behavior by adding an "IsMostRecent" bit to the changelog table. This would allow me to simply pull the row having that bit set, as opposed to the MAX() aggregation or recursive seek.
What strategy would you employ to make sure that bit is always appropriately set? Or is there some alternative you suggest which doesn't affect the current use of the logging table?
Currently I am considering a trigger approach, which turns the bit off all other rows, and then turns it on for the most recent row on an INSERT
I've done this before by having a "MostRecentRecorded" table which simply has the most recently inserted record (Id and entity ID) fired off a trigger.
Having an extra column for this isn't right - and can get you into problems with transactions and reading existing entries.
In the first version of this it was a simple case of
BEGIN TRANSACTION
INSERT INTO simlog (entityid, logmessage)
VALUES (11, 'test');
UPDATE simlogmostrecent
SET lastid = ##IDENTITY
WHERE simlogentityid = 11
COMMIT
Ensuring that the MostRecent table had an entry for each record in SimLog can be done in the query but ISTR we did it during the creation of the entity that the SimLog referred to (the above is my recollection of the first version - I don't have the code to hand).
However the simple version caused problems with multiple writers as could cause a deadlock or transaction failure; so it was moved into a trigger.
Edit: Started this answer before Richard Harrison answered, promise :)
I would suggest another table with the structure similar to below:
VersionID TableName UniqueVal LatestPrimaryKey
1 Orders 209 12548
2 Orders 210 12549
3 Orders 211 12605
4 Orders 212 10694
VersionID -- being the tables key
TableName -- just in case you want to roll out to multiple tables
UniqueVal -- is whatever groups multiple rows into a single item with history (eg Order Number or some other value)
LatestPrimaryKey -- is the identity key of the latest row you want to use.
Then you can simply JOIN to this table to return only the latest rows.
If you already have a trigger inserting rows into the changelog table this could be adapted:
INSERT INTO [MyChangelogTable]
(Primary, RowUpdateTime)
VALUES (#PrimaryKey, GETDATE())
-- Add onto it:
UPDATE [LatestRowTable]
SET [LatestPrimaryKey] = #PrimaryKey
WHERE [TableName] = 'Orders'
AND [UniqueVal] = #OrderNo
Alternatively it could be done as a merge to capture inserts as well.
One thing that comes to mind is to create a view to do all the messy MAX() queries, etc. behind the scenes. Then you should be able to query against the view. This way would not have to change your current setup, just move all the messiness to one place.

Why Is SQL Trigger Not Inserting Rows In Sequential Order?

Recently I inherited a new ASP web application that merely allows customers to pay their outstanding invoices online. The application was poorly designed and did not have a payment history table. The entire payments table was deleted by the web service that transports the payment records to the accounting system of record.
I just created a simple trigger on the Payments table that simply copies the data from the Payments table into a Payment_Log table. Initially, the trigger just did a select * on inserted to copy the data. However, I just modified the trigger to insert the date of the payment into the Payment_Log table since one of our customers is having some issues that I need to debug. The new trigger is below. My question is that I have noticed that with this new version of the trigger, the rows are being inserted into the middle of the table (i.e. not at the end). Can someone explain why this is happening?
ALTER trigger [dbo].[PaymentHistory] on [dbo].[Payments]
for insert as
Declare #InvoiceNo nvarchar(255),
#CustomerName nvarchar(255),
#PaymentAmount float,
#PaymentRefNumber nvarchar(255),
#BulkPaid bit,
#PaymentType nvarchar(255),
#PaymentDate datetime
Select #InvoiceNo = InvoiceNo,
#CustomerName = CustomerName,
#PaymentAmount = PaymentAmount,
#PaymentRefNumber = PaymentRefNumber,
#BulkPaid = BulkPaid,
#PaymentType = PaymentType
from inserted
Set #PaymentDate = GETDATE()
Insert into Payment_Log
values (#InvoiceNo, #CustomerName, #PaymentAmount, #PaymentRefNumber, #BulkPaid, #PaymentType, #PaymentDate)
Below is a screenshot of SQL Server Management Studio that shows the rows being inserted into the middle of the table data. Thanks in advance for the help guys.
Datasets don't have an order. This means that SELECT * FROM x can return the results in a different order every time.
The only time that data is guaranteed to come back in the same order is when you specify an ORDER BY clause.
That said, there are circumstances that make the data normally come back in a certain order. The most visible one is with a clustered index.
This makes me wonder if the two tables have a Primary Key or not. Check all the indexes on each table and, at the very least, enforce a Primary Key.
As an aside, triggers in SQL Server are not fired for each row, but each batch. This can mean that the inserted table can contain more than just one row. (For example, when bulk inserting test data, or re-loading a large batch of transactions.)
For this reason, copying the data into variable is not a standard practice. Instead, you could just do the following...
ALTER trigger [dbo].[PaymentHistory] on [dbo].[Payments]
for insert as
INSERT INTO
Payment_Log
SELECT
InvoiceNo, CustomerName, PaymentAmount, PaymentRefNumber,
BulkPaid, PaymentType, GetDate()
FROM
inserted
Ok This question has the same answer as why do rows come back in different orders when I do not use an order by clause in my SQL query. If you ask SQL to process rows in any way it will process them in the fastest way it can, cashed rows first, those nearest the read head on the hard drive next and finally the rest.
Put it another way: you would be annoyed if queries took 10 times longer with rows in order than just on a first come first served basis. SQL does what you ask as quickly as it can
Hope this helps

Query to View new/updated tables

I am using the following to list the row count for all of my tables:
select convert(varchar(30),object_name(id)) [Table Name], rows, ModifiedOn from sysindexes
where object_name(id) not like 'sys%' and indid = 1
order by object_name(id)
I confess that I found this somewhere and only have a conceptual idea of what it is doing. But for my purposes, where I want to perform an application action and reverse engineer what happened in the database, it works well to identify new rows (I copy and paste before/after results into excel to compare).
Now, I would also like to know which tables have been updated. On (almost) all of my tables there is a ModifiedOn column, so I am hoping I can add the max of this to my output, which will tell me when the table's contents were last updated.
I have no idea how to join these two, and any help is appreciated.
I would strongly suggest against this approach, as it is DB dependent and unreliable.
Creating an ON INSERT on ON UPDATE trigger it the correct solution, then in the trigger you can put the new or updated data into a separate table which you can query. Triggers are the tool to monitor changes in the database without doing anything in the applications using them.
Example trigger for 'after update' on table MY_TABLE(id, name):
CREATE TRIGGER mark_changes AFTER UPDATE ON my_table FOR EACH ROW
BEGIN
INSERT INTO tracking_table VALUES "Change in table my_table", OLD.id, NEW.id
END$$
This assumes you have a table tracking_table(description, old_id, new_id)

Making a sequence of SQL statements atomic

I need to make the following sequence of steps in a sproc atomic. Here is an approximate simplified example:
Customesrs table (CustomerId,..,OrderMax)
Products table (ProductId,...)
AvailableProducts view (ProductId and other properties)
Orders (CustomerId,OrderId)
select #OrderMax from the Customers table
select TOP #Ordermax from AvailableProducts view
update some properties in the Products based on the result set of step 2
insert orders into Orders table (based on the result set of step 2)
return/select orders that were inserted
As I understand, there has to be a transaction on the whole thing and UPDLOCK. What has to be secured is Products table for update and Orders table for insert. However, the rows are queried from the view that is constructed from both of these tables.
What is the right way to make this sequence atomic and secure update and insert on the above tables?
Need to wrap all you logic in a Begin Transaction, Commit Transaction. The update / insert does not really care if the data came from a join unless it somehow created a situation where it could not roll back the transaction but it would have to get real messy to create a situation like that. If 3. and 4. have complex logic you may be forced into a cursor or .NET (but you can do some pretty complex logic with regular queries).