SQL Database multiple values for same attribute - Best practices? - sql

I have found myself that some attributes from my Person table, need to hold multiple values/choices, which is not a good SQL practice so I created a second table, like this:
Before:
Person table
-ID (ex. 101)
-Name (ex. John)
-Accessories (ex. Scarf, Mask, Headband, etc..) - One person can have a combination of this
After:
Person Table
-ID
-Name
PersonDetails Table
-PersonID (FK to Person table)
-Attribute type
-Attribute value
and an example:
Person:
ID:13; Name: John Snow
PersonDetails:
PersonID: 13; Attribute type: Accessories; Attribute value: Scarf
PersonID: 13; Attribute type: Accessories; Attribute value: Mask
You can see that person with ID 13 has both Scarf and Mask.
Is this a good practice? What other ways are there to do this the most efficiently?
Also, what ways are there if an update comes up and Person with 13 doesn't have Scarf and Mask but only Glasses? (Delete the 2 separately and insert a new one? that means 3 queries for only one modify request)

I think this is rather n:m-related. You'd need one table Person holding ID, name and other person's details. Another table Accessory with ID, name and more accessory's details. And a third table PersonAccessory to store pairs of PersonID and AccessoryID (this is called mapping table)
Working example (SQL-Server syntax)
CREATE TABLE Person(ID INT IDENTITY PRIMARY KEY,Name VARCHAR(100));
INSERT INTO Person VALUES('John'),('Jim');
CREATE TABLE Accessory(ID INT IDENTITY PRIMARY KEY,Name VARCHAR(100));
INSERT INTO Accessory VALUES('Scarf'),('Mask');
CREATE TABLE PersonAccessory(PersonID INT NOT NULL FOREIGN KEY REFERENCES Person(ID)
,AccessoryID INT NOT NULL FOREIGN KEY REFERENCES Accessory(ID));
INSERT INTO PersonAccessory VALUES(1,1),(2,1),(2,2);
SELECT p.Name
,a.Name
FROM PersonAccessory AS pa
INNER JOIN Person AS p ON pa.PersonID=p.ID
INNER JOIN Accessory AS a ON pa.AccessoryID=a.ID;
GO
--DROP TABLE PersonAccessory;
--DROP TABLE Accessory;
--DROP TABLE Person
The result
John Scarf
Jim Scarf
Jim Mask

Here is a working example. Check this out
;with tmp(Personid, name,AttributeType, DataItem, Data) as (
select Personid, name,'Accessories' AttributeType, LEFT(Accessories, CHARINDEX(',',Accessories +',')-1),
STUFF(Accessories , 1, CHARINDEX(',',Accessories +','), '')
from Person
union all
select Personid, name,'Accessories' AttributeType, LEFT(Data, CHARINDEX(',',Data+',')-1),
STUFF(Data, 1, CHARINDEX(',',Data+','), '')
from tmp
where Data > ''
)
select Personid, name,AttributeType, DataItem
from tmp
order by Personid

Related

How to query parent & child table in one query?

First, I'll address concerns about duplicates:
How to query a parent table and inherited child table together in one query
This question is similar but it doesn't provide a concrete example
How can you represent inheritance in a database? suggests "Class Table Inheritance", which is the pattern I'm using, but does not explain how to query it effectively.
Here's a example of the problem I'm facing:
table Document {
id: Id
name: string
type: ??
}
table FooDoc {
id: Id
// Foreign key to Document
docId: Id
qux: string
}
table BarDoc {
id: Id
// Foreign key to document
docId: Id
baz: number
}
Ideally, I'd like to make it so that in 1 query, I can
grab a document based on its id
grab the relevant data from the correct child table
Is this possible?
There are six ways (afaik) to model table inheritance in relational databases. You chose the Permissive Class Table Inheritance option.
Now, you can use two left joins to retrieve information for child tables. The resulting columns from the non-matching type will be null.
For example:
select d.*, f.qux, b.baz
from document d
left join foodoc f on f.id = d.id
left join bardoc b on b.id = d.id
Result:
id name type qux baz
--- ----- ----- -------- ----
20 baz1 2 null 1240
10 foo1 1 content null
See running example at DB Fiddle. As you can see, column qux is null for type 2 and column baz is null for type 1.
The sample structure for this example is shown below:
create table document (
id int primary key not null,
name varchar(10),
type int not null check (type in (1, 2))
);
insert into document (id, name, type) values
(10, 'foo1', 1),
(20, 'baz1', 2);
create table foodoc (
id int primary key not null references document(id),
qux varchar(10)
);
insert into foodoc (id, qux) values (1, 'content');
create table bardoc (
id int primary key not null references document(id),
baz int
);
insert into bardoc (id, baz) values (2, 1240);
Note: Also please consider that to fully implement integrity you would need to include the type column in both foreign keys.

