Tuple Versioning and composite primary key - sql

I have to create a database and have to make sure that we can load the data as it was at a specific date, so I decided to use tuple versioning.
Let's say we have the following two tables:
CREATE TABLE Author
(
Id UNIQUEIDENTIFIER NOT NULL,
Firstname VARCHAR(100) NOT NULL,
Surname VARCHAR(200) NOT NULL,
ValidFrom DATETIME NOT NULL,
ValidUntil DATETIME NULL,
PRIMARY KEY (ID, ValidFrom)
)
CREATE TABLE Book
(
Id UNIQUEIDENTIFIER NOT NULL,
Title VARCHAR(100) NOT NULL,
ISBN VARCHAR(100) NOT NULL,
AuthorId UNIQUEIDENTIFIER NOT NULL,
ValidFrom DATETIME NOT NULL,
ValidUntil DATETIME NULL,
PRIMARY KEY (Id, ValidFrom)
)
The first time when I enter a new author I will generate a new GUID. I use this GUID in the book table as well to make a reference to the author.
If there is an update on the author, I create a new record with the same GUID, but define the current date as "ValidFrom" and also set the "ValidUntil" from the original record to the current date.
I don't have to change the book table because Author.Id did not change.
The problem I'm facing now is that I would like to add a foreign key constraint on Book.AuthorId = Author.Id
Unfortunately this does not work because I use a composite primary key. I do not want to add the Author.ValidFrom to my Book table because I just want to reference the most recent one and not a specific version.
Any idea on how I can solve this? I think I could add a trigger that makes sure that you can't delete an author if there is already a book recorded, but I have no solution to allow cascade delete.
I'm grateful for every hint or advise.

This works on 2008 (relies on using a MERGE statement to change which row is being referenced by Book atomically). It does introduce new columns, you might want to hide them behind a view:
CREATE TABLE Author
(
Id UNIQUEIDENTIFIER NOT NULL,
Firstname VARCHAR(100) NOT NULL,
Surname VARCHAR(200) NOT NULL,
ValidFrom DATETIME NOT NULL,
ValidUntil DATETIME NULL,
Active as CASE WHEN ValidUntil is null THEN CONVERT(datetime,'99991231',112) ELSE ValidUntil END Persisted
PRIMARY KEY (ID, ValidFrom),
UNIQUE (ID,Active)
)
go
CREATE TABLE Book
(
Id UNIQUEIDENTIFIER NOT NULL,
Title VARCHAR(100) NOT NULL,
ISBN VARCHAR(100) NOT NULL,
AuthorId UNIQUEIDENTIFIER NOT NULL,
ValidFrom DATETIME NOT NULL,
ValidUntil DATETIME NULL,
PRIMARY KEY (Id, ValidFrom),
FK_Link as CONVERT(datetime,'99991231',112) persisted,
Foreign key (AuthorID,FK_Link) references Author (Id,Active) on delete cascade
)
go
declare #AuthorId uniqueidentifier
set #AuthorId = NEWID()
insert into Author(Id,Firstname,Surname,ValidFrom)
select #AuthorId,'Boris','McBoris',CURRENT_TIMESTAMP
insert into Book(Id,Title,ISBN,AuthorId,ValidFrom)
select NEWID(),'How to use tuple versioning','12345678',#AuthorId,CURRENT_TIMESTAMP
;with newAuthorInfo as (
select #AuthorId as Id,'Steve' as Firstname,'McBoris' as Surname,t.Dupl
from (select 0 union all select 1) t(Dupl)
)
merge into Author a
using newAuthorInfo nai
on
a.Id = nai.Id and
a.ValidUntil is null and
nai.Dupl = 0
when matched then update set ValidUntil = CURRENT_TIMESTAMP
when not matched then insert (Id,Firstname,Surname,ValidFrom)
values (nai.Id,nai.Firstname,nai.Surname,CURRENT_TIMESTAMP);
;with newAuthorInfo as (
select #AuthorId as Id,'Steve' as Firstname,'Sampson' as Surname,t.Dupl
from (select 0 union all select 1) t(Dupl)
)
merge into Author a
using newAuthorInfo nai
on
a.Id = nai.Id and
a.ValidUntil is null and
nai.Dupl = 0
when matched then update set ValidUntil = CURRENT_TIMESTAMP
when not matched then insert (Id,Firstname,Surname,ValidFrom)
values (nai.Id,nai.Firstname,nai.Surname,CURRENT_TIMESTAMP);
go
select * from Author
select * from Book
delete from Author where ValidUntil is not null
select * from Author
select * from Book
delete from Author
select * from Author
select * from Book
For a pre-2008 solution, I don't think you can do better than triggers. You can introduce a second Author table that does just have the Id column (uniquely), which you can FK against from Book, and cascade delete from that table to Book. Then you just need a delete trigger on Author, such that if you're removing the final row from Author for a particular Author Id, you delete the row from this new table

