Please start any new threads on our new site at https://forums.sqlteam.com. We've got lots of great SQL Server experts to answer whatever question you can come up with.

 All Forums
 General SQL Server Forums
 Database Design and Application Architecture
 Common properties design

Author  Topic 

Ioan
Starting Member

5 Posts

Posted - 2008-01-14 : 13:36:56
I try to find the best design for a SQL Server 2005 database structure.

I have the following 'objects':

Facilities
Departments - 'dependents' of a Facility
Users

Addresses - each of the above can have none or more Addresses.

The basic tables design would be:
CREATE TABLE [dbo].[Facility](
[cFacilityID] [nvarchar](5) NOT NULL PRIMARY KEY CLUSTERED ,
[cFacilityName] [nvarchar](50) NOT NULL
)
GO
CREATE TABLE [dbo].[Department](
[cFacilityID] [nvarchar](5) NOT NULL FOREIGN KEY REFERENCES [dbo].[Facility] ([cFacilityID]) ON UPDATE CASCADE ON DELETE CASCADE,
[cDepartmentID] [nvarchar](3) NOT NULL,
[cDepartmentName] [nvarchar](50) NOT NULL,
PRIMARY KEY CLUSTERED ( [cFacilityID], [cDepartmentID])
)
GO
CREATE TABLE [dbo].[User](
[cUserID] [nvarchar](5) NOT NULL PRIMARY KEY CLUSTERED,
[cUserName] [nvarchar](50) NOT NULL
)
GO
CREATE TABLE [dbo].[Addresses](
[pkAddress] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED,
[cAddress] [nvarchar](255) NULL,
[cZip] [nvarchar](5) NULL,
[cEmail] [nvarchar](255) NULL
)
GO


The question regards the Addresses table:
How can I design the Addresses table(s) so I can enforce relational integrity and cascade update and delete for each Facility, Department and User tables?

I figured out the following options, but none achives my requirements:

1. An Address table for each 'object': FacilityAddress, DepartmentAddress, UserAddress with the corresponding foreign key in each Address table. This solution duplicates all the addresses fields (which are more than I showed in the example).

2. A common Addresses table with a discriminator field (1=Facility, 2=Department, 3=User). This doesn't allow me to enforce referential integrity with constraint, maybe only with triggers.

3. A common Addresses table with a foreign key for each 'object (fkFacility, fkDepartment, fkUser) allowing nulls. In this case, the 'dependence' between Facility and Department stops me to enforce cascade deletes/updates on both Facility and Department foreign key (circular...).

4. An intermediate (link) table between each 'object' and Addresses table with the foreign keys (Facility, Addresses). Again, no referential integrity.

So, is there a method to design such structure?

PS. Please keep in mind that this is just a simplified example (reduced at the key information) of my real structure.

Thanks,
Ioan

LoztInSpace
Aged Yak Warrior

940 Posts

Posted - 2008-01-14 : 18:30:23
5. Typed intermediate table, i.e. an address ID and facility ID, another with address ID and departmentID etc. Essentially its a combination of 1 and 4. This gives you RI and minimises the duplication of the structure. You might also need to include a sequence number to give you uniqueness if you allow more than one address per type.
Go to Top of Page

Ioan
Starting Member

5 Posts

Posted - 2008-01-15 : 08:31:37
quote:
Originally posted by LoztInSpace

5. Typed intermediate table, i.e. an address ID and facility ID, another with address ID and departmentID etc. Essentially its a combination of 1 and 4. This gives you RI and minimises the duplication of the structure. You might also need to include a sequence number to give you uniqueness if you allow more than one address per type.


Maybe I don't understand exactly, but your suggestion is exactly 4.

CREATE TABLE [dbo].[FacilityAddress](
[pkFacilityAddress] [int] IDENTITY(1,1) NOT NULL PRIMARY KEY,
[cFacilityID] [nvarchar](5) NOT NULL FOREIGN KEY REFERENCES Facility (cFacilityID) ON UPDATE CASCADE ON DELETE CASCADE,
[fkAddress] [int] NOT NULL FOREIGN KEY REFERENCES Addresses (pkAddress) ON UPDATE CASCADE ON DELETE CASCADE,
)


This case enforces cascade delete between Facility and FacilityAddress, but not between FacilityAddress and Addresses.

What I want is when I delete a Facility to get the corresponding Addresses records deleted. Same for Department, etc.

Please let me know if I'm wrong.

Thanks,
Ioan
Go to Top of Page

jsmith8858
Dr. Cross Join

7423 Posts

Posted - 2008-01-15 : 15:54:07
Hi Ioan -- well stated problem, very common. I would go with Option 3 in this case. You can have cascading RI on everything but Facility, I believe, and for that one item you are probably better off just using a trigger. I've experienced the same issue in the past, it can be frustrating and this type of design is difficult. I am currently stuck with a web app that uses option #4 and there are orphan "Address" rows all over the place that need to get cleaned up.

