How to combine results from a JOIN with another result set? - sql-server-2005

I have 2 tables, CLIENTS and MEMBERS. We are trying to get the member's address from the CLIENTS table and all the records from the MEMBERS table where the ID from MEMBERS matches the ID from CLIENTS.
At the same time, we would also like to show the rest of the member's info from the MEMBERS table where the ID is either NULL or doesn't exist in the CLIENTS table.
I thought the following query would do it, but it isn't getting all the records from MEMBERS tables:
SELECT c.ID
,m.ID
,m.fname
,m.lname
,m.address
,m.city
,m.state
,m.zip
from Client c
inner join members m on c.id = m.id
UNION ALL
select '',m.* from members m
where m.id IS NULL or m.id NOT IN (select ca.id from clients ca)
What am I doing wrong?

Unless I am missing something it appears that you just need an OUTER JOIN
SELECT COALESCE(c.ID, '') AS ID,
m.ID,
m.fname,
m.lname,
m.address,
m.city,
m.state,
m.zip
FROM members m
LEFT OUTER JOIN Client c
ON c.id = m.id
With respect to the issue in your question do you have any NULL values for id in the Clients table? That is often the cause of unexpected NOT IN behaviour.

Related

Left outer join with count, on 3 tables not returning all rows from left table

I have these 3 tables:
Areas - id, name
Persons - id, area_id
Special_Persons - id_person, date
I'd like to produce a list of all Areas, followed by a count of Special Persons in each area, including Areas with no Special Persons.
If I do a left join of Areas and Persons, like this:
select a.id as idArea, count(p.id) as count
from areas a
left join persons p on p.area_id = a.id
group by a.id;
This works just fine; Areas that have no Persons show up, and have a count of 0.
What I am not clear on is how to do the same thing with the special_persons table, which currently only has 2 entries, both in the same Area.
I have tried the following:
select a.id as idArea, count(sp.id_person) as count
from special_persons sp, areas a
left join persons p on p.area_id = a.id
where p.area_id = a.id
and sp.id_person = p.id
group by a.id;
And it only returns 1 row, with the Area that happens to have 2 Special Persons in it, and a count of 2.
To continue getting a list of all areas, do I need to use a sub-query? Another join? I'm not sure how to go about it.
You can add another left join to the Special_Persons table:
select a.id as idArea, count(p.id), count(sp.id_person)
from areas a
left join persons p on p.area_id = a.id
left join special_persons sp on sp.id_person = p.id
group by a.id;

SQL statement - difficulty producing two fields from one table

I am relatively new to SQL and able to write some simple statements with some JOIN, WHERE thrown in. I also have some experience with SSRS 2008. However I am having difficulty producing two (unique) lists of names based on one table.
I am trying to produce a report containing Staff Members which lists Clients they look after. All names (regardless whether Staff or Client) are held in the Person table. I can run a simple query listing all staff members or clients but I am unable to list both.
To produce the Client list my query would be simply
SELECT p.Forenames, p.Surname
FROM Person AS p
INNER JOIN Client AS c ON p.ID = c.ID
and to produce the Staff list my query would be as above but the INNER JOIN as follows:
INNER JOIN StaffMember AS s ON p.ID = s.ID
The link between Staff Member and Client with all the different links are as follows (for reference).
Client.ID = ClientRecordForm.Client_ID
Person.ID = ClientRecordForm.AssignedSupportWorker_ID
Person.ID = StaffMember.ID
StaffMember.ID = ClientRecordForm.AssignedSupportWorker_ID
To help illustrate this, I can run a query to list Staff Members who have been assigned to Clients.
SELECT DISTINCT p.Forenames, p.Surname
FROM Person AS p
INNER JOIN ClientRecordForm AS crf ON p.ID = crf.AssignedSupportWorker_ID
This last query is essentially what I want but I am struggling to add the Client names as I don't seem to be able to distinguish the Clients as I am already using the Person table
If you want to show persons and be able to distinguish clients as well, try a self join.
SELECT DISTINCT p.Forenames, p.Surname
FROM Person AS p
INNER JOIN ClientRecordForm AS crf ON p.ID = crf.AssignedSupportWorker_ID
inner join person as personClients on crf.clientid = personClients.id
As you can see, you could then join another persons table to get StaffMember, you can join from personClients to Client to get information there ...etc.
Is that what you want?
SELECT DISTINCT p.Forenames, p.Surname
FROM Person AS p
INNER JOIN ClientRecordForm AS crf
ON p.ID = crf.AssignedSupportWorker_ID
INNER JOIN Client AS c
ON c.ID = crf.Client_ID

