Trenin
Trenin

Reputation: 2063

SQL Update based on row count

I have recently started relearning SQL syntax after about 15 years of inactivity. I am familiar with the old ANSI-89 JOIN syntax and the new syntax is taking some getting used to.

Anyway, what I am trying to do is count the number of elements in two queries, and if they are equal, set a flag for a row.

Here is my table structure:

PERSON
 ID        -- unique ID for the person
HOUSE   
 ID        -- unique ID for the house
 PersonID  -- person who owns the house
BATHROOM
 HouseID   -- House that the room is in
 Type      -- The type of bathroom - ensuite, powder, etc.
 Pieces    -- The number of pieces in the bathroom.  

OK. So I would like to add a new field in the PERSON table that keeps track of whether or not all the bathrooms of all their houses are 3 piece ensuites. (Don't ask why - this is obviously a manufactured scenario, but it maps well to the real world scenario I am trying to solve).

So, I start with the following:

ALTER TABLE person ADD All_Three tinyint(1) NOT NULL DEFAULT 1 AFTER ID;

Now I have the field and it is defaulted to 1.

So, I need to run a query that will populate it. Here is my attempt:

UPDATE IGNORE person,
     (SELECT COUNT(*) AS count 
      FROM  house, bathroom
      WHERE house.PersonID=person.ID
        AND bathroom.HouseID=house.ID
        AND bathroom.Type='ensuite'
        AND bathroom.Pieces=3) num_three,
     (SELECT COUNT(*) AS count 
      FROM  house, bathroom
      WHERE house.PersonID=person.ID
        AND bathroom.HouseID=house.ID) all_bathrooms
SET person.All_Three=2  
WHERE num_three.count=all_bathrooms.count;

This seems pretty logical to me; The first sub-query counts the number of ensuites that have three pieces, and the second counts the total number of bathrooms. If they are the equal, (i.e. all the bathrooms owned by that person are 3 piece ensuites) then we can set the All_Three field.

This fails with the error

ERROR 1054 (42S22): Unknown column 'person.ID' in 'where clause'

So, how do I reference the person inside the sub-query? I tried aliasing it, but that also fails with the same error.

EDIT: It turns out my objective was wrong. What I need to do is set the all_three field if every house owned by the person contains only 3 piece ensuites, and every house has at least one ensuite (which obviously must be 3 piece).

Upvotes: 2

Views: 865

Answers (2)

Thorsten Kettner
Thorsten Kettner

Reputation: 94913

You want to update records in table person. The syntax for updates in a single table is:

UPDATE tablename SET ... WHERE ...

Instead you are trying to join the table with two so-called derived tables (select statements):

UPDATE tablename, (query 1), (query 2) SET ... WHERE ...

This would cross join the three parts. This is what always happens with the old comma-separated join syntax; you combine all records with each other. Then you'd usually name your join criteria in a WHERE clause to remove records from the results. So with the new sytax this would be:

UPDATE tablename CROSS JOIN (query 1) CROSS JOIN (query 2) SET ... WHERE ...

You give criteria on the join of the two derived queries, however, which turns their join into an inner join:

UPDATE tablename CROSS JOIN (query 1) INNER JOIN (query 2) ON ... SET ...

You are also trying to relate the table person to the derived tables. But inside the two derived table queries there is no knowledge of other tables or derived tables outside. You cannot reference the table person inside the derived table queries.

Not allowed:

UPDATE tablename CROSS JOIN (select something from othertable where othertable.colx = tablename.coly) SET ...

Instead:

UPDATE tablename INNER JOIN (select something, colx from othertable where ...) AS query1 ON query1.colx = tablename.coly) SET ...

But as mentioned, you don't want to update more than one table anyhow, so no need to join. Unlike derived tables, which cannot be related to tables outside, normal subqueries can. You can use such subqueries in the WHERE clause:

UPDATE person
SET ...
WHERE 
  (select count(*) from ... where ... = person.id) = 
  (select count(*) from ... where ... = person.id)

As to the problem you clarify in your edit: You want to find persons that only have houses with three-piece ensuite bathrooms. In other words: All persons that have a house (that's a simple EXISTS or IN clause), but no house with another bathroom type or another-piece ensuite batchroom (that's a more complicated NOT EXISTS clause).

In your NOT EXIST clause for a person's unsuitable houses you have again a NOT EXISTS clause (for the case no bothroom exists in that house, if such is possible) and an EXISTS clause (for the case a bathroom with wrong type or pieces number exists in that house).

UPDATE person p
SET all_three = 2
WHERE id IN (select personid from house)
AND NOT EXISTS -- an unsuitable house
( 
  select *
  from house h
  where not exists -- a bathroom for that house
  ( 
    select * 
    from bathroom b 
    where b.houseid = h.id
  )
  or exists -- bad bathroom for that house
  (
    select * 
    from bathroom b 
    where b.houseid = h.id 
    and (b.type <> 'ensuite' or b.pieces <> 3)
  )
  where h.personid = p.id
);

Whether to use (NOT) EXISTS or (NOT) IN clauses here is mainly a matter of personal taste by the way.

Upvotes: 2

Ron Smith
Ron Smith

Reputation: 3266

To do this, we will produce an inclusion list of PERSON.IDs meeting the criteria (BATHROOM.Type = 'ensuite' and BATHROOM.Pieces = 3), an exclusion list of PERSON.IDs not meeting the criteria (BATHROOM.Type != 'ensuite' or BATHROOM.Pieces != 3), and use in/not in to filter the update:

update PERSON
  set PERSON.All_Three = 2
where PERSON.ID in(select HOUSE.PersonID
                   from HOUSE
                     inner join BATHROOM
                       on BATHROOM.HouseID = House.ID
                       and BATHROOM.Type = 'ensuite'
                       and BATHROOM.Pieces = 3)
  and PERSON.ID not in(select HOUSE.PersonID
                       from HOUSE
                       inner join BATHROOM
                         on BATHROOM.HouseID = House.ID
                         and (BATHROOM.Type != 'ensuite'
                             or BATHROOM.Pieces != 3));

SQL FIDDLE

Upvotes: 1

Related Questions