SQL to find if refund would be processed or not - mysql

I have 2 tables, they can be created with the following query:
CREATE TABLE transactions(Id integer,ptime date, rtime date, sid text, itemid text, gtv integer);
/* Create few records in this table */
INSERT INTO transactions VALUES(3,'2019-09-19',null,'a','a1',58);
INSERT INTO transactions VALUES(12,'2019-12-10','2019-12-15','b','b2',475);
INSERT INTO transactions VALUES(3,'2020-09-01','2020-09-02','f','f9',33);
INSERT INTO transactions VALUES(2,'2020-04-30',null,'d','d3',250);
INSERT INTO transactions VALUES(1,'2020-10-22',null,'f','f2',91);
INSERT INTO transactions VALUES(8,'2020-04-16',null,'e','e7',24);
INSERT INTO transactions VALUES(5,'2019-09-23',null,'g','g6',61);
CREATE TABLE Items(sid text , itemid text, category text, name text);
/* Create a few records in this table */
INSERT INTO Items VALUES('a','a1','pants','denimpants');
INSERT INTO Items VALUES('a','a2','tops','blouse');
INSERT INTO Items VALUES('f','f1','table','coffee table');
INSERT INTO Items VALUES('f','f5','chair','loungechair');
INSERT INTO Items VALUES('f','f6','chair','armchair');
INSERT INTO Items VALUES('d','d2','jewelry','bracelet');
INSERT INTO Items VALUES('b','b4','earphone','airpods');
Select * from NAMES;
COMMIT;
Create a flag in the transaction items table indicating whether the refund can be processed or not. The condition for a refund to be processed is that it has to happen within 72 of Purchase time.
Expected Output: Only 1 of the three refunds would be processed in this case
Create a rank by buyer_id column in the transaction items table and filter for only the second purchase per
buyer. (Ignore refunds here)
Expected Output: Only the second purchase of buyer_id 3 should the output
How will you find the second transaction time per buyer (don’t use min/max; assume there were more
transactions per buyer in the table)
Expected Output: Only the second purchase of buyer_id along with a timestamp
I have been trying to wrap my head around this, but cant seem to understand where to start.