Conditional inner joins based on row values MSSQL

I have a MSSQL table that is used for messages. I have clients, workers, and users, each with their own respective tables. For this messaging component, the message can be sent to a client worker or user. To differentiate between which type the the sender/receiver is, I have a column in the message table that is a tiny-int, 1 for users, 2 for clients, 3 for workers.
So, when i am attempting to receive the messages, what is the ideal way to retrieve the name of the sender from the corresponding table(clients, users, or workers) without doing multiple queries.
I know I can first do a query to check the type column, then do another query to get the name but I would rather not do this for performance issues.
You could create a generic view which simply lists the relevant columns from each plus the message type and then join to that from your messages table.
CREATE VIEW ExampleView
AS
begin
SELECT
firstname,
lastname,
1 AS MessageType
FROM
dbo.Users
union ALL
SELECT
firstname,
lastname,
2 AS MessageType
FROM
dbo.Clients
union ALL
SELECT
firstname,
lastname,
3 AS MessageType
FROM
dbo.Workers
END
Query:
SELECT
a.MessageText,
isnull(b.Firstname,'')+ISNULL(b.lastname,'') AS Name
FROM Messaging a
INNER JOIN ExampleView b ON a.MessageType = b.MessageType
If space isn't a massive issue you could store the message type directly on the Users,Clients and Workers table and just join to that column... Better performance but not really as nice database design.
This is a rather large guess at your table structure based on your description. The performance will be down to indexes you have.
Select m.*,
COALESCE(c.ClientName, w.WorkerName, u.UserName) as Name
FROM Messages m
LEFT JOIN Clients c on c.ClientId = m.UserId AND m.type = 2
LEFT JOIN Workers w on w.WorkerId = m.UserId AND m.type = 3
LEFT JOIN Users u on u.UserId = m.UserId AND m.type = 1
Where (c.ClientId is not null
OR
w.WorkerId is not null
OR
u.UserId is not null)

Left outer join two levels deep in Postgres results in cartesian product

