MySQL INSERT IF (custom if statements) - mysql

First, here's the concise summary of the question:
Is it possible to run an INSERT statement conditionally?
Something akin to this:
IF(expression) INSERT...
Now, I know I can do this with a stored procedure.
My question is: can I do this in my query?
Now, why would I want to do that?
Let's assume we have the following 2 tables:
products: id, qty_on_hand
orders: id, product_id, qty
Now, let's say an order for 20 Voodoo Dolls (product id 2) comes in.
We first check if there's enough Quantity On Hand:
SELECT IF(
( SELECT SUM(qty) FROM orders WHERE product_id = 2 ) + 20
<=
( SELECT qty_on_hand FROM products WHERE id = 2)
, 'true', 'false');
Then, if it evaluates to true, we run an INSERT query.
So far so good.
However, there's a problem with concurrency.
If 2 orders come in at the exact same time, they might both read the quantity-on-hand before any one of them has entered the order.
They'll then both place the order, thus exceeding the qty_on_hand.
So, back to the root of the question:
Is it possible to run an INSERT statement conditionally, so that we can combine both these queries into one?
I searched around a lot, and the only type of conditional INSERT statement that I could find was ON DUPLICATE KEY, which obviously does not apply here.

INSERT INTO TABLE
SELECT value_for_column1, value_for_column2, ...
FROM wherever
WHERE your_special_condition
If no rows are returned from the select (because your special condition is false) no insert happens.
Using your schema from question (assuming your id column is auto_increment):
insert into orders (product_id, qty)
select 2, 20
where (SELECT qty_on_hand FROM products WHERE id = 2) > 20;
This will insert no rows if there's not enough stock on hand, otherwise it will create the order row.
Nice idea btw!

Try:
INSERT INTO orders(product_id, qty)
SELECT 2, 20 FROM products WHERE id = 2 AND qty_on_hand >= 20
If a product with id equal to 2 exists and the qty_on_hand is greater or equal to 20 for this product, then an insert will occur with the values product_id = 2, and qty = 20. Otherwise, no insert will occur.
Note: If your product ids are note unique, you might want to add a LIMIT clause at the end of the SELECT statement.

Not sure about concurrency, you'll need to read up on locking in mysql, but this will let you be sure that you only take 20 items if 20 items are available:
update products
set qty_on_hand = qty_on_hand - 20
where qty_on_hand >= 20
and id=2
You can then check how many rows were affected. If none were affected, you did not have enough stock. If 1 row was affected, you have effectively consumed the stock.

You're probably solving the problem the wrong way.
If you're afraid two read-operations will occur at the same time and thus one will work with stale data, the solution is to use locks or transactions.
Have the query do this:
lock table for read
read table
update table
release lock

I wanted to insert into a table using values so I found this solution to insert the values using the IF condition
DELIMITER $$
CREATE PROCEDURE insertIssue()
BEGIN
IF (1 NOT IN (select I.issue_number from issue as I where I.series_id = 1)) THEN
INSERT IGNORE INTO issue ( issue_number, month_published, year_published, series_id, mcs_issue_id) VALUES (1, 1, 1990, 1, 1);
END IF;
END$$
DELIMITER ;
If you later on want to call the procedure it's as simple as
CALL insertIssue()
You can find more information about PROCEDURES and if conditions in this site

Related

MySQL INSERT ... SELECT ... never ends, CPU at max

