Merge statement executing unmatch even though it matches - tsql

I have this table
CREATE TABLE [dbo].[Book]
(
[PrimaryAuthorId] INT,
[AssociatedAuthorId] INT,
[Description] nvarchar(50),
[FromDate] DATETIME2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL CONSTRAINT [DF_Book_FromDate] DEFAULT SYSUTCDATETIME(),
[ToDate] DATETIME2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL CONSTRAINT [DF_Book_ToDate] DEFAULT CONVERT(DATETIME2, '9999-12-31 23:59:59.9999999'),
PERIOD FOR SYSTEM_TIME ([FromDate], [ToDate]),
CONSTRAINT [PK_AuthorsBook] PRIMARY KEY ([PrimaryAuthorId], [AssociatedAuthorId]),
CONSTRAINT [FK_PrimaryAuthorId_AuthorId] FOREIGN KEY ([PrimaryAuthorId]) REFERENCES [dbo].[Authors]([AuthorId]),
CONSTRAINT [FK_AssociateAuthorId_AuthorId] FOREIGN KEY ([AssociatedAuthorId]) REFERENCES [dbo].[Authors]([AuthorId]),
) WITH (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.[BookHistory]));
I created an spwith a merge statement which updates when combination of <PrimaryAuthorId, AssociatedAuthorId> already exist while it insert when combination doesnt exist. It does this for
2 of the combinations <PrimaryAuthorId, AssociatedAuthorId> and <AssociatedAuthorId, PrimaryAuthorId>
It has this code
declare #PrimaryAuthorId INT =16
declare #AssociatedAuthorId INT =45
declare #Description nvarchar(50)
DECLARE #Book TABLE
(
[PrimaryAuthorId] INT,
[AssociatedAuthorId] INT,
[Description] nvarchar(50),
)
INSERT INTO #Book([PrimaryAuthorId], [AssociatedAuthorId], [Description]) VALUES(#PrimaryAuthorId, #AssociatedAuthorId)
MERGE [dbo].Book D
USING (
SELECT [PrimaryAuthorId]
,[AssociatedAuthorId]
,[EndDate]
,#UpdatedBy
FROM
#AuthorAssociation
) S
(
[PrimaryAuthorId]
,[AssociatedAuthorId]
,[EndDate]
,[UpdatedBy]
)
ON D.[PrimaryAuthorId] = S.[PrimaryAuthorId] AND D.[AssociatedAuthorId] = S.[AssociatedAuthorId]
WHEN MATCHED THEN
UPDATE SET
D.[PrimaryAuthorId] = S.[PrimaryAuthorId],
D.[AssociatedAuthorId] = S.[AssociatedAuthorId],
D.[EndDate] = S.[EndDate],
D.[UpdatedBy] = S.[UpdatedBy]
WHEN NOT MATCHED THEN
INSERT
(
[PrimaryAuthorId]
,[AssociatedAuthorId]
,[StartDate]
,[EndDate]
)
VALUES
(
#PrimaryAuthorId
,#AssociatedAuthorId
,#StartDate
,#EndDate
);
MERGE [dbo].Book D
USING (
SELECT [PrimaryAuthorId]
,[AssociatedAuthorId]
,[EndDate]
,#UpdatedBy
FROM
#AuthorAssociation
) S
(
[PrimaryAuthorId]
,[AssociatedAuthorId]
,[EndDate]
,[UpdatedBy]
)
ON D.[PrimaryAuthorId] = S.[AssociatedAuthorId] AND D.[AssociatedAuthorId] = S.[PrimaryAuthorId]
WHEN MATCHED THEN
UPDATE SET
D.[PrimaryAuthorId] = S.[PrimaryAuthorId],
D.[AssociatedAuthorId] = S.[AssociatedAuthorId],
D.[EndDate] = S.[EndDate],
D.[UpdatedBy] = S.[UpdatedBy]
WHEN NOT MATCHED THEN
INSERT
(
[PrimaryAuthorId]
,[AssociatedAuthorId]
,[StartDate]
,[EndDate]
)
VALUES
(
#AssociatedAuthorId
,#PrimaryAuthorId
,#StartDate
,#EndDate
);
It works the first time it executes which inserts 2 records of the combination of the authorids
The problem is when I run the same script again, I expect it to trigger the update as there is a matching record, but it seems to give me this error on the second merge statement
Violation of PRIMARY KEY constraint PK_AuthorsBook
Why does it trigger the insert when there is a match?

Related