Given the following 4 tables:
CREATE TABLE events ( id, name )
CREATE TABLE profiles ( id, event_id )
CREATE TABLE donations ( amount, profile_id )
CREATE TABLE event_members( id, event_id, user_id )
I'm attempting to get a list of all events, along with a count of any members, and a sum of any donations. The issue is the sum of donations is coming back wrong (appears to be a cartesian result of donations * # of event_members).
Here is the SQL query (Postgres)
SELECT events.name, COUNT(DISTINCT event_members.id), SUM(donations.amount)
FROM events
LEFT OUTER JOIN profiles ON events.id = profiles.event_id
LEFT OUTER JOIN donations ON donations.profile_id = profiles.id
LEFT OUTER JOIN event_members ON event_members.event_id = events.id
GROUP BY events.name
The sum(donations.amount) is coming back = to the actual sum of donations * number of rows in event_members. If I comment out the count(distinct event_members.id) and the event_members left outer join, the sum is correct.
As I explained in an answer to the referenced question you need to aggregate before joining to avoid a proxy CROSS JOIN. Like:
SELECT e.name, e.sum_donations, m.ct_members
FROM (
SELECT e.id AS event_id, e.name, SUM(d.amount) AS sum_donations
FROM events e
LEFT JOIN profiles p ON p.event_id = e.id
LEFT JOIN donations d ON d.profile_id = p.id
GROUP BY 1, 2
) e
LEFT JOIN (
SELECT m.event_id, count(DISTINCT m.id) AS ct_members
FROM event_members m
GROUP BY 1
) m USING (event_id);
IF event_members.id is the primary key, then id is guaranteed to be UNIQUE in the table and you can drop DISTINCT from the count:
count(*) AS ct_members
You seem to have this two independent structures (-[ means 1-N association):
events -[ profiles -[ donations
events -[ event members
I wrapped the second one into a subquery:
SELECT events.name,
member_count.the_member_count
COUNT(DISTINCT event_members.id),
SUM(donations.amount)
FROM events
LEFT OUTER JOIN profiles ON events.id = profiles.event_id
LEFT OUTER JOIN donations ON donations.profile_id = profiles.id
LEFT OUTER JOIN (
SELECT
event_id,
COUNT(*) AS the_member_count
FROM event_members
GROUP BY event_id
) AS member_count
ON member_count.event_id = events.id
GROUP BY events.name
Of course you get a cartesian product between donations and events for every event since both are only bound to the event, there is no join relation between donations and event_members other than the event id, which of course means that every member matches every donation.
When you do your query, you ask for all events - let's say there are two, event Alpha and event Beta - and then JOIN with the members. Let's say that there is a member Alice that participates on both events.
SELECT events.name, COUNT(DISTINCT event_members.id), SUM(donations.amount)
FROM events
LEFT OUTER JOIN profiles ON events.id = profiles.event_id
LEFT OUTER JOIN donations ON donations.profile_id = profiles.id
LEFT OUTER JOIN event_members ON event_members.event_id = events.id
GROUP BY events.name
On each row you asked the total for Alice's donations. If Alice donated 100 USD, then you asked for:
Alpha Alice 100USD
Beta Alice 100USD
So it's not surprising that when asking for the sum total Alice comes out as having donated 200 USD.
If you wanted the sum of all donations, you'd better doing with two distinct queries. Trying to do everything with a single query, while possible, would be a classical SQL Antipattern (actually the one in chapter #18, "Spaghetti Query"):
Unintended Products
One common consequence of producing all your
results in one query is a Cartesian product. This happens when two of
the tables in the query have no condition restricting their
relationship. Without such a restriction, the join of two tables pairs
each row in the first table to every row in the other table. Each such
pairing becomes a row of the result set, and you end up with many more
rows than you expect.

SQL: Matching multiple columns value

I have a database set up so that the username (username) and id (id) are stored in the members table.
I have another table that records reports and I record each column in the table (fid), who reported it (rid) and who they were reporting (id) which both match to the user's id in the members table.
How could I get a query to pull the username for both the rid and id?
My current query is
SELECT selfreport.fid, selfreport.rid,
selfreport.id, members.username as username
FROM members, selfreport
WHERE members.id = selfreport.id
ORDER BY fid
but this only gets the username for who they were reporting. How can I get it to pull the username for both?
You need to join to your members table twice. Try something like this:
SELECT selfreport.fid,
selfreport.rid,
selfreport.id,
COALESCE(WhoReported.username, 'Not Specified') AS WhoReportedUN,
COALESCE(ReportedTo.username, 'Not Specified') AS ReportedToUN
FROM selfreport
LEFT JOIN members WhoReported ON WhoReported.id = selfreport.id
LEFT JOIN members ReportedTo ON ReportedTo.id = selfreport.rid
ORDER BY fid
Do not use implicit SQL '89 joins they are an antipattern.
Use explicit join syntax instead.
SELECT s.fid, s.rid, s.id, m1.username as username, m2.username as rusername
FROM selfreport S
INNER JOIN members m1 ON (m1.id = s.id)
INNER JOIN members m2 ON (m2.id = s.rid)
ORDER BY s.fid
If id or rid is optional, use a left join.
SELECT
s.fid, s.rid, s.id
, COALESCE(m1.username, 'nobody') as username
, COALESCE(m2.username, 'nobody') as rusername
FROM selfreport S
LEFT JOIN members m1 ON (m1.id = s.id)
LEFT JOIN members m2 ON (m2.id = s.rid)
ORDER BY s.fid
You need to join members twice:
SELECT selfreport.fid,
selfreport.rid,
selfreport.id,
m1.username AS ReportToUsername,
m2.username AS ReporteeUsername
FROM selfreport
INNER JOIN members m1
ON m1.id = selfreport.id
INNER JOIN members m2
ON m2.id = selfreport.rid
ORDER BY fid
Since you were doing an implicit join in your original query, I believe INNER JOIN will suit you well. However, if it's possible to have null values in selfreport.id or selfreport.rid, you should use LEFT JOIN instead.