SQL to INSERT-DELETE disjoint rows into MySQL? - mysql

I have a food_table and a person_table. Then I have a third table fav_food_table that stores the relation between food and person using food_id and person_id.
When the person goes to account info and updates his favourite food, the input data is passed to the PHP (HTTP) script as an array of selected food_id. A person can have multiple fav_food, the relation is one-to-many.
The naïve way to update fav_food_table is to delete from fav_food_table all that belongs to person_id then re-insert all the rows again. Thus, using 2 statements.
Is there a single statement that can do the same thing?
PSUEDO CODE:
CREATE TABLE food_table (food_id, food_name);
CREATE TABLE person_table (person_id, person_name);
CREATE TABLE fav_food (person_id, food_id);

You have a m:n relationship between person_table and food_table. This means you have multiple records in your relationship table related to the same person. When a person updates their favourite foods, any combination of four independent cases can occur:
Food A was favourite before, but is not anymore (DELETE FROM fav_food_table)
Food B was favourite before and still is! (do nothing)
Food C was no favourite before, but now is a favourite (INSERT INTO fav_food_table)
Food D was no favourite before, and still is no favourite! (do nothing)
To correctly keep your database up to date, you have to handle all four cases. Cases 2 and 4 are covered easily. Just don't do anything :)
That means, you have to do at least two steps to keep your database up to date: 1 and 3.
Your goal seems to be to reduce the number of sql statements, that have to be executed.
Deleting all favourites for one person from the table can always be done with a single statement:
DELETE
FROM fav_food_table
WHERE person_id = ?;
To delete selected favourites for one person, only an AND food_id IN (?,?,?) has to be added to the WHERE clause.
Inserting into the table can also be done with a single statement:
INSERT INTO fav_food_table (person_id,food_id)
VALUES (?,?),
(?,?),
(?,?),
.....;
Summary as of right now:
No matter, whether we delete all old records for this person and then insert all new records, or whether we only delete selected records and insert new ones: We can do it with two statements!
In the second case (the "smart" case), however, we need to know not only the new state of the relation, but also the old state to compute the difference between the two. This will result in either one more SELECT statement or needs some smart "client" (PHP) logic.
Your two step process doesn't seem to be naive, but easy and effective to me. There is no way to reduce this process to less than two statements.
If you want to reduce the number of times you have to effectively send commands from PHP to the server, you can either look into mysqli_multi_query() or into creating a stored procedure which holds both your DELETE and INSERT statement. But bottom line, this will be the same thing as executing the two queries on its own.
You can also look into MySQL Transactions to implement a safer process and be able to rollback your DELETE command should an error occur later on.

May I suggest something different. Instead of deleting, updating?
Deleting data in common is something that should be thought off. Deleting data cannot be retrieved. So check this out.
We are adjusting your code a bit.
CREATE TABLE food_table (food_id, food_name);
CREATE TABLE person_table (person_id, person_name);
CREATE TABLE fav_food (person_id, food_id, fav_food_active);
/* see the last column i added. It should be a bit type and can only hold the values 0 and 1. */
Now you technically can update this everytime. No need for deleting the values. This statement is
/* deletion */
UPDATE Fav_Food
SET fav_food_active = 0
WHERE food_id = (your food_id)
AND person_id = (your person_id)
/* activating it again */
UPDATE Fav_Food
SET fav_food_active = 1
WHERE food_id = (your food_id)
AND person_id = (your person_id)
So now you switch between those 2 for activating and deleting it, without having the consequences of deleting hard data. Overal, you can just call it like this in your code
SELECT *
FROM fav_food
WHERE (here your where clause on which you wanna search for)
AND fav_food_active = 1
Remember when you enter something in the database, you should always add it as 1. You can do that in PHP myadmin as auto value, or hard code it in your INSERT statement.
I am not sure what your backend code is (okay taking that back, its PHP, i read your post) looking at your question you shooting everything through an array, but try to work in some checks that foreach fav_food entry, check first in the db if the query excists. If it does, update it, if not, insert it. and let that run in a loop.
So something like
foreach ($food_id as $value){
// check here the overal statement if it excists in your db
// give back count
if ($count == 1){
// update query to delete it.
}else{
// create your insert query here
}
}
Hope this helps. Happy coding!