How to do multiple columns update on different where condition using PostgreSQL Upsert Using INSERT ON CONFLICT statement

Suppose I have a table like this
create schema test;
CREATE TABLE test.customers (
customer_id serial PRIMARY KEY,
name VARCHAR UNIQUE,
email VARCHAR NOT NULL,
active bool NOT NULL DEFAULT TRUE,
is_active_datetime TIMESTAMP(3) NOT NULL DEFAULT'1900-01-01T00:00:00.000Z'::timestamp(3)
updated_datetime TIMESTAMP(3) NOT NULL DEFAULT '1900-01-01T00:00:00.000Z'::timestamp(3),
);
Now If i want to update email on conflict name
WHERE $tableName.updated_datetime < excluded.updated_datetime
and i want to update is_active_datetime on conflict name but that condition for this update is where active flag has changed.
WHERE customer.active != excluded.active
basically want to track when active status is changed. so can I do that in single statement like this
Initial insert :
insert INTO test.customers (NAME, email)
VALUES
('IBM', 'contact#ibm.com'),
(
'Microsoft',
'contact#microsoft.com'
),
(
'Intel',
'contact#intel.com'
);
To achieve my purpose I am trying something like this :
select * from test.customers;
INSERT INTO customers (name, email)
VALUES
(
'Microsoft',
'hotline#microsoft.com'
)
ON CONFLICT (name)
DO
UPDATE
SET customers.email = EXCLUDED.email
WHERE $tableName.updated_datetime < excluded.updated_datetime
on CONFLICT (name)
do
update
set is_active_datetime = current_timestamp()
WHERE customer.active != excluded.active ;
Is it possible to do this ? How to do this using this method.
You could update multiple columns with CASE conditions in a single DO UPDATE clause.
INSERT INTO customers (
name
,email
,updated_datetime
)
VALUES (
'Microsoft'
,'hotline#microsoft.com'
,now()
) ON CONFLICT(name) DO
UPDATE
SET email = CASE
WHEN customers.updated_datetime < excluded.updated_datetime
THEN excluded.email
ELSE customers.email --default when condition not satisfied
END
,is_active_datetime = CASE
WHEN customers.active != excluded.active
THEN current_timestamp
ELSE customers.is_active_datetime
END;
Demo

Filtering grouped records in crystal reports