Related

Get a descending list of items using two tables

CREATE TABLE [dbo].[HistoricoSeries] (
[IDHistoricoSeries] INT IDENTITY (1, 1) NOT NULL,
[IDUtilizador] INT NOT NULL,
[codepisodio] INT NOT NULL,
CONSTRAINT [PK_historicoseries] PRIMARY KEY CLUSTERED ([IDHistoricoSeries] ASC),
CONSTRAINT [FK_96] FOREIGN KEY ([IDUtilizador]) REFERENCES [dbo].[Utilizadores] ([IDUtilizador]),
CONSTRAINT [FK_hse] FOREIGN KEY ([codepisodio]) REFERENCES [dbo].[EPISODIOS] ([codepisodio])
);
CREATE TABLE [dbo].[EPISODIOS] (
[idepisodio] INT IDENTITY (1, 1) NOT NULL,
[codepisodio] INT NOT NULL,
[codserie] INT NOT NULL,
[codtemporada] INT NOT NULL,
[numeroepisodio] INT NOT NULL,
[tituloepisodio] VARCHAR (53) NOT NULL,
[duracaominutos] INT NOT NULL,
PRIMARY KEY CLUSTERED ([codepisodio] ASC)
);
These are my table definitions.
string maisVistoEpisodio = "SELECT * FROM EPISODIOS WHERE EPISODIOS.codepisodio IN (SELECT codepisodio, count(codepisodio) AS mais_vistos FROM HISTORICOSERIES GROUP BY codepisodio ORDER BY COUNT (codepisodio) DESC)";
This is my SQL Server query that i've been from quite sometime hacking away with no results.
My end goal was to have a listing of the most watched episodes from the table EPISODIOS, but the error
Only one expression can be specified in the select list when the subquery is not introduced with EXISTS.
arose itself and I don't know how to fix.
Can anyone shed a bit of light?
Thanks.
Your code looks like SQL Server. If so, you can use TOP WITH TIES in the subquery:
SELECT E.*
FROM EPISODIOS E
WHERE E.codepisodio IN (SELECT TOP (1) WITH TIES HS.codepisodio
FROM HISTORICOSERIES HS
GROUP BY HS.codepisodio
ORDER BY COUNT(*) DESC
)

I need to JOIN using a linking table