Related

Methods for conditional insertion into tables (MariaDB)

This question deals with how one should handle conditional insertions into tables.
Suppose we customers and employees.
A customer can only be assigned 4 employees at a time.
We will come back to this in a moment.
On the database level, we have checks and triggers.
In MariaDB, CHECK constrains cannot have subqueries, so we cannot impose constraints
regarding degree of participation in this way. For instance, we cannot say something
like
CHECK ( customer_id IN (SUBQUERY that returns count of Employees with == 4 employees) ).
Triggers may be the solution.
insert-or-update). If a manager attempts to assign a customer to an employee who already has 4 customers, we should not allow that record to be inserted into the table that links customers to their employees. In this case we want a trigger that acts before insertion. We only want the
insertion to occur if that employee is not in the subquery that lists
employees with 4 customers.
We want to stop the insertion, but by law, the trigger will not do this
based on a condition that is merely stated in the trigger. From my understanding, the only way to do this is to send a signal (look at Use a trigger to stop an insert or update
This leads to my next two questions.
Is using a signal 'ideal'? Is it problematic? Is there a better way to insert into a table
based on a condition, instead of merely performing side effects prior to
an insertion or perhaps after an insertion?
It appears that the db would send a signal if the constraint was violated to begin with, so would this ever impact the application built on top of it?
Some flavors of restraint can be dealt with thus:
INSERT INTO TABLE (a,b,c)
SELECT ...
WHERE <-- put the logic here (if possible)
That is, arrange to have the SELECT deliver more or fewer rows based on your business logic.

#1109 - Unknown table 'ConcertDetails' in field list

I was attempting to run a simple Insert into my CustomerOrders table and got the error: #1109 - Unknown table 'ConcertDetails' in field list
I did some searches on this and looked at about 7 different stack overflow posts on it but still not sure what is wrong. I also looked up information on triggers and there seems to be different syntax on different sites. The weird thing is this trigger used to work just fine, not sure what has been altered since 4 days ago.
I tried changing some things, for instance I removed my trigger and it let me insert, but when I put the trigger back I couldn't insert any more, so there must be something wrong with the trigger. This is what I have for the trigger:
DROP TRIGGER IF EXISTS `alterPurchasePrice`;
DELIMITER //
CREATE TRIGGER `alterPurchasePrice` BEFORE INSERT ON `CustomerOrders`
FOR EACH ROW BEGIN
IF new.DiscountCode = 'yes' THEN
SET new.PurchasePrice = ConcertDetails.Cost - 10;
END IF;
END
//
DELIMITER ;
The purpose of the trigger is to lower the price by $10 if the user types 'yes' into the DiscountCode field.
This involves the tables:
CustomerOrders: ConcertID, CustomerName, Discount Code, OrderID,
PurchasePrice ConcertDetails: ConcertDate, ConcertID, Cost
I think you need a SELECT to retrieve values from other tables.
To get the value of "cost" from "ConcertDetails" table, for a specific concert, we could write a query like this:
SELECT ConcertDetails.cost
FROM ConcertDetails
WHERE ConcertDetails.concertid = ?
assuming that "concertid" is the primary key (or unique key) of the ConcertDetails table, we would be guaranteed that the query would return at most one row.
To put that to use in the trigger, we should be able to do something like this
SET NEW.PurchasePrice
= ( SELECT d.cost - 10 AS discount_price
FROM ConcertDetails d
WHERE d.concertid = NEW.concertid
);
The value of the concertid column of the row to be inserted from the CustomerOrders table will be supplied for the query through the reference to NEW.concertid. The return from that query will be assigned to the purchaseprice column.
If the query returns more than one row (which could happen if we don't have any kind of guarantee that "concertid" is unique in the "ConcertDetails" table), the trigger will throw a "too many rows" error.
If there are no rows returned, we'd expect a NULL value to be assigned would be assigned to "purchaseprice". We would also get a NULL returned if the "cost" column is set to NULL.
Is there some sort of guarantee that "cost" will never be less than 10? If the returned Cost is 6, then the value assigned to "purchaseprice" would be -4. If we want the value assigned to "purchaseprice" to never be less than zero, we could do something like this:
SET NEW.purchaseprice
= ( SELECT GREATEST(d.cost-10,0) AS discount_price
FROM ConcertDetails d
WHERE d.concertid = NEW.concertid
);
Other notes:
I'd recommend a different naming convention for your triggers. With multiple tables and multiple triggers, it can get kind of confusing, when looking for triggers on CustomerOrders table, to remember that the name of the BEFORE INSERT trigger is "alterPurchasePrice". Especially if you (or someone else) is coming back to work on the system six months or six years from now.
The convention we follow for trigger names is to use the name of the table, followed by an underscore, followed by one of: bi, bu, bd, ai, au, ad (for Before/After Insert/Update/Delete). (Since MySQL doesn't allow more than one trigger for each of those, we don't get naming collisions. And it makes it easier to check whether a BEFORE INSERT trigger exists on a table, before someone writes a BEFORE INSERT trigger that does something else.)
I also mention, in regards to the use of CamelCase table names... the MySQL Reference Manual says this:
To avoid problems ... it is best to adopt a consistent convention, such as always creating and referring to databases and tables using lowercase names. This convention is recommended for maximum portability and ease of use.
https://dev.mysql.com/doc/refman/5.7/en/identifier-case-sensitivity.html
Then again, these are just conventions. We also follow a convention to name tables by naming what a single row represents. If we had a requirement to create this table, we would assign the name customer_order. And the BEFORE INSERT trigger on the table would be named customer_order_bi

MySQL on duplicate key delete

I am looking for a (not too convoluted) solution for a MySQL problem. Say I have the following table (with a joint index on group and item):
Group item
nogroup item_a
group_a item_a
Then, eventually, item_a no longer belongs to group_a. So I want to do something like:
update table set group = "nogroup" where item = "item_a" on duplicate key delete.
(obviously this is not a valid symtax but I am looking for a way around this)
I still want to keep a copy of the record with nogroup because, if later on, item_a comes back, i can change its group back to group_a or any other group depending on the case. Whenever item_a is added, there is an insert and it copies all the data from the nogroup record and sets a proper group label. At that point there are two records for item_a: one with group_a and one with no group. The reason it is done this way is to reuse previous data as much as possible as a new entry(with no previous record) is much more involved and take significantly more time and processing.
Say an item belongs to group_a and group_b but suddenly it does not belong to any group: the first update to set group to "nogroup" will work but the second update will create a duplicate key entry error.
The option of "not updating the group column at all" and using "insert on duplicate key update" does not work because there won't be duplicates when the groups are different and this will lead to cases where an item does not belong to a group anymore and yet the record will still be present in the database. The option of verifying if "nogroup" exists first and then updating it to a specific group does not work either because if item_a belongs to more than one group this would update all other records to the same group.
Basically, an item can belong to 1) any number of groups including "nogroup" or 2) solely belonging to "nogroup" and there should always be a copy of at least nogroup somewhere in the database.
It looks like I won't be able to do this in just one query but if someone has a clean way of dealing with this, that would be much appreciated. Maybe some of my assumptions above are wrong and there is an easy way to do it.
Your whole process of maintaining this items-to-groups mapping sounds too complicated. Why not just have a table that has a mapping? Then, when an item is removed from a group, delete it from the table. When it is added, add it to the table. Don't bother with "nogroup".
If you want an archive table, then create one. Have an insert/update/delete trigger (whichever is or are appropriate) that will populate an archive with information that you want to keep over time.
I do not understand why re-using an existing row would be beneficial in terms of performance. There is no obvious database reason why this would be the case.
I am also confused as to why you need a "nogroup" tag at all. If you need a list of items, maintain that list in its own table. And call the table Items -- a much clearer name than "nogroup".
I agree with Gordan's approach. However if you have to do it with a single table it cannot be done in 1 SQL query. You will have to use 2 queries 1 for update and 1 for delete.