One thought about multiple addresses: Consider creating an AddressType table to be sure that you constrain the multiple addresses to one per type per entity. This way, the multiple addresses per entity will have clear, unambiguous meaning. Not always applicable, but I just about always do this and it works well.

Here's my sample script based on your schema. To keep my typing short, I just used a single AddressData int column to represent address data.


create table AddressType
(
TypeCode nvarchar(10) not null primary key,
TypeName nvarchar(100) not null
)

create table Address
(
AddressId int identity not null primary key,
AddressType nvarchar(10) not null references AddressType(TypeCode) on delete cascade,
FacilityID nvarchar(5) references Facility(cFacilityID), -- can't cascade this one ...
DepartmentFacilityID nvarchar(5),
DepartmentID nvarchar(3),
UserID nvarchar(5) references [User](cUserID) on delete cascade,
AddressData int, -- ints are short to type!
foreign key (DepartmentFacilityID, DepartmentID) references Department(cFacilityID, cDepartmentID) on delete cascade,
check (case when FacilityID is null then 0 else 1 end +
case when DepartmentID is null then 0 else 1 end +
case when UserID is null then 0 else 1 end = 1 ),
constraint address_uc unique (AddressType, FacilityID, DepartmentFacilityID , DepartmentID, UserID)
)

-- create some address types:
insert into AddressType values ('HOME','Home Address')
insert into AddressType values ('BILL','Billing Address')

-- add some sample data:
insert into Facility values('F1','Facility 1')
insert into Department values('F1','D1','Dept 1')
insert into [User] values ('u1','User 1')

-- add a home address for facility F1:
insert into Address (FacilityID, AddressType, AddressData)
values ('F1','HOME',0)

--ERROR: disallow two HOME addresses for this facility:
insert into Address (FacilityID, AddressType, AddressData)
values ('F1','HOME',1)

-- adding BILLING address for the facility:
insert into Address (FacilityID, AddressType, AddressData)
values ('F1','BILL',1)

-- ERROR: cannot have an address with more than 1 entity
insert into Address (FacilityID, UserID, AddressType, AddressData)
values ('F1','U1','HOME',1)

-- add user address:
insert into Address (UserID, AddressType, AddressData)
values ('U1','HOME',1)

-- delete User 'U1', verify User addresses are gone only
delete from [User] where cuserID = 'u1'
select * from Address


Just some ideas. Notice the CHECK constraint that only allows one set of values in the FK columns. Also, note that we need to separate the two FacilityID columns, one for Facilities and the other as part of the Department FK.

Hope this helps.

- Jeff
http://weblogs.sqlteam.com/JeffS
Go to Top of Page

Ioan
Starting Member

5 Posts

Posted - 2008-01-15 : 16:34:32
quote:
Originally posted by jsmith8858

I've experienced the same issue in the past, it can be frustrating and this type of design is difficult.

Exactly, Jeff. It's very frustrating!

quote:
Originally posted by jsmith8858
One thought about multiple addresses: Consider creating an AddressType table to be sure that you constrain the multiple addresses to one per type per entity. This way, the multiple addresses per entity will have clear, unambiguous meaning. Not always applicable, but I just about always do this and it works well.

Right, I do have this in my full design, I just left it out for simplicity.

I will try your advice, although I have to be careful because the problem is more complicated: I try to use this re-designed database with the new Entity Framework data access concept. And this force me to some other restrictions (like the trigger idea for Facility address clean up ).

quote:
Originally posted by jsmith8858
Just some ideas. Notice the CHECK constraint that only allows one set of values in the FK columns. Also, note that we need to separate the two FacilityID columns, one for Facilities and the other as part of the Department FK.

Two excellent ideas! I'll definitely try this design.

Thanks a lot Jeff!
Ioan
Go to Top of Page

LoztInSpace
Aged Yak Warrior

940 Posts

Posted - 2008-01-15 : 19:07:02
Hi Ioan,
I think I misunderstood your 4th option. Pretty much what you have is what I was suggesting although I would not have a surrogate PK.
In my world we never delete anything so this issue never comes up. Perhaps a trigger is in order here (I hate them) but personally I would secure all tables and ensure all operations are done via SPs anyway. Then you can implement whatever you need.
Go to Top of Page

Ioan
Starting Member

5 Posts

Posted - 2008-01-16 : 09:25:55
quote:
Originally posted by LoztInSpace

...although I would not have a surrogate PK.

You're right, I'll eliminate it.

quote:
Originally posted by LoztInSpace

Perhaps a trigger is in order here (I hate them) but personally I would secure all tables and ensure all operations are done via SPs anyway. Then you can implement whatever you need.


Right, I don't want to use trigger either.

And yes, the SPs would be good, but, as I said to Jeff above, I'm trying to use Entity Framework data layer, which would be useless if I go with the SPs.

Thanks for your comments.
Ioan
Go to Top of Page

Ioan
Starting Member

5 Posts

Posted - 2008-01-16 : 14:58:13
quote:
Originally posted by jsmith8858

