I'm going crazy trying to get UPDATE to work with a CTE in MySQL.
Here's a simplified schema of sa_general_journal:
CREATE TABLE `sa_general_journal` (
`ID` int(10) unsigned NOT NULL AUTO_INCREMENT,
`Transaction_ID` int(10) unsigned DEFAULT NULL COMMENT 'NULL if not split, same as ID for split records',
`Date` timestamp NOT NULL DEFAULT current_timestamp(),
…
`Statement_s` int(10) unsigned DEFAULT NULL,
…
`Name` varchar(255) DEFAULT NULL,
…
PRIMARY KEY (`ID`),
…
) ENGINE=InnoDB AUTO_INCREMENT=25929 DEFAULT CHARSET=utf8;
Some records are "split," for example, a credit card statement amount might have a sales tax amount that is split out. In such cases, both parts of the split record have the same ID in the Transaction_ID field.
When records are imported in bulk, they can't refer to last_insert_ID in order to fill in the Transaction_ID field, thus the need to go clean these up afterward.
This was my first, naive attempt, which said I had an error near UPDATE. Well duh.
WITH cte AS (
SELECT
ID,
MIN(ID) OVER(PARTITION BY `Date`, `Name`, Statement_s) AS Trans,
Transaction_ID
FROM sa_general_journal
WHERE Transaction_ID = 0)
UPDATE cte
SET Transaction_ID = Trans
The CTE itself seems to work, as I can follow it with SELECT * FROM cte and get what I expected.
So I started searching StackOverflow, and discovered that CTEs are not updatable, but that you need to join them to what you want to update. "No problem!" I think, as I code this up:
WITH cte AS (
SELECT
ID,
MIN(ID) OVER(PARTITION BY `Date`, `Name`, Statement_s) AS Trans,
Transaction_ID
FROM sa_general_journal
WHERE Transaction_ID = 0)
UPDATE sa_general_journal gj, cte
SET gj.Transaction_ID = cte.Trans
WHERE gj.ID = cte.ID
No joy. Same error message.
My understanding is that in MySQL, you don't need a column list, but I did also try this using the column list (a, b, c), with the proper columns referenced in the UPDATE statement, but it still said I had a problem near UPDATE.
There are incredibly few examples of using UPDATE with WITH on the Internet! I found one, from Percona, which I used to create my attempt above, and then found another very similar example from MySQL itself.
Thanks in advance for any help offered!
CTE is a part of subquery definition, not a part of the whole query. The query must be specified after CTE. CTE cannot be used itself. So
UPDATE sa_general_journal gj
JOIN (WITH cte AS ( SELECT
ID,
MIN(ID) OVER(PARTITION BY `Date`, `Name`, Statement_s) AS Trans,
Transaction_ID
FROM sa_general_journal
WHERE Transaction_ID = 0)
SELECT * FROM cte) subquery ON gj.ID = subquery.ID
SET gj.Transaction_ID = subquery.Trans
CTEs work with UPDATE in MySQL 8, but not MariaDB 10.x.
Related
Once upon a time, I had a table like this:
CREATE TABLE `Events` (
`EvtId` INT UNSIGNED NOT NULL AUTO_INCREMENT,
`AlarmId` INT UNSIGNED,
-- Other fields omitted for brevity
PRIMARY KEY (`EvtId`)
);
AlarmId was permitted to be NULL.
Now, because I want to expand from zero-or-one alarm per event to zero-or-more alarms per event, in a software update I'm changing instances of my database to have this instead:
CREATE TABLE `Events` (
`EvtId` INT UNSIGNED NOT NULL AUTO_INCREMENT,
-- Other fields omitted for brevity
PRIMARY KEY (`EvtId`)
);
CREATE TABLE `EventAlarms` (
`EvtId` INT UNSIGNED NOT NULL,
`AlarmId` INT UNSIGNED NOT NULL,
PRIMARY KEY (`EvtId`, `AlarmId`),
CONSTRAINT `fk_evt` FOREIGN KEY (`EvtId`) REFERENCES `Events` (`EvtId`)
ON DELETE CASCADE ON UPDATE CASCADE
);
So far so good.
The data is easy to migrate, too:
INSERT INTO `EventAlarms`
SELECT `EvtId`, `AlarmId` FROM `Events` WHERE `AlarmId` IS NOT NULL;
ALTER TABLE `Events` DROP COLUMN `AlarmId`;
Thing is, my system requires that a downgrade also be possible. I accept that downgrades will sometimes be lossy in terms of data, and that's okay. However, they do need to work where possible, and result in the older database structure while making a best effort to keep as much original data as is reasonably possible.
In this case, that means going from zero-or-more alarms per event, to zero-or-one alarm per event. I could do it like this:
ALTER TABLE `Events` ADD COLUMN `AlarmId` INT UNSIGNED;
UPDATE `Events`
LEFT JOIN `EventAlarms` USING(`EvtId`)
SET `Events`.`AlarmId` = `EventAlarms`.`AlarmId`;
DROP TABLE `EventAlarms`;
… which is kind of fine, since I don't really care which one gets kept (it's best-effort, remember). However, as warned, this is not good for replication as the result may be unpredictable:
> SHOW WARNINGS;
Unsafe statement written to the binary log using statement format since
BINLOG_FORMAT = STATEMENT. Statements writing to a table with an auto-
increment column after selecting from another table are unsafe because the
order in which rows are retrieved determines what (if any) rows will be
written. This order cannot be predicted and may differ on master and the
slave.
Is there a way to somehow "order" or "limit" the join in the update, or shall I just skip this whole enterprise and stop trying to be clever? If the latter, how can I leave the downgraded AlarmId as NULL iff there were multiple rows in the new table between which we cannot safely distinguish? I do want to migrate the AlarmId if there is only one.
As a downgrade is a "one-time" maintenance operation, it doesn't have to be exactly real-time, but speed would be nice. Both tables could potentially have thousands of rows.
(MariaDB 5.5.56 on CentOS 7, but must also work on whatever ships with CentOS 6.)
First, we can perform a bit of analysis, with a self-join:
SELECT `A`.`EvtId`, COUNT(`B`.`EvtId`) AS `N`
FROM `EventAlarms` AS `A`
LEFT JOIN `EventAlarms` AS `B` ON (`A`.`EvtId` = `B`.`EvtId`)
GROUP BY `B`.`EvtId`
The result will look something like this:
EvtId N
--------------
370 1
371 1
372 4
379 1
380 1
382 16
383 1
384 1
Now you can, if you like, drop all the rows representing events that map to more than one alarm (which you suggest as a fallback solution; I think this makes sense, though you could modify the below to leave one of them in place if you really wanted).
Instead of actually DELETEing anything, though, it's easier to introduce a new table, populated using the self-joining query shown above:
CREATE TEMPORARY TABLE `_migrate` (
`EvtId` INT UNSIGNED,
`n` INT UNSIGNED,
PRIMARY KEY (`EvtId`),
KEY `idx_n` (`n`)
);
INSERT INTO `_migrate`
SELECT `A`.`EvtId`, COUNT(`B`.`EvtId`) AS `n`
FROM `EventAlarms` AS `A`
LEFT JOIN `EventAlarms` AS `B` ON(`A`.`EvtId` = `B`.`EvtId`)
GROUP BY `B`.`EvtId`;
Then your update becomes:
UPDATE `Events`
LEFT JOIN `_migrate` ON (`Events`.`EvtId` = `_migrate`.`EvtId` AND `_migrate`.`n` = 1)
LEFT JOIN `EventAlarms` ON (`_migrate`.`EvtId` = `EventAlarms`.`EvtId`)
SET `Events`.`AlarmId` = `EventAlarms`.`AlarmId`
WHERE `EventAlarms`.`AlarmId` IS NOT NULL
And, finally, clean up after yourself:
DROP TABLE `_migrate`;
DROP TABLE `EventAlarms`;
MySQL still kicks out the same warning as before, but since know that at most one value will be pulled from the source tables, we can basically just ignore it.
It should even be reasonably efficient, as we can tell from the equivalent EXPLAIN SELECT:
EXPLAIN SELECT `Events`.`EvtId` FROM `Events`
LEFT JOIN `_migrate` ON (`Events`.`EvtId` = `_migrate`.`EvtId` AND `_migrate`.`n` = 1)
LEFT JOIN `EventAlarms` ON (`_migrate`.`EvtId` = `EventAlarms`.`EvtId`)
WHERE `EventAlarms`.`AlarmId` IS NOT NULL
id select_type table type possible_keys key key_len ref rows Extra
---------------------------------------------------------------------------------------------------------------------
1 SIMPLE _migrate ref PRIMARY,idx_n idx_n 5 const 6 Using index
1 SIMPLE EventAlarms ref PRIMARY,fk_AlarmId PRIMARY 8 db._migrate.EvtId 1 Using where; Using index
1 SIMPLE Events eq_ref PRIMARY PRIMARY 8 db._migrate.EvtId 1 Using where; Using index
Use a subquery and user variables to select just one EventAlarms
In your update instead of EventAlarms use
( SELECT `EvtId`, `AlarmId`
FROM ( SELECT `EvtId`, `AlarmId`,
#rn := if ( #EvtId = `EvtId`
#rn + 1,
if ( #EvtId := `EvtId` , 1, 1)
) as rn
FROM `EventAlarms`
CROSS JOIN ( SELECT #EvtId := 0, #rn := 0) as vars
ORDER BY EvtId, AlarmId
) as t
WHERE rn = 1
) as SingleEventAlarms
Hello, everybody.
I've made a query that use INSERT .. UPDATE .. ON DUPLICATE.
But query that I made didn't work because syntax problem and I don't know why!
Here's what I tried to do.
What I try to do is to give three 'magic_potion' to every account.
First, I made two tables.
CREATE TABLE account(
account_no INT AUTO_INCREMENT,
account_id VARCHAR(32) NOT NULL,
account_pw VARCHAR(40) NOT NULL,
PRIMARY KEY(account_no)
);
CREATE TABLE item(
item_no INT AUTO_INCREMENT,
account_id VARCHAR(32) NOT NULL,
item_name VARCHAR(32) NOT NULL,
item_count SMALLINT NOT NULL,
item_status SMALLINT NOT NULL,
PRIMARY KEY(item_no)
);
Second, I put three account for test.
INSERT INTO account (account_id, account_pw) VALUES ('James', MD5('James')), ('Andy', MD5('James')), ('Angela', MD5('James'));
Third, I gave magic_potion to every account.
INSERT
INTO item
SELECT NULL, A.account_id, item_name, item_count, item_status
FROM account A
CROSS JOIN (SELECT 'magic_potion' AS item_name, 3 AS item_count, 1 AS item_status) B;
fourth, I put another account.
INSERT INTO account (account_id, account_pw) VALUES ('Judy', MD5('Judy')), ('Tom', MD5('Tom'));
fifth, Now, I want to give(=INSERT) two 'magic_potion' to newly added account and want to add(=UPDATE) one 'magic_potion' to previous account. So I made below query.
INSERT
INTO item
SELECT NULL, A.account_id, item_name, item_count, item_status
FROM account A
CROSS JOIN (SELECT 'magic_potion' AS item_name, 3 AS item_count, 1 AS item_status) B
ON DUPLICATE KEY UPDATE item_name = 'magic_potion', item_count = item_count + 1, item_status = 1;
But this query didn't work. System message said,
Error code: 1064
You have an error in your SQL syntax; check the manual that corresponds to your MariaDB server version for the right syntax to use near 'KEY UPDATE item_name = 'magic_potion', item_count = item_count + 1, item_status ' at line 6
I don't know what's wrong with my query.
Please, help me.
Thank you!
The syntax error is easy -- [CROSS] JOIN can take an optional ON clause, so the parser thinks ON DUPLICATE ... is such a clause. To avoid the syntax error, you need to enclose the SELECT in brackets:
INSERT
INTO item (
SELECT NULL, A.account_id, item_name, item_count, item_status
FROM account A
CROSS JOIN (SELECT 'magic_potion' AS item_name, 3 AS item_count, 1 AS item_status) B
) ON DUPLICATE KEY UPDATE item_name = 'magic_potion', item_count = item_count + 1, item_status = 1;
For a note, CROSS is redundant here.
Then you'll hit a context problem (ambiguous item_count). You don't need to solve it yet, because the query won't do what you expect anyway. It will never reach the ON DUPLICATE KEY UPDATE clause, because your only unique key is the primary key item_no, and you are inserting NULL into it, which means it will be always generated by auto-increment -- that is, you'll keep inserting rows every time.
There are different solutions for this, depending on what exactly you want to achieve -- you can modify the query, or you can add a UNIQUE KEY on whichever field or combination is supposed to be unique in that table (maybe it's account_id, or maybe it's a combination account_id + item_name -- it's unclear from the data sample).
I have a MySQL db with a MappingTable which consists of two columns. First column is a date column and another is ID - Autoincrement int column. I created this table for mapping dates and the ID's. When I query the date column with dates to retrieve the ID, no rows are getting selected. Any reason?
I tried
date_format in the SELECT query
str_to_date while checking in the WHERE clause
Compared like current_date > "2016-07-12" AND current_date <= "2016-07-12"
IfI compare LIKE "2016-07-1%" I'm getting matching rows but if I select "2016-07-12%" though there are matching rows, it is giving 0 rows.
I defined my column as DATE only.
Anything I'm missing here?
CREATE TABLE `mapping_table` (
`Current_date` date DEFAULT NULL,
`id` int(11) NOT NULL AUTO_INCREMENT, PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=26 DEFAULT CHARSET=utf8;
My question is, I want to select something like this.
select id from mapping_table where current_date="2016-07-12";
I tried with all approaches as mentioned above, but no rows are not retrieving.
use back tick on columns and table names so it wont be read/parse as keyword.
select `id` from `mapping_table` where `current_date` = "2016-07-12";
In the sample you provided you should use a date_format
select id from mapping_table where current_date= DATE_FORMAT("2016-07-12",'%Y-%d-%m') ;
or use a range
select id from mapping_table where current_date
BETWEEN DATE_FORMAT("2016-07-12",'%Y-%d-%m')
and DATE_FORMAT("2016-07-10",'%Y-%d-%m')
I have Book_Details table
CREATE TABLE `Book_Details` (
`bookId` int(11) NOT NULL,
`book_name` varchar(45) DEFAULT NULL,
`book_desc` varchar(45) DEFAULT NULL,
`price` int(11) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
And this table contains three rows
insert into book_details values (1,'Java for beginners','Java Book for beginners',99);
insert into book_details values (2,'Learn Advanced java','Advanced Java Book',223);
insert into book_details values (3,'Learn Practical java','Practical Java Book',78);
While I am executing the below query
select min(price) min_price,book_name from book_details;
I was expecting below result
min_price | book_name
-----------------------
78 | Learn Practical java
but surprisingly I am getting below output
min_price | book_name
-----------------------
78 | Java for beginners
Could someone please explain me why I am getting this output? Where I am wrong?
My MYsql DB version is 5.5.38. Please help.
You get the minimum price and arbitrary values for the other columns. You are using a MySQL extension that you should just simply stop using -- always be sure that all columns in the select that are not arguments to aggregation functions -- are in the group by.
Try this instead:
select bd.*
from book_details
order by price
limit 1;
This SELECT statement is not proper SQL but MySQL accepts it. The query always returns a single row, but several books may have the same price and the query could return any of the book_name values. You should use a SELECT that returns multiple rows for all the books that have the minimum price
select price,book_name from book_details where price = (select min(price) from book_details)
Is there any way to create a trigger that automatically calculates the sum and updates the rows in database?
For now i have this query which sums my rows and displays a total.
Select
PreAgg.id,
PreAgg.debit,
#PrevBal := #PrevBal + PreAgg.debit As total
From
(Select
YT.id,
YT.debit
From
test.accounts YT
Order By
YT.id) As PreAgg,
(Select
#PrevBal := 0.00) As SqlVars
This gives me:
id debit total
1 1000 1000
2 2000 3000
My question is how can this be converted into a trigger that calculates the sum after every insert and inserts it into the total field? please give me complete detail and query. thanks.
After doing some research i came up with this trigger:
CREATE TRIGGER `update_bal` BEFORE INSERT ON `sp_records` FOR EACH ROW INSERT INTO ledger
SELECT
PreAgg.id,
PreAgg.tot_amnt,
#PrevBal := #PrevBal + PreAgg.tot_amnt as balance
from
( select
YT.id,
YT.tot_amnt
from
sp_records YT
order by
YT.id ) as PreAgg,
( select #PrevBal := 0.00 ) as SqlVars
But it doesn't let me update my table, says "Column count doesn't match value count at row 1" when i insert something in my sp_records table. It works fine though without this trigger.
I have two tables one is sp_records that i want my tot_amnt field from and the other is ledger in which i want to insert the "balance". Both tables have additional fields in addition to the field i have mentioned.
CREATE TABLE `ledger` (
`id` int(11) NOT NULL,
`date` varchar(15) NOT NULL,
`debit` float NOT NULL,
`credit` float NOT NULL,
`balance` float NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8
heres my ledger.
I think you can use formula field on your table. Where you want to show the calculation. The reason why I am suggestion this trigger may drop your SQL engine performance.
(I don’t have enough points to add comments that’s the why I am putting as an answer. )
Cheers.