New to SQL - Why is my Insert into trying to insert NULL into primary key? - sql

What I want to do is insert a range of dates into multiple rows for customerID=1. I have and insert for dbo.Customer(Dates), specifying my that I want to insert a record into the Dates column for my Customer table, right? I am getting error:
Cannot insert the value NULL into column 'CustomerId', table 'dbo.Customers'
Sorry if I am way off track here. I have looked at similar threads to find out what I am missing, but I'm not piecing this together. I am thinking it wants to overwrite the existing customer ID as NULL, but I am unsure why exactly since I'm specifying dbo.Customer(Dates) and not the existing customerID for that record.
declare #date_Start datetime = '03/01/2011'
declare #date_End datetime = '10/30/2011'
declare #date datetime = #date_Start
while #date <= #date_End
begin
insert into dbo.Customer(Dates) select #date
if DATEPART(dd,#date) = 0
set #date = DATEADD(dd, -1, DATEADD(mm,1,#date))
else
set #date = DATEADD(dd,1,#date)
end
select * from dbo.Customer

The primary key is customerId, but you are not inserting a value.
My guess is that you declared it as a primary key with something like this:
customerId int primary key,
You want it to be an identity column, so the database assigns a value:
customerId int identity(1, 1) primary key
Then, you don't need to assign a value into the column when you insert a new row -- the database does it for you.

Your Customer table has a column named CustomerId and which column is NOT Nullable so you have to provide that column value as well. If your column type is Int try the bellow code:
declare #date_Start datetime = '03/01/2011'
declare #date_End datetime = '10/30/2011'
declare #date datetime = #date_Start
DECLARE #cusId INT
SET #cusId = 1
while #date <= #date_End
begin
insert into dbo.Customer(CustomerId, Dates) select #cusId, #date
if DATEPART(dd,#date) = 0
set #date = DATEADD(dd, -1, DATEADD(mm,1,#date))
else
set #date = DATEADD(dd,1,#date)
SET #cusId = #cusId + 1;
end
select * from dbo.Customer

thank you for the feedback. I think I'm scrapping this and going to go with creating a separate table to JOIN. Not sure why I didn't start doing that before

Related

Is it safe to save previous value of a column as a variable in update?

Think of a simple update stored procedure like this:
CREATE PROCEDURE [UpdateMyTable] (
#Id int,
#ModifiedOn datetime,
#GeneratedOn datetime
)
AS
UPDATE
[MyTable]
SET
[ModifiedOn] = #ModifiedOn
WHERE
[Id] = #Id AND [ModifiedOn] <= #GeneratedOn
Now, to return a result based on previous value of ModifiedOn, I changed it like this:
ALTER PROCEDURE [UpdateMyTable] (
#Id int,
#ModifiedOn datetime,
#GeneratedOn datetime
)
AS
DECLARE #PreviousModifiedOn datetime
UPDATE
[MyTable]
SET
[ModifiedOn] = #ModifiedOn,
#PreviousModifiedOn = [ModifiedOn]
WHERE
[Id] = #Id AND [ModifiedOn] <= #GeneratedOn
IF #PreviousModifiedOn <= #GeneratedOn
SELECT #ModifiedOn
ELSE
SELECT -1
Is it safe to fill #PreviousModifiedOn variable, with previous value of ModifiedOn, in SET part? Or is it possible that ModifiedOn value changes before it is saved into variable?
UPDATE
Same query using OUTPUT:
ALTER PROCEDURE [UpdateMyTable] (
#Id int,
#ModifiedOn datetime,
#GeneratedOn datetime
)
AS
DECLARE #PreviousModifiedOn AS TABLE (ModifiedOn datetime)
UPDATE
[MyTable]
SET
[ModifiedOn] = #ModifiedOn
OUTPUT
Deleted.[ModifiedOn] INTO #PreviousModifiedOn
WHERE
[Id] = #Id AND [ModifiedOn] <= #GeneratedOn
IF EXISTS (SELECT * FROM #PreviousModifiedOn WHERE [ModifiedOn] <= #GeneratedOn)
SELECT #ModifiedOn
ELSE
SELECT -1
It seems that OUTPUT is the correct way to solve the problem, but because of the variable table, I think it has more performance cost.
So my question is... Why using OUTPUT is better than my solution? Is there anything wrong with my solution? Which one is better in terms of performance and speed?
I believe that this is safe. Although variable assignment is a proprietary extension, the rest of the SET clause follows the SQL Standard here - all assignments are computed as if they occur in parallel. That is, all expressions on the right of assignments are computed based on pre-update values for all columns.
This is e.g. why UPDATE Table SET A=B, B=A will swap the contents of two columns, not set them both equal to whatever B was previously.
The one thing to be wary of here, for me, would be that the UPDATE may have performed no assignments (due to the WHERE clause) and so still be NULL, or may have performed multiple assignments; In the latter case, your variable will be set to one of the previous values but it is not guaranteed which row's value it will have retained.
It is not required, since MS SQL Server 2005 you can use OUTPUT for this kind of scenarios.
ALTER PROCEDURE [UpdateMyTable] (
#Id int,
#ModifiedOn datetime,
#GeneratedOn datetime
)
AS
DECLARE #PreviousModifiedOn datetime
--Declare a table variable for storing the info from Output
DECLARE #ModifiedOnTable AS TABLE
(
ModifiedOn DATETIME
)
UPDATE
[MyTable]
SET
[ModifiedOn] = #ModifiedOn,
#PreviousModifiedOn = [ModifiedOn]
OUTPUT DELETED.ModifiedOn INTO #ModifiedOnTable
WHERE
[Id] = #Id AND [ModifiedOn] <= #GeneratedOn
IF #PreviousModifiedOn <= #GeneratedOn
SELECT ModifiedOn FROM #ModifiedOnTable
ELSE SELECT -1

Assigning variables to use in query

I am moving from Oracle to SQL Server and I am noticing differences regarding assigning variables in a query. I wonder if someone could write me a simple example of how I can do this in SSMS please?
In the example below I am looking to assign the variable #date1 at the beginning of the select statement so that I can simply change the date at the top instead of having to change it several times in the query where #date1 is used several times.
SELECT *
FROM table
where date = #date1
Thanks
Based on your example the syntax would be as follows:
DECLARE #date1 DATETIME
SET #date1 = '2017-01-01 00:00:00.000'
Then reference #date1 in your query as you have above.
More broadly, the syntax is:
DECLARE #<name of variable> <type>
SET #<name of variable> = <value>
-- Simple declares
DECLARE #Variable1 VARCHAR(100)
DECLARE #Variable2 DATE
DECLARE #VariableTable TABLE (
numberColumnName INT,
textColumnName VARCHAR(MAX))
-- Chained declares
DECLARE
#Variable3 VARCHAR(100),
#Variable4 INT
-- Declare with initiation
DECLARE #Variable5 INT = 150
DECLARE #Variable6 DATE = '2018-05-05' -- Implicit conversion (varchar to date)
DECLARE #Variable7 FLOAT = 1945.15 * 1648.12 / #Variable5 -- Expressions can be used
DECLARE #Variable8 INT = (SELECT COUNT(1) FROM sys.objects)
-- Chained declares with initiation
DECLARE
#Variable9 VARCHAR(100) = 'Afla',
#Variable10 INT = 9164 * #Variable5
-- Change variable values (without declaring)
SET #Variable1 = 'Some value'
SET #Variable2 = CONVERT(DATE, GETDATE())
For your example:
DECLARE #DateFilter DATE = '2018-05-16' -- Use ISO standard date format (yyyy-MM-dd) when you hard-code them as literals
SELECT
*
FROM
YourTable AS T
WHERE
T.DateToFilter >= #DateFilter
DECLARE #date1 DATE = '2018-04-11'
This code may be fine, but be aware of dates formats :date (Transact-SQL)
and the need of using either Date, Datetime, or Datetime2.

How to add an additional column to the result set returned by a SP without modifying the SP?

I have a Stored Procedure (SP), named myStoredProcedure, returning me such output based on startDate and endDate user-defined parameters:
PrimaryName SecondaryName Volume
A B 20
C D 30
A D 50
...
So, Volume represents the sum of all the cases between the dates defined.
In another SP, named mySecondStoredProcedure, I am using the first SP to get the result there. However, my problem is that I need an additional attribute in my output, which is year, I want to see year based volumes. Therefore, the output I would like to see is something like that
assume startDate: 2014, endDate: 2015:
PrimaryName SecondaryName Volume Year
A B 12 2014
C D 14 2014
A D 20 2014
A B 8 2015
C D 16 2015
A D 30 2015
...
I am not allowed to modify myStoredProcedure. Therefore I build a while loop in the second SP to receive it. My code is like:
declare #temp_table table
(
PrimaryGroup varchar(10),
SecondaryGroup varchar(10),
Volume int
)
while #startDate < #endDate
begin
insert into #temp_table
exec myStoredProcedure #startDate #endDate
set #startDate = DATEADD(YEAR,1,#startDate)
end
select * from #temp_table
This is giving me the result without the year column. I need a year column like I showed in my example output above. I could not find a way to add it. There is no primary key in the result set returned by myStoredProcedure. Also, SQL Server 2008 does not let me add a year column in #temp_table, saying that fields are not matching. How can I add the year column properly? Any help would be appreciated!
EDIT: When I add year column in the definition of #temp_table, the error I receive: Column name or number of supplied values does not match table definition.
You're close with the syntax you currently have, you'll just need to add the year to the temp table and supply it after calling the stored procedure. In addition, you will also need to specify the columns being inserted (a practice well worth getting in the habit of) as your procedure doesn't return the same number of columns.
declare #temp_table table
(
PrimaryGroup varchar(10),
SecondaryGroup varchar(10),
Volume int,
Year int
)
while #startDate < #endDate
begin
insert into #temp_table (PrimaryGroup, SecondaryGroup, Volume)
exec myStoredProcedure #startDate #endDate
Update #temp_table
Set Year = #StartDate
Where Year Is Null
set #startDate = DATEADD(YEAR,1,#startDate)
end
select * from #temp_table
Add a Year column to your temp table, and apply the structured insert
declare #temp_table table
(
PrimaryGroup varchar(10),
SecondaryGroup varchar(10),
Volume int,
Year int
)
while #startDate < #endDate
begin
insert into #temp_table (PrimaryName,SecondaryName,Volume)
exec myStoredProcedure #startDate #endDate
Update #temp_table set Year = #startDate where Year is Null
set #startDate = DATEADD(YEAR,1,#startDate)
end
select * from #temp
Create a second table variable that will hold the result:
declare #result_table table
(
Year int,
PrimaryGroup varchar(10),
SecondaryGroup varchar(10),
Volume int
)
Then in the while loop after fetching the result into #temp_table:
insert into #result_table
select <year>, PrimaryGroup, SecondaryGroup, Volume from #temp_table;
truncate #temp_table;

Is there a way to dynamically set a parameter in a stored procedure to the result of a query?

I would like to be able to set a parameter of a stored procedure dynamically, based on the results of a SQL query. The stored procedure calculates the distance traveled between a particular date and today. That particular date could be different for each record in the database. (The date is calculated in a separate stored procedure.) See the example.
The stored procedure has two parameters: #DateFrom and #DateTo. #DateFrom should be the date in the DateFrom column, which, as you can see, is different for every record. Is there a way to loop through or something and set the #DateFrom parameter to the value in the DateFrom column for each record? #DateTo will always be today's date. Any help is greatly appreciated.
This is what I got from your question, it's my first answer to a post please excuses typos or code format
USE tempdb
GO
IF OBJECT_ID(N'Tempdb.dbo.#DataTest') IS NULL
BEGIN
CREATE TABLE #DataTest
(
ID INT IDENTITY
,Name VARCHAR(100)
,DateTo DATETIME
,DateFrom DATETIME
)
END
GO
INSERT INTO #DataTest (Name,DateTo,DateFrom) VALUES ('DataValues1', '20151201',GETDATE() + 1)
INSERT INTO #DataTest (Name,DateTo,DateFrom) VALUES ('DataValues3', '20151203',GETDATE() + 2)
INSERT INTO #DataTest (Name,DateTo,DateFrom) VALUES ('DataValues5', '20151205',GETDATE() + 3)
INSERT INTO #DataTest (Name,DateTo,DateFrom) VALUES ('DataValues7', '20151207',GETDATE() + 4)
INSERT INTO #DataTest (Name,DateTo,DateFrom) VALUES ('DataValues9', '20151209',GETDATE() + 5)
GO
CREATE PROC #CalculateData
(
#DateTo DATETIME,
#DateFrom DATETIME
)
AS
SELECT DATEDIFF(SECOND,#DateTo,#DateFrom) AS DataResult
GO
DECLARE #Count INT = (SELECT MIN(ID) FROM #DataTest)
DECLARE #DateToParam DATETIME
DECLARE #DateFromToParam DATETIME
WHILE #Count IS NOT NULL
BEGIN
SET #DateToParam = (SELECT DateTo FROM #DataTest WHERE ID = #Count)
SET #DateFromToParam = (SELECT DateFrom FROM #DataTest WHERE ID = #Count)
EXEC #CalculateData #DateToParam, #DateFromToParam
SET #Count = (SELECT MIN(ID) FROM #DataTest WHERE ID > #Count)
END
GO
DROP TABLE #DataTest
DROP PROCEDURE #CalculateData

SQL Server 2005 - writing an insert and update trigger for validation

I'm pretty bad at SQL, so I need someone to check my trigger query and tell me if it solves the problem and how acceptable it is. The requirements are a bit convoluted, so please bear with me.
Suppose I have a table declared like this:
CREATE TABLE Documents
(
id int identity primary key,
number1 nvarchar(32),
date1 datetime,
number2 nvarchar(32),
date2 datetime
);
For this table, the following constraints must be observed:
At least one of the number-date pairs should be filled (both the number and the date field not null).
If both number1 and date1 are not null, a record is uniquely identified by this pair. There cannot be two records with the same number1 and date1 if both fields are not null.
If either number1 or date1 is null, a record is uniquely identified by the number2-date2 pair.
Yes, there is a problem of poor normalization, but I cannot do anything about that.
As far as I know, I cannot write unique indexes on the number-date pairs that check whether some of the values are null in SQL Server 2005. Thus, I tried validating the constraints with a trigger.
One last requirement - the trigger should have no inserts of its own, only validation checks. Here's what I came up with:
CREATE TRIGGER validate_requisite_uniqueness
ON [Documents]
FOR INSERT, UPDATE
AS
BEGIN
DECLARE #NUMBER1 NVARCHAR (32)
DECLARE #DATE1 DATETIME
DECLARE #NUMBER2 NVARCHAR (32)
DECLARE #DATE2 DATETIME
DECLARE #DATETEXT VARCHAR(10)
DECLARE inserted_cursor CURSOR FAST_FORWARD FOR SELECT number1, date1, number2, date2 FROM Inserted
IF NOT EXISTS (SELECT * FROM INSERTED)
RETURN
OPEN inserted_cursor
FETCH NEXT FROM inserted_cursor into #NUMBER1, #DATE1, #NUMBER2, #DATE2
WHILE ##FETCH_STATUS = 0
BEGIN
IF (#NUMBER1 IS NULL OR #DATE1 IS NULL)
BEGIN
IF (#NUMBER2 IS NULL OR #DATE2 IS NULL)
BEGIN
ROLLBACK TRANSACTION
RAISERROR ('Either the first or the second number-date pair should be filled.', 10, 1)## Heading ##
END
END
IF (#NUMBER1 IS NOT NULL AND #DATE1 IS NOT NULL)
BEGIN
IF ((SELECT COUNT(*) FROM Documents WHERE number1 = #NUMBER1 AND date1 = #DATE1) > 1)
BEGIN
ROLLBACK TRANSACTION
SET #DATETEXT = CONVERT(VARCHAR(10), #DATE1, 104)
RAISERROR ('A document with the number1 ''%s'' and date1 ''%s'' already exists.', 10, 1, #NUMBER1, #DATETEXT)
END
END
ELSE IF (#NUMBER2 IS NOT NULL AND #DATE2 IS NOT NULL) /*the DATE2 check is redundant*/
BEGIN
IF ((SELECT COUNT(*) FROM Documents WHERE number2 = #NUMBER2 AND date2 = #DATE2) > 1)
BEGIN
ROLLBACK TRANSACTION
SET #DATETEXT = CONVERT(VARCHAR(10), #DATE2, 104)
RAISERROR ('A document with the number2 ''%s'' and date2 ''%s'' already exists.', 10, 1, #NUMBER2, #DATETEXT)
END
END
FETCH NEXT FROM inserted_cursor
END
CLOSE inserted_cursor
DEALLOCATE inserted_cursor
END
Please tell me how well-written and efficient this solution is.
A couple of questions I can come up with:
Will this trigger validate correctly against existing rows and newly inserted/updated rows in case of bulk modifications? It should, because the modifications are already applied to the table in the scope of this transaction, right?
Are the constraint violations handled correctly? Meaning, was I right to use the rollback transaction and raiserror pair?
Is the "IF NOT EXISTS (SELECT * FROM INSERTED) RETURN" statement used correctly?
Is the use of COUNT to check the constraints acceptable, or should I use some other way of checking the uniqueness of number-date pairs?
Can this solution be optimized in terms of execution speed? Should I add non-unique indexes on both number-date pairs?
Thanks.
EDIT:
A solution using a check constraint and an indexed view, based on Damien_The_Unbeliever's answer:
CREATE TABLE dbo.Documents
(
id int identity primary key,
number1 nvarchar(32),
date1 datetime,
number2 nvarchar(32),
date2 datetime,
constraint CK_Documents_AtLestOneNotNull CHECK (
(number1 is not null and date1 is not null) or
(number2 is not null and date2 is not null)
)
);
go
create view dbo.UniqueDocuments
with schemabinding
as
select
CASE WHEN (number1 is not null and date1 is not null)
THEN CAST(1 AS BIT)
ELSE CAST(0 AS BIT)
END as first_pair_filled,
CASE WHEN (number1 is not null and date1 is not null)
THEN number1
ELSE number2
END as number,
CASE WHEN (number1 is not null and date1 is not null)
THEN date1
ELSE date2
END as [date]
from
dbo.Documents
go
create unique clustered index IX_UniqueDocuments on dbo.UniqueDocuments(first_pair_filled,number,[date])
go
I would avoid the trigger, and use a check constraint and an indexed view:
CREATE TABLE dbo.Documents
(
id int identity primary key,
number1 nvarchar(32),
date1 datetime,
number2 nvarchar(32),
date2 datetime,
constraint CK_Documents_AtLestOneNotNull CHECK (
(number1 is not null and date1 is not null) or
(number2 is not null and date2 is not null)
)
);
go
create view dbo.UniqueDocuments
with schemabinding
as
select
COALESCE(number1,number2) as number,
COALESCE(date1,date2) as [date]
from
dbo.Documents
go
create unique clustered index IX_UniqueDocuments on dbo.UniqueDocuments(number,[date])
go
Which has the advantage that, although there is some "trigger-like" behaviour because of the indexed view, it's well-tested code that's already been deeply integrated into SQL Server.
I would use this logic instead (I didn't type it all as it takes ages), and definitely use SELECT 1 FROM ... in the IF EXISTS() statement as it helps performance. Also remove the cursors like marc_s said.
CREATE TRIGGER trg_validate_requisite_uniqueness
ON dbo.[Documents]
AFTER INSERT, UPDATE
AS
DECLARE #Number1 NVARCHAR(100) = (SELECT TOP 1 number1 FROM dbo.Documents ORDER BY Id DESC)
DECLARE #Date1 DATETIME = (SELECT TOP 1 date1 FROM dbo.Documents ORDER BY Id DESC)
DECLARE #Number2 NVARCHAR(100) = (SELECT TOP 1 number2 FROM dbo.Documents ORDER BY Id DESC)
DECLARE #Date2 DATETIME = (SELECT TOP 1 date2 FROM dbo.Documents ORDER BY Id DESC)
DECLARE #DateText NVARCHAR(100)
IF EXISTS (SELECT 1 FROM dbo.Documents AS D
INNER JOIN INSERTED AS I ON D.id = I.id WHERE I.Number1 IS NULL AND I.number2 IS NULL)
BEGIN
ROLLBACK TRANSACTION
RAISERROR ('Either the first or the second number pair should be filled.', 10, 1)
END
ELSE IF EXISTS (SELECT 1 FROM dbo.Documents AS D
INNER JOIN INSERTED AS I ON D.id = I.id WHERE I.Date1 IS NULL AND I.Date2 IS NULL)
BEGIN
ROLLBACK TRANSACTION
RAISERROR ('Either the first or the second date pair should be filled.', 10, 1)
END
ELSE IF EXISTS (SELECT 1 FROM dbo.Documents AS D
GROUP BY D.number1, D.date1 HAVING COUNT(*) >1
)
BEGIN
ROLLBACK TRANSACTION
SET #DateText = (SELECT CONVERT(VARCHAR(10), #Date1, 104))
RAISERROR ('Cannot have duplicate values', 10, 1, #Number1, #DateText )
END