I have an update statement that's like this:
update user_stats set
requestsRecd = (select count(*) from requests where requestedUserId = 1) where userId = 1,
requestsSent = (select count(*) from requests where requesterUserId = 2) where userId = 2;
What I'm trying to do, is update the same table, but different users in that table, with the count of friend requests received for one user, and the count of friend requests sent by another user.
What I'm doing works if I remove the where clauses, but then, that updates all the users in the entire table.
Any idea how I can do something like this with the where clauses in there or achieve the same results using another approach?
(As proposed in several other answers, obviously, you could run two separate statements, but to answer the question you asked, whether it was possible, and how to do it...)
Yes, it is possible to accomplish the update operation with a single statement. You'd need conditional tests as part of the statement (like the conditions in the WHERE clauses of your example, but those conditions can't go into a WHERE clause of the UPDATE statement.
The big restriction we have with doing this in one UPDATE statement is that the statement has to assign a value to both of the columns, for both rows.
One "trick" we can make use of is assigning the current value of the column back to the column, e.g.
UPDATE mytable SET mycol = mycol WHERE ...
Which results in no change to what's stored in the column. (That would still fire BEFORE/AFTER update trigger on the rows that satisfy the WHERE clause, but the value currently stored in the column will not be changed.)
So, we can't conditionally specify which columns are to be updated on which rows, but we can include a condition in the expression that we're assigning to the column. As an example, consider:
UPDATE mytable SET mycol = IF(foo=1, 'bar', mycol)
For rows where foo=1 evaluates to TRUE, we'll assign 'bar' to the column. For all other rows, the value of the column will remain unchanged.
In your case, you want to assign a "new" value to a column if a particular condition is true, and otherwise leave it unchanged.
Consider the result of this statement:
UPDATE user_stats t
SET t.requestsRecd = IF(t.userId=1, expr1, t.reqestsRecd)
, t.requestsSent = IF(t.userId=2, expr2, t.reqestsSent)
WHERE t.userId IN (1,2);
(I've omitted the subqueries that return the count values you want to assign, and replaced that with the "expr1" and "expr2" placeholders. This just makes it easier to see the pattern, without cluttering it up with more syntax, that hides the pattern.)
You can replace expr1 and expr2 in the statement above with your original subqueries that return the counts.
As an alternative form, it's also possible to return those counts on a single row, using in an inline view (aliased as v here), and then specify a join operation. Something like this:
UPDATE user_stats t
CROSS
JOIN ( SELECT (select count(*) from requests where requestedUserId = 1) AS c1
, (select count(*) from requests where requesterUserId = 2) AS c2
) v
SET t.requestsRecd = IF(t.userId=1, v.c1 ,t.reqestsRecd)
, t.requestsSent = IF(t.userId=2, v.c2 ,t.reqestsSent)
WHERE t.userId IN (1,2)
Since the inline view returns a single row, we don't need any ON clause or predicates in the WHERE clause. (*I typically include the CROSS keyword here, but it could be omitted without affecting the statement. My primary rationale for including the CROSS keyword is to make the intent clear to a future reader, who might be confused by the omission of join predicates, expecting to find some in the ON or WHERE clause. The CROSS keyword alerts the reader that the omission of join predicates was intended.)
Also note that the statement would work the same even if we omitted the predicates in the WHERE clause, we could spin through all the rows in the entire table, and only the rows with userId=1 or userId=2 would be affected. (But we want to include the WHERE clause, for improved performance; there's no reason for us to obtain locks on rows that we don't want to modify.)
So, to summarize: yes, it is possible to perform the sort of conditional update of two (or more) rows within a single statement. As to whether you want to use this form, or use two separate statements, that's up for you to decide.
What you're trying to do is two updates try splitting these out:
update user_stats set
requestsRecd = (select count(*) from requests where requestedUserId = 1) where userId = 1;
update user_stats set
requestsSent = (select count(*) from requests where requesterUserId = 2) where userId = 2;
There may be a way using CASE statements to dynamically chose a column but I'm not sure if that's possible.
You are trying to update two different rows at the same time. That is not possible. Use two update queries then.
update user_stats set
requestsRecd = (select count(*) from requests where requestedUserId = 1) where userId = 1;
and
update user_stats set
requestsSent = (select count(*) from requests where requesterUserId = 2) where userId = 2;
Tell me if that works or not.
Related
I want a select that if/exists it returns the 'link' value instead of regular output.
so instead of '1' it returns the 'link'
Is that possible?
SELECT IF(
EXISTS(
SELECT link FROM modules WHERE module='license'
), 1, '/dashboard/'
)
Use aggregation with MAX() (or MIN()):
SELECT COALESCE(MAX(link), '/dashboard/') link
FROM modules
WHERE module = 'license';
If that specific link does not exist in modules then MAX() will return null in which case COALESCE() will return '/dashboard/'.
#forpas's solution is correct, but selecting MAX(link) can become slow if your table is large. We do not need to compute the maximum, since we are only interested in the existence of the link satisfying the condition.
This is a more complicated, but quicker solution:
SELECT COALESCE(t2.link, '/dashboard/')
FROM
(
SELECT 1 AS foo
) t
LEFT JOIN (
SELECT link
FROM modules
WHERE module='license' AND
(NOT (link IS NULL))
LIMIT 0, 1
) t2
ON 1 = 1
Explanation:
we left join a dummy table with a generated relation that has the links you need if they exist
t is a dummy table and serves the purpose to have a result even if t2 is empty
t2 will have 0 records if there is no record meeting the criteria
t2 will have all the links you need if at least a record is meeting the criteria
if there are multiple records meeting the criteria and you need them all, then you can remove the LIMIT clause from t2
the LIMIT makes sure that your search stops when you find the first match instead of searching for a maximum
I need to run some scripts for MySQL databases. And I need to adjust the value in queries each time I run them. However, I need to specify the same value in two different places. I don't want to accidentally leave one value unchanged. Is there a way to specify the value as a constant in the script? I only have read privilege in the databases. I couldn't find the information through searching. Thanks.
You could probably do what you want with MySQL #variables, or via a simple select statement as your first table and join with no ON clause (thus a cross-join), but a Cartesian result with only 1 record will never create duplicates. Then you can use that column consistently throughout. For example...
select
from
( select #someNumber := 123,
#someDate := '2017-07-23' ) sqlVars,
OtherTable OT
where
OT.SomeNumberField = #someNumber
OR OT.OtherDateField = #someDate
but you can probably do similar as just a column such as
select
from
( select 123 as someNumber,
'2017-07-23' as someDate ) sqlVars,
OtherTable OT
where
OT.SomeNumberField = sqlVars.someNumber
OR OT.OtherDateField = sqlVars.someDate
Of course, standard join, left-join, etc to multiple tables should be able to see the columns either way with the table as the first in the list so it is visible all down-stream.
If you define a variable at the beginning of your query, you can use that throughout.
I have a table filled with tasting notes written by users, and another table that holds ratings that other users give to each tasting note.
The query that brings up all notes that are written by other users that you have not yet rated looks like this:
SELECT tastingNotes.userID, tastingNotes.beerID, tastingNotes.noteID, tastingNotes.note, COALESCE(sum(tasteNoteRate.Score), 0) as count,
CASE
WHEN tasteNoteRate.userVoting = 1162 THEN 1
ELSE 0
END AS userScored
FROM tastingNotes
left join tasteNoteRate on tastingNotes.noteID = tasteNoteRate.noteID
WHERE tastingNotes.userID != 1162
Group BY tastingNotes.noteID
HAVING userScored < 1
ORDER BY count, userScored
User 1162 has written a note for note 113. In the tasteNoteRate table it shows up as:
noteID | userVoting | score
113 1162 0
but it is still returned each time the above query is run....
MySQL allows you to use group by in a rather special way without complaining, see the documentation:
If ONLY_FULL_GROUP_BY is disabled, a MySQL extension to the standard SQL use of GROUP BY permits the select list, HAVING condition, or ORDER BY list to refer to nonaggregated columns even if the columns are not functionally dependent on GROUP BY columns. [...] In this case, the server is free to choose any value from each group, so unless they are the same, the values chosen are indeterminate, which is probably not what you want.
This behaviour was the default behaviour prior to MySQL 5.7.
In your case that means, if there is more than one row in tasteNoteRate for a specific noteID, so if anyone else has already voted for that note, userScored, which is using tasteNoteRate.userVoting without an aggregate function, will be based on a random row - likely the wrong one.
You can fix that by using an aggregate:
select ...,
max(CASE
WHEN tasteNoteRate.userVoting = 1162 THEN 1
ELSE 0
END) AS userScored
from ...
or, because the result of a comparison (to something other than null) is either 1 or 0, you can also use a shorter version:
select ...,
coalesce(max(tasteNoteRate.userVoting = 1162),0) AS userScored
from ...
To be prepared for an upgrade to MySQL 5.7 (and enabled ONLY_FULL_GROUP_BY), you should also already group by all non-aggregate columns in your select-list: group by tastingNotes.userID, tastingNotes.beerID, tastingNotes.noteID, tastingNotes.note.
A different way of writing your query (amongst others) would be to do the grouping of tastingNoteRates in a subquery, so you don't have to group by all the columns of tastingNotes:
select tastingNotes.*,
coalesce(rates.count, 0) as count,
coalesce(rates.userScored,0) as userScored
from tastingNotes
left join (
select tasteNoteRate.noteID,
sum(tasteNoteRate.Score) as count,
max(tasteNoteRate.userVoting = 1162) as userScored
from tasteNoteRate
group by tasteNoteRate.noteID
) rates
on tastingNotes.noteID = rates.noteID and rates.userScored = 0
where tastingNotes.userID != 1162
order by count;
This also allows you to get the notes the user voted on by changing rates.userScored = 0 in the on-clause to = 1 (or remove it to get both).
Change to an inner join.
The tasteNoteRate table is being left joined to the tastingNotes, which means that the full tastingNotes table (matching the where) is returned, and then expanded by the matching fields in the tasteNoteRate table. If tasteNoteRate is not satisfied, it doesn't prevent tastingNotes from returning the matched fields. The inner join will take the intersection.
See here for more explanation of the types of joins:
What's the difference between INNER JOIN, LEFT JOIN, RIGHT JOIN and FULL JOIN?
Make sure to create an index on noteID in both tables or this query and use case will quickly explode.
Note: Based on what you've written as the use case, I'm still not 100% certain that you want to join on noteID. As it is, it will try to give you a joined table on all the notes joined with all the ratings for all users ever. I think the CASE...END is just going to interfere with the query optimizer and turn it into a full scan + join. Why not just add another clause to the where..."and tasteNoteRate.userVoting = 1162"?
If these tables are not 1-1, as it looks like (given the sum() and "group by"), then you will be faced with an exploding problem with the current query. If every note can have 10 different ratings, and there are 10 notes, then there are 100 candidate result rows. If it grows to 1000 and 1000, you will run out of memory fast. Eliminating a few rows that the userID hasn't voted on will remove like what 10 rows from eventually 1,000,000+, and then sum and group them?
The other way you can do it is to reverse the left join:
select ...,sum()... from tasteNoteRate ... left join tastingNotes using (noteID) where userID != xxx group by noteID, that way you only get tastingNotes information for other users' notes.
Maybe that helps, maybe not, but yeah, SCHEMA and specific use cases/example data would be helpful.
With this kind of "ratings of ratings", sometimes its better to maintain a summary table of the vote totals and just track which the user has already voted on. e.g. Don't sum them all up in the select query. Instead, sum it up in the insert...on duplicate key update (total = total + 1); At least thats how I handle the problem in some user ranking tables. They just grow so big so fast.
I need to sort selected_booking by cost first and then assign the index i to every row. My variant doesn't work properly (outer SELECT breaks the order):
SELECT (#i:=#i + 1) AS i, selected_booking.*
FROM (SELECT * FROM booking ORDER BY cost DESC) AS selected_booking;
Is there any way to save the order of inner selection when doing outer one?
Q: Is there any way to save the order of inner selection when doing outer selection?
A: Absent an ORDER BY clause on the outer query, MySQL is free to return the rows in any order it chooses.
If you want rows from the inline view (derived table) returned in a specific order, you need to specify that in the outer query... you'd need to add an ORDER BY clause on the outer query.
NOTE: The behavior of user-defined variables as in your query is not guaranteed, the MySQL Reference Manual warns of this. But in spite of that warning, we do observe repeatable behavior in MySQL 5.1 and 5.5.
It's not at all clear why you need an inline view (aka a derived table, in the MySQL venacular) in the example you give.
It seems like this query would return the result you seem to want:
SET #i = 0 ;
SELECT #i:=#i+1 AS i
, b.*
FROM booking b
ORDER BY b.cost DESC ;
Alternatively, you could do this in a single statement, and initialize #i within the query, rather than a separate SET statement.
SELECT #i:=#i+1 AS i
, b.*
FROM booking b
JOIN (SELECT #i:=0) i
ORDER BY b.cost DESC
(This initialization works, again, because of the way the MySQL processes inline views, the inline view query is run BEFORE the outer query. This isn't guaranteed behavior, and may change in a future release (it may have already changed in 5.6)
NOTE: For improved performance of this query, if a suitable index is available with cost as the leading column, e.g.
... ON booking (cost)
that may allow MySQL to use that index to return rows in order and avoid a "Using filesort" operation.
I'm having trouble getting a query to work properly. I feel that this should be easy but for some reason I can't get it correct.
I have two tables joined by an ID field. I'm trying to get all the records that are in t1 and don't show up in t2.
This works currently:
select * from at_templates a
left join at_vault b on a.id = b.template
where b.at_id is null
BUT, I also want to put another condition in the query to limit the data to a subset and it is not working:
select * from at_templates a
left join at_vault b on a.id = b.template
where b.at_id != 1
The second query comes up empty but I want the same results as the first, based upon the input of at_id.
Any ideas?
Your working example implies that the "first table" you want to see records from is a and the "second table" you want to use to exclude records is b. If you are excluding all records that exist in b, then you can't further limit the result set by any value like b.at_id because there are no values associated with b in your result set.
Additionally, if the condition b.at_id is null is true, the condition b.at_id != 1 will never be true because an inequality comparison with null will always return null. (The reason for this is that null is not a value; it is a placeholder indicating the absence of a value.)
If you want to get the same results from both queries, based on a comparison between some user input parameter and the field b.at_id (and noting that your second query currently returns an empty set), you might be able to use MySQL's null-safe equality operator in the following way:
SELECT
*
FROM
at_templates AS a
LEFT JOIN
at_vault AS b ON a.id = b.template
WHERE NOT b.at_id <=> 1;
This is a MySQL extension, not a standard syntax; unfortunately the ANSI SQL standard syntax, IS [NOT] DISTINCT FROM, doesn't appear to be widely supported. Some alternate ways to rewrite this condition are discussed in How to rewrite IS DISTINCT FROM and IS NOT DISTINCT FROM?.
Keep in mind that if in the future you have some values of b.at_id that are not 1, this query would return those rows as well, and not just the rows returned by your first query.