Here is a test script treating the questions. NB I am running MariaDB. There may be adjustments needed for mySQL
We set up the database, written to be reusable.
USE test;
DROP TABLE IF EXISTS transactions;
DROP TABLE IF EXISTS items;
CREATE TABLE IF NOT EXISTS transactions(Id integer,ptime date, rtime date, sid text, itemid text, gtv integer);
/* Create few records in this table */
INSERT INTO transactions VALUES(3,'2021-09-19',null,'a','a1',58);
INSERT INTO transactions VALUES(12,'2021-12-10','2022-03-15','b','b2',475);
INSERT INTO transactions VALUES(3,'2021-09-01','2021-09-02','f','f9',33);
INSERT INTO transactions VALUES(2,'2021-12-30',null,'d','d3',250);
INSERT INTO transactions VALUES(1,'2021-10-22',null,'f','f2',91);
INSERT INTO transactions VALUES(8,'2021-04-16',null,'e','e7',24);
INSERT INTO transactions VALUES(5,'2022-01-23',null,'g','g6',61);
CREATE TABLE IF NOT EXISTS items(sid text , itemid varchar(25) PRIMARY KEY, category text, name text);
/* Create a few records in this table */
INSERT INTO items VALUES('a','a1','pants','denimpants');
INSERT INTO items VALUES('a','a2','tops','blouse');
INSERT INTO items VALUES('f','f1','table','coffee table');
INSERT INTO items VALUES('f','f5','chair','loungechair');
INSERT INTO items VALUES('f','f6','chair','armchair');
INSERT INTO items VALUES('d','d2','jewelry','bracelet');
INSERT INTO items VALUES('b','b4','earphone','airpods');
COMMIT;
Select * from transactions;
Select * from items;
Here we add a virtual column which gives us the refund status using the sales date and the request date.
request date before sales date = error
no request date = Not requested
request date 0 to 72 days after sales date = accepted
request date more then 72 days after sales date = too late
ALTER TABLE transactions ADD COLUMN refund VARCHAR(20) AS (CASE WHEN rtime is NULL THEN 'Not requested' WHEN rtime < ptime THEN "Error" WHEN DATEDIFF(rtime,ptime) > 72 THEN "too late" ELSE "accepted" END);
Select ptime,rtime,datediff(ptime,rtime) dif,refund from transactions;
To get the 2nd purchase using rank we create a query assigning rank as follows:
SELECT ID,
ptime,
RANK() OVER(PARTITION BY ID ORDER BY ptime ASC ) Rank
FROM transactions
ORDER BY ID,
Rank;
Which we can then use as a CTE and use a where to only return the second record for each I'd.
WITH rankings AS (
SELECT ID,
ptime,
RANK() OVER(PARTITION BY ID ORDER BY ptime ASC ) Rank
FROM transactions
ORDER BY ID,
Rank )
SELECT ID,ptime
FROM rankings
WHERE Rank = 2;
This is all very well but the request was for a virtual column and not a query.
When we try to use aggregation functions in a virtual column we get errors because the definition of a virtual column should only reference fields in the same row, of columns already declared in the table definition. (These lines are commented out to avoid blocking the script.
Gives an error
#ALTER TABLE transactions ADD COLUMN rank_id INT AS ( RANK() OVER ( PARTITION BY ID ORDER BY ptime ASC ));
#ERROR 1901 (HY000) at line 38: Function or expression 'rank()' cannot be used in the GENERATED ALWAYS AS clause of `rank_id`
#ALTER TABLE transactions ADD COLUMN rank_id INT AS ( RANKX(transactions.id,sum(idl)));
#ERROR 1901 (HY000) at line 41: Function or expression 'sum()' cannot be used in the GENERATED ALWAYS AS clause of `rank_id`
#ALTER TABLE transactions ADD COLUMN rank_id INT AS ( RANKX(transactions.id,id));
#ERROR 1901 (HY000) at line 55: Function or expression '`RANKX`()' cannot be used in the GENERATED ALWAYS AS clause of `rank_id`
It seems that some aggregation functions can be used in virtual columns some SQL engines. I've tried a few but I have yet to find one which works here.
I am wondering whether this is a "trick" question to encourage you to research the principales of virtual columns?

Related

insert a new record into a mysql table with one of the values incremented by 1

I've got the following table:
productId price
1 price_value1
2 price_value2
3 price_value3
I would like to insert a new product into the table and assign it a new productId. In this case its value equals to 4.
So I want my new table to look like so:
productId price
1 price_value1
2 price_value2
3 price_value3
4 price_value4
So as far as I understand, in order to do that I have to somehow retrieve the max value of productId and insert it using INSERT INTO mytable VALUES (productId + 1, price_value4).
But how do I find out the maximum value of productId?
I tried INSERT INTO mytable VALUES (SELECT MAX(productId) + 1 FROM mytable, price_value4) but it didn't work.
This should Work:
Select the max(productID) and price_value4 as a columns from mytable and insert the result.
INSERT INTO mytable (SELECT MAX(productId) + 1, 'price_value4' FROM mytable);
However, if you are not going to jump some number you can just add an auto increment id key to product_id and then you will have only to insert the price, the product ID will be incremented automatically..
This will do so :
ALTER TABLE mytable
MODIFY COLUMN `productId` INT(10) UNSIGNED PRIMARY KEY AUTO_INCREMENT;
you can change INT(10) with the INT(5) for example depanding on the size you want to give to your productId column
EDIT :
In return to the OP question in comments why his solution wouldn't work
Some suggetions says you have to make the SELECT statment in insert always between parenthesis
INSERT INTO mytable VALUES ( (SELECT MAX(ID)+1 FROM mytable) , price_value4)
.. In my Case it Return
(1093): You can't specify target table
'mytable' for update in FROM clause
AND HERE IS WHY (Quoting From the documentation)
When selecting from and inserting into the same table, MySQL creates
an internal temporary table to hold the rows from the SELECT and then
inserts those rows into the target table. However, you cannot use
INSERT INTO t ... SELECT ... FROM t when t is a TEMPORARY table,
because TEMPORARY tables cannot be referred to twice in the same
statement
BUT there is away to overcome by using a query instead of the table itself in the FROM, which has the effect of copying the requested table values instead of referencing the one that you are updating..
INSERT INTO mytable VALUES (
(SELECT MAX(ID)+1 FROM (SELECT * FROM mytable ) as mytmp ),
'price_value4');
OR (Quoting From the documentation)
To avoid ambiguous column reference problems when the SELECT and the
INSERT refer to the same table, provide a unique alias for each table
used in the SELECT part, and qualify column names in that part with
the appropriate alias.
INSERT INTO mytable Values ( (SELECT MAX(ID)+1 FROM mytable as mytmp) , 'price_value4')
This is a duplicate question. In order to take advantage of the auto-incrementing capability of the column, do not supply a value for that column when inserting rows.
A simple syntax to create table
CREATE TABLE Product (
productId MEDIUMINT NOT NULL AUTO_INCREMENT,
price INT NOT NULL,
PRIMARY KEY (productid)
);
While inserting supplied default or leave column as blank or supplied value as NULL. Take a look at below code snippet.
INSERT INTO Product (price) VALUES
('10'),('20'),('4'),
('30');
refer this link

MySQL Trigger Value from Previous Row with Same Column Name

Inventory Table:
Inventory History Table:
The query:
INSERT INTO inventory_history (SKU, Quantity, timestamp)
SELECT SKU, Quantity, modifiedtime FROM inventory WHERE modifiedtime BETWEEN '2016-12-25 00:00:00' AND '2016-12-26 00:00:00';
The Trigger:
CREATE TRIGGER `sold_diff` BEFORE INSERT ON `inventory_history`
FOR EACH ROW begin
declare prev_quantity int(11) default 0;
declare prev_sku varchar(255) default null;
select sku
into prev_sku
from inventory_history
where prev_sku = NEW.sku
order by id desc
limit 1;
select quantity
into prev_quantity
from inventory_history
order by id desc
limit 1;
set NEW.sold = prev_quantity
;
end
The Result:
Now, how it's set-up is it's taking prev_quantity from the previous row, and putting it into the sold column.
I can not figure out a way to bind SKU in with prev_quantity, so that it will give me the previous Quantity value from the corresponding SKU.
Desired Result:
I've messed with a variety of different WHERE clauses on the two declared, but nothing is working right.. so I'm thinking this is not the right path to take.
How can this be achieved?
I think you are taking the wrong approach.
You seem to want an insert on the inventory table. When a new value is inserted or updated (or deleted), you then insert a row in the inventory_history table with the old and new values.
You then don't need an explicit insert on inventory_history.

Inserting running total

Problem to insert running total in MySQL transactional database. need your help for solutions and opinion. Table structure of my table is,
create table `wtacct` (
`ID` int(11) NOT NULL AUTO_INCREMENT,
`ACCOUNT_NO` varchar(16),
`AMOUNT` float(16,2),
`BALANCE` float(16,2)
);
[Please note other fields have been removed to make it simple example]
I am doing Transaction as,
Dr 10 USD from account 1001 and
Cr 10 USD to account 2002
Insert query
INSERT INTO wtacct (ID, ACCOUNT_NO, AMOUNT, BALANCE)
VALUES ('', 1001, -10, 100), ('', 2002, 10, 5000);
I want the Balance as,
BALANCE of Account no 1001 = Last transaction Balance of account 1001 - 10.
My solutions and limitations
Solution 1
In insert statement put sub query in balance field:
select balance from wtacct where account_no=1001 and id in(select max(id) from wtacct where account_no=1001)
Limitation: Mysql does not support same table select query (wtacct) where inserting the data (wtacct).
Solution 2
Using insert into select statement
insert into wtacct select '' ID, 1001 ACCOUNT_NO, -10 AMOUNT, (BALANCE-10) BALANCE where account_no=1001 and id in(select max(id) from wtacct where account_no=1001)
Limitation: For first transaction there is no record in wtacct for the account 1001 so select query will not return any record for first transaction.
Solution 3
Taking balance in variable and use it in insert statement.
select #balance1001 :=balance from wtacct
where account_no=1001 and id in(select max(id) from wtacct where account_no=1001)
select #balance2002 :=balance from wtacct
where account_no=2002 and id in(select max(id) from wtacct where account_no=2002)
INSERT INTO wtacct (ID, ACCOUNT_NO, AMOUNT, BALANCE)
VALUES ('', 1001, -10, #balance1001-10), ('', 2002, 10, #balance2002+10);
Limitation: there is a chance to be change the balance in time between select and insert query execution. also its costly, 3 query execution required.
Solution 4
Insert and then update Balance
INSERT INTO wtacct (ID, ACCOUNT_NO, AMOUNT, BALANCE)
VALUES ('', 1001, -10, 0);
UPDATE wtacct set balance = (ifnull(Select balance from wtacct where account_no=1001 and id in(select max(id) from wtacct where id <last_insert_id() and account_no=1001),0) -10)
where id =last_insert_id() and account_no=1001
........
Limitation: query is costly. its required 4 (two insert and 2 update) query execution. note last_insert_id() is php function
Solution 5
Using a trigger on insert statement. In the trigger, the balance will be updated calculating last transaction value and insert amount.
Limitation: Trigger not support transaction behavior and may fail.
Please give your solution and opinion on the above solutions. Please note in the above example their may be some syntax error/error. Please ignore them.
A big limitation I didn't see listed is a potential race condition, where two rows are being inserted into the table at the same time. There's a chance that the two inserts will both get the current "balance" from the same previous row.
One question: do you also have a separate "current balance" table that keeps a single value of the current "balance" for each account? Or are you only relying on the "balance" from the previous transaction.
Personally, I would track the current balance on a separate "account balance" table. And I would use BEFORE INSERT/UPDATE triggers to maintain the value in that row, and use that to return the current balance for the account.
For example, I would define a trigger like this which gets fired when a row is inserted into `wtacct` table:
CREATE TRIGGER wtacct_bi
BEFORE INSERT ON wtacct
FOR EACH ROW
BEGIN
IF NEW.amount IS NULL THEN
SET NEW.amount = 0;
END IF
;
UPDATE acct a
SET a.balance = (#new_balance := a.balance + NEW.amount)
WHERE a.account_no = NEW.account_no
;
SET NEW.balance = #new_balance
;
END$$
The setup for that trigger...
CREATE TABLE acct
( account_no VARCHAR(16) NOT NULL PRIMARY KEY
, balance DECIMAL(20,2) NOT NULL DEFAULT 0
) ENGINE=InnoDB
;
CREATE TABLE wtacct
( id BIGINT NOT NULL PRIMARY KEY AUTO_INCREMENT
, account_no VARCHAR(16) NOT NULL COMMENT 'FK ref acct.account_no'
, amount DECIMAL(20,2) NOT NULL
, balance DECIMAL(20,2) NOT NULL
, FOREIGN KEY FK_wtacct_acct (account_no) REFERENCES acct (account_no)
ON UPDATE CASCADE ON DELETE RESTRICT
) ENGINE=InnoDB
;
My reason for using a separate "current balance" table is that there is only one row for the given account_no, and that row retains the current balance of the account.
The UPDATE statement in the trigger should obtain an exclusive lock on the row being updated. And that exclusive lock prevents any other UPDATE statement from simultaneously updating the same row. The execution of the UPDATE statement will add the `amount` from the current transaction row being inserted to the current balance.
If we were using Oracle or PostgreSQL, we could use a RETURNING clause to get the value that was assigned to the \'balance\' column.
In MySQL we can do a wonky workaround, using a user-defined variable. The new value we are going to assign to the column is first assigned to the user_defined variable, and then that is assigned to the column.
And we can assign the value of the user-defined variable to the `balance` column of the row being inserted into `wtacct`.
The purpose of this approach is to make the retrieval and update of the current balance in a single statement, to avoid any race conditions.
The UPDATE statement locates the row, obtains an exclusive (X) lock on the row, retrieves the current balance (value from the \'balance\' column), calculates the new current balance, and assigns it back to the \'balance\' column. Then continues to hold the lock until the transaction completes.
Once the trigger completes, the INSERT statement (which initially fired the trigger) proceeds, attempting to insert the new row into `wtacct`. If that fails, then all of the changes made by the INSERT statement and execution of the trigger are rolled back, keeping everything consistent.
Once a COMMIT or ROLLBACK is issued by the session, the exclusive (X) lock held on the row(s) in `acct` are released, and other sessions can obtain locks on that row in `acct`.
I have done it using Store Procedure for MySql
CREATE DEFINER=`root`#`%` PROCEDURE `example_add`(IN dr Int, IN cr Int)
BEGIN
DECLARE LID int;
Declare Balance decimal(16,2);
INSERT INTO example (Debit,Credit)
VALUES (dr, cr);
SET LID = LAST_INSERT_ID();
SET Balance = (select SUM(Debit) - SUM(Credit) as Balance from example);
UPDATE Example SET Balance = Balance WHERE ID = LID;
END
Use it example_add(10,0) or example_add(0,15) then select and see the result.

SQL Insert into table only if record doesn't exist [duplicate]

This question already has answers here:
Check if a row exists, otherwise insert
(12 answers)
MySQL Conditional Insert
(13 answers)
Closed 9 years ago.
I want to run a set of queries to insert some data into an SQL table but only if the record satisfying certain criteria are met. The table has 4 fields: id (primary), fund_id, date and price
I have 3 fields in the query: fund_id, date and price.
So my query would go something like this:
INSERT INTO funds (fund_id, date, price)
VALUES (23, '2013-02-12', 22.43)
WHERE NOT EXISTS (
SELECT *
FROM funds
WHERE fund_id = 23
AND date = '2013-02-12'
);
So I only want to insert the data if a record matching the fund_id and date does not already exist. If the above is correct it strikes me as quite an inefficient way of achieving this as an additional select statement must be run each time.
Is there a better way of achieving the above?
Edit: For clarification neither fund_id nor date are unique fields; records sharing the same fund_id or date will exist but no record should have both the same fund_id and date as another.
This might be a simple solution to achieve this:
INSERT INTO funds (ID, date, price)
SELECT 23, DATE('2013-02-12'), 22.5
FROM dual
WHERE NOT EXISTS (SELECT 1
FROM funds
WHERE ID = 23
AND date = DATE('2013-02-12'));
p.s. alternatively (if ID a primary key):
INSERT INTO funds (ID, date, price)
VALUES (23, DATE('2013-02-12'), 22.5)
ON DUPLICATE KEY UPDATE ID = 23; -- or whatever you need
see this Fiddle.
Although the answer I originally marked as chosen is correct and achieves what I asked there is a better way of doing this (which others acknowledged but didn't go into). A composite unique index should be created on the table consisting of fund_id and date.
ALTER TABLE funds ADD UNIQUE KEY `fund_date` (`fund_id`, `date`);
Then when inserting a record add the condition when a conflict is encountered:
INSERT INTO funds (`fund_id`, `date`, `price`)
VALUES (23, DATE('2013-02-12'), 22.5)
ON DUPLICATE KEY UPDATE `price` = `price`; --this keeps the price what it was (no change to the table) or:
INSERT INTO funds (`fund_id`, `date`, `price`)
VALUES (23, DATE('2013-02-12'), 22.5)
ON DUPLICATE KEY UPDATE `price` = 22.5; --this updates the price to the new value
This will provide much better performance to a sub-query and the structure of the table is superior. It comes with the caveat that you can't have NULL values in your unique key columns as they are still treated as values by MySQL.
Assuming you cannot modify DDL (to create a unique constraint) or are limited to only being able to write DML then check for a null on filtered result of your values against the whole table
FIDDLE
insert into funds (ID, date, price)
select
T.*
from
(select 23 ID, '2013-02-12' date, 22.43 price) T
left join
funds on funds.ID = T.ID and funds.date = T.date
where
funds.ID is null

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).