The three tables are as follows:
CREATE TABLE Artist
(
ArtistKey char(20) NOT NULL PRIMARY KEY,
ArtistName varchar(50) NOT NULL
)
CREATE TABLE AlbumInfo
(
AlbumInfoKey char(20) NOT NULL PRIMARY KEY,
AlbumTitle varchar(50) NOT NULL,
AlbumDate date NULL,
AlbumStudio varchar(50) NULL
)
CREATE TABLE AlbumArtist
(
AlbumInfoKey char(20) NOT NULL,
ArtistKey char(20) NOT NULL,
PRIMARY KEY CLUSTERED
(
AlbumInfoKey ASC,
ArtistKey ASC
))
My objective is to list all of the artists and their albums. I can't seem to get anything to work.
I have tried:
SELECT
Artist.ArtistName,
AlbumInfo.AlbumTitle
FROM Artist
JOIN AlbumArtist
ON Artist.ArtistKey = AlbumArtist.ArtistKey
JOIN AlbumInfo
On AlbumInfo.AlbumInfoKey = AlbumArtist.AlbumInfoKey
However this gives me back nothing not even an error.
Alright, I had to re-do your whole task, and I have come up with more professional, and better way of managing database. You need to drop those tables, and re-do whole thing like show in code below :
--First create Artist table
CREATE TABLE Artist
(
Artist_key int PRIMARY KEY IDENTITY(1,1),
ArtistName varchar(50) NOT NULL,
);
--Then create Album table
CREATE TABLE AlbumInfo
(
Album_key int NOT NULL PRIMARY KEY IDENTITY(1,1),
AlbumTitle varchar(50) NOT NULL,
AlbumDate date NULL,
AlbumStudio varchar(50) NULL,
Artist_key int FOREIGN KEY (Artist_key) REFERENCES Artist(Artist_key)
);
-- Must have Artist data before referencing in the album table
INSERT into Artist (ArtistName) values ('John')
INSERT into AlbumInfo (AlbumTitle,AlbumDate,AlbumStudio,Artist_key) values ('ABC3','2020-6-12','Def3',(select Artist_key from Artist where Artist_key = 1 ))
--test if data has been inserted
SELECT * FROM Artist
SELECT * FROM AlbumInfo
-- And finally this query will show the Artist with their relevant Albums
SELECT ArtistName,af.AlbumTitle,AlbumStudio from Artist a join AlbumInfo af on af.Artist_key = a.Artist_key
And the result is :

How to save auto generated primary key Id in foreign key column in same table

