insertion in a self referenced table - sql

If I have a table
Table
{
ID int primary key identity,
ParentID int not null foreign key references Table(ID)
}
how does one insert first row into a table?
From a business logic point of view not null constraint on ParentID should not be dropped.

In SQL Server, a simple INSERT will do:
create table dbo.Foo
(
ID int primary key identity,
ParentID int not null foreign key references foo(ID)
)
go
insert dbo.Foo (parentId) values (1)
select * from dbo.Foo
results in
ID ParentID
----------- -----------
1 1
If you're trying to insert a value that will be different from your identity seed, the insertion will fail.
UPDATE:
The question is not too clear on what the context is (i.e. is the code supposed to work in a live production system or just a DB setup script) and from the comments it seems hard-coding the ID might not be an option. While the code above should normally work fine in the DB initialization scripts where the hierarchy root ID might need to be known and constant, in case of a forest (several roots with IDs not known in advance) the following should work as intended:
create table dbo.Foo
(
ID int primary key identity,
ParentID int not null foreign key references foo(ID)
)
go
insert dbo.Foo (parentId) values (IDENT_CURRENT('dbo.Foo'))
Then one could query the last identity as usual (SCOPE_IDENTITY, etc.). To address #usr's concerns, the code is in fact transactionally safe as the following example demonstrates:
insert dbo.Foo (parentId) values (IDENT_CURRENT('dbo.Foo'))
insert dbo.Foo (parentId) values (IDENT_CURRENT('dbo.Foo'))
insert dbo.Foo (parentId) values (IDENT_CURRENT('dbo.Foo'))
select * from dbo.Foo
select IDENT_CURRENT('dbo.Foo')
begin transaction
insert dbo.Foo (parentId) values (IDENT_CURRENT('dbo.Foo'))
rollback
select IDENT_CURRENT('dbo.Foo')
insert dbo.Foo (parentId) values (IDENT_CURRENT('dbo.Foo'))
select * from dbo.Foo
The result:
ID ParentID
----------- -----------
1 1
2 2
3 3
currentIdentity
---------------------------------------
3
currentIdentity
---------------------------------------
4
ID ParentID
----------- -----------
1 1
2 2
3 3
5 5

