Postgresql left join issue - sql

I have two tables
create table sources (
user_id bigint,
source varchar,
created timestamp default now(),
unique(user_id, source)
)
create table subscriptions (
user_id bigint unique primary key,
plan int not null references plans(id),
starts timestamp default now(),
ends timestamp default now() + interval '30' day
)
I try to select everything from sources when user has active subscription, I use this query
SELECT src.* FROM sources AS src LEFT JOIN subscriptions as sub ON sub.user_id=src.user_id WHERE now() < sub.ends
However, it does not return all data, the problem became clear when I tried
SELECT * FROM sources AS src LEFT JOIN subscriptions as sub ON sub.user_id=src.user_id
What may be the problem and how itspossible for me to get this info in the other way?
PS subscriptions table has rows with other user_id's.
Thank you very much!

UPDATE
Your left join is working. If your image is of SELECT * FROM sources AS src LEFT JOIN subscriptions as sub ON sub.user_id=src.user_id then some of your users have no subscriptions.
SELECT * FROM sources AS src LEFT JOIN subscriptions ... only ensures every matching row in sources will be fetched regardless of whether it has a matching row in subscriptions.
[I wrote the rest when I thought it was a different problem, but the advice remains.]
This is possible because sources.user_id is not set up as a foreign key so the database cannot enforce referential integrity. As far as the database is concerned, sources.user_id is just some number. If you delete a user, their sources and subscriptions will hang around.
In general...
Clever primary keys just bring complications. Give everything more complicated than a simple join table id bigserial primary key and be done with it.
Declare everything not null unless you have a good reason it should be null.
Use on delete cascade to clean up when a user is deleted, but see below.
create table sources (
id bigserial primary key,
-- When a user is deleted, its sources will also be deleted
user_id bigint not null references users(id) on delete cascade,
source varchar not null,
created timestamp not null default now(),
unique(user_id, source)
)
create table subscriptions (
id bigserial primary key,
-- When a user is deleted, its subscriptions will also be deleted
user_id bigint not null unique references foreign key users(id) on delete cascade,
-- Make foreign key names consistent, and use a consistent type for ids.
plan_id bigint not null references plans(id) on delete cascade,
starts timestamp not null default now(),
ends timestamp not null default now() + interval '30' day
)
Whether to use on delete cascade when a deleted user still has sources and subscriptions depends on how you want to handle that situation. You might want to prevent deleting a user which still has active sources and subs, in which case leave off on delete cascade and handle cleaning up the user's sources and subs manually.

Related

How can I prevent a table not having references pointing to it?

