Tracking Increasing or decreasing prices in MySQL - mysql

I tried to write a query that returns the id, product, price, and change columns. The change column should follow this logic. If the item price has increased it should write positive and if it has decreased negative depending on the product and excluding the first initial product price. The last product price should be taken into consideration.
This is how the result should look like.
id product price change
1 apple 1
2 apple 1.5 positive
3 apple 3 positive
4 melon 4
5 melon 3 negative
6 apple 2 negative
I have tried to use Case When statement but failed.
select
p.id,
p.product,
p.price,
CASE
WHEN p.product = p.product AND p.price > p.price THEN 'Positive'
WHEN p.product = p.product AND p.price > p.price THEN 'Negative'
END AS 'Change'
from products p
Create and insert statements
CREATE TABLE `products` (
`id` INT(11) NOT NULL,
`product` VARCHAR(50) NOT NULL COLLATE 'utf8_unicode_ci',
`price` DECIMAL(10,2) NOT NULL DEFAULT '0.00'
)
COLLATE='utf8_unicode_ci'
ENGINE=InnoDB
;
INSERT INTO products (id, product, price)
VALUES (1, 'apple', 1);
INSERT INTO products (id, product, price)
VALUES (2, 'apple', 1.5);
INSERT INTO products (id, product, price)
VALUES (3, 'apple', 3);
INSERT INTO products (id, product, price)
VALUES (4, 'melon', 4);
INSERT INTO products (id, product, price)
VALUES (5, 'melon', 3);
INSERT INTO products (id, product, price)
VALUES (6, 'apple', 2);

Use lag():
select p.*,
(case when lag(price) over (partition by product order by id) < price
then 'negative'
when lag(price) over (partition by product order by id) > price
then 'positive'
end)
from products p;
In archaic versions of MySQL, you can use a correlated subquery:
select p.*
(case when prev_price < price
then 'negative'
when prev_price > price
then 'positive'
end)
from (select p.*,
(select p2.price
from product p2
where p2.product = p.product and p2.id < p.id
order by p2.id desc
limit 1
) as prev_price
from product p
) p;

Related

Is it possible to fetch needed data in one query?

I have a database containing tickets. Each ticket has a unique number but this number is not unique in the table. So for example ticket #1000 can be multiple times in the table with different other columns (Which I have removed here for the example).
create table countries
(
isoalpha varchar(2),
pole varchar(50)
);
insert into countries values ('DE', 'EMEA'),('FR', 'EMEA'),('IT', 'EMEA'),('US','USCAN'),('CA', 'USCAN');
create table tickets
(
id int primary key auto_increment,
number int,
isoalpha varchar(2),
created datetime
);
insert into tickets (number, isoalpha, created) values
(1000, 'DE', '2021-01-01 00:00:00'),
(1001, 'US', '2021-01-01 00:00:00'),
(1002, 'FR', '2021-01-01 00:00:00'),
(1003, 'CA', '2021-01-01 00:00:00'),
(1000, 'DE', '2021-01-01 00:00:00'),
(1000, 'DE', '2021-01-01 00:00:00'),
(1004, 'DE', '2021-01-02 00:00:00'),
(1001, 'US', '2021-01-01 00:00:00'),
(1002, 'FR', '2021-01-01 00:00:00'),
(1005, 'IT', '2021-01-02 00:00:00'),
(1006, 'US', '2021-01-02 00:00:00'),
(1007, 'DE', '2021-01-02 00:00:00');
Here is an example:
http://sqlfiddle.com/#!9/3f4ba4/6
What I need as output is the number of new created tickets for each day, devided into tickets from USCAN and rest of world.
So for this Example the out coming data should be
Date | USCAN | Other
'2021-01-01' | 2 | 2
'2021-01-02' | 1 | 3
At the moment I use this two queries to fetch all new tickets and then add the number of rows with same date in my application code:
SELECT MIN(ti.created) AS date
FROM tickets ti
LEFT JOIN countries ct ON (ct.isoalpha = ti.isoalpha)
WHERE ct.pole = 'USCAN'
GROUP BY ti.number
ORDER BY date
SELECT MIN(ti.created) AS date
FROM tickets ti
LEFT JOIN countries ct ON (ct.isoalpha = ti.isoalpha)
WHERE ct.pole <> 'USCAN'
GROUP BY ti.number
ORDER BY date
but that doesn't look like a very clean method. So how can I improved the query to get the needed data with less overhead?
Ii is recommended that is works with mySQL 5.7
You may logically combine the queries using conditional aggregation:
SELECT
MIN(CASE WHEN ct.pole = 'USCAN' THEN ti.created END) AS date_uscan,
MIN(CASE WHEN ct.pole <> 'USCAN' THEN ti.created END) AS date_other
FROM tickets ti
LEFT JOIN countries ct ON ct.isoalpha = ti.isoalpha
GROUP BY ti.number
ORDER BY date;
You can create unique entries for each date/country then use that value to count USCAN and non-USCAN
SELECT created,
SUM(1) as total,
SUM(CASE WHEN pole = 'USCAN' THEN 1 ELSE 0 END) as uscan,
SUM(CASE WHEN pole != 'USCAN' THEN 1 ELSE 0 END) as nonuscan
FROM (
SELECT created, t.isoalpha, MIN(pole) AS pole
FROM tickets t JOIN countries c ON t.isoalpha = c.isoalpha
GROUP BY created,isoalpha
) AS uniqueTickets
GROUP BY created
Results:
created total uscan nonuscan
2021-01-01T00:00:00Z 4 2 2
2021-01-02T00:00:00Z 3 1 2
http://sqlfiddle.com/#!9/3f4ba4/45/0
Regarding the answer of SQL Hacks I found the right solution
SELECT created,
SUM(1) as total,
SUM(CASE WHEN pole = 'USCAN' THEN 1 ELSE 0 END) as uscan,
SUM(CASE WHEN pole != 'USCAN' THEN 1 ELSE 0 END) as nonuscan
FROM (
SELECT created, t.isoalpha, MIN(pole) AS pole
FROM tickets t JOIN countries c ON t.isoalpha = c.isoalpha
GROUP BY t.number
) AS uniqueTickets
GROUP BY SUBSTR(created, 1 10)

