Naming Category and SubCategory Tables - sql

I'm trying to create a bunch of lookup tables in a database but am stuck when it comes to naming them. The tables are like this:
1. dbo.AccountType (this is the highest level category)
2. dbo.AccountSubType (this is a 2nd level category)
3. dbo.AccountSubSubType (this is a 3rd level category)
The above naming convention breaks easily. So perhaps this is better:
1. dbo.AccountType1 (highest level)
2. dbo.AccountType2 (second level)
3. dbo.AccountType3 (third level)
4. dbo.AccountType-N (and so on...)
I know naming conventions are opinion based, but surely there has to be some logical way to do this that is scalable and not confusing to developers.
Example of how the data looks in the dbo.AccountType2 table using the second solution:
AccountTypeID (FK) | AccountType1ID (FK) | AccountType2ID (PK) | AccountType2
=============================================================================
1 4 1 Credit Card
1 5 2 Savings
Is there any better way to store hierarchical data in a database and name the tables correctly?

This would probably be better represented as a single table with a hierarchical relationship:
E.g.
CREATE TABLE [dbo].[AccountType] (
Id int NOT NULL
,ParentId int NULL
CONSTRAINT [FK_AccountType_AccountType_Parent] REFERENCES [dbo].[AccountType] (Id)
,Name nvarchar(200) NOT NULL
CONSTRAINT [PK_AccountType] PRIMARY KEY CLUSTERED ([Id])
)
Then populate it with data as follows:
INSERT INTO dbo.AccountType (Id, ParentId, Name) VALUES (1, NULL, 'Credit Card')
INSERT INTO dbo.AccountType (Id, ParentId, Name) VALUES (2, 1, 'Credit Card Sub-Type')
INSERT INTO dbo.AccountType (Id, ParentId, Name) VALUES (3, 2, 'Credit Card Sub-Sub-Type')
INSERT INTO dbo.AccountType (Id, ParentId, Name) VALUES (4, NULL, 'Savings')
INSERT INTO dbo.AccountType (Id, ParentId, Name) VALUES (5, 4, 'Savingsd Sub-Type')
INSERT INTO dbo.AccountType (Id, ParentId, Name) VALUES (6, 5, 'Savings Sub-Sub-Type')
Anything with a ParentId of NULL is a root value, otherwise it is a child of the specified parent...
Edit: To query you'd use a CTE. E.g.
WITH ParentAccountType ( Id, ParentId, Name, ParentName )
AS
(
SELECT Id, ParentId, Name, CAST('N/A' AS nvarchar(200)) AS ParentName
FROM AccountType
WHERE ParentId IS NULL
UNION ALL
SELECT c.Id, c.ParentId, c.Name, p.Name AS ParentName
FROM
AccountType c
INNER JOIN ParentAccountType p ON c.ParentId = p.Id
)
SELECT ParentName, Name
FROM ParentAccountType
GO
SQL Fiddler here

Related

Can you sort the result in GROUP BY?