Any way to merge delete/insert queries?

I have the following two queries being run when the assignment of staff on a team changes:
delete from `team_staff` where `team_id`=5
insert into `team_staff` (`team_id`,`staff_id`) values (5,1),(5,2)
In this case, I'm changing the staff roster of Team 5 to be Staff 1 and Staff 2. Anyone else who was assigned is now unassigned.
It works, but I like reducing the number of queries being run - it's good for the query cache and such.
Usually I would do something like this:
insert ignore into `team_staff` (`team_id`,`staff_id`) values (5,1),(5,2)
However this won't remove any staff who aren't Staff 1 or Staff 2.
Is there any way to do this in a single query, or am I stuck with two?
There isn't an insert & delete MySQL function, but that doesn't mean you're out of luck. Consider these options:
1) Your current method deletes without reference to whether your insert will re-insert something you've deleted, creating more work that doesn't really need to be done. Also, deleting all those rows all of the time will require some OPTIMIZE commands to be run periodically. You can optimize your delete command to ignore those you'll insert, but will still need to optimize periodically.
2) Since your teams are numbered, if there are only a few team numbers ever used, you may want to look at using a SET on the staff table. Then you will only need to execute an update command.
3) If SET is too small, you may want to look at one of the INT columns and use binary numbers &'d together. For example, team 1 is 2^1, team 2 is 2^2, ... Then when you update the INT column on the staff table for the team membership, you would update it to represent the value of the teams they were part of (e.g. UPDATE staff SET teams=2^1 & 2^2 & 2^3). This would allow you to search for matches using only numbers.
Those are the potential optimizations which come to my mind, though none of them directly answer your specific question, they do address the intent.