SQL SUM and divide linked tables

I have the following tables:
create table Cars
(
CarID int,
CarType varchar(50),
PlateNo varchar(20),
CostCenter varchar(50),
);
insert into Cars (CarID, CarType, PlateNo, CostCenter) values
(1,'Coupe','BC18341','CALIFORNIA'),
(2,'Hatchback','AU14974','DAKOTA'),
(3,'Hatchback','BC49207','NYC'),
(4,'SUV','AU10299','FLORIDA'),
(5,'Coupe','AU32703','NYC'),
(6,'Coupe','BC51719','CALIFORNIA'),
(7,'Hatchback','AU30325','IDAHO'),
(8,'SUV','BC52018','CALIFORNIA');
create table Invoices
(
InvoiceID int,
InvoiceDate date,
CostCenterAssigned bit,
InvoiceValue money
);
insert into Invoices (InvoiceID, InvoiceDate, CostCenterAssigned, InvoiceValue) values
(1, '2021-01-02', 0, 978.32),
(2, '2021-01-15', 1, 168.34),
(3, '2021-02-28', 0, 369.13),
(4, '2021-02-05', 0, 772.81),
(5, '2021-03-18', 1, 469.37),
(6, '2021-03-29', 0, 366.83),
(7, '2021-04-01', 0, 173.48),
(8, '2021-04-19', 1, 267.91);
create table InvoicesCostCenterAllocations
(
InvoiceID int,
CarLocation varchar(50)
);
insert into InvoicesCostCenterAllocations (InvoiceID, CarLocation) values
(2, 'CALIFORNIA'),
(2, 'NYC'),
(5, 'FLORIDA'),
(5, 'NYC'),
(8, 'DAKOTA'),
(8, 'CALIFORNIA'),
(8, 'IDAHO');
How can I calculate the total invoice values allocated to that car based on its cost center?
If the invoice is allocated to cars in specific cost centers, then the CostCenterAssigned column is set to true and the cost centers are listed in the InvoicesCostCenterAllocations table linked to the Invoices table by the InvoiceID column. If there is no cost center allocation (CostCenterAssigned column is false) then the invoice value is divided by the total number of cars and summed up.
The sample data in Fiddle: http://sqlfiddle.com/#!18/9bd18/3
The data structure here isn't perfect, hence we need some extra code to solve for this. I needed to gather the amount of cars in each location, as well as to allocate the amounts for each invoice, depending on whether or not it was assigned to a location. I broke out the totals for each invoice type so that you can see the components which are being put together, you won't need those in your final result.
;WITH CarsByLocation AS(
SELECT
CostCenter
,COUNT(*) AS Cars
FROM Cars
GROUP BY CostCenter
UNION ALL
SELECT
''
,COUNT(*) AS Cars
FROM Cars
),CostCenterAssignedInvoices AS (
SELECT
InvoicesCostCenterAllocations.CarLocation
,SUM(invoicevalue) / CarsByLocation.cars AS InvoiceTotal
FROM Invoices
INNER JOIN InvoicesCostCenterAllocations ON invoices.InvoiceID = InvoicesCostCenterAllocations.InvoiceID
INNER JOIN CarsByLocation on InvoicesCostCenterAllocations.CarLocation = CarsByLocation.CostCenter
WHERE CostCenterAssigned = 1 --Not needed, put here for clarification
GROUP BY InvoicesCostCenterAllocations.CarLocation,CarsByLocation.Cars
),UnassignedInvoices AS (
SELECT
'' AS Carlocation
,SUM(invoicevalue)/CarsByLocation.Cars InvoiceTotal
FROM Invoices
INNER JOIN CarsByLocation on CarsByLocation.CostCenter = ''
WHERE CostCenterAssigned = 0
group by CarsByLocation.Cars
)
SELECT
Cars.*
,cca.InvoiceTotal AS AssignedTotal
,ui.InvoiceTotal AS UnassignedTotal
,cca.InvoiceTotal + ui.InvoiceTotal AS Total
FROM Cars
LEFT OUTER JOIN CostCenterAssignedInvoices CCA ON Cars.CostCenter = CCA.CarLocation
LEFT OUTER JOIN UnassignedInvoices UI ON UI.Carlocation = ''
ORDER BY
Cars.CostCenter
,Cars.PlateNo;