I would go with Option 3 in this case. You can have cascading RI on everything but Facility, I believe, and for that one item you are probably better off just using a trigger.

Trying to implement this solution, I got into another trouble: the Facility's trigger that implements the cascading update on Address table.

So, I have the following structure:
CREATE TABLE [dbo].[Facility](
[cFacilityID] [nvarchar](5) NOT NULL PRIMARY KEY CLUSTERED ,
[cFacilityName] [nvarchar](50) NOT NULL
)
GO
CREATE TABLE [dbo].[Department](
[cFacilityID] [nvarchar](5) NOT NULL FOREIGN KEY REFERENCES [dbo].[Facility] ([cFacilityID]) ON UPDATE CASCADE ON DELETE CASCADE,
[cDepartmentID] [nvarchar](3) NOT NULL,
[cDepartmentName] [nvarchar](50) NOT NULL,
PRIMARY KEY CLUSTERED ( [cFacilityID], [cDepartmentID])
)
GO
CREATE TABLE [dbo].[User](
[cUserID] [nvarchar](5) NOT NULL PRIMARY KEY CLUSTERED,
[cUserName] [nvarchar](50) NOT NULL
)
GO

create table AddressType
(
TypeCode nvarchar(10) not null primary key,
TypeName nvarchar(100) not null
)
GO

create table Address
(
AddressId int identity not null primary key,
AddressType nvarchar(10) not null references AddressType(TypeCode) on delete cascade,
FacilityID nvarchar(5) references Facility(cFacilityID), -- can't cascade this one ...
DepartmentFacilityID nvarchar(5),
DepartmentID nvarchar(3),
UserID nvarchar(5) references [User](cUserID) on delete cascade,
AddressData int, -- ints are short to type!
foreign key (DepartmentFacilityID, DepartmentID) references Department(cFacilityID, cDepartmentID) on delete cascade,
check (case when FacilityID is null then 0 else 1 end +
case when DepartmentID is null then 0 else 1 end +
case when UserID is null then 0 else 1 end = 1 ),
constraint address_uc unique (AddressType, FacilityID, DepartmentFacilityID , DepartmentID, UserID)
)
GO

The Address table has all the RI and cascading except the cascade delete and update for cFacilityID.

I try to implement the cascading delete and update using triggers.

For Delete I created a INSTEAD OF DELETE trigger in Facility which deletes first the records in Address and then in Facility.

But for Update I cannot find a functional solution!

So, what will be the trigger that when [Facility].[cFacilityID] is changed to change all the corresponding records in [Address]?

Thanks,
Ioan
Go to Top of Page

newsqlguy
Starting Member

13 Posts

Posted - 2008-01-22 : 20:12:07
Hi Ioan,

Sorry for the long response, but the issue you raise doesnt have short answer in my opinion.

I experienced your issue on an enterprise scale system with hundreds of objects that many can have references to shared objects such as Address, User, Attachment, Note, PhoneLog, Activity, etc.

Unfortunately the design of SQL doesn't provide good solution for such scenarios. I had devised various solutions each with its own pros and cons and need to be used in the appropriate environment.

First, I make a distinction between two important aspects of RI (Referential Integrity).
1. Prevent zombie records.
2. cascade deletes

while the first feature is critical for data integrity and is a must in my opinion the second is just a nice feature that automates the process for you. Its true that you could implement the integrity on your own too, using code or triggers, but if you have a bug anywhere in your entire system, you begin to leak zombie records which will hunt you down the road. It is very difficult to ensure that there is no bug which could lead to zombie records. On the other hand, as long as you have enforcement, if you forget to delete the records or insert in the right order you get an error immediately. So while I appreciate the second feature when possible, I require the first for the validity and integrity of the system.

generalization of your scenario could be one of the following options:

1. One-to-Many. i.e. An object that could be referenced by a single parent record from various tables.
2. Many-to-Many. i.e. An object that can be referenced by many records from various tables.

(I assume that your scenario is like option 1 above)

If you design link table per type (like your option 4), you will be able to get RI though no cascade of deletes. I would go for it.

the other issue with this design is that if you have many tables like this Address table you will have many link tables (in worst case scenario for a database with N tables that each can be linked to all the other you will need additiona N*N link tables).

To address the issue of many tables you can design the following structure. Create a new table "Collection" with the column "CollectionId (autoId)" and "CollectionType". Add column "OwnerOfCollectionId" to Facility, User and Department. when you create one of these objects create a record in Collection and use that created CollectionId as the value for the OwnerOfCollectionId column of the new object. In the address table add MemberOfCollectionId column with FK to Collections. Create Cascade deletes from Collection to Address (and you might also be able to cascade deletes from Collection to Facility, Department and User. not sure. but you sure get integrity enforcement)

this solution will have issues with performance on high load of inserts and deletes to the collection table (unlikely to be your problem) and is also limiting the total number of Facilities, Users and Departments together to 2^32.

Handling the Many-to-Many situation involves more... so only if you interested let me know.

Hope I helped somewhat,
EazyWay
Go to Top of Page
   

- Advertisement -