If you need to use an explicit value for the first ID, when you insert your first record, you can disable the checking of the IDENTITY value (see: MSDN: SET IDENTITY_INSERT (Transact-SQL)).
Here's an example that illistrates this:
CREATE TABLE MyTable
(
ID int PRIMARY KEY IDENTITY(1, 1),
ParentID int NOT NULL,
CONSTRAINT MyTable_ID FOREIGN KEY (ParentID) REFERENCES MyTable(ID)
);
SET IDENTITY_INSERT MyTable ON;
INSERT INTO MyTable (ID, ParentID)
VALUES (1, 1);
SET IDENTITY_INSERT MyTable OFF;
WHILE ##IDENTITY <= 5
BEGIN
INSERT INTO MyTable (ParentID)
VALUES (##IDENTITY);
END;
SELECT *
FROM MyTable;
IF OBJECT_ID('MyTable') IS NOT NULL
DROP TABLE MyTable;

It seems like the NOT NULL constraint is not true for the root node in the tree. It simply does not have a parent. So the assumption that ParentID is NOT NULL is broken from the beginning.
I suggest you make it nullable and add an index on ParentID to validate that there is only one with value NULL:
create unique nonclustered index ... on T (ParentID) where (ParentID IS NULL)
It is hard to enforce a sound tree structure in SQL Server. You can get multiple roots for example or cycles in the graph. It is hard to validate all that and it is unclear if it is worth the effort. It might well be, depending on the specific case.

Related

How can I insert values into a table with a composite primary key?

These are the tables I already have:
CREATE TABLE Gyartok
(
GyID INT IDENTITY(2, 3),
Nev VARCHAR(20),
CONSTRAINT PK_Gyartok PRIMARY KEY (GyID)
)
CREATE TABLE Focicsuka
(
CsID INT IDENTITY(2, 2),
Meret INT,
CONSTRAINT PK_Focicsuka PRIMARY KEY (CsID)
)
CREATE TABLE FcsGyartjaGya
(
GyID INT IDENTITY(3, 2),
CsID INT,
Ar INT,
CONSTRAINT FK_FcsGyartjaGya1
FOREIGN KEY (GyID) REFERENCES Gyartok(GyID),
CONSTRAINT FK_FcsGyartjaGya2
FOREIGN KEY (CsID) REFERENCES Focicsuka(CsID),
CONSTRAINT PK_FcsGyartjaGya
PRIMARY KEY (GyID, CsID)
)
The problem is that every time I try to add new values to the table (like such)
INSERT INTO FcsGyartjaGya (Ar) VALUES (300);
I get an error saying I didn't initialize the CsID INT column:
Cannot insert the value NULL into column 'CsID', table 'Lab3.dbo.FcsGyartjaGya'; column does not allow nulls. INSERT fails.
I know I must initialize it with something, but I have no idea what do to it with, because IDENTITY(x, y) doesn't work (it's occupied by another column already) and adding another parameter to the code (like such)
INSERT INTO FcsGyartjaGya (Ar, CsID) VALUES (300, 7);
creates another error which says
The INSERT statement conflicted with the FOREIGN KEY constraint "FK_FcsGyartjaGya1". The conflict occurred in database "Lab3a", table "dbo.Gyartok", column 'GyID'.
It is important to note that I already filled every column with data, so that couldn't be the problem.
As I mention in the comments, your INSERT will work fine, provided the stars align correctly. For your table Gyartok you have GyID as your PRIMARY KEY, which is defined as a IDENTITY(2,3); so the first value generated is 2 and then each row attempted to be INSERTed will increment by 3.
So, if we run the following, we get the IDs 2, 5, 7 and 17. (11 and 14 are skipped as the INSERT failed).
CREATE TABLE Gyartok (
GyID INT IDENTITY(2, 3),
Nev VARCHAR(20),
CONSTRAINT PK_Gyartok PRIMARY KEY (GyID)
);
GO
INSERT INTO dbo.Gyartok (Nev)
VALUES ('asdfjahsbvd'),
('ashjkgdfakd'),
('kldfbhjo');
GO
INSERT INTO dbo.Gyartok (Nev)
VALUES (REPLICATE('A',25)), --Force a truncation error
('ashjkgdfakd');
GO
INSERT INTO dbo.Gyartok (Nev)
VALUES (REPLICATE('A',15));
Let's now add some data for your other table:
CREATE TABLE Focicsuka (
CsID INT IDENTITY(2, 2),
Meret INT,
CONSTRAINT PK_Focicsuka PRIMARY KEY (CsID)
)
INSERT INTO dbo.Focicsuka (Meret)
VALUES(12),
(25);
Now we want to INSERT into the table FcsGyartjaGya, defined as the following:
CREATE TABLE FcsGyartjaGya (
GyID INT IDENTITY(3, 2),
CsID INT,
Ar INT,
CONSTRAINT FK_FcsGyartjaGya1 FOREIGN KEY (GyID) REFERENCES Gyartok(GyID),
CONSTRAINT FK_FcsGyartjaGya2 FOREIGN KEY (CsID) REFERENCES Focicsuka(CsID),
CONSTRAINT PK_FcsGyartjaGya PRIMARY KEY (GyID, CsID)
)
This has a IDENTITY on GyID, but defined as an IDENTITY(3,2), so the first value is 3 and then incremented by 2.
As this has 2 foreign keys, on GyID and CsID when we INSERT the row the values must appear in the respective tables. As GyID is defined as anIDENTITY(3,2) however, this is where we need to rely on the Stars luck for the INSERT to work. Why? Well 2 + (3*n) and 3+(2*n) can give very different numbers. The first are as you saw at the start of this answer. For the latter, we have numbers like 3, 5, 7, 9, 11. As you can see, only 1 in 3 of these numbers match a number in our original sequence, so luck is what we are going to be relying on.
Let's, therefore, try a single INSERT.
INSERT INTO dbo.FcsGyartjaGya (CsID,Ar)
VALUES(2,1);
The INSERT statement conflicted with the FOREIGN KEY constraint "FK_FcsGyartjaGya1". The conflict occurred in database "Sandbox", table "dbo.Gyartok", column 'GyID'.
Well, that didn't work, but it was expected. 3 isn't a value in the table Gyartok. Let's try again!
INSERT INTO dbo.FcsGyartjaGya (CsID,Ar)
VALUES(2,2);
It worked! The stars Luck was our side, and the IDENTITY value was a value in the table Gyartok. Let's try a couple of rows this time!
INSERT INTO dbo.FcsGyartjaGya (CsID,Ar)
VALUES(4,3),
(4,4);
The INSERT statement conflicted with the FOREIGN KEY constraint "FK_FcsGyartjaGya1". The conflict occurred in database "Sandbox", table "dbo.Gyartok", column 'GyID'.
No!! Not again. :( That's because the stars didn't align; 7 and 9 aren't in the other table. But wait, 11 was in the sequence, so let's try that:
INSERT INTO dbo.FcsGyartjaGya (CsID,Ar)
VALUES(4,5);
Error, again?! No, it cannot be!!! :( Oh wait, I forgot, the stars were against us before, because that INSERT failed against Gyartok for the value of 11. I need to wait for 17!
--13 fails
INSERT INTO dbo.FcsGyartjaGya (CsID,Ar)
VALUES(4,6);
GO
--15 fails
INSERT INTO dbo.FcsGyartjaGya (CsID,Ar)
VALUES(4,6);
GO
--17 works!
INSERT INTO dbo.FcsGyartjaGya (CsID,Ar)
VALUES(4,6);
And now we have another row in our table.
So what is the problem? Your design. GyID is defined as an IDENTITY and a FOREIGN KEY; meaning you are at the "whims" of SQL Server generating a value valid. This is not what you want. Just don't define the column as an IDENTITY and then INSERT the data with all 3 of your columns defined:
CREATE TABLE FcsGyartjaGya (
GyID int,-- IDENTITY(3, 2),
CsID INT,
Ar INT,
CONSTRAINT FK_FcsGyartjaGya1 FOREIGN KEY (GyID) REFERENCES Gyartok(GyID),
CONSTRAINT FK_FcsGyartjaGya2 FOREIGN KEY (CsID) REFERENCES Focicsuka(CsID),
CONSTRAINT PK_FcsGyartjaGya PRIMARY KEY (GyID, CsID)
)
GO
INSERT INTO dbo.FcsGyartjaGya (GyID, CsID, Ar)
VALUES(2,2,1),
(2,4,2),
(5,4,3),
(8,2,4),
(8,4,5);
And all these rows insert fine.
I think there is a bit confusion, if I understand correctly what You're trying to do, then you have two tables each with their own id, which is based on an identity column, so you get new values in those for free.
Then you are trying to make a relation table with extra data.
Issue 1: You cannot have FcsGyartjaGya.GyID be identity if it refers to Gyartok.GyID because you will want to insert into it and not rely on an auto increment. If it doesn't refer to the same it should have another name or my head will possibly explode :))
Issue 2: When populating a relation table you need to insert it with what pairs you want, there is no way SQL server can know how it should match these identity pairs in the relation table
I think this is what people are aiming at in the comments, for example
to insert a relationship between row with Focicsuka.CsID = 1 to Gyartok.GyID 7 and adding Ar = 300 have to look like
INSERT INTO FCSGYARTJAGYA(GYID, CSID, AR)
VALUES(7, 1, 300)
Unless You've forgotten to mention that you want to put some value for each of some value or based on something which can be scripted, in other words unless You have logics to define the pairs and their values, relationship tables cannot have defaults on their foreign key fields.

CHECK constraint in SQL Server not allowing to exceed value from foreign key

I have the following two tables:
CREATE TABLE test1
(
ID int IDENTITY UNIQUE,
length int not null
)
CREATE TABLE test2
(
ID int IDENTITY UNIQUE,
test1number int references test1(ID),
distance int not null
)
Example: lets insert into test1 values 1 and 100 (ID=1 and length=100). Now lets insert into test2 values 1 as ID and test1number=1 as reference from test1. I want to create a constraint which will not allow to write distance bigger than 100 (length from test1).
Any other way than procedure?
If this is for individual rows, and we don't need to assert some property about all rows with the same test1number values then one way to do it is this:
CREATE TABLE test1
(
ID int IDENTITY UNIQUE,
length int not null,
constraint UQ_test1_Length_XRef UNIQUE (ID,Length)
)
go
CREATE TABLE _test2
(
ID int IDENTITY UNIQUE,
test1number int references test1(ID),
_test1length int not null,
distance int not null,
constraint FK_test2_test1_length_xref foreign key (test1number,_test1length)
references test1 (ID,length) on update cascade,
constraint CK_length_distance CHECK (distance <= _test1length)
)
go
create view test2
as
select ID,test1number,distance from _test2
go
create trigger T_I_t2 on test2
instead of insert
as
insert into _test2(test1number,_test1length,distance)
select test1number,length,distance
from inserted i inner join test1 t on i.test1number = t.id
go
We only need the view and trigger if you're trying to hide the existence of this extra column in the _test2 table from your users.

Bulk adding records to one table and using their auto-ids in a relation table

I'm building an application that generates forms, however since these forms can have 100's of fields on them I would like to create them in a few queries as possible. I have no issues against using stored procedures or functions.
Forms
id - auto_inc
...
Fields
id - auto_inc
...
FormFields
formID - foreign key
fieldID - foreign key
When I create a form I run
INSERT INTO forms (xx) VALUES ('xx')
and then use
SELECT SCOPE_IDENTITY()
to get the ID from the form
Next I would like to add all the fields and their formfields relation in one or two queries.
INSERT INTO fields (xx, xx) VALUES ('xx', 'xx'), ('xx', 'xx'), ('xx', 'xx')...
Insert INTO formfields (formID, fieldID)
VALUES (#FORM_ID, #fieldID1), (#FORM_ID #fieldID2)...
I'm using php to generate these queries dynamically so #FORM_ID would actually be a php variable.
If I understand correctly you can use a combination of SCOPE_IDENTITY() and OUTPUT, here's an example
CREATE TABLE Foo (
PK INT IDENTITY(1,1) PRIMARY KEY,
I CHAR(1)
)
CREATE TABLE Bar (
PK INT IDENTITY(1,1) PRIMARY KEY,
J INT
)
CREATE TABLE Map (
FooPK INT,
BarPK INT
)
GO
INSERT INTO Foo(I) VALUES ('A')
DECLARE #FooPK INT = SCOPE_IDENTITY()
DECLARE #temp TABLE (BarPK INT)
INSERT INTO Bar(J)
OUTPUT INSERTED.PK INTO #temp
VALUES (4), (5),(6),(7)
INSERT INTO Map(FooPK, BarPK)
SELECT #FooPK, BarPK
FROM #temp
SELECT * FROM Foo
SELECT * FROM Bar
SELECT * FROM Map
GO
DROP TABLE Foo
DROP TABLE Bar
DROP TABLE Map
GO

Create custom "auto-increment" Compound Primary Key?

I have a set of parent-child tables (1 to many relationships). I'm building the tables, and have some doubts about the use of PKs and auto-increment.
Parent table has an autonumber PK (is used for storing sales ticket header). One record here means on ticket.
Child table is used for storing ticket details. One record here is one line item in the ticket (e.g. coke, mars bar, etc)
I understand that PK for child table should have 2 fields:
Parent tables's PK
A number that makes the line item unique within this ticket
If I use IDENTITY, it will not "restart" after parent's PK changes.
I'll show it with an example:
A) What SQL does
Parent table
Col1 Col2
1 1000
2 2543
3 3454
Note: Col1 is IDENTITY
Child Table
Col1 Col2 Col3
1 1 Coke
1 2 Mars Bar
2 3 Sprite
3 4 Coke
3 5 Sprite
3 6 Mars Bar
Note: Col1 is taken from Parent Table; Col2 is IDENTITY
B) What I want to achieve
Parent table is the same as above
Child Table
Col1 Col2 Col3
1 1 Coke
1 2 Mars Bar
2 1 Sprite
3 1 Coke
3 2 Sprite
3 3 Mars Bar
Note: Col1 is taken from Parent Table; Col2 resets after change in Col1; Col1 composed with Col2 are unique.
Does SQL Server implement this use of keys? Or should I need to code it?
Just as an example:
create table dbo.tOrders (
OrderID int not null identity primary key,
CustomerID int not null
);
create table dbo.tOrderPos (
OrderID int not null foreign key references dbo.tOrders,
OrderPosNo int null,
ProductID int null
);
create clustered index ciOrderPos on dbo.tOrderPos
(OrderID, OrderPosNo);
go
create trigger dbo.trInsertOrderPos on dbo.tOrderPos for insert
as begin
update opo
set OrderPosNo = isnull(opo2.MaxOrderPosNo,0) + opo.RowNo
from (select OrderID, OrderPosNo,
RowNo = row_number() over (partition by OrderID order by (select 1))
from dbo.tOrderPos opo
where OrderPosNo is null) opo
cross apply
(select MaxOrderPosNo = max(opo2.OrderPosNo)
from dbo.tOrderPos opo2
where opo2.OrderID = opo.OrderID) opo2
where exists (select * from inserted i where i.OrderID = opo.OrderID);
end;
go
declare #OrderID1 int;
declare #OrderID2 int;
insert into dbo.tOrders (CustomerID) values (11);
set #OrderID1 = scope_identity();
insert into dbo.tOrderPos (OrderID, ProductID)
values (#OrderID1, 1), (#OrderID1, 2), (#OrderID1, 3);
insert into dbo.tOrders (CustomerID) values (12);
set #OrderID2 = scope_identity();
insert into dbo.tOrderPos (OrderID, ProductID)
values (#OrderID2, 4), (#OrderID2, 5);
insert into dbo.tOrderPos (OrderID, ProductID)
values (#OrderID1, 6);
select * from dbo.tOrderPos;
go
drop trigger dbo.trInsertOrderPos;
drop table dbo.tOrderPos;
drop table dbo.tOrders;
go
The difficulty has been to allow multiple inserts and delayed inserts.
HTH
Another option is using an instead-of-trigger:
create trigger dbo.trInsertOrderPos on dbo.tOrderPos instead of insert
as begin
insert into dbo.tOrderPos
(OrderID, OrderPosNo, ProductID)
select OrderID,
OrderPosNo =
isnull( (select max(opo.OrderPosNo)
from dbo.tOrderPos opo
where opo.OrderID = i.OrderID), 0) +
row_number() over (partition by OrderID order by (select 1)),
ProductID
from inserted i;
end;
Unfortunately it doesn't seem to be possible to set the OrderPosNo "not null" because multiple inserts would lead to a duplicate key. Therefor I couldn't use a primary key and used a clustered index instead.
You don't have a one-to-many relationship.
You have a many-to-many relationship.
A parent can have many items.
A coke can belong to more than one parent.
You want three tables. The in-between table is sometimes called a junction table.
http://en.wikipedia.org/wiki/Junction_table
Note: In the wiki article they only show two columns in the junction table, I believe a best practice is for that table to also have a unique auto-incrementing field.
Note: The two joining fields are usually made a unique index.
You will have to code the logic for this yourself. You might make the task easier by implementing it through triggers, and using window functions (row_number() over (partition by parent_id order by ...).
You can also let the primary key be simply an identity column (the parent_id doesn't have to be part of the PK), and have a "Sequence_Num" column to keep track of the int that you want to reset with each parent_id. You can even do this and still set a clustered index on the parent_id / sequence_num cols.
IMHO the 2nd option is better because it allows more flexibility without any major drawback. It also makes the window function easier to write because you can order by the surrogate key (the identity column) to preserve the insert order when regenerating the sequence_num's. In both cases you have to manage the sequencing of your "sequenec_num" column yourself.

Restrict database tree depth

Typically when I represent a parent child hierarchy I have a table as follows (I might also add additional depth columns to speed things up), where parents and children are both foreign key relationships to rows from the same entity table.
Entity Relationships
composite pkey
child id
parent id
What I am trying to figure out is how to limit the depth of the tree to one. In other words, if somebody is the parent of a child, how to I prevent that parent from being a child in itself, so it is impossible to have grandparents or even further?
Depending on your RDBMS, you can handle something like this in an INSERT/UPDATE trigger. For simply restricting a parent to not also be a child it shouldn't be too bad (although I hate using triggers any more than necessary). If you were trying to limit to a certain number of levels (say 100) then you might start to run into performance issues.
In MS SQL Server you can also use user-defined functions in constraints. There are limits however, so I don't know if it would work here. I'll try to test it though.
EDIT:
I just tested this on MS SQL Server 2008 and it looks like it works correctly:
CREATE FUNCTION dbo.Is_Child (#parent_id INT) RETURNS BIT
AS
BEGIN
DECLARE #return BIT
IF EXISTS (SELECT * FROM dbo.Test_Trees WHERE child_id = #parent_id)
SET #return = 1
ELSE
SET #return = 0
RETURN #return
END
GO
CREATE TABLE dbo.Test_Tree_Objects (
my_id INT NOT NULL,
CONSTRAINT PK_Test_Tree_Objects PRIMARY KEY CLUSTERED (my_id)
)
CREATE TABLE dbo.Test_Trees (
my_id INT NOT NULL IDENTITY,
parent_id INT NOT NULL CHECK (dbo.Is_Child(parent_id) = 0),
child_id INT NOT NULL,
CONSTRAINT PK_Test_Trees PRIMARY KEY CLUSTERED (my_id),
CONSTRAINT FK_Test_Trees_parent_id FOREIGN KEY (parent_id) REFERENCES dbo.Test_Tree_Objects (my_id),
CONSTRAINT FK_Test_Trees_child_id FOREIGN KEY (child_id) REFERENCES dbo.Test_Tree_Objects (my_id)
)
GO
INSERT INTO dbo.Test_Tree_Objects (my_id) VALUES (1), (2), (3), (4), (5)
GO
INSERT INTO dbo.Test_Trees (parent_id, child_id) VALUES (1, 2)
INSERT INTO dbo.Test_Trees (parent_id, child_id) VALUES (2, 3)
DROP TABLE dbo.Test_Trees
DROP TABLE dbo.Test_Tree_Objects
DROP FUNCTION dbo.Is_Child
GO