Following is the table structure:
CREATE TABLE [User] (
[Id] bigint identity(1,1) not null,
[FirstName] nvarchar(100) not null,
[LastName] nvarchar(100) not null,
[Title] nvarchar(5) null,
[UserName] nvarchar(100) not null,
[Password] nvarchar(100) not null,
[Inactive] bit null,
[Created] Datetime not null,
[Creator] bigint not null,
[Modified] DateTime null,
[Modifier] bigint null
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED
(
[Id] Asc
)
);
IF NOT EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'[FK_User_Creator]') AND parent_object_id = OBJECT_ID(N'[User]'))
ALTER TABLE [User] ADD CONSTRAINT [FK_User_Creator] FOREIGN KEY([Creator]) REFERENCES [User]([Id])
GO
INSERT INTO [User] (Creator) Values ([Id] ?)
This is a case when table is empty and first user is going to add in table. Otherwise I don't have issue.
How can I insert Id in creator column with insert statement at the same time?
One way could be using Sequence instead of identity column. The below script might serve the same purpose:
CREATE SEQUENCE dbo.useridsequence
AS int
START WITH 1
INCREMENT BY 1 ;
GO
CREATE TABLE [User] (
[Id] bigint DEFAULT (NEXT VALUE FOR dbo.useridsequence) ,
[FirstName] nvarchar(100) not null,
[LastName] nvarchar(100) not null,
[Title] nvarchar(5) null,
[UserName] nvarchar(100) not null,
[Password] nvarchar(100) not null,
[Inactive] bit null,
[Created] Datetime not null,
[Creator] bigint DEFAULT NEXT VALUE FOR dbo.useridsequence ,
[Modified] DateTime null,
[Modifier] bigint null
CONSTRAINT [PK_User] PRIMARY KEY CLUSTERED
(
[Id] Asc
)
);
IF NOT EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'[FK_User_Creator]') AND parent_object_id = OBJECT_ID(N'[User]'))
ALTER TABLE [User] ADD CONSTRAINT [FK_User_Creator] FOREIGN KEY([Creator]) REFERENCES [User]([Id])
GO
INSERT INTO [User]
(
-- Id -- this column value is auto-generated
FirstName,
LastName,
Title,
UserName,
[Password],
Inactive,
Created,
Creator,
Modified,
Modifier
)
VALUES
(
'Foo',
'Bar',
'Title',
'UserName ',
'Password',
0,
GETDATE(),
DEFAULT,
GETDATE(),
1
)
SELECT * FROM [User] AS u
Result :
The short answer is that you can't do this. And I suggest your model is logically flawed in the first place. Do you intend to define all actual database users (e.g., create user ... for login ...) as rows in [Users]? You need to think about that - but the typical answer is no. If the answer is yes, then you don't need the creator column at all because it is redundant. All you need is the created date - for which you probably should have defined a default.
But if you want to do this, you will need to do it in two steps (and you will need to make the column nullable). You insert a row (or rows) with values for the "real" data columns. Then update those same rows with the identity values generated for id. An example showing different ways to do this
use tempdb;
set nocount on;
CREATE TABLE dbo.[user] (
[user_id] smallint identity(3,10) not null primary key,
[name] nvarchar(20) not null,
[active] bit not null default (1),
[created] Datetime not null default (current_timestamp),
[creator] smallint null
);
ALTER TABLE dbo.[user] ADD CONSTRAINT [fk_user] FOREIGN KEY(creator) REFERENCES dbo.[user](user_id);
GO
-- add first row
insert dbo.[user] (name) values ('test');
update dbo.[user] set creator = SCOPE_IDENTITY() where user_id = SCOPE_IDENTITY()
-- add two more rows
declare #ids table (user_id smallint not null);
insert dbo.[user] (name) output inserted.user_id into #ids
values ('nerk'), ('pom');
update t1 set creator = t1.user_id
from #ids as newrows inner join dbo.[user] as t1 on newrows.user_id = t1.user_id;
select * from dbo.[user] order by user_id;
-- mess things up a bit
delete dbo.[user] where name = 'pom';
-- create an error, consume an identity value
insert dbo.[user](name) values (null);
-- add 2 morerows
delete #ids;
insert dbo.[user] (name) output inserted.user_id into #ids
values ('nerk'), ('pom');
update t1 set creator = t1.user_id
from #ids as newrows inner join dbo.[user] as t1 on newrows.user_id = t1.user_id;
select * from dbo.[user] order by user_id;
drop table dbo.[user];
And I changed the identity specification to demonstrate something few developers realize. It isn't always defined as (1,1) and the next inserted value can jump for many reasons - errors and caching/restarts for example. Lastly, I think you will regret naming a table with a reserved word since references to it will require the use of delimiters. Reduce the pain.

Get AutoIncrement ID and insert into Foreign Key Table

