Here is the scenario. I have a MySQL table called modules which contains one or more entries each identified by a unique string - the module ID (mid).
There are a few other tables (scripts,images,sets...) which contain objects each of which"belong" to one of the modules - identified by the column 'mid' in each table.
Prior to allowing a user to drop a module entry, I need to check that the operation will not leave any orphaned objects in any of the other tables. Here is an example to make this clearer
Table modules
mname mid
Mod1 abcd1234
Mod2 wxyz9876
Table scripts
sname mid
A abcd1234
B wxyz9876
Table images
iname mid
A abcd1234
Table sets
sname mid
One or more of the tables may contain no, or no matching, entries.
I have written and tested a spot of SQL to handle this.
SELECT COUNT(*) FROM `images` WHERE mid = 'abcd1234'
UNION
SELECT COUNT(*) FROM `sets` WHERE mid = 'abcd1234'
UNION
SELECT COUNT(*) FROM `scripts` WHERE mid = 'abcd1234'
which very obligingly returns 1 implying that the module is "in use" and cannot be dropped. However, my SQL skills are pretty basic. I would much appreciate anyone who could tell me if this is a safe way to do things.
Not really a good way.
The UNION without ALL removes duplicate results. That would give you 1 if you had 3 rows returning 1. UNION ALL will make it return 3 rows with the count for each table, even when they are duplicate. After that you SUM them and you get the final count.
You should do:
SELECT SUM(cnt) FROM (
SELECT COUNT(*) as cnt FROM `images` WHERE mid = 'abcd1234'
UNION ALL
SELECT COUNT(*) FROM `sets` WHERE mid = 'abcd1234'
UNION ALL
SELECT COUNT(*) FROM `scripts` WHERE mid = 'abcd1234'
) a
You could build something around the following concept, given that there is a one-to-many relation between modules and the other tables.
select mid
,count(scripts.sname) as scripts
,count(images.iname) as images
,count(sets.sname) as sets
from modules
left join images using(mid)
left join sets using(mid)
left join scripts using(mid)
where mid = 'abcd1234'
group
by mid;
You could for example add the count(..) together, or including a HAVING clause.
Related
I have a table, System, with a bunch of fields including System.serial.
I have a list of serial numbers that I want to get the status of.
Simple enough:
Select * from System where System.serial in ('s1','s2', 'sn');
However the list of serial numbers also has serials NOT IN the System table.
Obviously they are not in the results.
I want the missing serials to show in the results also but with no data.
The best way I can think of doing this is to make a temporary table with one column, serial, and then left join System on it.
How can I do this without creating a temporary table?
Something like:
Select listOfSerials.serial, System.*
from (Select ('s1','s2', 'sn') as serial ) as ListOfSerials
left join System on System.serial = ListOfSerials.serial;
Thanks,
Ryan
You're on the right track with your solution of creating a virtual table with which to do LEFT JOIN against your real data.
You can create a derived table as a series of UNIONed SELECT statements that select literal values with no table reference.
SELECT listOfSerials.serial, System.*
FROM (
SELECT 's1' AS serial
UNION SELECT 's2'
UNION SELECT 'sn'
) AS ListOfSerials
LEFT JOIN System ON System.serial = ListOfSerials.serial;
You only need to define a column alias in the first SELECT in the UNION. The rest are required to use that column alias.
Creating a reference table to store the list of serials is probably your best option. That would allow you to write a query like:
SELECT r.serial reference_serial, s.serial system_serial
FROM reference_table r
LEFT JOIN system_table s ON s.serial = r.serial
With the LEFT JOIN, serials declared in the reference table but unavailable in the system table will have second column set to NULL.
A quick and dirty work around is to use UNIONed subqueries to emulate the reference table:
SELECT r.serial reference_serial, s.serial system_serial
FROM (
SELECT 'serial1' AS serial
UNION ALL SELECT 'serial2'
UNION ALL SELECT 'serial2'
...
) r
LEFT JOIN system_table s ON s.serial = r.serial
I'm aware of the INSERT INTO table_name QUERY; however, I'm unsure how to go about achieving the desired result in this case.
Here's a slightly contrived example to explain what I'm looking for, but I'm afraid I cannot put it more succiently.
I have two tables in a database designed for a hotel.
BOOKING and CUSTOMER_BOOKING
Where BOOKING contains PK_room_number, room_type, etc. and CUSTOMER_BOOKING contains FK_room_number, FK_cusomer_id
CUSTOMER_BOOKING is a linking table (many customers can make many bookings, and many bookings can consist of many customers).
Ultimately, in the application back-end I want to be able to list all rooms that have less than 3 customers associated with them. I could execute this a separate query and save the result in the server-side scripting.
However, a more elegant solution (from my point of view) is to store this within the BOOKING table itself. That is to add a column no_of_bookings that counts the number of times the current PK_room_number appears as the foreign key FK_room_number within the CUSTOMER_BOOKING table. And why do this instead? Because it would be impossible for me to write a single complicated query which will both include the information from all ROOMS, among other tables, and also count the occurrences of bookings, without excluding ROOMS that don't have any bookings. A very bad thing for a hotel website attempting to show free rooms!
So it would look like this
BOOKING: PK_room_number (104B) room_type (double) room_price (high), no_of_bookings (3)
BOOKING: PK_room_number (108C) room_type (single) room_price (low), no_of_bookings (1)
CUSTOMER_BOOKING: FK_room_number (104B) FK_customer_id (4312)
CUSTOMER_BOOKING: FK_room_number (104B) FK_customer_id (6372)
CUSTOMER_BOOKING: FK_room_number (104B) FK_customer_id (1112)
CUSTOMER_BOOKING: FK_room_number (108C) FK_customer_id (9181)
How would I go about creating this?
Because it would be impossible for me to write a single complicated
query which will both include the information from all ROOMS, among
other tables, and also count the occurrences of bookings, without
excluding ROOMS that don't have any bookings.
I wouldn't say it's impossible and unless you're running into performance issues, it's easier to implement than adding a new summary column:
select b.*, count(cb.room_number)
from bookings b
left join customer_booking cb on b.room_number = cb.room_number
group by b.room_number
Depending on your query may need to use a derived table containing the booking counts for each room instead instead
select b.*, coalesce(t1.number_of_bookings,0) number_of_bookings
from bookings b
left join (
select room_number, count(*) number_of_bookings
from customer_booking
group by room_number
) t1 on t1.room_number = b.room_number
You have to left join the derived table and select coalesce(t1.number_of_bookings,0) in case a room does not have any entries in the derived table (i.e. 0 bookings).
A summary column is a good idea when you're running into performance issues with counting the # of bookings each time. In that case I recommend creating insert and delete triggers on the customer_booking table that either increment or decrement the number_of_bookings column.
You could do it in a single straight select like this:
select DISTINCT
b1.room_pk,
c1.no_of_bookings
from cust_bookings b1,
(select room_pk, count(1) as no_of_bookings
from cust_bookings
group by room_pk) c1
where b1.room_pk = c1.room_pk
having c1.no_of_bookings < 3
Sorry i used my own table names to test it but you should figure it out easily enough. Also, the "having" line is only there to limit the rows returned to rooms with less than 3 bookings. If you remove that line you will get everything and could use the same sql to update a column on the bookings table if you still want to go that route.
Consider below solutions.
A simple aggregate query to count the customers per each booking:
SELECT b.PK_room_number, Count(c.FK_customer_id)
FROM Booking b
INNER JOIN Customer_Booking c ON b.PK_room_number = c.FK_room_number
GROUP BY b.PK_room_number
HAVING Count(c.FK_customer_id) < 3; # ADD 3 ROOM MAX FILTER
And if you intend to use a new column no_of_booking, here is an update query (using aggregate subquery) to run right after inserting new value from web frontend:
UPDATE Booking b
INNER JOIN
(SELECT b.PK_room_number, Count(c.FK_customer_id) As customercount
FROM Booking b
INNER JOIN Customer_Booking c ON b.PK_room_number = c.FK_room_number
GROUP BY b.PK_room_number) As r
ON b.PK_room_number = r.PK_room_number
SET b.no_of_booking = r.customercount;
the following generates a list showing all of the bookings and a flag of 0 or 1 if the the room has a customer for each of the rooms. it will display some rooms multiple times if there are multiple customers.
select BOOKING.*,
case CUSTOMER_BOOKING.FK_ROOM_NUMBER is null THEN 0 ELSE 1 END AS BOOKING_FLAG
from BOOKING LEFT OUTER JOIN CUSTOMER_BOOKING
ON BOOKING.PK_room_numer = CUSTOMER_BOOKING.FK_room_number
summing and grouping we arrive at:
select BOOKING.*,
SUM(case when CUSTOMER_BOOKING.FK_ROOM_NUMBER is null THEN 0 ELSE 1 END) AS BOOKING_COUNT
from BOOKING LEFT OUTER JOIN CUSTOMER_BOOKING
ON BOOKING.PK_room_number = CUSTOMER_BOOKING.FK_room_number
GROUP BY BOOKING.PK_room_number
there are at least two other solutions I can think of off the top of my head...
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).
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.
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
I have two tables in my MySQL database, one is a library of all of the books in the database, and the other is containing individual rows corresponding to which books are in a user's library.
For example:
Library Table
`id` `title`...
===== ===========
1 Moby Dick
2 Harry Potter
Collection Table
`id` `user` `book`
===== ====== =======
1 1 2
2 2 2
3 1 1
What I want to do is run a query that will show all the books that are not in a user's collection. I can run this query to show all the books not in any user's collection:
SELECT *
FROM `library`
LEFT OUTER JOIN `collection` ON `library`.`id` = `collection`.`book`
WHERE `collection`.`book` IS NULL
This works just fine as far as I can tell. Running this in PHPMyAdmin will result in all of the books that aren't in the collection table.
However, how do I restrict that to a certain user? For example, with the above dummy data, I want book 1 to result if user 2 runs the query, and no books if user 1 runs the query.
Just adding a AND user=[id] doesn't work, and with my extremely limited knowledge of JOIN statements I'm not getting anywhere really.
Also, the ID of the results being returned (of query shown, which doesn't do what I want but does function) is 0-- how do I make sure the ID returned is that of library.id?
You'll have to narrow down your LEFT JOIN selection to only the books that a particular user has, then whatever is NULL in the joined table will be rows(books) for which the user does not have in his/her collection:
SELECT
a.id,
a.title
FROM
library a
LEFT JOIN
(
SELECT book
FROM collection
WHERE user = <userid>
) b ON a.id = b.book
WHERE
b.book IS NULL
An alternative is:
SELECT
a.id,
a.title
FROM
library a
WHERE
a.id NOT IN
(
SELECT book
FROM collection
WHERE user = <userid>
)
However, the first solution is more optimal as MySQL will execute the NOT IN subquery once for each row rather than just once for the whole query. Intuitively, you would expect MySQL to execute the subquery once and use it as a list, but MySQL is not smart enough to distinguish between correlated and non-correlated subqueries.
As stated here:
"The problem is that, for a statement that uses an IN subquery, the
optimizer rewrites it as a correlated subquery."
How about this? It's just off the top of my head - I don't have access to a database to test on right now. (sorry)
SELECT
*
FROM
library lib
WHERE
lib.id NOT IN (
SELECT
book
FROM
collection coll
WHERE
coll.user =[id]
)
;
I need to gather posts from two mysql tables that have different columns and provide a WHERE clause to each set of tables. I appreciate the help, thanks in advance.
This is what I have tried...
SELECT
blabbing.id,
blabbing.mem_id,
blabbing.the_blab,
blabbing.blab_date,
blabbing.blab_type,
blabbing.device,
blabbing.fromid,
team_blabbing.team_id
FROM
blabbing
LEFT OUTER JOIN
team_blabbing
ON team_blabbing.id = blabbing.id
WHERE
team_id IN ($team_array) ||
mem_id='$id' ||
fromid='$logOptions_id'
ORDER BY
blab_date DESC
LIMIT 20
I know that this is messy, but i'll admit, I am no mysql veteran. I'm a beginner at best... Any suggestions?
You could put the where-clauses in subqueries:
select
*
from
(select * from ... where ...) as alias1 -- this is a subquery
left outer join
(select * from ... where ...) as alias2 -- this is also a subquery
on
....
order by
....
Note that you can't use subqueries like this in a view definition.
You could also combine the where-clauses, as in your example. Use table aliases to distinguish between columns of different tables (it's a good idea to use aliases even when you don't have to, just because it makes things easier to read). Example:
select
*
from
<table> as alias1
left outer join
<othertable> as alias2
on
....
where
alias1.id = ... and alias2.id = ... -- aliases distinguish between ids!!
order by
....
Two suggestions for you since a relative newbie in SQL. Use "aliases" for your tables to help reduce SuperLongTableNameReferencesForColumns, and always qualify the column names in a query. It can help your life go easier, and anyone AFTER you to better know which columns come from what table, especially if same column name in different tables. Prevents ambiguity in the query. Your left join, I think, from the sample, may be ambigous, but confirm the join of B.ID to TB.ID? Typically a "Team_ID" would appear once in a teams table, and each blabbing entry could have the "Team_ID" that such posting was from, in addition to its OWN "ID" for the blabbing table's unique key indicator.
SELECT
B.id,
B.mem_id,
B.the_blab,
B.blab_date,
B.blab_type,
B.device,
B.fromid,
TB.team_id
FROM
blabbing B
LEFT JOIN team_blabbing TB
ON B.ID = TB.ID
WHERE
TB.Team_ID IN ( you can't do a direct $team_array here )
OR B.mem_id = SomeParameter
OR b.FromID = AnotherParameter
ORDER BY
B.blab_date DESC
LIMIT 20
Where you were trying the $team_array, you would have to build out the full list as expected, such as
TB.Team_ID IN ( 1, 4, 18, 23, 58 )
Also, not logical "||" or, but SQL "OR"
EDIT -- per your comment
This could be done in a variety of ways, such as dynamic SQL building and executing, calling multiple times, once for each ID and merging the results, or additionally, by doing a join to yet another temp table that gets cleaned out say... daily.
If you have another table such as "TeamJoins", and it has say... 3 columns: a date, a sessionid and team_id, you could daily purge anything from a day old of queries, and/or keep clearing each time a new query by the same session ID (as it appears coming from PHP). Have two indexes, one on the date (to simplify any daily purging), and second on (sessionID, team_id) for the join.
Then, loop through to do inserts into the "TempJoins" table with the simple elements identified.
THEN, instead of a hard-coded list IN, you could change that part to
...
FROM
blabbing B
LEFT JOIN team_blabbing TB
ON B.ID = TB.ID
LEFT JOIN TeamJoins TJ
on TB.Team_ID = TJ.Team_ID
WHERE
TB.Team_ID IN NOT NULL
OR B.mem_id ... rest of query
What I ended up doing is;
I added an extra column to my blabbing table called team_id and set it to null as well as another field in my team_blabbing table called mem_id
Then I changed the insert script to also insert a value to the mem_id in team_blabbing.
After doing this I did a simple UNION ALL in the query:
SELECT
*
FROM
blabbing
WHERE
mem_id='$id' OR
fromid='$logOptions_id'
UNION ALL
SELECT
*
FROM
team_blabbing
WHERE
team_id
IN
($team_array)
ORDER BY
blab_date DESC
LIMIT 20
I am open to any thought on what I did. Try not to be too harsh though:) Thanks again for all the info.