Insert Into...Select - get and use autoincrement ID

I have a table which contains all information about a player. What I'd like to do is take this information and split the data into different tables. For example:
Table PlayerData has all the information. Table Address holds information about the player's residence and table Info holds information such as Name, date of birth and the Address ID (pointing to the Address table).
I can use an INSERT INTO...SELECT to copy data across. However, my issue comes in doing this sequentially such that the correct ID outputted from the Address table is inserted into the Info table otherwise there would be a mix up between which address belongs to which player. How can I get the identity created for an Address insert and use that in the subsequent Staff insert?
Speed is not a priority as this is only done once to initialise the database, the integrity is crucial.
Thanks
If you are learning how to do this, then learn the right way: the OUTPUT clause (documented here).
This allows you to put the results into a temporary table (usually a table variable). An example is:
DECLARE #ids TABLE (AddressId int);
INSERT INTO ADDRESS( . . .)
OUTPUT inserted.AddressId INTO #ids
VALUES ( . . . );
INSERT INTO info( . . ., AddressId)
SELECT . . . , i.AddressId
FROM #ids i;
You should really be using foreign keys to reference a main table. Now I've kept this simple but if you can follow this then you will be able to edit it for your needs.
Ok, let's make some sample data. This is an equivalent of your current table;
CREATE TABLE PlayerData (PlayerID int, PlayerName varchar (20), DateOfBirth date, Address1 varchar(20), Address2 varchar(20))
INSERT INTO PlayerData (PlayerID, PlayerName, DateOfBirth, Address1, Address2)
VALUES
(1,'Mike Hunt','1980-01-01','Mike Street','Hunt Town')
,(2,'Harry Dong','1970-02-02','Harry Street','Dong Town')
,(3,'Hugh Gass','1960-03-03','Hugh Street','Gass Town')
,(4,'Neil Down','1950-04-04','Neil Street','Down Town')
,(5,'Seymore Butts','1940-05-05','Seymore Street','Butts Town')
I'm going to create one table that holds a unique list of my player id numbers, this is where I would put a little further information that doesn't fit into the other tables. For this example I've just got the one field;
CREATE TABLE PlayerNum (PlayerID int PRIMARY KEY CLUSTERED)
I'm now going to make my new AddressData table. Notice it's got it's own identity field but also has a PlayerID that will reference the PlayerNum table;
CREATE TABLE AddressData (AddressID int identity(10,1) PRIMARY KEY CLUSTERED, PlayerID int, Address1 varchar(20), Address2 varchar(20), FOREIGN KEY (PlayerID) REFERENCES PlayerNum(PlayerID))
I'm going to do the same for the table that will contain my player's personal info;
CREATE TABLE PlayerPersonalInfo (InfoID int identity(50,1) PRIMARY KEY CLUSTERED, PlayerID int, PlayerName varchar(20), DateOfBirth date, FOREIGN KEY (PlayerID) REFERENCES PlayerNum(PlayerID))
So I've now got my new 3 tables that are empty and one table with data to insert into them.
Let's first populate our PlayerNum table, this needs to be first because of the foreign key constraints on the other tables;
INSERT INTO PlayerNum (PlayerID)
SELECT PlayerID
FROM PlayerData
Now I've done that, let's insert our data into AddressData. Notice I'm not inserting data into the AddressID field as it's an identity field. It will start from 10 and increment by 1 as per the table definition;
INSERT INTO AddressData (PlayerID, Address1, Address2)
SELECT PlayerID, Address1, Address2
FROM PlayerData
I'm going to do the same with my PlayerPersonalInfo data. The identity for this table will start from 50 and increment by 1;
INSERT INTO PlayerPersonalInfo (PlayerID, PlayerName, DateOfBirth)
SELECT PlayerID, PlayerName, DateOfBirth
FROM PlayerData
You can now get rid of the PlayerData table if you're confident you don't need it.
DROP TABLE PlayerData
You'll now have 3 tables;
PlayerNum
PlayerID
1
2
3
4
5
AddressData
AddressID PlayerID Address1 Address2
10 1 Mike Street Hunt Town
11 2 Harry Street Dong Town
12 3 Hugh Street Gass Town
13 4 Neil Street Down Town
14 5 Seymore Street Butts Town
PlayerPersonalInfo
InfoID PlayerID PlayerName DateOfBirth
50 1 Mike Hunt 1980-01-01
51 2 Harry Dong 1970-02-02
52 3 Hugh Gass 1960-03-03
53 4 Neil Down 1950-04-04
54 5 Seymore Butts 1940-05-05
Notice that the PlayerID in the final two tables can now be linked to PlayerNum in order to retrieve your data.
As we're using foreign keys, you cannot have a player with information in AddressData or PlayerPersonalInfo without a corresponding entry in PlayerNum