I want to to find a way to get my appropriate result in 1 mysql query

I have a table name order_history where I store both old_status and new_status of company orders.
the schema of table :
CREATE TABLE order_history (
id int(11) NOT NULL AUTO_INCREMENT,
old_status longtext COLLATE utf8_unicode_ci,
new_status longtext COLLATE utf8_unicode_ci,
created_at datetime NOT NULL,
order_id int(11) DEFAULT NULL,
PRIMARY KEY (id)
}
The insert to populate is :
INSERT INTO order_history (id, old_status, new_status, created_at, order_id) VALUES (1, '56', '714', '2020-12-20 21:37:54', 94471496);
INSERT INTO order_history (id, old_status, new_status, created_at, order_id) VALUES (2, '714', '61', '2020-12-20 21:37:56', 94471496);
INSERT INTO order_history (id, old_status, new_status, created_at, order_id) VALUES (3, '61', '713', '2020-12-20 21:38:17', 94471496);
INSERT INTO order_history (id, old_status, new_status, created_at, order_id) VALUES (4, '713', '42', '2020-12-20 21:38:26', 94471496);
INSERT INTO order_history (id, old_status, new_status, created_at, order_id) VALUES (5, '42', '51', '2020-12-20 21:59:17', 94471496);
INSERT INTO order_history (id, old_status, new_status, created_at, order_id) VALUES (6, '56', '714', '2020-12-20 22:21:27', 94471496);
INSERT INTO order_history (id, old_status, new_status, created_at, order_id) VALUES (7, '714', '61', '2020-12-20 22:21:29', 94471496);
INSERT INTO order_history (id, old_status, new_status, created_at, order_id) VALUES (8, '61', '713', '2020-12-20 22:24:28', 94471496);
INSERT INTO order_history (id, old_status, new_status, created_at, order_id) VALUES (9, '713', '42', '2020-12-20 22:24:43', 94471496);
And Now the question I want to find the TIMEDIFF of created_ats between rows that new_status=61 and rows that new_status=42 and old_status=713.
So in the example the affected rows should be (2,4,7,9) , and the right answer will be the TIMEDIFF between rows with ids (2,4) and rows with ids (7,9). But my query returns 3 results instead of 2 and it also calculate the TIMEDIFF between rows (2,9).
How can I exclude this result?
Here is my query:
select *
from (select oschStart.order_id as order_id, TIMEDIFF(oschEnd.created_at, oschStart.created_at) as confirm_time
from (select osch1.order_id, osch1.created_at
from order_history osch1
where osch1.old_status = 713
and osch1.new_status = 42
) oschEnd
join (select osch1.order_id, osch1.created_at
from order_history osch1
where osch1.new_status = 61
) oschStart
on oschStart.order_id = oschEnd.order_id and oschEnd.created_at > oschStart.created_at) order_time;
A simpler approach is to use a correlated sub query
select *,
timediff(
(select created_at from order_history oh1
where oh1.order_id = oh.order_id and
oh1.id > oh.id and
oh1.old_status = '713' and oh1.new_status = '42'
order by oh1.id asc limit 1),oh.created_at) diff
from order_history oh
where new_status = 61;
Why you have the unwanted results?
oschStart will result rows[2,7] and oschEnd will result rows [4,9]. Joining these subqueries will result in 4 rows [(2,4),(2,9),(7,4),(7,9)]. Your condition (on oschStart.order_id = oschEnd.order_id and oschEnd.created_at > oschStart.created_at) will result in these three rows: [(2,4),(2,9),(7,9)]. It wont prune (2,9) because also 9[created_date] > 2[created_date]. So your query will match a oschStart with all oschEnds that occurs after it. But You need it to be matched with the first occurring oschEnd
Solution
Use group by. If you group by your query results on a field and put other fields on your select part, Mysql will fill those fields with first row of that "group". So assuming that order_history is sorted on created_date you may use this query:
select order_time.id , order_time.*
from (
select oschStart.id as id, oschStart.order_id as order_id,
TIMEDIFF(oschEnd.created_at, oschStart.created_at) as confirm_time
from (select osch1.order_id, osch1.created_at
from order_history osch1
where osch1.old_status = 713
and osch1.new_status = 42
) oschEnd
join (select osch1.id as id, osch1.order_id, osch1.created_at
from order_history osch1
where osch1.new_status = 61
) oschStart
on oschStart.order_id = oschEnd.order_id
and oschEnd.created_at > oschStart.created_at)
order_time
group by order_time.id;