TSQL Use record currently being inserted to select data in another table

Very new to TSQL...
I have the following table called "tblinit":
Account_Num UserID Task_Percent
----------- ------------ ------------
1 john.smith 0.75
I would like to update the "Task Percent" value in "tblRaw" below.
Account_Num UserID Task_Percent
----------- ------------ ------------
1 john.smith 0.5
2 mary.mickle 0.9
3 don.donalds 1
My plan is to use a TSQL stored procedure executed by a trigger on insert into "tblinit". The stored procedure will move the data into "tblRaw" (either a merge or a delete and insert) and then truncate "tblinit" when the procedure is done. tblInit is only used to stage incoming data.
I have read about SCOPE_IDENTITY and ##IDENTIY but don't fully grasp the concept. Is the scope defined by the trigger which executes the stored procedure? In attempting my own SELECT statements using SCOPE_IDENTITY and ##IDENTITY I always return with a "NULL" result. The referenced MSDN article seems to return primary keys that don't correlate to the data specified in the article's example. Clearly I am reading something incorrectly. I want to grab the record that was just inserted and use it in my query.
In essence, how do I update john.smith's new percentage value automatically on insert or, alternatively, how do I add a new record entirely?
how do I update john.smith's new percentage value automatically on insert
This trigger could be used to do exactly that:
create trigger tblinit_to_tblRaw
on tblinit
for insert
as
begin
update r
set r.Task_Percent = i.Task_Percent
from inserted i
join tblRaw r on i.UserID = r.UserID -- Join on Account_Num instead?
end
This does not take into account new records (no existing match in tblRaw). For that you might want to run if exists(... or merge.
I must confess to being a bit confused as to your intent given the various concepts you've referred to.
If you want to delete from the original table after your trigger fires in the update/delete then you're doing it wrong.
If you just want to keep a running total in another table for performance reasons then check out "indexed views".
If you want to add something to one table then update another and remove from the original then you are either looking for a queue or simply a stored procedure to perform the update on the appropriate table. You do not need to do complex steps with triggers and stuff.
No idea where the IDENTITY stuff comes from. Pretty sure you don't need it here.
I think you're making it more complex than it needs be.
I could be wrong - feel free to elaborate.