Moving table columns to new table and referencing as foreign key in PostgreSQL

Suppose we have a DB table with fields
"id", "category", "subcategory", "brand", "name", "description", etc.
What's a good way of creating separate tables for
category, subcategory and brand
and the corresponding columns and rows in the original table becoming foreign key references?
To outline the operations involved:
get all unique values in each column of the original table which should become foreign keys;
create tables for those
create foreign key reference columns in the original table (or a copy)
In this case, the PostgreSQL DB is accessed via Sequel in a Ruby app, so available interfaces are the command line, Sequel, PGAdmin, etc...
The question: how would you do this?
-- Some test data
CREATE TABLE animals
( id SERIAL NOT NULL PRIMARY KEY
, name varchar
, category varchar
, subcategory varchar
);
INSERT INTO animals(name, category, subcategory) VALUES
( 'Chimpanzee' , 'mammals', 'apes' )
,( 'Urang Utang' , 'mammals', 'apes' )
,( 'Homo Sapiens' , 'mammals', 'apes' )
,( 'Mouse' , 'mammals', 'rodents' )
,( 'Rat' , 'mammals', 'rodents' )
;
-- [empty] table to contain the "squeezed out" domain
CREATE TABLE categories
( id SERIAL NOT NULL PRIMARY KEY
, category varchar
, subcategory varchar
, UNIQUE (category,subcategory)
);
-- The original table needs a "link" to the new table
ALTER TABLE animals
ADD column category_id INTEGER -- NOT NULL
REFERENCES categories(id)
;
-- FK constraints are helped a lot by a supportive index.
CREATE INDEX animals_categories_fk ON animals (category_id);
-- Chained query to:
-- * populate the domain table
-- * initialize the FK column in the original table
WITH ins AS (
INSERT INTO categories(category, subcategory)
SELECT DISTINCT a.category, a.subcategory
FROM animals a
RETURNING *
)
UPDATE animals ani
SET category_id = ins.id
FROM ins
WHERE ins.category = ani.category
AND ins.subcategory = ani.subcategory
;
-- Now that we have the FK pointing to the new table,
-- we can drop the redundant columns.
ALTER TABLE animals DROP COLUMN category, DROP COLUMN subcategory;
-- show it to the world
SELECT a.*
, c.category, c.subcategory
FROM animals a
JOIN categories c ON c.id = a.category_id
;
Note: the fragment:
WHERE ins.category = ani.category
AND ins.subcategory = ani.subcategory
will lead to problems if these columns contain NULLs.
It would be better to compare them using
(ins.category,ins.subcategory)
IS NOT DISTINCT FROM
(ani.category,ani.subcategory)
I'm not sure I completely understand your question, if this doesn't seem to answer it, then please leave a comment and possibly improve your question to clarify, but it sounds like you want to do a CREATE TABLE xxx AS. For example:
CREATE TABLE category AS (SELECT DISTINCT(category) AS id FROM parent_table);
Then alter the parent_table to add a foreign key constraint.
ALTER TABLE parent_table ADD CONSTRAINT category_fk FOREIGN KEY (category) REFERENCES category (id);
Repeat this for each table you want to create.
Here is the related documentation:
CREATE TABLE
ALTER TABLE
Note: code and references are for Postgresql 9.4

need help in primary key and foreign key