I have these tables:
CREATE TABLE s_users (
user_id serial PRIMARY KEY,
username text
);
CREATE TABLE s_projects (
project_id serial PRIMARY KEY,
projectname text
);
CREATE TYPE s_MEMBERSHIP_TIER AS ENUM ('pending', 'member', 'admin');
CREATE TABLE s_memberships (
user_id INT NOT NULL,
project_id INT NOT NULL,
membership_tier MEMBERSHIP_TIER,
CONSTRAINT s_one_membership_type
UNIQUE(user_id, project_id)
);
I want to ensure that it will be impossible to get to a situation where:
(1) a membership refers to a project or user that doesn't exist, and
(2) a project is without members.
I think I can achieve (1) by changing the first rows in CREATE TABLE s_memberships ( to
user_id INT NOT NULL REFERENCES s_users ON DELETE CASCADE ON UPDATE CASCADE,
project_id INT NOT NULL REFERENCES s_projects ON DELETE CASCADE ON UPDATE CASCADE,
But how can I avoid a project without members?
I.e.
INSERT INTO s_users (username) values ('mickey');
INSERT INTO s_users (username) values ('donald');
INSERT INTO s_projects (projectname) values ('p1'); -- this should not succeed to create a project without any members.
I've tried something like:
ALTER TABLE s_projects ADD CONSTRAINT pid FOREIGN KEY(project_id) REFERENCES s_memberships(project_id);
But I'm getting an error:
ERROR: there is no unique constraint matching given keys for referenced table "s_memberships"
Is there some other constraint I can add? Or, alternatively, is there a better way to organise my tables?
There are two options I can think of:
You create a foreign key from s_projects to s_membership that identifies a "special member" (project leader?) that must always be there.
You have a column member_count in s_project that is maintained by a trigger on s_membership, so that it always contains the number of members in that project. Then you place a check constraint on s_project that forces that number to be greater than 0.
A project without members is quite tricky. You can't insert a membership unless there is a project. And you want to requite that a project has members.
One solution is to use a deferrable constraint so you can insert both rows at the same time. You can alter the table to defer the constraint check, insert the rows, and then undefer the constraint.
However, I prefer other solutions.
One is to include a member count in projects. Maintaining this requires triggers -- which are yucky -- but you can then get "active" projects using a where clause: where num_members > 0.
Or just create a view:
create view active_projects as
select p.*
from projects p
where exists (select 1 from memberships m where m.project_id = p.project_id);
In other words, these solutions allow "inactive" projects, but then just hide them when desired.

How to delete in Oracle SQL from 2 tables where foreign key constraint by JDBC

Basically, I need to know the SQL query to perform the JDBC coding. I made my table with the following query:
CREATE TABLE MASTER
(
ROOM_ID NUMBER NOT NULL PRIMARY KEY,
MARKET VARCHAR2(3 CHAR) NOT NULL,
MODIFIED_DATE DATE,
MODIFIED_BY VARCHAR2(60 CHAR)
);
CREATE TABLE FEATURES
(
MASTER_ROOM_ID NUMBER NOT NULL,
ELEMENT NUMBER NOT NULL,
MODIFIED_DATE DATE,
MODIFIED_BY VARCHAR2(60 CHAR),
CONSTRAINT FK_MASTER_FEATURES
FOREIGN KEY (MASTER_ROOM_ID) REFERENCES MASTER (ROOM_ID)
ON DELETE CASCADE
);
Then tried to delete as :
DELETE MASTER FEATURES
JOIN FEATURES ON room_id = master_room_id
WHERE room_id = master_room_id;
It is not a valid query. How to do it?
Updated question: it is on delete cascade. When I delete as delete from master where market = 'xxx'; It only deletes master table rows. Not from the feature. Someone commented that I have to delete from the Features table first. But the problem is that I can't really delete from feature table first. I want to delete by the market. I for the feature table I know only the MASTER_ROOM_ID which is the foreign key for the feature table. So how can I write a query to delete from the feature table first? Please tell me how?
If the FK was really created with ON DELETE CASCADE, then deleting from MASTER will delete from the features table demo here
If the FK was not created with ON DELETE CASCADE, then the delete on the master will fail as long as there is at least one feature referencing it demo here
In that case you need to delete from features first using a sub-query to find the right rows:
delete from features
where master_room_id in (select room_id
from master
where marked = 'm1');
Then you can delete from the master table:
delete from master
where marked = 'm1';
Demo here

Modelling Post and Flag relationship in SQL

I am modeling the data for my web I am building. I use Postgresql database.
In the app there are posts like SO posts and also the flags for posts as Github flags or marks, whatever the correct term for it. A post can have only one flag at a time. There are plenty of posts ever increasing, but four or five flags and they will not increase.
First approach, normalized; I have modeled this part of my data with three tables; two for the corresponding entities posts and flags, and one for the relationship as post_flag. No reference in any of the entity tables mentioned to the other entity table for relationship. All relationship is recorded in the relationship table post_flag, and that is only the id pair for ids of a post and a flag.
Table structure in that case would be:
CREATE TABLE posts
(
id bigserial PRIMARY KEY,
created_at timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
title character varying(100),
text text,
score integer DEFAULT 0,
author_id integer NOT NULL REFERENCES users (id),
product_id integer NOT NULL REFERENCES products (id),
);
CREATE TABLE flags
(
id bigserial PRIMARY KEY,
created_at timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
flag character varying(30) NOT NULL -- planned, in progress, fixed
);
CREATE TABLE post_flag
(
created_at timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
post_id integer NOT NULL REFERENCES posts (id),
flag_id integer NOT NULL REFERENCES flags (id)
);
To get posts flagged as fixed I have to use:
-- homepage posts- fixed posts tab
SELECT
p.*,
f.flag
FROM posts p
JOIN post_flag p_f
ON p.id = p_f.post_id
JOIN flags f
ON p_f.flag_id = f.id
WHERE f.flag = 'fixed'
ORDER BY p_f.created_at DESC
Second approach; I have two tables posts and flags. The table posts has a flag_id column that references a flag in the flags table.
CREATE TABLE posts
(
id bigserial PRIMARY KEY,
created_at timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
title character varying(100),
text text,
score integer DEFAULT 0,
author_id integer NOT NULL REFERENCES users (id),
product_id integer NOT NULL REFERENCES products (id),
flag_id integer DEFAULT NULL REFERENCES flags (id)
);
CREATE TABLE flags
(
id bigserial PRIMARY KEY,
created_at timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
flag character varying(30) NOT NULL -- one of planned, in progress, fixed
);
For same data;
-- homepage posts- fixed posts tab
SELECT
p.*,
f.flag
FROM posts p
JOIN flags f
ON p.flag_id = f.id
WHERE f.flag = 'fixed'
ORDER BY p.created_at DESC
Third approach denormalized; I have only one table posts. Posts table has a flag column to store the flag assigned to the post.
CREATE TABLE posts
(
id bigserial PRIMARY KEY,
created_at timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP,
title character varying(100),
text text,
score integer DEFAULT 0,
author_id integer NOT NULL REFERENCES users (id),
product_id integer NOT NULL REFERENCES products (id),
flag character varying(30)
);
Here I would only have for same data;
-- homepage posts- fixed posts tab
SELECT
p.*,
FROM posts p
WHERE p.flag = 'fixed'
ORDER BY p.created_at DESC
I wonder if first approach is an overkill in terms of normalization of data in a RDBMS like Postgresql? For a post comment relationship that first approach would be great and indeed I make use of it. But I have some very few quantity data used as meta data for posts as badges, flags, tags. As you see in fact in the most normal form, the first approach, I already use some product_id etc for a using one less JOIN but to another table as a different relation, not to the flags. So, there my approach fits into my second approach. Should I use the more denormalized approach, the third one, having posts table and a flag column in it? What is the better approach in terms of performance, expansion, and maintainability?
Use the second approach.
The first is a many-to-many data structure and you say
A post can have only one flag at a time.
So you would then have to build the business logic in to the front-end or set up complex rules to check a post never have more than one flag.
The third approach will result in messy data, again unless you implement checks or rules to ensure the flags are not misspelled or new ones added.
Expansion and maintainability are provided in the second approach; it is also self documenting. Worry about performance when it actually becomes a problem, and not before.
Personally I would make the flag_id field in the posts table NULL, which would allow you to model a post without a flag.
Blending two approaches
Assuming your flag names are unique, you can use the flag name as a natural key. Your table structures would then be
CREATE TABLE posts
(
id bigserial PRIMARY KEY,
... other fields
flag character varying(30) REFERENCES flags (flag)
);
CREATE TABLE flags
(
flag character varying(30) NOT NULL PRIMARY KEY,
created_at timestamp without time zone NOT NULL DEFAULT CURRENT_TIMESTAMP
);
You then get the benefit of being able to write queries for flag without having to JOIN to the flags table while having flag names checked by the table reference.

Deleting millions of record in bunch in postgresql

I have to delete rows from table that has 120 millions records.
The data that has highest(entry_date) and second highest(entry_date) should not be deleted.
Table has many constraints.
One PRIMARY key
Two FOREIGN keys
and two indexes other than index on primary key.
I have already successfully tried method to delete as creating temp table and moving required data into temp table.
Then dropping the present table and then again moving back filtered data from temp to main table.And it worked fine.
But I need a way to delete records in bunch .
CREATE TABLE values
(
value_id bigint NOT NULL,
content_definition_id bigint NOT NULL,
value_s text,
value_n double precision,
order integer,
scope_id integer NOT NULL,
answer boolean NOT NULL,
date timestamp without time zone NOT NULL,
entry_date timestamp without time zone NOT NULL,
CONSTRAINT "value_PK" PRIMARY KEY (value_id),
CONSTRAINT content_definition_id_fk FOREIGN KEY (content_definition_id)
REFERENCES content_definition (content_definition_id) MATCH SIMPLE
ON UPDATE NO ACTION ON DELETE NO ACTION,
CONSTRAINT scope_fk FOREIGN KEY (scope_id)
REFERENCES scopes (scope_id) MATCH SIMPLE
ON UPDATE RESTRICT ON DELETE RESTRICT
)
-- Index: fki_content_definition_id_fk
-- Index: fki_value_value_scope_id
How to delete record in bunch like first only 1 million data should be deleted and on.
This assumes you have no conflicting locks. Note that index page locks may slow things down as well.
Recent PostgreSQL allows you to use a CTE in a delete statement. I.e. you can:
WITH ids_to_delete (
SELECT value_id FROM values
where ...
limit ...
)
delete from values where value_id in (select value_id from ids_to_delete)
Can you try merge with conditions of your temp tables and use delete part of it. That should give you a good performance

Multiple yet mutually exclusive foreign keys - is this the way to go?

I have three tables: Users, Companies and Websites.
Users and companies have websites, and thus each user record has a foreign key into the Websites table. Also, each company record has a foreign key into the Websites table.
Now I want to include foreign keys in the Websites table back into their respective "parent" records. How do I do that? Should I have two foreign keys in each website record, with one of them always NULL? Or is there another way to go?
If we look into the model here, we will see the following:
A user is related to exactly one website
A company is related to exactly one website
A website is related to exactly one user or company
The third relation implies existence of a "user or company" entity whose PRIMARY KEY should be stored somewhere.
To store it you need to create a table that would store a PRIMARY KEY of a website owner entity. This table can also store attributes common for a user and a website.
Since it's a one-to-one relation, website attributes can be stored in this table too.
The attributes not shared by users and companies should be stored in the separate table.
To force the correct relationships, you need to make the PRIMARY KEY of the website composite with owner type as a part of it, and force the correct type in the child tables with a CHECK constraint:
CREATE TABLE website_owner (
type INT NOT NULL,
id INT NOT NULL,
website_attributes,
common_attributes,
CHECK (type IN (1, 2)) -- 1 for user, 2 for company
PRIMARY KEY (type, id)
)
CREATE TABLE user (
type INT NOT NULL,
id INT NOT NULL PRIMARY KEY,
user_attributes,
CHECK (type = 1),
FOREIGN KEY (type, id) REFERENCES website_owner
)
CREATE TABLE company (
type INT NOT NULL,
id INT NOT NULL PRIMARY KEY,
company_attributes,
CHECK (type = 2),
FOREIGN KEY (type, id) REFERENCES website_owner
)
you don’t need a parent column, you can lookup the parents with a simple select (or join the tables) on the users and companies table. if you want to know if this is a user or a company website i suggest using a boolean column in your websites table.
Why do you need a foreign key from website to user/company at all? The principle of not duplicating data would suggest it might be better to scan the user/company tables for a matching website id. If you really need to you could always store a flag in the website table that denotes whether a given website record is for a user or a company, and then scan the appropriate table.
The problem I have with the accepted answer (by Quassnoi) is that the object relationships are the wrong way around: company is not a sub-type of a website owner; we had companies before we had websites and we can have companies who are website owners. Also, it seems to me that website ownership is a relationship between a website and either a person or a company i.e. we should have a relationship table (or two) in the schema. It may be an acceptable approach to keep personal website ownership separate from corporate website ownership and only bring them together when required e.g. via VIEWs:
CREATE TABLE People
(
person_id CHAR(9) NOT NULL UNIQUE, -- external identifier
person_name VARCHAR(100) NOT NULL
);
CREATE TABLE Companies
(
company_id CHAR(6) NOT NULL UNIQUE, -- external identifier
company_name VARCHAR(255) NOT NULL
);
CREATE TABLE Websites
(
url CHAR(255) NOT NULL UNIQUE
);
CREATE TABLE PersonalWebsiteOwnership
(
person_id CHAR(9) NOT NULL UNIQUE
REFERENCES People ( person_id ),
url CHAR(255) NOT NULL UNIQUE
REFERENCES Websites ( url )
);
CREATE TABLE CorporateWebsiteOwnership
(
company_id CHAR(6) NOT NULL UNIQUE
REFERENCES Companies( company_id ),
url CHAR(255) NOT NULL UNIQUE
REFERENCES Websites ( url )
);
CREATE VIEW WebsiteOwnership AS
SELECT url, company_name AS website_owner_name
FROM CorporateWebsiteOwnership
NATURAL JOIN Companies
UNION
SELECT url, person_name AS website_owner_name
FROM PersonalWebsiteOwnership
NATURAL JOIN People;
The problem with the above is there is no way of using database constraints to enforce the rule that a website is either owned by a person or a company but not both.
If we can assuming the DBMS enforces check constraints (as the accepted answer does) then we can exploit the fact that a (human) person and a company are both legal persons and employ a super-type table (LegalPersons) but still retain relationship table approach (WebsiteOwnership), this time using the VIEWs to separate personal website ownership from separate from corporate website ownership but this time with strongly typed attributes:
CREATE TABLE LegalPersons
(
legal_person_id INT NOT NULL UNIQUE, -- internal artificial identifier
legal_person_type CHAR(7) NOT NULL
CHECK ( legal_person_type IN ( 'Company', 'Person' ) ),
UNIQUE ( legal_person_type, legal_person_id )
);
CREATE TABLE People
(
legal_person_id INT NOT NULL
legal_person_type CHAR(7) NOT NULL
CHECK ( legal_person_type = 'Person' ),
UNIQUE ( legal_person_type, legal_person_id ),
FOREIGN KEY ( legal_person_type, legal_person_id )
REFERENCES LegalPersons ( legal_person_type, legal_person_id ),
person_id CHAR(9) NOT NULL UNIQUE, -- external identifier
person_name VARCHAR(100) NOT NULL
);
CREATE TABLE Companies
(
legal_person_id INT NOT NULL
legal_person_type CHAR(7) NOT NULL
CHECK ( legal_person_type = 'Company' ),
UNIQUE ( legal_person_type, legal_person_id ),
FOREIGN KEY ( legal_person_type, legal_person_id )
REFERENCES LegalPersons ( legal_person_type, legal_person_id ),
company_id CHAR(6) NOT NULL UNIQUE, -- external identifier
company_name VARCHAR(255) NOT NULL
);
CREATE TABLE WebsiteOwnership
(
legal_person_id INT NOT NULL
legal_person_type CHAR(7) NOT NULL
UNIQUE ( legal_person_type, legal_person_id ),
FOREIGN KEY ( legal_person_type, legal_person_id )
REFERENCES LegalPersons ( legal_person_type, legal_person_id ),
url CHAR(255) NOT NULL UNIQUE
REFERENCES Websites ( url )
);
CREATE VIEW CorporateWebsiteOwnership AS
SELECT url, company_name
FROM WebsiteOwnership
NATURAL JOIN Companies;
CREATE VIEW PersonalWebsiteOwnership AS
SELECT url, person_name
FROM WebsiteOwnership
NATURAL JOIN Persons;
What we need are new DBMS features for 'distributed foreign keys' ("For each row in this table there must be exactly one row in one of these tables") and 'multiple assignment' to allow the data to be added into tables thus constrained in a single SQL statement. Sadly we are a far way from getting such features!
First of all, do you really need this bi-directional link? It is a good practice to avoid it unless absolutely needed.
I understand it that you wish to know whether the site belongs to a user or to a company. You can achieve that by having a simple boolean field in the Website table - [BelongsToUser]. If true, then you look up a user, if false - you look up a company.
A bit late, but all the existing answers seemed to fall somewhat short of the mark:
Owner to website is a 1:Many relation
Website to owner is a 1:1 relation
Users and Companies tables should not have a foreign key into the Websites table
None of the website data, common to users and companies or not, should be in the Users or Companies tables
None of the owner's information, common or not, should be in the Websites table
MySQL ignores, silently, CHECK constraints on tables (no enforcement of referential integrity)
The DBMS ought to handle the 'relation' logic, not the application using the database
Some of this is recognized in the answer from onedaywhen, yet that answer still missed the opportunity to make MySQL do the heavy lifting and enforce the referential integrity.
A website can only have one owner, legally, anyway. A person, or company, can have any number of websites, including none. A link in the database from owner to website can only be 1:1 at any level of normalization. In reality the relation is 1:Many, and would require having multiple table entries for each owner that happens to own more than one website. A link from website to owner is 1:1 in both database terms and in reality. Having the link from website to owner represents the model better. With an index in the website table, doing the 1:Many lookup for a given owner becomes reasonably efficient.
The CHECK attribute in SQL would be an excellent solution, if MySQL didn't happen to silently ignore it.
MySQL Docs 13.1.20 CREATE TABLE Syntax
The CHECK clause is parsed but ignored by all storage engines.
MySQL's functionality does offer two solutions as work-arounds to implement the behavior of CHECK and keep the referential integrity of the data. Triggers with stored procedures is one, and works well with all manner of constraints. Easier to implement, though less versatile, is using a VIEW with a WITH CHECK OPTION clause, which MySQL will implement.
MySQL Docs 24.5.4 The View WITH CHECK OPTION Clause
The WITH CHECK OPTION clause can be given for an updatable view to prevent inserts to rows for which the WHERE clause in the select_statement is not true. It also prevents updates to rows for which the WHERE clause is true but the update would cause it to be not true (in other words, it prevents visible rows from being updated to nonvisible rows).
The MySQLTUTORIAL site gives a good example of both options in their Introduction to the SQL CHECK constraint tutorial. (You have to think around the typos, but good otherwise.)
Having found this question while trying to resolve a similar mutually exclusive foreign key split and developing a solution, with hints generated by the answers, it seems only proper to share my solution in return.
Recommended Solution
For the minimum impact to the existing schema, and the application accessing the data, retain the Users and Companies tables as they are. Rename the Websites table and replace it with a VIEW named Websites which the application can continue to access. Except when dealing with the ownership information, all the old queries to Websites should still work. So:
The setup
-- Keep the `Users` table about "users"
CREATE TABLE `Users` (
`id` INT SERIAL PRIMARY KEY,
`name` VARCHAR(180),
-- user_attributes
);
-- Keep the `Companies` table about "companies"
CREATE TABLE `Companies` (
`id` SERIAL PRIMARY KEY,
`name` VARCHAR(180),
-- company_attributes
);
-- Attach ownership information about the website to the website's record in the `Websites` table, renamed to `WebsitesData`
CREATE TABLE `WebsitesData` (
`id` SERIAL PRIMARY KEY,
`name` VARCHAR(255),
`is_personal` BOOL,
`owner_user` BIGINT UNSIGNED DEFAULT NULL,
`owner_company` BIGINT UNSIGNED DEFAULT NULL,
website_attributes,
FOREIGN KEY `WebsiteOwner_User` (`owner_user`)
REFERENCES `Users` (`id`)
ON DELETE RESTRICT ON UPDATE CASCADE,
FOREIGN KEY `WebsiteOwner_Company` (`owner_company`)
REFERENCES `Companies` (`id`)
ON DELETE RESTRICT ON UPDATE CASCADE,
);
-- Create a new `VIEW` with the original name of `Websites` as the gateway to the website records which can enforce the constraints you need
CREATE VIEW `Websites` AS
SELECT * FROM `WebsitesData` WHERE
(`is_personal`=TRUE AND `owner_user` IS NOT NULL AND `owner_company` IS NULL) OR
(`is_personal`=FALSE AND `owner_user` IS NULL AND `owner_company` IS NOT NULL)
WITH CHECK OPTION;
Usage
-- Use the Websites VIEW for the INSERT, UPDATE, and SELECT operations as you normally would and leave the WebsitesData table in the background.
INSERT INTO `Websites` SET
`is_personal`=TRUE,
`owner_user`=$userID;
INSERT INTO `Websites` SET
`is_personal`=FALSE,
`owner_company`=$companyID;
-- Or, using different field lists based on the type of owner
INSERT INTO `Websites` (`is_personal`,`owner_user`, ...)
VALUES (TRUE, $userID, ...);
INSERT INTO `Websites` (`is_personal`,`owner_company`, ...)
VALUES (FALSE, $companyID, ...);
-- Or, using a common field list, and placing NULL in the proper place
INSERT INTO `Websites` (`is_personal`,`owner_user`,`owner_company`,...)
VALUES (TRUE, $userID, NULL, ...);
INSERT INTO `Websites` (`is_personal`,`owner_user`,`owner_company`,...)
VALUES (FALSE, NULL, $companyID, ...);
-- Change the company that owns a website
-- Will ERROR if the site was owned by a User.
UPDATE `Websites` SET `owner_company`=$new_companyID;
-- Force change the ownership from a User to a Company
UPDATE `Websites` SET
`owner_company`=$new_companyID,
`owner_user`=NULL,
`is_personal`=FALSE;
-- Force change the ownership from a Company to a User
UPDATE `Websites` SET
`owner_user`=$new_userID,
`owner_company`=NULL,
`is_personal`=TRUE;
-- Selecting the owner of a site without needing to know if it is personal or not
(SELECT `Users`.`name` AS `Owner`
FROM `Websites`
JOIN `Users` ON `Websites`.`owner_user`=`Users`.`id`
WHERE `is_personal`=TRUE AND `Websites`.`id`=$siteID)
UNION
(SELECT `Companies`.`name` AS `Owner`
FROM `Websites`
JOIN `Companies` ON `Websites`.`owner_company`=`Companies`.`id`
WHERE `is_personal`=FALSE AND `Websites`.`id`=$siteID);
-- Selecting the sites owned by a User
SELECT `name` FROM `Websites`
WHERE `is_personal`=TRUE AND `id`=$userID;
SELECT `Websites`.`name`
FROM `Websites`
JOIN `Users` ON `Websites`.`owner_user`=`Users`.$userID
WHERE `is_personal`=TRUE AND `Users`.`name`="$user_name";
-- Selecting the sites owned by a Company
SELECT `name` FROM `Websites` WHERE `is_personal`=FALSE AND `id`=$companyID;
SELECT `Websites`.`name`
FROM `Websites`
JOIN `Comnpanies` ON `Websites`.`owner_company`=`Companies`.$userID
WHERE `is_personal`=FALSE AND `Companies`.`name`="$company_name";
-- Listing all websites and their owners
(SELECT `Websites`.`name` AS `Website`,`Users`.`name` AS `Owner`
FROM `Websites`
JOIN `Users` ON `Websites`.`owner_user`=`Users`.`id`
WHERE `is_personal`=TRUE)
UNION ALL
(SELECT `Websites`.`name` AS `Website`,`Companies`.`name` AS `Owner`
FROM `Websites`
JOIN `Companies` ON `Websites`.`owner_company`=`Companies`.`id`
WHERE `is_personal`=FALSE)
ORDER BY Website, Owner;
-- Listing all users or companies which own at least one website
(SELECT `Websites`.`name` AS `Website`,`Users`.`name` AS `Owner`
FROM `Websites`
JOIN `Users` ON `Websites`.`owner_user`=`Users`.`id`
WHERE `is_personal`=TRUE)
UNION DISTINCT
(SELECT `Websites`.`name` AS `Website`,`Companies`.`name` AS `Owner`
FROM `Websites`
JOIN `Companies` ON `Websites`.`owner_company`=`Companies`.`id`
WHERE `is_personal`=FALSE)
GROUP BY `Owner` ORDER BY `Owner`;
Normalization Level Up
As a technical note for normalization, the ownership information could be factored out of the Websites table and a new table created to hold the ownership data, including the is_normal column.
CREATE TABLE `Websites` (
`id` SERIAL PRIMARY KEY,
`name` VARCHAR(255),
`owner` BIGINT UNSIGNED DEFAULT NULL,
website_attributes,
FOREIGN KEY `Website_Owner` (`owner`)
REFERENCES `WebOwners` (id`)
ON DELETE RESTRICT ON UPDATE CASCADE
);
CREATE TABLE `WebOwnersData` (
`id` SERIAL PRIMARY KEY,
`is_personal` BOOL,
`user` BIGINT UNSIGNED DEFAULT NULL,
`company` BIGINT UNSIGNED DEFAULT NULL,
FOREIGN KEY `WebOwners_User` (`user`)
REFERENCES `Users` (`id`)
ON DELETE RESTRICT ON UPDATE CASCADE,
FOREIGN KEY `WebOwners_Company` (`company`)
REFERENCES `Companies` (`id`)
ON DELETE RESTRICT ON UPDATE CASCADE,
);
CREATE VIEW `WebOwners` AS
SELECT * FROM WebsitesData WHERE
(`is_personal`=TRUE AND `user` IS NOT NULL AND `company` IS NULL) OR
(`is_personal`=FALSE AND `user` IS NULL AND `company` IS NOT NULL)
WITH CHECK OPTION;
I believe, however, that the created VIEW, with its constraints, prevents any of the anomalies that normalization aims to remove, and adds complexity that is not needed in the situation. The normalization process is always a trade off anyway.