I have two tables one is objects with the attribute of id and is_green.The other table is object_closure with the attributes of ancestor_id, descendant_od, and created_at. ie.
Objects: id, is_green
Object_closure: ancestor_id, descendant_od, created_at
There are more attributes in the Object table but not necessary to mention in this question.
I have a query like this:
-- create a table
CREATE TABLE objects (
id INTEGER PRIMARY KEY,
is_green boolean
);
CREATE TABLE object_Closure (
ancestor_id INTEGER ,
descendant_id INTEGER,
created_at date
);
-- insert some values
INSERT INTO objects VALUES (1, 1 );
INSERT INTO objects VALUES (2, 1 );
INSERT INTO objects VALUES (3, 1 );
INSERT INTO objects VALUES (4, 0 );
INSERT INTO objects VALUES (5, 1 );
INSERT INTO objects VALUES (6, 1 );
INSERT INTO object_Closure VALUES (1, 2, 12-12-2020 );
INSERT INTO object_Closure VALUES (1, 3, 12-13-2020 );
INSERT INTO object_Closure VALUES (2, 3, 12-14-2020 );
INSERT INTO object_Closure VALUES (4, 5, 12-15-2020 );
INSERT INTO object_Closure VALUES (4, 6, 12-16-2020 );
INSERT INTO object_Closure VALUES (5, 6, 12-17-2020 );
-- fetch some values
SELECT
O.id,
P.id,
group_concat(DISTINCT P.id ) as p_ids
FROM objects O
LEFT JOIN object_Closure OC on O.id=OC.descendant_id
LEFT JOIN objects P on OC.ancestor_id=P.id AND P.is_green=1
GROUP BY O.id
The result is
query result
I would like to see P.id for O.id=6 is also 5 instead of null. Afterall,5 is still a parentID (p.id). More importantly, I also want the id shown in P.id as the first created id if there are more than one. (see P.created_at).
I understand the reason why it happens is that the first one the system pick is null, and the null was created by the join with the condition of is_green; however, I need to filter out those objects that are green only in the p.id.
I cannot do an inner join (because I need the other attributes of the table and sometimes both P.id and p_ids are null, but still need to show in the result) I cannot restructure the database. It is already there and cannot be changed. I also cannot just use a Min() or Max() aggregation because I want the ID that is picked is the first created one.
So is there a way to skip the null in the join?
or is there a way to filter the selection in the select clause?
or do an order by before the grouping?
P.S. My original code concat the P.id by the order of P.created_at. For some reason, I cannot replicate it in the online SQL simulator.

Adding a LEFT JOIN on a INSERT INTO....RETURNING

My query Inserts a value and returns the new row inserted
INSERT INTO
event_comments(date_posted, e_id, created_by, parent_id, body, num_likes, thread_id)
VALUES(1575770277, 1, '9e028aaa-d265-4e27-9528-30858ed8c13d', 9, 'December 7th', 0, 'zRfs2I')
RETURNING comment_id, date_posted, e_id, created_by, parent_id, body, num_likes, thread_id
I want to join the created_by with the user_id from my user's table.
SELECT * from users WHERE user_id = created_by
Is it possible to join that new returning row with another table row?
Consider using a WITH structure to pass the data from the insert to a query that can then be joined.
Example:
-- Setup some initial tables
create table colors (
id SERIAL primary key,
color VARCHAR UNIQUE
);
create table animals (
id SERIAL primary key,
a_id INTEGER references colors(id),
animal VARCHAR UNIQUE
);
-- provide some initial data in colors
insert into colors (color) values ('red'), ('green'), ('blue');
-- Store returned data in inserted_animal for use in next query
with inserted_animal as (
-- Insert a new record into animals
insert into animals (a_id, animal) values (3, 'fish') returning *
) select * from inserted_animal
left join colors on inserted_animal.a_id = colors.id;
-- Output
-- id | a_id | animal | id | color
-- 1 | 3 | fish | 3 | blue
Explanation:
A WITH query allows a record returned from an initial query, including data returned from a RETURNING clause, which is stored in a temporary table that can be accessed in the expression that follows it to continue work on it, including using a JOIN expression.
You were right, I misunderstood
This should do it:
DECLARE mycreated_by event_comments.created_by%TYPE;
INSERT INTO
event_comments(date_posted, e_id, created_by, parent_id, body, num_likes, thread_id)
VALUES(1575770277, 1, '9e028aaa-d265-4e27-9528-30858ed8c13d', 9, 'December 7th', 0, 'zRfs2I')
RETURNING created_by into mycreated_by
SELECT * from users WHERE user_id = mycreated_by

Creating cte statement to traverse a tree created with a bridge table