I need help in auto populating the primary key values in foreign key table while inserting data in foreign key table. For Example: I have created table:
create table Patient
(
PatientId int IDENTITY(1,1) primary key,
FirstName varchar(50),
SurName varchar(50),
Gender char(20),
)
Say 5 rows are there in this Patient Table:
Say First Row value is: 1, Priya, Kumari, Female
I have created the Guardians Table:
create table Guardians
(
GuardiansId int identity(1,1) primary key,
PatientId int foreign key references Patient(PatientId),
FirstName varchar(50),
SurName varchar(50),
Gender char(20),
RelationToPatient varchar(50),
)
In this table Insert operations are like this:
insert into Guardians(FirstName, SurName, Gender,RelationToPatient)values('Sumit','Kumar','Male','Wife')
While selecting the Guardians Table PatientId showing NULL values: My query is while inserting the values in Guardians Table PatientId should be auto Populated which will come from Patient Table...
My second problem is: How to create the Identity column as varchar. For example: suppose I want to increment my Guardians Table with 'GRD0001', 'GRD0002', 'GRD0003' like this...
Thanks,
S.D
Your question is not very clear - what exactly do you want to do??
When you insert something into the Guardians table, you want to automatically also insert it into the Patients table? I don't quite follow. Can you make a complete example, maybe??
If you need to capture the insert IDENTITY value from the Patient table, do this:
DECLARE #NewPatientID INT
INSERT INTO dbo.Patient(fields) VALUES(.......)
SET #NewPatientID = SCOPE_IDENTITY()
INSERT INTO dbo.Guardians(PatientId, ......) VALUES(#NewPatientID, ......)
As for your second question: leave you GuardiansId IDENTITY as it is (only an INT column can be an IDENTITY and you want to keep that - trust me!) and add a computed column to your table:
ALTER TABLE dbo.Guardians
ADD GuardianIDWithPrefix AS
'GDR' + RIGHT('0000' + CAST(GuardiansId AS VARCHAR(4)), 4) PERSISTED
Since it's a PERSISTED field, you can even index on it and use it like a normal field in every respect.
That should do the trick!

SQL Server: Extracting a Column Into a Table

I have a table with a column that I want to extract out and put into a separate table.
For example, lets say I have a table named Contacts. Contacts has a column named Name which stores a string. Now I want to pull out the names into another table named Name and link the Contact.Name column to the Id of the Name table.
I can only use SQL to do this. Any ideas on the best way to go about this?
Let me know if I can clarify anything, thanks!
[edit]
One problem is that different contacts can be tied to the same name. So when different contacts have the same name and it gets exported the Name table would only have one unique row for that name and all the contacts would point to that row. I guess this wouldn't make sense if I were actually working on a contact book, but I'm just using it to illustrate my problem.
CREATE TABLE Name (NameID int IDENTITY(1, 1), [Name] varchar(50))
INSERT INTO Name ([Name])
SELECT DISTINCT [Name]
FROM Contact
ALTER TABLE Contact
ADD COLUMN NameID int
UPDATE Contact
SET NameID = [Name].NameID
FROM Contact
INNER JOIN [Name]
ON Contact.[Name] = [Name].[Name]
ALTER TABLE Contact
DROP COLUMN [Name]
Then add foreign key constraint, etc.
Create the new table with a Foreign key that points back to the contact table. Then insert the names and contactids from the contact table into this new table. After that you can drop the "name" column from the contact table.
CREATE TABLE Name
(
ContactId int,
Name nvarchar(100)
);
INSERT Name(Name)
SELECT ContactId, Name From Contact;
ALTER TABLE Contact
DROP Column name;
EDIT: Since you have edited the question to mention that one name can be associated with multiple contacts, this changes things in the opposite way.
CREATE TABLE Name
(
NameId int IDENTITY,
Name nvarchar(100)
);
INSERT Name(Name)
SELECT DISTINCT Name From Contact;
ALTER TABLE Contact
ADD NameId int;
UPDATE c
SET c.NameId = n.NameId
FROM Contact c
JOIN Name n on n.Name = c.Name;
ALTER Table Contact
Drop Column Name;
NOTE: Make sure that you create the appropiate foreign key between the Contact and Name tables using the NameId on the Contact table and also create a UNIQUE constraint on the "name" column in the Name table.
insert into another_table( contact_id, name )
select id, name
from contacts;
insert into new_table (contact_id, name)
select min(id), name
from contacts
group by name;
This is one way of ensuring only one row per name - you can substitute other functions for min (like, for eg max).
I'm not too sure why you would want to do this, though. No matter what, you will end up with some contacts that don't have a name linked to them...
ALTER TABLE `Contacts` ADD `name_id` INT( 12 ) NOT NULL
ALTER TABLE `Name` ADD `Name` VARCHAR( 200 ) NOT NULL
INSERT INTO Name (id, name) SELECT id, Name FROM Contacts
ALTER TABLE `Contacts` DROP `Name`
The problem is the name_id field, which is filles with "0" and should be have the same value as the id in the Contacts-Table. Here you can use the LOOP or ITERATE statement (if you using MySQL).