I have an issue that I'm not sure how to overcome. I need to filter a my groups in crystal reports based on a field within the group. Not only that I need it to filter the groups based on if there are two different values in this field within the same group. For example, say I have a table of issues and votes for a council. There is one entry per vote on an issue containing the issue name and the vote cast (either yes, no, or abstain). I will group the table by issue name and I want to filter the groups to show only the issues with a vote split between yes and no (i.e. no abstains and not unanimous). How do I go about doing this?
The data you're feeding into the top-level of the grouping needs to be pre-aggregated so as to show which Votes have multiple different responses. You'd need to do this in whatever your back-end data source is. If I were to do it in SQL, e.g., given the tables:
create table dbo.Issues (
IssueID int identity(1,1) not null ,
constraint pkc_Issues primary key clustered ( IssueID ) ,
IssueText varchar(1000) )
--Note - not putting anything unique on VoterName because there may be 2 Joe Blows in the voter population.
create table dbo.Voters (
VoterID int identity(1,1) not null ,
constraint pkc_Voters primary key clustered ( VoterID ) ,
VoterName varchar(512) not null ) )
create table dbo.Votes (
VoteID int identity(1,1) not null ,
constraint pkn_Votes primary key nonclustered ( VoteID ) ,
VoterID int not null ,
constraint fk_VoterID#Votes foreign key ( VoterID ) references dbo.Voters ( VoterID ) ,
IssueID int not null ,
constraint fk_IssueID#Votes foreign key ( IssueID ) references dbo.Issues ( IssueID ) ,
constraint uci_IssueID_VoterID#Votes unique clustered ( IssueID , VoterID ) ,
VoteResponse varchar(16) null )
I'd pull the data using multiple steps (but feel free to do subqueries, if you think that's more understandable):
select IssueID , Count(VoteResponse) as ResponseCount
into #hasMultiple from (select distinct IssueID , VoteResponse from Votes)
I'd then join back to that, to feed to Crystal:
select dbo.Issues.IssueID ,
dbo.Issues.IssueText ,
cast(case when #hasMultiple.ResponseCount > 1 then 1 else 0 end as bit) as HasMultiple ,
dbo.Votes.VoteID ,
dbo.Votes.VoterID ,
dbo.Votes.VoteResponse ,
dbo.Voters.VoterName
from dbo.Issues
inner join dbo.Votes
on dbo.Issues.IssueID = dbo.Votes.IssueID
left join dbo.Voters
on dbo.Votes.VoterID = dbo.Voters.VoterID
left join #hasMultiple
on dbo.Issues.IssueID = #hasMultiple.IssueID

Use a sequence to populate multiple columns with successive values

How can I use default constraints, triggers, or some other mechanism to automatically insert multiple successive values from a sequence into multiple columns on the same row of a table?
A standard use of a sequence in SQL Server is to combine it with default constraints on multiple tables to essentially get a cross-table identity. See for example the section "C. Using a Sequence Number in Multiple Tables" in the Microsoft documentation article "Sequence Numbers".
This works great if you only want to get a single value from the sequence for each row inserted. But sometimes I want to get multiple successive values. So theoretically I would create a sequence and table like this:
CREATE SEQUENCE DocumentationIDs;
CREATE TABLE Product
(
ProductID BIGINT NOT NULL IDENTITY(1, 1) PRIMARY KEY
, ProductName NVARCHAR(100) NOT NULL
, MarketingDocumentationID BIGINT NOT NULL DEFAULT ( NEXT VALUE FOR DocumentationIDs )
, TechnicalDocumentationID BIGINT NOT NULL DEFAULT ( NEXT VALUE FOR DocumentationIDs )
, InternalDocumentationID BIGINT NOT NULL DEFAULT ( NEXT VALUE FOR DocumentationIDs )
);
Unfortunately this will insert the same value in all three columns. This is by design:
If there are multiple instances of the NEXT VALUE FOR function specifying the same sequence generator within a single Transact-SQL statement, all those instances return the same value for a given row processed by that Transact-SQL statement. This behavior is consistent with the ANSI standard.
Increment by hack
The only suggestion I could find online was to use a hack where you have the sequence increment by the number of columns you need to insert (three in my contrived example) and manually add to the NEXT VALUE FOR function in the default constraint:
CREATE SEQUENCE DocumentationIDs START WITH 1 INCREMENT BY 3;
CREATE TABLE Product
(
ProductID BIGINT NOT NULL IDENTITY(1, 1) PRIMARY KEY
, ProductName NVARCHAR(100) NOT NULL
, MarketingDocumentationID BIGINT NOT NULL DEFAULT ( NEXT VALUE FOR DocumentationIDs )
, TechnicalDocumentationID BIGINT NOT NULL DEFAULT ( ( NEXT VALUE FOR DocumentationIDs ) + 1 )
, InternalDocumentationID BIGINT NOT NULL DEFAULT ( ( NEXT VALUE FOR DocumentationIDs ) + 2 )
)
This does not work for me because not all tables using my sequence require the same number of values.
One possible way using AFTER INSERT trigger is following.
Table definition need to be changed slighlty (DocumentationID columns should be defaulted to 0, or allowed to be nullable):
CREATE TABLE Product
(
ProductID BIGINT NOT NULL IDENTITY(1, 1)
, ProductName NVARCHAR(100) NOT NULL
, MarketingDocumentationID BIGINT NOT NULL CONSTRAINT DF_Product_1 DEFAULT (0)
, TechnicalDocumentationID BIGINT NOT NULL CONSTRAINT DF_Product_2 DEFAULT (0)
, InternalDocumentationID BIGINT NOT NULL CONSTRAINT DF_Product_3 DEFAULT (0)
, CONSTRAINT PK_Product PRIMARY KEY (ProductID)
);
And the trigger doing the job is following:
CREATE TRIGGER Product_AfterInsert ON Product
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON;
IF NOT EXISTS (SELECT 1 FROM INSERTED)
RETURN;
CREATE TABLE #DocIDs
(
ProductID BIGINT NOT NULL
, Num INT NOT NULL
, DocID BIGINT NOT NULL
, PRIMARY KEY (ProductID, Num)
);
INSERT INTO #DocIDs (ProductID, Num, DocID)
SELECT
i.ProductID
, r.n
, NEXT VALUE FOR DocumentationIDs OVER (ORDER BY i.ProductID, r.n)
FROM INSERTED i
CROSS APPLY (VALUES (1), (2), (3)) r(n)
;
WITH Docs (ProductID, MarketingDocID, TechnicalDocID, InternalDocID)
AS (
SELECT ProductID, [1], [2], [3]
FROM #DocIDs d
PIVOT (MAX(DocID) FOR Num IN ([1], [2], [3])) pvt
)
UPDATE p
SET
p.MarketingDocumentationID = d.MarketingDocID
, p.TechnicalDocumentationID = d.TechnicalDocID
, p.InternalDocumentationID = d.InternalDocID
FROM Product p
JOIN Docs d ON d.ProductID = p.ProductID
;
END

Calculating Running Totals

I am trying to re-calculate a few different columns of data for a particular EmployeeID.
I want the hrs_YTD column to keep a running total. What is a good way of updating this info?
HRS_YTD currently has 0.00 values. I wan't to achieve the results in the table below.
ID | CHEKDATE | CHEKNUMBR | HRS | HRS_YTD
EN344944 | 01/1/2014 | dd1001 | 40.00 | 40.00
EN344944 | 01/8/2014 | dd1002 | 30.00 | 70.00
EN344944 | 1/15/2014 | dd1003 | 32.50 | 102.50
etc.....
DECLARE #k_external_id varchar(32)
SET #k_external_id = 'EN344944'
SELECT * INTO #tmpA
FROM dbo.gp_check_hdr a
WHERE a.EMPLOYID = #k_external_id
SELECT a.ID, a.CHEKNMBR, a.CHEKDATE,
(SELECT CAST(SUM(a.[hours]) as decimal(18,2)) FROM #tmpA b
WHERE (b.CHEKDATE <= a.CHEKDATE and YEAR(b.CHEKDATE) = 2013)) AS hrs_ytd
FROM #tmpA a
WHERE YEAR(a.CHEKDATE) = 2013
I really don't know if I can alias a table like I did with #tmpA b, but it's worked for me in the past. That doesn't mean its a good way of doing things though. Can someone show me a way to achieve the results I need?
havent tested this, but you can give this a try
DECLARE #k_external_id varchar(32)
SET #k_external_id = 'EN344944'
SELECT g1.primarykey, g1.ID,g1.CHEKDATE, g1.CHEKNUMBR, g1.HRS ,(SELECT SUM(g2.HRS)
FROM dbo.gp_check_hdr g2
WHERE g2.ID = #k_external_id AND
(g2.primarykey <= g1.primarykey)) as HRS_YTD
FROM dbo.gp_check_hdr g1
WHERE g1.ID = #k_external_id
ORDER BY g1.primarykey;
http://www.codeproject.com/Articles/300785/Calculating-simple-running-totals-in-SQL-Server
The way I'd do this is a combination of a computed column and a user defined function.
The function allows to aggregate the data. In a computed column, you can only work with fields of the same row, hence calling a function (which is allowed) is necessary.
The computed column allows this to work continuously without any additional queries or temp tables, etc. Once it's set, you don't need to run nightly updates or triggers or anything of the sort to keep the data updated, including when records change or get deleted.
Here's my solution ... and SqlFiddle: http://www.sqlfiddle.com/#!3/cd8d6/1/0
Edit:
I've updated this to reflect your need to calculate the running totals per employee. SqlFiddle also updated.
The function:
Create Function udf_GetRunningTotals (
#CheckDate DateTime,
#EmployeeID int
)
Returns Decimal(18,2)
As
Begin
Declare #Result Decimal(18,2)
Select #Result = Cast(Sum(rt.Hrs) As Decimal(18,2))
From RunningTotals rt
Where rt.CheckDate <= #CheckDate
And Year(rt.CheckDate) = Year(#CheckDate)
And rt.EmployeeID = #EmployeeID
Return #Result
End
The Table Schema:
Create Table [dbo].[RunningTotals](
[ID] [int] Identity(1,1) NOT NULL,
[EmployeeID] [int] NOT NULL,
[CheckDate] [datetime] NOT NULL,
[CheckNumber] [int] NOT NULL,
[Hrs] [decimal](18, 2) NOT NULL,
[Hrs_Ytd] AS ([dbo].[udf_GetRunningTotals]([CheckDate],[EmployeeID])), -- must add after table creation and function creation due to inter-referencing of table and function
Constraint [PK_RunningTotals3] Primary Key Clustered (
[ID] ASC
) With (
PAD_INDEX = OFF,
STATISTICS_NORECOMPUTE = OFF,
IGNORE_DUP_KEY = OFF,
ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON
)
) On [PRIMARY]
Result will tally up the YTD hrs for each year.
Note --
You cannot create the function or the table as is since they reference each other.
First, create the table with all but the computed column;
Then, create the function.
Finally, alter the table and add the computed column.
Here's a full running test script:
-- Table schema
Create Table [dbo].[RunningTotals](
[ID] [int] Identity(1,1) NOT NULL,
[EmployeeID] [int] NOT NULL,
[CheckDate] [datetime] NOT NULL,
[CheckNumber] [int] NOT NULL,
[Hrs] [decimal](18, 2) NOT NULL,
Constraint [PK_RunningTotals3] Primary Key Clustered (
[ID] ASC
) With (
PAD_INDEX = OFF,
STATISTICS_NORECOMPUTE = OFF,
IGNORE_DUP_KEY = OFF,
ALLOW_ROW_LOCKS = ON,
ALLOW_PAGE_LOCKS = ON
)
) On [PRIMARY]
Go
-- UDF Function to compute totals
Create Function udf_GetRunningTotals (
#CheckDate DateTime,
#EmployeeID int
)
Returns Decimal(18,2)
As
Begin
Declare #Result Decimal(18,2)
Select #Result = Cast(Sum(rt.Hrs) As Decimal(18,2))
From RunningTotals rt
Where rt.CheckDate <= #CheckDate
And Year(rt.CheckDate) = Year(#CheckDate)
And rt.EmployeeID = #EmployeeID
Return #Result
End
Go
-- Add the computed column to the table
Alter Table RunningTotals Add [Hrs_Ytd] As (dbo.udf_GetRunningTotals(CheckDate, EmployeeID))
Go
-- Insert some test data
Insert into RunningTotals Values (334944, '1/1/2014', '1001', 40.00)
Insert into RunningTotals Values (334944, '1/5/2014', '1002', 30.00)
Insert into RunningTotals Values (334944, '1/15/2014', '1003', 32.50)
Insert into RunningTotals Values (334945, '1/5/2014', '1001', 10.00)
Insert into RunningTotals Values (334945, '1/6/2014', '1002', 20.00)
Insert into RunningTotals Values (334945, '1/8/2014', '1003', 12.50)
-- Test the computed column
Select * From RunningTotals
Your sub query should work just fine.
I used a table variable in place of a temp table.
I also limited the results inserted in the temp table to 2013 to simplify the final select statement and limit the results in the temp table to just what you need. The only other thing is joining the sub query to the main query using the ID but what you have should work as you are limiting the result in your temp table to a specific ID.
DECLARE
#k_external_id varchar(32)
,#k_reporting_year int
SET #k_external_id = 'EN344944'
SET #k_reporting_year = 2013
DECLARE #temp TABLE(
ID NVARCHAR(32)
,CheckDate DATE
,CheckNumber NVARCHAR(6)
,HRS DECIMAL(18,2)
)
INSERT INTO #temp (
ID
,CheckDate
,CheckNumber
,HRS
)
SELECT
ID
,CHEKDATE
,CHEKNMBR
,[hours]
FROM
dbo.gp_check_hdr
WHERE
EMPLOYID = #k_external_id
AND YEAR(a.CHEKDATE) = #k_reporting_year
SELECT
ID
,CheckDate
,CheckNumber
,HRS
,(SELECT SUM(HRS) FROM #temp b WHERE a.ID = b.ID AND b.CheckDate <= a.CheckDate) AS hrs_ytd
FROM
#temp a

Insert and updation of table

I have a table in which datas are given as follows
SkillId SkillName Experience KnowledgeLevelXId
6 c++ NULL NULL
7 Asp.net NULL NULL
9 Flex NULL NULL
10 Flash builder NULL NULL
I wrote a Sp for insertion
ALTER PROCEDURE [dbo].[SkillSettingSave]
(
#SkillName varchar(100)
)
AS
BEGIN
INSERT INTO [HRM_SkillSetting]
(
[SkillName]
)
VALUES
(
#SkillName
)
I need to add update query in above sp, where skillname should not be repeated.
You might check for existance of a record using exists. To avoid race condition you would do it in a single statement:
ALTER PROCEDURE [dbo].[SkillSettingSave]
(
#SkillName varchar(100)
)
AS
BEGIN
INSERT INTO [HRM_SkillSetting]
(
[SkillName]
)
-- Instead of using values you might use select
SELECT #SkillName
-- And insert row only if it does not exists
WHERE NOT EXISTS (SELECT NULL
FROM [HRM_SkillSetting]
WHERE SkillName = #SkillName
)
END