MySQL insert values if condition is met

I am trying to insert into a table only if the qty has changed down in another table see example
INSERT INTO sales_items (sale_id, item_id, quantity_purchased, item_cost_price, item_unit_price)
VALUES ('1', '1546', '3', '10', '10')
WHEN (SELECT quantity FROM location_items WHERE location_id =4 AND item_id =1546) < 10;
You can do the following:
INSERT INTO sales_items
(sale_id, item_id, quantity_purchased, item_cost_price, item_unit_price)
VALUES
(SELECT '1', '1546', '3', '10', '10'
FROM location_items
WHERE location_id = 4
AND item_id = 1546
AND quantity < 10
);
Or, if you want to do it all in one query, including updates:
REPLACE INTO sales_items
(item_id, quantity_purchased, item_cost_price, item_unit_price)
VALUES
(SELECT item_id, ??, ??, ??
FROM location_items
WHERE quantity < 10
AND quantity > 0
);
...where you have to fill the ?? with references to columns holding the values for item_cost_price and item_unit_price, and you have a unique constraint on item_id
Not possible like that. An INSERT query cannot have a where clause, period.
You can, hover, do an insert select from:
INSERT INTO ...
SELECT ... FROM ... WHERE (...) < 10
If the SELECT finds no rows, then nothing gets inserted.

Subtracting value of a particular column where a value of particular column is same in both tables

My db has two tables stock and damage.
stock table looks like
item_code, ss_no, item_name, rack_no, shelf_no, cold_storage, batch_no, qty, packing, expiry_date, mrp, purchase_price, selling_price, margin, formulation, stock_date, min_qty, ss_flag_id, ban_flag_id, sales_discount
'1', 1, 'ABZORB POWDER', 'A-1', ' ', ' ', '9086626', 18, 1, '2017-06-01', 87.00, 66.29, 87.00, 0.00, 'POWDER', '2015-05-11', 0, 0, 0, 0.0
damage table looks like the below
damage_stock_date, invoice_no, invoice_date, dist_name, contact_no, item_code, item_name, batch_no, mfr_name, expiry_date, qty, damaged_qty, unit_price, unit_vat, unit_discount, sub_total, total_amount, remarks, ds_flag_id, packing
'2015-06-19', '56', '2015-06-19', 'Ganapati Drugs', '', '0', 'SAXIM_', '1', '', '', 50, 10, 2.00, 5.00, 0.00, 21.00, 21.00, '', 0, 0
If I want to select a row from stock where the value of item_name column in both the tables, I would use
select * from stock s where item_name in
( select item_name from damage);
which does the job exactly what I want.
Now I want to subtract the value of qty col in damage from stock's qty col where the value of item_name from both the columns must be same.
I think I have to use variables but I don't know how..
It looks like there can be multiple damage entries for each stock item. Therefore, you need to use an aggregating subquery on damage before joining.
SELECT *, stock.qty - total_damage.qty AS remaining_qty
FROM stock,
(SELECT item_name, SUM(qty) AS qty
FROM damage
GROUP BY item_name) AS total_damage
WHERE stock.item_name = total_damage.item_name;
Try using join:
select *, (s.qty-d.qty) as available
from stock s LEFT JOIN
damage d on d.item_name=s.item_name
where d.qty IS NOT NULL
Example in SQL Fiddle.
Explanation:
A left join with table damage with condition d.qty IS NOT NULL would do the same thing. It is same as using IN (as in your question). But the advantage is that, you can use the columns of damage table in the main query.
So you can find the available quantity by (s.qty-d.qty)
EDIT:
For updating the table:
UPDATE stock s LEFT JOIN
damage d ON d.item_name = s.item_name
SET s.qty=(s.qty-d.qty)
WHERE d.qty IS NOT NULL