I need to delete duplicates from a table while keeping one item. Since I cannot delete from a table while accessing this very same table in a subquery inside the where-statement, i decided to store my affected IDs in a temporary table:
create temporary my_temp_table (
id int not null
) engine memory;
and then insert IDs using a select:
insert into my_temp_table
-- select query works
select
id
from
-- innodb
table_with_duplicates
where
hash_code in (
select
hash_code
from
table_with_duplicates
group by
hash_code
having
count(id) > 1
)
and date_created < '2015-01-01'
;
Later I want to use these IDs to delete them:
delete from table_with_duplicates
where id in (
select id from my_temp_table
)
;
Just executing the select part of the insert statement works fine. Adding the insert part, however, causes 1 CPU core to go up to 100% and the query seems to never end. Nothing is inserted.
On my dev environment, table_with_duplicates contains about 20000 rows with 1 duplicate.
Any ideas?
EDIT:
Thank you for your answers. I have tried a select distinct(... approach, which didn't help much. Maybe I've used in the wrong place/subselect. Played around with it quite a lot already. To clarify, I have something like this:
ID date_created hash_code
1 2013-06-06 ABCDEFGH <-- delete this one
2 2013-08-08 HGFEDCBA
3 2015-11-11 ABCDEFGH <-- keep this one
Well, I chose different approach to solve this problem. I've written a small PHP command-line script that fetches all my IDs into an array and then executes a DELETE, using all IDs as ....
"delete from table_with_duplicates where id in (".explode(',' $arrIDs).")";
Having about 9000 affected rows and this (hopefully) being a one-time action, this worked for me.
I also played around with
SET SESSION optimizer_search_depth = 1;
but had no luck either.

Add a row if there are less than 5, otherwise replace the lowest number in MySQL

I'm pretty stuck on a Mysql query.
I have a table with three columns;
user_id | person_id | score.
The table is going to be used to store top 5 highscores for each person.
I need at query that checks if there is less than five rows for a specific person.
Is there is less, insert new row. But if there is five rows I have to replace the lowest score with the new one.
It is for a webservice written in PHP and the data about the new score is posted to the method as params.
Been stuck for some hours now — is it even possible to make this happen in one query ?
You can use stored procedure in mysql. I dont know the names of the tables but if you look closer you will understand how it works.
DELIMITER $$
DROP PROCEDURE IF EXISTS test $$
CREATE PROCEDURE test( IN testparam VARCHAR(22) )
BEGIN
DECLARE count INT(11);
SET count = (SELECT COUNT(*) FROM persons );
IF count < 5 THEN
insert into table_needed_for_insert values(testparam);
ELSE
update table_needed_for_insert where score=(select min(score) from table_needed_for_insert);
END IF;
select * from table_needed_for_insert
END $$
DELIMITER;
And how to execute this thing CALL test(1); 1 is the parameter, you can create as many as you need.
And from php you can call directly as like
$result = mysql_query("call test(".$param.")");
And here you can check a tutorial on mysql stored procedures:
http://www.mysqltutorial.org/mysql-stored-procedure-tutorial.aspx
It might be possible if you have a unique key which identifies the lowest score. Then you could use the
INSERT ... ON DUPLICATE KEY construct. But you would have to install a trigger which keeps explicit track of the lowest score.
I would propose this scenario (I have not tried it, it is just an idea):
as I understand, you only need 5 ids. you can run a subqueries like these
SELECT MAX(id) AS last_id FROM table
SELECT MIN(score), id AS lowest_id FROM table
then
insert or replace into table (id, ...) values ( MIN(last_id+1, lowest_id), ... )
there are possible mistakes and also only one subquery is possible, but I hope you get the main idea
The simplest way imo is to insert data,
INSERT INTO top_scores (user_id, person_id, score_id) VALUES (1,2,3)
then delete inappropriate rows
DELETE top_scores FROM top_scores
INNER JOIN
(SELECT * FROM top_scores WHERE person_id = 2 ORDER BY score ASC LIMIT 5, 1000000) AS inappropriate_rows
USING (user_id, person_id, score)

MySQL INSERT Using Subquery with COUNT() on the Same Table

I'm having trouble getting an INSERT query to execute properly, and I can't seem to find anything on Google or Stack Overflow that solves this particular issue.
I'm trying to create a simple table for featured entries, where the entry_id is saved to the table along with it's current order.
My desired output is this:
If the featured table currently has these three entries:
featured_id entry_id featured_order
1 27 0
2 54 1
4 23 2
I want the next entry to save with featured_order=3.
I'm trying to get the following query to work with no luck:
INSERT INTO `featured`
(
`entry_id`, `featured_order`
)
VALUES
(
200,
(SELECT COUNT(*) AS `the_count` FROM `featured`)
)
The error I'm getting is: You can't specify target table 'featured' for update in FROM clause.
Can anyone help with a solution that gets the count without causing an error?
Thanks in advance!
Here is a cool thing: MySQL's INSERT . . . SELECT:
INSERT INTO `featured`
(
`entry_id`, `featured_order`
)
SELECT 200, COUNT(*) + 1
FROM `featured`
No subquery required.
#Bohemian has a good point:
Better to use max(featured_order) + 1 if you use this approach
So a better query would probably be:
INSERT INTO `featured`
(
`entry_id`, `featured_order`
)
SELECT 200, MAX(`featured_order`) + 1
FROM `featured`
His trigger method describe in his answer is also a good way to accomplish what you want.
The potential problem with query 1 is if you ever delete a row the rank will be thrown off, and you'll have a duplicate in featured_order. With the second query this is not a problem, but you will have gaps, just as if you were using an auto-increment column.
If you absolutely must have an order with no gaps the best solution I know of is to run this series of queries:
SET #pos:=0;
DROP TABLE IF EXISTS temp1;
CREATE TEMPORARY TABLE temp1 LIKE featured;
ALTER TABLE featured ORDER BY featured_order ASC;
INSERT INTO temp1 (featured_id, entry_id, featured_order)
SELECT featured_id, entry_id, #pos:=#pos+1 FROM words;
UPDATE featured
JOIN temp1 ON featured.featured_id = temp1.featured_id
SET featured.rank = temp1.rank;
DROP TABLE temp1;
Whenever you delete a row
Use a trigger:
drop trigger if exists featured_insert_trigger;
delimiter //
create trigger featured_insert_trigger before insert on featured
for each row
begin
set new.featured_order = ifnull((select max(featured_order) from featured), -1) + 1;
end; //
delimiter ;
Now your inserts look like this:
insert into featured (entry_id) values (200);
featured_order will be set to the highest featured_order value plus one. This caters for rows being deleted/updated and always guarantee uniqueness.
The ifnull is there in case there are no rows in the table, in which case the first value will be zero.
This code has been tested as works correctly.
INSERT INTO `featured`
(
`entry_id`, `featured_order`
)
VALUES
(
200,
(SELECT COUNT(*) AS `the_count` FROM `featured` F1)
)
Correction is just adding "F1" table alias.
This standard sql solution works fine on various dbms (not only mysql)
I also suggest an improvement over:
SELECT COUNT(*) +1 (Problem: if some row gets deleted you may collide with existing index)
SELECT MAX(featured_order)+1 (Problem: the first insert with empty table gets error)
SELECT (COALESCE(MAX(featured_order), 0)+1) (no Problem)
You have to simpley use alias that will solve the problem :
INSERT INTO `featured`
(
`entry_id`, `featured_order`
)
VALUES
(
200,
(SELECT COUNT(*) AS `the_count` FROM `featured` as f1)
)
From the MySQL manual regarding subqueries:
Another restriction is that currently you cannot modify a table and select from the same table in a subquery.
Perhaps an alias or a join (otherwise useless) in the subquery would help here.
EDIT: It turns out that there's a work-around. The work-around is described http://www.xaprb.com/blog/2006/06/23/how-to-select-from-an-update-target-in-mysql/.

MySQL Problem with inserting a row with certain conditions

I have a problem inserting a row in a MySQL table when the table is completely empty.
I use this query :
INSERT IGNORE INTO test (id, amount)
SELECT 6, 50 FROM test WHERE NOT EXISTS
(SELECT 1 FROM test WHERE amount >= 50 AND id = 6) LIMIT 1
It works fine when there is at least one entry in the table, whatever the data in the columns are. It doesn't work if the table is completely empty.
Basically, I want to insert a row if a row with the same ID and an amount equals or higher doesn't exists.
I tried with a COUNT also, still the same problem. Is there another way of doing this?
I think the only thing wrong with this is on line two, remove FROM test.. You can't select 6, 50 from test.. 6 and 50 are not columns in test, and test has no records. Try it like this:
INSERT IGNORE INTO test (id, amount)
SELECT * from (select 6, 50) as a
WHERE NOT EXISTS (SELECT 1 FROM test
WHERE amount >= 50 AND id = 6)

MySQL outputting multiple rows if QTY > 1

Is there any way to output multiple table rows if a certain field in the table is greater than 1.
Here's my example:
I'm building an auction website, where we sell tickets for a raffle.
The tickets are stored in a table like so:
id, order_id, product_id, qty, price
When the time comes to print the tickets, I want to dump all of it into a CSV.
So far, I'm doing this query (simplifying, omitting INNER JOIN):
SELECT id, order_id, product_id, qty, price FROM order_details
And then running something like the following loop on it:
foreach($rows as $row) {
for($i = 0; $i < $row['qty']; $i++) {
$tickets[] = $row;
}
}
so that I get a separate entry for each qty (so that people get the correct amount of entries...).
Is there any way to accomplish this in SQL itself, so that each row is multiplied x times, where x is a certain field in the table (qty in this example)?
You can accomplish this purely in MySQL using a blackhole table and a trigger
Set up tables
First create the blackhole table you're going to insert to and the memory (or temporary table) the blackhole will reroute to.
CREATE TABLE Blackhole1 LIKE order_details ENGINE = BLACKHOLE;
CREATE TABLE temp_order_results LIKE order_details ENGINE = MEMORY;
Set up trigger
Now create a trigger on the blackhole table that will reroute the insert to the memory table, duplicating the rows with qty > 1.
DELIMITER $$
CREATE TRIGGER ai_Blackhole1_each AFTER INSERT ON blackhole1 FOR EACH ROW
BEGIN
DECLARE ACount INTEGER;
SET ACount = new.qty;
WHILE ACount > 1 DO BEGIN
INSERT INTO temp_order_results
VALUES (new.id, new.order_id, new.product_id, 1, new.price)
SET ACount = ACount - 1;
END; END WHILE;
END $$
DELIMITER ;
Statements to do the query
Now do a insert .. select into the blackhole
INSERT INTO blackhole1
SELECT id, order_id, product_id, qty, price FROM order_details;
And a select on temp_order_results.
SELECT id, order_id, product_id, qty, price FROM order_details;
To expand on #zdennis' answer, you could do this in MySQL:
SELECT order_details.*
FROM order_details
INNER JOIN kilo
ON kilo.i < order_details.qty;
where the "kilo" relation has the integers 0 - 999, a contrivance adapted from a post by xaprb:
CREATE TABLE deca (i integer not null);
INSERT INTO deca (i) VALUES (0), (1), (2), (3), (4), (5), (6), (7), (8), (9);
CREATE VIEW kilo (i) AS
SELECT iii.i * 100 + ii.i * 10 + i.i
FROM deca iii
CROSS JOIN deca ii
CROSS JOIN deca i;
There's not really a performance reason to. MySQL has a couple of strong suits: sorting, indexing, searching, storing, etc. You might as well do this in PHP.
The appropriate response is likely to use dual connect by level. See this question for related information: How can I return multiple identical rows based on a quantity field in the row itself?
Although this doesn't work in MySQL, see: How do I make a row generator in MySQL?
If you're using MySQL you'll need to be content with doing it in PHP or doing something gross (like the trigger that Johan posted). I'd vote to simply do it in PHP if that was the case.
I think this might be possible in Sql Server or Oracle by using a recursive common table expression (CTE) that joins the original table to itself and includes Qty-1 as an expression in place of Qty in the select list of the CTE. Sadly, last I heard MySql doesn't support CTEs yet.
Another option is to build a simple sequence table that just includes a numeric column and rows that start with 1 and end with the largest number you'll realistically have in the Qty column of your original table. You can join this to your orders table with a WHERE clause limiting the digits results to less than the Qty field and duplicate the rows this way. To quickly build the sequence table, create a digits table with records for 0 through 9 and cross join it to itself once for each power of 10.
I was required to do the same thing in order to avoid a cursor. My solution is for SQL Server and is really simple because for my case, qty is never greater than 99, so here is a sample using temporary tables:
create table #t (
id int
,qty int
)
insert into #t values (1,2)
insert into #t values (2,3)
create table #n (
id int
)
insert into #n values (1)
insert into #n values (2)
insert into #n values (3)
insert into #n values (4)
insert into #n values (5)
select t.*
from #t t
inner join #n n on
n.id <= t.qty
You just need to insert into #n the max qty you expect (in my case 99).