I have 3 tables User, Profile and ProfilePicture. Profile and ProfilePicture have a foreign key relation with User table. What I want to do here is whenever I insert data into the User table through web application their AutoGeneratedID get and inserted into Profile and ProfilePicture tables.
CREATE TABLE User
(
UserId INT(11) NOT NULL AUTO_INCREMENT,
Username VARCHAR(45) NOT NULL,
Password VARCHAR(50) NOT NULL,
PRIMARY KEY (`UserId`),
UNIQUE INDEX `UserIdId_UNIQUE` (`UserId` ASC)
);
CREATE TABLE Profile
(
UserId INT(11) NOT NULL,
Firstname VARCHAR(50) NULL,
Lastname VARCHAR(50) NULL,
FOREIGN KEY (UserId) REFERENCES User (UserId)
)
CREATE TABLE ProfilePicture
(
UserId INT(11) NOT NULL,
Picture image NULL,
insertdate date NULL,
FOREIGN KEY (UserId) REFERENCES User (UserId)
)
I know I have to use a trigger but I don't understand how to do this.
I am supposing that you are doing this using stored procedure or raw queries. This thing can be achieved by using OUTPUT clause.
Define a local table with column Id
DECLARE #OutputTbl TABLE (ID INT)
Now when you save the User then insert the new gnerated id into #OutputTbl
INSERT INTO User (Username, Password)
OUTPUT INSERTED.UserId INTO #OutputTbl(ID)
VALUES ('name', 'password')
Now when you need this id in Profile/ProfilePicture, get this id from local table
insert into Profile (
UserId ,
Firstname,
Lastname) Values ((Select ID from #OutputTbl),'fName','lName')
You can try something as below :
CREATE TABLE #tempUser(
UserId INT,
ShopRef INT
)
INSERT INTO [User] (UserPassword,Name,MobileNo,Gender,Dob,Country,State,City,StreetAddress
,ZipCode,IsActive
,CreatedDate,ModifiedBy,CreatedBy,IsAdmin,EmailOtp,UserImage,Rating
,ContactNo)
OUTPUT inserted.UserId, inserted.EmailOtp INTO #tempUser
SELECT 'NA', [Name], [MobileNo], '-','1900-01-01',[Country],[State],[City],[StreetAddress],
[ZipCode], 1
,#Date,#UserId,#UserId,0,ID,'NA',0
,'NA'
FROM #temp WHERE Status ='SUCCESS'
If you are executing these inserts in the same SP then you can use, make sure UserId is an Identity Column :
SET #UserId = SCOPE_IDENTITY()
you can after insert into table, call SCOPE_IDENTITY() function, to get the latest
identity inserted
for more informatin see:
https://msdn.microsoft.com/en-us/library/ms190315.aspx
http://www.codeproject.com/Articles/103610/Difference-between-IDENTITY-SCOPE-IDENTITY-IDENT-C

How to select IDs from a table if two conditions in other tables match

I am developing a user feedback system using ASP.NET and C#. I have multiple tables and am trying to populate dropdown lists so that the feedback can be filtered.
My tables:
CREATE TABLE tblModules
(
Module_ID nvarchar(10) PRIMARY KEY,
Module_Title nvarchar(MAX) NOT NULL
);
CREATE TABLE tblQuestions
(
Q_ID int PRIMARY KEY IDENTITY(1,1),
Question_Text varchar(1000) NOT NULL
);
CREATE TABLE tblFeedback
(
Submission_ID int PRIMARY KEY IDENTITY(1,1),
Username varchar(100) NOT NULL,
Domain varchar(50) NOT NULL,
DateTime_Submitted datetime NOT NULL
Module_ID nvarchar(10)
FOREIGN KEY (Module_ID) REFERENCES tblModules (Module_ID);
);
CREATE TABLE tblAnswers
(
Q_ID int NOT NULL,
Submission_ID int NOT NULL,
Answer_Text varchar(max),
FOREIGN KEY (Q_ID) REFERENCES tblQuestions(Q_ID),
FOREIGN KEY (Submission_ID) REFERENCES tblFeedback(Submission_ID)
);
I have two dropdown lists. First one is populated with all modules from a table. The second needs to be populated with Questions from tblQuestions but only if any answers to it exist (therefore if the Question ID 'Q_ID' exists in tblAnswers).
I can get the selectedModuleID from the first dropdown list. I have a List of all Questions referenced by Q_ID in tblAnswers. How do I crossreference this list with the module ID?
Each feedback submission gets a Submission ID and Module ID.
You want:
questions that have answers
questions that have a module parent (via tblfeedback)
So, my guess at what you want:
SELECT
*
FROM
tblQuestions Q
WHERE
EXISTS (SELECT *
FROM
tblAnswers A
JOIN
tblFeedback F ON A.Submission_ID = F.Submission_ID
WHERE
Q.Q_ID = A.Q_ID AND F.Module_ID = #moduleID)
This should do the trick...
SELECT Q_ID, Question_Text tblQuestions a
WHERE EXISTS (SELECT NULL FROM tblAnswers a WHERE a.Q_ID = q.Q_ID)