Ok, so I have a table which holds bets on games.
The table holds the columns: user_id, event_id, bet.
A user can send his/her (multiple) bets to the server in one request.
I need to insert multiple bets using one query, while checking that none of the bets
are on an event that already started/finished.
In case of at least 1 started/finished event, I don't really care if the whole query cancels, or just ignores the 'unqualified' bets.
Question
How can I insert multiple bets (rows) with one query, while conditioning the insert on a select statement (which checks for each of the events' statuses)?
Here is the query I would've used if it worked (and it doesn't of course):
INSERT INTO bet_on_event (user_id, event_id, bet)
VALUES (1,5,1), (1,6,2)
IF (SELECT COUNT(*) FROM events WHERE _id IN(5,6) AND status=0) = ?;
Explanation
1. As mentioned, the values are pre-made - requested by the user.
2. Games/events have status. 0 means a game hasn't started, so it's ok to bet.
3. The select statement just counts how many of the requested events have status 0.
4. The 'IF' should check if the count from (3) equals the number of events the user requested to bet on, thus confirming that all the events are ok to bet on.
The 'IF' should be replaced with something that work, and the whole statement can be replaced if you have a better idea for what I'm trying to achieve.
A simpler query (which isn't enough for my case, but works with 1 row) is:
INSERT INTO bet_on_event (user_id, event_id, bet)
SELECT 1,5,1 FROM dual
WHERE (SELECT COUNT(*) FROM events WHERE _id IN(5,6) AND status=0) = ?;
Any idea? Is this even possible? Betting is gonna be used a lot, so I want to do it as quick as possible - with 1 query.
Thank you so much.
EDIT
That is what I ended up doing, taken from Thorsten's answer (I changed it to a dynamically built query, as that is what I need):
var query='INSERT INTO bet_on_event (user_id, event_id, bet)';
for(var i=0; i<eventIds.length; i++){
query+= ' SELECT ' + userId + ',' + eventIds[i] + ',' + bets[i]
+ ' FROM dual WHERE EXISTS (SELECT * FROM events WHERE id = ' + eventIds[i]
+ ' AND Status = 0)';
if(i < eventIds.length - 1){
query += ' UNION ALL';
}
}
Where eventIds and bets are in a corresponding order (like a map)
EDIT 2
I also wanted to UPDATE the bets which already exist (in case the user wanted to...).
So there's a need to update each row with the relevant bet in the array. This is the solution:
ON DUPLICATE KEY UPDATE bet=VALUES(bet)
Just added (concatenated) to the end of the query...
Does this work for you? It inserts 1,5,1 if there is no event for id 5 that has started. Same for 1,6,1 and id 6.
INSERT INTO bet_on_event (user_id, event_id, bet)
SELECT 1,5,1 FROM dual WHERE NOT EXISTS
(SELECT * FROM events WHERE _id = 5 AND Status <> 0)
UNION ALL
SELECT 1,6,1 FROM dual WHERE NOT EXISTS
(SELECT * FROM events WHERE _id = 6 AND Status <> 0);
EDIT: If you don't want to insert anything in case one or more of the games have started, you can simply replace WHERE _id = 5 and WHERE _id = 6 with WHERE _id IN (5,6). Or have just one exists clause:
INSERT INTO bet_on_event (user_id, event_id, bet)
SELECT *
FROM
(
SELECT 1,5,1 FROM dual
UNION ALL
SELECT 1,6,1 FROM dual
) tmp
WHERE NOT EXISTS (SELECT * FROM events WHERE _id IN (5,6) AND Status <> 0);
have you tried with UNION ?
INSERT INTO bet_on_event (user_id, event_id, bet)
(SELECT 1,5,1 FROM dual
WHERE (SELECT COUNT(*) FROM events WHERE _id IN(5,6) AND status=0) = ?
UNION
SELECT 1,6,2 FROM dual
WHERE (SELECT COUNT(*) FROM events WHERE _id IN(5,6) AND status=0) = ? );
Related
I have 2 queries:
SELECT COUNT(*) FROM a WHERE id = 1
//if row == 1
INSERT INTO a VALUES(fielda) VALUES('value')
Is there a way to merge these two queries into one? I tried with 'IF (count> 0, ..)' and similar, but the query is incorrect.
This involves inserting a new record into the table, taking care not to exceed a pre-set number of records for each field.
In theory it should be similar to an INSERT IF ...
Edit:
#Barmar I tried but I think I did not understand what you wrote (in fact I made a mistake in the query), I try to answer like this:
THE QUERY AFTER YOUR RESPONSE:
INSERT INTO table1 SELECT MAX(id) FROM table1 WHERE field1 = (SELECT id from a WHERE f = field2) HAVING COUNT(*) = 1 (all fields request) VALUES (all values request)
//field1 = id from table2
//field2 = id from another table: associative value
//ORIGINAL QUERY
//FIRST COUNT:
SELECT COUNT(*) from table1 WHERE field1 = (SELECT id FROM table2 WHERE f = field2)
//AFTER THE INSERT:
INSERT INTO table1 (all fields request) VALUES (all values request)
I came to mind this example I try to show you:
TABLE PLAYER: fields(ID, TEAMID, NAME) => (id=int, teamid=int associate to table team, name=varchar)
TABLE TEAM: fields(ID NAME) => (id=int, name=varchar)
Suppose that the players in a team are maximum 20, so you expect maximum 20 records associated by the player table for the same teamid value, or at least this is what we humans think, because for the computer can also be infinite. I was looking for a way to allow the insertion only in the case in which it is actually permissible to insert records, in this case the condition is that in the players table there are less than 21 records per team.
You can use INSERT ... SELECT, and put the condition in the SELECT query.
INSERT INTO player (teamid, name)
SELECT #teamid, #playername
FROM DUAL
WHERE (SELECT COUNT(*) FROM player
WHERE teamid = #teamid) < 20
DUAL is a dummy table when you need to select literal data, but need to include other clauses in the query.
First question ever on stack, I really couldn't find the answer elsewhere
I am making a ticket reservation system and I would like one query to create a ticketsorder if the total of ordered tickets after this call would be equal or smaller than the maxtickets value of tickettype and the maxticketstype of the event.
I have 3 tables
events with columns id, maxtickets
tickettypes with columns id, event_id, maxtickets
ticketsorders with columns id, tickettype_id, amount (amount is number of tickets)
The reason I want to do it in 1 call is so that there can never be more tickets ordered than the specified maximum on the ticketype or the event. If I would get the two sums in php and than write if my php calculations are okay there will be time between the getting of sums and writing the new value and possibly there will be more tickets ordered than allowed.
Maybe I'm looking for the wrong solution and maybe I should do more homework, any direction is appreciated, a fully working query would be most awesome :)
I mentioned laravel because it uses eloquent databases queries but I can use raw SQL when I want so no problem if the solution is raw SQL.
Thank you in advance!
Update: (Got it working with help from mentioned question)
INSERT into ticketsorders (order_id, tickettype_id, event_id, amount)
SELECT '7', '23', '1', '10'
FROM dual
WHERE ((SELECT COALESCE(SUM(amount), 0) FROM ticketsorders WHERE tickettype_id = 23) + '10' ) <= (SELECT maxtickets from tickettypes where id = '23')
AND ((SELECT COALESCE(SUM(amount), 0) FROM ticketsorders WHERE event_id = 1) + '10' ) <= (SELECT maxtickets from events where id = '1')
LIMIT 1
only uglieness I had to add was event_id to ticketsorders, we learned in school that you shouldnt do this because its available throug relation event->tickettype->ticketsorder but for now this makes quering for event objects alot easier.
Final update concerning laravel:
after adding in the vars and wrapping the sql in a raw statement like this:
$sql = "INSERT into ticketsorders (order_id, tickettype_id, event_id, amount) SELECT $order->id, $tickettype->id, $eventid, $val FROM dual WHERE ((SELECT COALESCE(SUM(amount), 0) FROM ticketsorders WHERE tickettype_id = $tickettype->id) + $val ) <= (SELECT max from tickettypes where id = $tickettype->id) AND ((SELECT COALESCE(SUM(amount), 0) FROM ticketsorders WHERE event_id = $eventid) + $val ) <= (SELECT max from events where id = $eventid) LIMIT 1";
$testupdate = DB::insert(DB::raw("$sql"));
I found out that the query always returns 1 which means true, even if the insert doesn't go through because the query still ran successfully. Rather than breaking my head on this issue trying to get the actual result I decided to do a new select and see if it succeeded and base my return message to the user on the intended insert compared to the select.
Thank you for the comments they helped a lot
I'm trying to write a query like
if (select count(*) from Users where fkId=5000 and status='r') =
(select count(*) from Users where fkId=5000) then ..
in just one query.
What this means is, if all the rows that have fkId=5000 also have status=r, then do something.
There can be any number of rows with fkId=5000, and any fraction of those rows could have status=r, status=k, status=l, status=a etc. I'm interested in the case where ALL the rows that have fkId=5000 also have status=r (and not any other status).
The way I'm doing it now is
how many rows with id=5000 and status = 'r'?
how many rows with id=5000?
are those numbers equal? then ..
I'm trying to figure out how to rewrite this query using only 1 query, instead of 2. Keyword ALL didn't seem to be able to write such a query (<> ALL is equivalent to NOT IN). I tried a couple of GROUP BY formulations but could not get the correct result to appear.
The most efficient way to do this is:
if not exists (select 1
from users
where fkid = 5000 and (status <> 'r' or status is null)
)
This will stop the query at the first non-matching row.
I suggest you to check for any rows with status not equal to 'r'
SELECT count(*)>0 FROM Users WHERE fkId = 5000 AND status != 'r'
In the following case, if the number 1 is "true" (which it is) then you'll get Yes back, and if not you'll get No back:
SELECT IF(1, 'Yes', 'No') AS yesorno
(Go ahead -- try it!)
In your case however, the following would be more appropriate:
SELECT IF (
(SELECT COUNT(*) FROM Users WHERE fkId=5000 AND status IN('r') AND status NOT IN('1', 'a', 'k')) = (SELECT COUNT(*) FROM Users WHERE fkId=5000),
'They are equal.',
'They are not equal.'
)
AS are_they_equal
By adding AS, you can manipulate the name of the "column" that's returned to you.
Hope that helps... Also, see this page if you'd like more info.
:)
EASY!
Simply join back to the same table. Here is the complete code for testing:
CREATE TABLE Users(id int NOT NULL AUTO_INCREMENT, fkID int NOT NULL, status char(1), PRIMARY KEY (id));
INSERT Users (fkID, status) VALUES (5000, 'r');
INSERT Users (fkID, status) VALUES (5000, 'r');
INSERT Users (fkID, status) VALUES (5000, 'r');
-- The next query produces "0" to indicate no miss-matches
SELECT COUNT(*) FROM Users u1 LEFT JOIN Users u2 ON u1.id=u2.id AND u2.status='r' WHERE u1.fkID=5000 AND u2.id IS NULL;
-- now change one record to create a miss-match
UPDATE Users SET status='l' WHERE id=3 ;
-- The next query produces "1" to indicate 1 miss-match
SELECT COUNT(*) FROM Users u1 LEFT JOIN Users u2 ON u1.id=u2.id AND u2.status='r' WHERE u1.fkID=5000 AND u2.id IS NULL;
DROP TABLE Users;
So all you need to test for in the result is that it's 0 (zero) meaning everything has fkID=5000 also has status='r'
If you properly index your table then joining back to the same table is not an issue and certainly beats having to do a 2nd query.
Besides the NOT EXISTS version - which should be the most efficient as it does no counting at all and exits as soon as it finds a value that doesn't match the conditions, there is one more way, that will work if status is not nullable and will be efficient if there is an index on (fkId, status):
IF EXISTS
( SELECT 1
FROM Users
WHERE fkId = 5000
HAVING MIN(status) = 'r'
AND MAX(status) = 'r'
)
There is one difference though. The above will show false if there are no rows at all with fkId=5000, while the NOT EXISTS version will show true - which is probably what you want anyway.
I have a list of events that have a date. I'm trying to count how many events take place in the current year, and 5 years on either side (regardless of whether any events took place) in mySQL using simple joins, selects, etc (no subqueries) in a single statement.
I have a table that produces the years and the number of events in that year, but am having problems when the year has no events taking place
Look into date functions on mysql http://dev.mysql.com/doc/refman/5.1/en/date-and-time-functions.html#function_datediff
You can use datediff which will give you difference in days. Ex;
WHERE abs(datediff(now(), event_date)) < 365*5
or dateadd(), if your event dates are timestamps, use timestampdiff()
Sample query
SELECT count(*) FROM mytable
WHERE abs(datediff(now(), event_date)) < 365*5
UPDATE
based on some of the comments I've read here, here's a query for you
SELECT year(event_date) as event_year, count(event_date)
FROM mytable
WHERE
abs(datediff(now(), event_date)) < 365*5
GROUP by year(event_date)
Feel free to adjust 5 in (365 * 5) for different range
UPDATE 2
This is NOT very pretty but you can try this with pure mysql. You can also modify this to be a stored proc if necessary:
SET #y6 = year(now());
SET #y5 = #y6-1;
SET #y4 = #y5-1;
SET #y3 = #y4-1;
SET #y2 = #y3-1;
SET #y1 = #y2-1;
SET #y7 = #y6+1;
SET #y8 = #y7+1;
SET #y9 = #y8+1;
SET #y10 = #y9+1;
SET #y11 = #y10+1;
CREATE TEMPORARY TABLE event_years (event_year int not null);
INSERT INTO event_years SELECT #y1;
INSERT INTO event_years SELECT #y2;
INSERT INTO event_years SELECT #y3;
INSERT INTO event_years SELECT #y4;
INSERT INTO event_years SELECT #y5;
INSERT INTO event_years SELECT #y6;
INSERT INTO event_years SELECT #y7;
INSERT INTO event_years SELECT #y8;
INSERT INTO event_years SELECT #y9;
INSERT INTO event_years SELECT #y10;
INSERT INTO event_years SELECT #y11;
SELECT ey.event_year , (SELECT count(event_date) from mytable where year(event_date) = ey.event_year)
from event_years ey;
temporary table will get dropped by itself after your connection is closed. If you add DROP TABLE after SELECT, you might not get your results back.
did you try to use join left?
MODIFIED:
SELECT tleft.YEARS, COUNT(tright.EVENTS)
FROM ONLY_YEARS tleft LEFT JOIN TABLE1 tright
ON (tleft.YEARS = tright.YEARS)
GROUP BY tleft.YEARS;
With that modification, you need to point to a table that holds all the years (ONLY_YEARS), maybe a dummy table with one column that goes from 1990 to 2020...
Left join optimization for MySQL link
To select a count of events that happened between two years, grouped by years, the following sql should suffice:
select year(event.date), count(*) from event where event.date >= '2006' and event.date <= '2016' group by year(event.date);
However, if no events occurred in a year, no result will be returned for it.
Databases are not really designed for such dynamic things and I'd suggest such logic should be put in a business (or possibly data-access) layer.
I have a table like this (MySQL 5.0.x, MyISAM):
response{id, title, status, ...} (status: 1 new, 3 multi)
I would like to update the status from new (status=1) to multi (status=3) of all the responses if at least 20 have the same title.
I have this one, but it does not work :
UPDATE response SET status = 3 WHERE status = 1 AND title IN (
SELECT title FROM (
SELECT DISTINCT(r.title) FROM response r WHERE EXISTS (
SELECT 1 FROM response spam WHERE spam.title = r.title LIMIT 20, 1)
)
as u)
Please note:
I do the nested select to avoid the famous You can't specify target table 'response' for update in FROM clause
I cannot use GROUP BY for performance reasons. The query cost with a solution using LIMIT is way better (but it is less readable).
EDIT:
It is possible to do SELECT FROM an UPDATE target in MySQL. See solution here
The issue is on the data selected which is totaly wrong.
The only solution I found which works is with a GROUP BY:
UPDATE response SET status = 3
WHERE status = 1 AND title IN (SELECT title
FROM (SELECT title
FROM response
GROUP BY title
HAVING COUNT(1) >= 20)
as derived_response)
Thanks for your help! :)
MySQL doesn't like it when you try to UPDATE and SELECT from the same table in one query. It has to do with locking priorities, etc.
Here's how I would solve this problem:
SELECT CONCAT('UPDATE response SET status = 3 ',
'WHERE status = 1 AND title = ', QUOTE(title), ';') AS sql
FROM response
GROUP BY title
HAVING COUNT(*) >= 20;
This query produces a series of UPDATE statements, with the quoted titles that deserve to be updated embedded. Capture the result and run it as an SQL script.
I understand that GROUP BY in MySQL often incurs a temporary table, and this can be costly. But is that a deal-breaker? How frequently do you need to run this query? Besides, any other solutions are likely to require a temporary table too.
I can think of one way to solve this problem without using GROUP BY:
CREATE TEMPORARY TABLE titlecount (c INTEGER, title VARCHAR(100) PRIMARY KEY);
INSERT INTO titlecount (c, title)
SELECT 1, title FROM response
ON DUPLICATE KEY UPDATE c = c+1;
UPDATE response JOIN titlecount USING (title)
SET response.status = 3
WHERE response.status = 1 AND titlecount.c >= 20;
But this also uses a temporary table, which is why you try to avoid using GROUP BY in the first place.
I would write something straightforward like below
UPDATE `response`, (
SELECT title, count(title) as count from `response`
WHERE status = 1
GROUP BY title
) AS tmp
SET response.status = 3
WHERE status = 1 AND response.title = tmp.title AND count >= 20;
Is using GROUP BY really that slow ? The solution you tried to implement looks like requesting again and again on the same table and should be way slower than using GROUP BY if it worked.
This is a funny peculiarity with MySQL - I can't think of a way to do it in a single statement (GROUP BY or no GROUP BY).
You could select the appropriate response rows into a temporary table first then do the update by selecting from that temp table.
you'll have to use a temporary table:
create temporary table r_update (title varchar(10));
insert r_update
select title
from response
group
by title
having count(*) < 20;
update response r
left outer
join r_update ru
on ru.title = r.title
set status = case when ru.title is null then 3 else 1;