I will preface this by saying that I am completely new to cte statements.
I have 2 tables an items table and a linker table. the linker table containing the parent id and child id of the relationship.
I have seen lots of examples on how to traverse a tree with what I believe is called an associative system, where the parent id is stored on the child record instead of in a bridge table. But I can not seem to extrapolate this out to my scenario.
I believe I need to have a bridge table like this because any single item could have multiple parents and each parent will most likely have multiple children.
I hacked together my own way of traversing the tree but it was within another language, xojo, and I really did not like how it turned out. Essentially I was using recursive functions to dig down into each tree and queried the database each time I needed a child.
Now I am trying to create a cte statement that does the same thing. Keeping the descendants ordered below the parents is not a huge deal
So I have created a sample database and other materials to describe my issue:
This diagram shows visually what the relationships are:
diagram
This is what I would like returned from the database (some items show up in multiple places:
1 : Audio
2 : Speaker
3 : Microphone
4 : Mic Pack
3 : Microphone
5 : Di
6 : Passive Di
11 : Rapco Di
13 : Dbx Di
7 : Lighting
9 : Safety
12 : Small Safety
8 : Rigging
10 : Light Rigging
9 : Safety
12 : Small Safety
An example table:
CREATE TABLE items ( id INTEGER, name TEXT, PRIMARY KEY(id) );
CREATE TABLE `linker` ( `parent` INTEGER, `child` INTEGER, PRIMARY KEY(`parent`,`child`) );
Insert Into items(id, name) Values(1, 'Audio');
Insert Into items(id, name) Values(2, 'Speaker');
Insert Into items(id, name) Values(3, 'Microphone');
Insert Into items(id, name) Values(4, 'Mic Pack');
Insert Into items(id, name) Values(5, 'Di');
Insert Into items(id, name) Values(6, 'Passive Di');
Insert Into items(id, name) Values(7, 'Lighting');
Insert Into items(id, name) Values(8, 'Rigging');
Insert Into items(id, name) Values(9, 'Safety');
Insert Into items(id, name) Values(10, 'Lighting Rigging');
Insert Into items(id, name) Values(11, 'Rapco Di');
Insert Into items(id, name) Values(12, 'Small Safety');
Insert Into items(id, name) Values(13, 'Dbx Di');
Insert Into linker(parent, child) Values(1, 2);
Insert Into linker(parent, child) Values(1, 4);
Insert Into linker(parent, child) Values(1, 3);
Insert Into linker(parent, child) Values(4, 3);
Insert Into linker(parent, child) Values(4, 5);
Insert Into linker(parent, child) Values(5, 6);
Insert Into linker(parent, child) Values(6, 11);
Insert Into linker(parent, child) Values(6, 13);
Insert Into linker(parent, child) Values(7, 9);
Insert Into linker(parent, child) Values(9, 12);
Insert Into linker(parent, child) Values(8, 10);
Insert Into linker(parent, child) Values(10, 9);
This is the cte that I came up with that I believe came the closest, but its probably still pretty far off:
with cte As
(
Select
id,
name,
0 as level,
Cast(name as varchar(255) as sort
From items i
Left outer Join
linker li
On i.id = li.child
And li.parent is Null
Union All
Select
id,
name,
cte.level + 1,
Cast(cte.sort + '.' + i.name As Varchar(255)) as sort
From cte
Left Outer Join linker li
on li.child = cte.id
Inner Join items i
On li.parent = i.id
)
Select
id,
name,
level,
sort
From cte
Order By Sort;
Thanks for any help in advance. I am very open to the idea that everything I am doing from the data structure up is wrong, so keep that in mind when you are answering.
Edit: It is probably worth noting that the results don't need to be in order. I plan on creating a ancestry path field in the cte statement, and using that patb to populate my tree.
Edit: oops, I copied and pasted the wrong bit of cte code. I am on mobile so I did my best to change it to what I was doing on my desktop. Once I have a chance I will double check the cte statement against my notes.
Alright, seems like my brain just needed some rest. I have figured out a solution to my issue with much less frustration than yesterday.
My cte statement was:
with cte As(
Select
id,
name,
li.parent,
li.child,
Cast(name as Varchar(255)) as ancestory
From items i
Left Outer Join linker li
On i.id = li.child
Where li.parent is null
Union All
Select
i.id,
i.name,
li.parent,
li.child,
Cast(cte.ancestory || "." || i.name as Varchar(100)) as ancestory
From cte
Left join linker li
On cte.id = li.parent
Inner Join items i
On li.child = i.id
)
select * from cte
The results come back as:
id name parent child ancestory
1 Audio Audio
7 Lighting Lighting
8 Rigging Rigging
2 Speaker 1 2 Audio.Speaker
3 Microphone 1 3 Audio.Microphone
4 Mic Pack 1 4 Audio.Mic Pack
9 Safety 7 9 Lighting.Safety
10 Lighting 8 10 Rigging.Lighting Rigging
Rigging
3 Microphone 4 3 Audio.Mic Pack.Microphone
5 Di 4 5 Audio.Mic Pack.Di
12 Small 9 12 Lighting.Safety.Small Safety
Safety
9 Safety 10 9 Rigging.Lighting Rigging.Safety
6 Passive Di 5 6 Audio.Mic Pack.Di.Passive Di
12 Small Safety 9 12 Rigging.Lighting Rigging.Safety.Small Safety
11 Rapco Di 6 11 Audio.Mic Pack.Di.Passive Di.Rapco Di
13 Dbx Di 6 13 Audio.Mic Pack.Di.Passive Di.Dbx Di

tree structure in different tables

I have four tables,
Level1 (id, name, idFather, LevelFather)
Level2 (id, name, idFather, LevelFather)
Level3 (id, name, idFather, LevelFather)
Level4 (id, name, idFather, LevelFather)
This logic allow build a tree, where the leaves are the items in Level4, and his father can take level 1, 2 or 3. In the same way, the items in Level3, can have a father that is in the 2 o 1.
There are any query to obtain the tree below for a given an id and a level, until a given level?
For example, is we have the nextdata:
Level1 - 001, GroupEnterprise1, 001, 1
Level2 - 001-1, Enterprise1, 001, 1
Level2 - 001-2, Enterprise2,001, 1
Level3 - 002-1, Enterprise3, 001-1, 2
Level4 - 003-1, Office 1, 001-1,3
Level4 - 003-2, Office 2, 001-2,3
Level4 - 003-3, Office 3, 001-2,3
Level4 - 003-4, Office 4, 001-1,3
I can want consult all the offices (items in level 4), that are are daughters, granddaughters and great-granddaughters off the group GroupEnterprise1, or the offices that are daughters of Enterprise3, o the enterprises that are daughters of GroupEnterprise1.
The parameters for query are Id, Level and Level until I wish build the tree.
I'm not sure I quite understand what you're trying to do. If you need a hierarchy, you should use a single table called "Levels" and have all of your Level information in that one table. You don't need 4 tables for this. You can perform a self-join to easily return parent information.
The problem is that in order to move through the chain, you would probably have to use some sort of loop. This is usually to be avoided in SQL statements because the query optimizer would have to generate an execution plan for every iteration of the loop (which is inefficient). It is possible to do with only SQL, but I recommend pulling a table with all the information, and parsing through it with a non-SQL programming language that isn't geared towards set-based operations.
Below is some sample code using a single Table for all 4 levels. Once the tree is built in the table, you just need to move through a FOR loop to display it how you want.
--Creates Temp table (This table will expire upon connection close)
CREATE TABLE [#Levels] ([id] INT, [name] NVARCHAR(256), [level] INT, [idFather] INT);
--Populate Temp table with some sample data
INSERT INTO #Levels VALUES (1,'AbsoluteParent',1,null)
INSERT INTO #Levels VALUES (2,'ChildItem1',2,1)
INSERT INTO #Levels VALUES (3,'ChildItem2',2,1)
INSERT INTO #Levels VALUES (4,'GrandChild',3,2)
--Display populated table
SELECT * FROM [#Levels]
--Create 2 instances of our Temp table and join (id > idFather) so that we can return information about the parent table.
SELECT [T1].[name] AS 'Name'
, [T2].[name] AS 'Parent Name'
FROM [#Levels] AS T1
LEFT JOIN [#Levels] T2 ON [T1].[idFather] = [T2].[id]
--We can even link another instance of our Temp table and give information about grandparents!
SELECT [T1].[name] AS 'Name'
, [T2].[name] AS 'Parent Name'
, [T3].[name] AS 'Grand Parent Name'
FROM [#Levels] AS T1
LEFT JOIN [#Levels] T2 ON [T1].[idFather] = [T2].[id]
LEFT JOIN [#Levels] T3 ON [T2].[idFather] = [T3].[id]
Perhaps what you are looking for is a recursive common table expression that feeds the output back into the function to display all children recursively. Here is a microsoft example: https://technet.microsoft.com/en-us/library/ms186243(v=sql.105).aspx
I simplified the microsoft example a little:
-- Create a temp Employee table.
CREATE TABLE #MyEmployees
(
EmployeeID smallint NOT NULL,
FirstName nvarchar(30) NOT NULL,
LastName nvarchar(40) NOT NULL,
Title nvarchar(50) NOT NULL,
ManagerID int NULL,
);
-- Populate the table with values.
INSERT INTO #MyEmployees VALUES
(1, N'Ken', N'Sánchez', N'Chief Executive Officer',NULL)
,(273, N'Brian', N'Welcker', N'Vice President of Sales',1)
,(274, N'Stephen', N'Jiang', N'North American Sales Manager',273)
,(275, N'Michael', N'Blythe', N'Sales Representative',274)
,(276, N'Linda', N'Mitchell', N'Sales Representative',274)
,(285, N'Syed', N'Abbas', N'Pacific Sales Manager',273)
,(286, N'Lynn', N'Tsoflias', N'Sales Representative',285)
,(16, N'David',N'Bradley', N'Marketing Manager',273)
,(23, N'Mary', N'Gibson', N'Marketing Specialist',16);
WITH DirectReports (ManagerID, EmployeeID, Title, Level)
AS
(
SELECT e.ManagerID, e.EmployeeID, e.Title, 0 AS Level
FROM #MyEmployees AS e
WHERE e.ManagerID is null
UNION ALL
SELECT e.ManagerID, e.EmployeeID, e.Title, d.Level + 1
FROM #MyEmployees AS e
INNER JOIN DirectReports AS d
ON e.ManagerID = d.EmployeeID
)
SELECT ManagerID, EmployeeID, Title, Level
FROM DirectReports

How to detect duplicate records with sub table records

Let's say I'm creating an address book in which the main table contains the basic contact information and a phone number sub table -
Contact
===============
Id [PK]
Name
PhoneNumber
===============
Id [PK]
Contact_Id [FK]
Number
So, a Contact record may have zero or more related records in the PhoneNumber table. There is no constraint on uniqueness of any column other than the primary keys. In fact, this must be true because:
Two contacts having different names may share a phone number, and
Two contacts may have the same name but different phone numbers.
I want to import a large dataset which may contain duplicate records into my database and then filter out the duplicates using SQL. The rules for identifying duplicate records are simple ... they must share the same name and the same number of phone records having the same content.
Of course, this works quite effectively for selecting duplicates from the Contact table but doesn't help me to detect actual duplicates given my rules:
SELECT * FROM Contact
WHERE EXISTS
(SELECT 'x' FROM Contact t2
WHERE t2.Name = Contact.Name AND
t2.Id > Contact.Id);
It seems as if what I want is a logical extension to what I already have, but I must be overlooking it. Any help?
Thanks!
In my question, I created a greatly simplified schema that reflects the real-world problem I'm solving. Przemyslaw's answer is indeed a correct one and did what I was asking both with the sample schema and, when extended, with the real one.
But, after doing some experiments with the real schema and a larger (~10k records) dataset, I found that performance was an issue. I don't claim to be an index guru, but I wasn't able to find a better combination of indices than what was already in the schema.
So, I came up with an alternate solution which fills the same requirements but executes in a small fraction (< 10%) of the time, at least using SQLite3 - my production engine. In hopes that it may assist someone else, I'll offer it as an alternative answer to my question.
DROP TABLE IF EXISTS Contact;
DROP TABLE IF EXISTS PhoneNumber;
CREATE TABLE Contact (
Id INTEGER PRIMARY KEY,
Name TEXT
);
CREATE TABLE PhoneNumber (
Id INTEGER PRIMARY KEY,
Contact_Id INTEGER REFERENCES Contact (Id) ON UPDATE CASCADE ON DELETE CASCADE,
Number TEXT
);
INSERT INTO Contact (Id, Name) VALUES
(1, 'John Smith'),
(2, 'John Smith'),
(3, 'John Smith'),
(4, 'Jane Smith'),
(5, 'Bob Smith'),
(6, 'Bob Smith');
INSERT INTO PhoneNumber (Id, Contact_Id, Number) VALUES
(1, 1, '555-1212'),
(2, 1, '222-1515'),
(3, 2, '222-1515'),
(4, 2, '555-1212'),
(5, 3, '111-2525'),
(6, 4, '111-2525');
COMMIT;
SELECT *
FROM Contact c1
WHERE EXISTS (
SELECT 1
FROM Contact c2
WHERE c2.Id > c1.Id
AND c2.Name = c1.Name
AND (SELECT COUNT(*) FROM PhoneNumber WHERE Contact_Id = c2.Id) = (SELECT COUNT(*) FROM PhoneNumber WHERE Contact_Id = c1.Id)
AND (
SELECT COUNT(*)
FROM PhoneNumber p1
WHERE p1.Contact_Id = c2.Id
AND EXISTS (
SELECT 1
FROM PhoneNumber p2
WHERE p2.Contact_Id = c1.Id
AND p2.Number = p1.Number
)
) = (SELECT COUNT(*) FROM PhoneNumber WHERE Contact_Id = c1.Id)
)
;
The results are as expected:
Id Name
====== =============
1 John Smith
5 Bob Smith
Other engines are bound to have differing performance which may be quite acceptable. This solution seems to work quite well with SQLite for this schema.
The author stated the requirement of "two people being the same person" as:
Having the same name and
Having the same number of phone numbers and all of which are the same.
So the problem is a bit more complex than it seems (or maybe I just overthought it).
Sample data and (an ugly one, I know, but the general idea is there) a sample query which I tested on below test data which seems to be working correctly (I'm using Oracle 11g R2):
CREATE TABLE contact (
id NUMBER PRIMARY KEY,
name VARCHAR2(40))
;
CREATE TABLE phone_number (
id NUMBER PRIMARY KEY,
contact_id REFERENCES contact (id),
phone VARCHAR2(10)
);
INSERT INTO contact (id, name) VALUES (1, 'John');
INSERT INTO contact (id, name) VALUES (2, 'John');
INSERT INTO contact (id, name) VALUES (3, 'Peter');
INSERT INTO contact (id, name) VALUES (4, 'Peter');
INSERT INTO contact (id, name) VALUES (5, 'Mike');
INSERT INTO contact (id, name) VALUES (6, 'Mike');
INSERT INTO contact (id, name) VALUES (7, 'Mike');
INSERT INTO phone_number (id, contact_id, phone) VALUES (1, 1, '123'); -- John having number 123
INSERT INTO phone_number (id, contact_id, phone) VALUES (2, 1, '456'); -- John having number 456
INSERT INTO phone_number (id, contact_id, phone) VALUES (3, 2, '123'); -- John the second having number 123
INSERT INTO phone_number (id, contact_id, phone) VALUES (4, 2, '456'); -- John the second having number 456
INSERT INTO phone_number (id, contact_id, phone) VALUES (5, 3, '123'); -- Peter having number 123
INSERT INTO phone_number (id, contact_id, phone) VALUES (6, 3, '456'); -- Peter having number 123
INSERT INTO phone_number (id, contact_id, phone) VALUES (7, 3, '789'); -- Peter having number 123
INSERT INTO phone_number (id, contact_id, phone) VALUES (8, 4, '456'); -- Peter the second having number 456
INSERT INTO phone_number (id, contact_id, phone) VALUES (9, 5, '123'); -- Mike having number 456
INSERT INTO phone_number (id, contact_id, phone) VALUES (10, 5, '456'); -- Mike having number 456
INSERT INTO phone_number (id, contact_id, phone) VALUES (11, 6, '123'); -- Mike the second having number 456
INSERT INTO phone_number (id, contact_id, phone) VALUES (12, 6, '789'); -- Mike the second having number 456
-- Mike the third having no number
COMMIT;
-- does not meet the requirements described in the question - will return Peter when it should not
SELECT DISTINCT c.name
FROM contact c JOIN phone_number pn ON (pn.contact_id = c.id)
GROUP BY name, phone_number
HAVING COUNT(c.id) > 1
;
-- returns correct results for provided test data
-- take all people that have a namesake in contact table and
-- take all this person's phone numbers that this person's namesake also has
-- finally (outer query) check that the number of both persons' phone numbers is the same and
-- the number of the same phone numbers is equal to the number of (either) person's phone numbers
SELECT c1_id, name
FROM (
SELECT c1.id AS c1_id, c1.name, c2.id AS c2_id, COUNT(1) AS cnt
FROM contact c1
JOIN contact c2 ON (c2.id != c1.id AND c2.name = c1.name)
JOIN phone_number pn ON (pn.contact_id = c1.id)
WHERE
EXISTS (SELECT 1
FROM phone_number
WHERE contact_id = c2.id
AND phone = pn.phone)
GROUP BY c1.id, c1.name, c2.id
)
WHERE cnt = (SELECT COUNT(1) FROM phone_number WHERE contact_id = c1_id)
AND (SELECT COUNT(1) FROM phone_number WHERE contact_id = c1_id) = (SELECT COUNT(1) FROM phone_number WHERE contact_id = c2_id)
;
-- cleanup
DROP TABLE phone_number;
DROP TABLE contact;
Check at SQL Fiddle: http://www.sqlfiddle.com/#!4/36cdf/1
Edited
Answer to author's comment: Of course I didn't take that into account... here's a revised solution:
-- new test data
INSERT INTO contact (id, name) VALUES (8, 'Jane');
INSERT INTO contact (id, name) VALUES (9, 'Jane');
SELECT c1_id, name
FROM (
SELECT c1.id AS c1_id, c1.name, c2.id AS c2_id, COUNT(1) AS cnt
FROM contact c1
JOIN contact c2 ON (c2.id != c1.id AND c2.name = c1.name)
LEFT JOIN phone_number pn ON (pn.contact_id = c1.id)
WHERE pn.contact_id IS NULL
OR EXISTS (SELECT 1
FROM phone_number
WHERE contact_id = c2.id
AND phone = pn.phone)
GROUP BY c1.id, c1.name, c2.id
)
WHERE (SELECT COUNT(1) FROM phone_number WHERE contact_id = c1_id) IN (0, cnt)
AND (SELECT COUNT(1) FROM phone_number WHERE contact_id = c1_id) = (SELECT COUNT(1) FROM phone_number WHERE contact_id = c2_id)
;
We allow a situation when there are no phone numbers (LEFT JOIN) and in outer query we now compare the number of person's phone numbers - it must either be equal to 0, or the number returned from the inner query.
The keyword "having" is your friend. The generic use is:
select field1, field2, count(*) records
from whereever
where whatever
group by field1, field2
having records > 1
Whether or not you can use the alias in the having clause depends on the database engine. You should be able to apply this basic principle to your situation.