I have a table that looks something like this:
|date_start | date_end |amount |
+------------+-------------+-------+
|2015-02-23 | 2015-03-01 |50 |
|2015-03-02 | 2015-03-08 |50 |
|2015-03-09 | 2015-03-15 |100 |
|2015-03-16 | 2015-03-22 |800 |
|2015-03-23 | 2015-03-29 |50 |
and I'd like to work out the percent increase/decrease for column amount, from the previous date. For example the result would be something like this,
|date_start | date_end |amount | perc_change |
+------------+-------------+-------+-------------+
|2015-02-23 | 2015-03-01 |50 |
|2015-03-02 | 2015-03-08 |50 | 0
|2015-03-09 | 2015-03-15 |100 | 50
|2015-03-16 | 2015-03-22 |800 | 700
|2015-03-23 | 2015-03-29 |50 | -750
I've searched and racked my brain for a couple of days now. Usually, I simply do this using server side code but now I need to contain it all within the query.
Try this:
SELECT t.*,
amount - (SELECT amount FROM transactions prev WHERE prev.date_end < t.date_start ORDER BY date_start DESC LIMIT 1) AS changes
FROM transactions t
If we assume that the previous row always ends exactly one day before the current begins (as in your sample data), then you can use a join. The percentage increase would be:
select t.*,
100 * (t.amount - tprev.amount) / tprev.amount
from atable t left join
atable tprev
on tprev.date_end = t.date_start - interval 1 day;
However, your results seem to just have the difference, which is easier to calculate:
select t.*,
(t.amount - tprev.amount) as diff
from atable t left join
atable tprev
on tprev.date_end = t.date_start - interval 1 day;
you can use window function it would be easier
select date_start,
date_end,
amount,
LAG(amount, 1, 0) OVER (ORDER BY date_start) as previous_amount,
amount - LAG(amount, 1, 0) OVER (ORDER BY date_start) as amount_diff,
((amount - LAG(amount, 1, 0) OVER (ORDER BY date_start)) / (amount - LAG(amount, 1, 0) OVER (ORDER BY date_start))) * 100 amount_diff_percentage
from your_table_name
I started by joining each row in the table with the one that comes after it, like this:
SELECT m.date_start AS mStart, mt.date_start AS mtStart
FROM myTable m
JOIN myTable mT ON m.date_start < mt.date_start AND mt.date_start = (SELECT MIN(date_start) FROM myTable WHERE date_start > m.date_start);
This will join the tables so that each row can be seen with the following date if there is one. If there's not, the date is not returned.
Once you have those values, you can adjust the SELECT query to show you the percent change from the date before, like this:
SELECT
mT.date_start AS secondDate,
mT.amount - m.amount AS percentChange
FROM myTable m
JOIN myTable mT ON m.date_start < mT.date_start AND mt.date_start = (SELECT MIN(date_start) FROM myTable WHERE date_start > m.date_start);
I would like to make a note here that while you say 'percent difference' in your question, your expected results have nothing to do with percentage, but just difference in value. If you need to calculate this a different way, you can just adjust the select query above to meet your needs.
The last thing you will have to do is join this back to your original table to see all of the values together. This has to be done using a left join, in order for the first date of the table to be seen. Here is the final query:
SELECT m.date_start, m.date_end, m.amount, tmp.percentChange
FROM myTable m
LEFT JOIN(
SELECT
mT.date_start AS secondDate,
mT.amount - m.amount AS percentChange
FROM myTable m
JOIN myTable mT ON m.date_start < mT.date_start AND mt.date_start = (SELECT MIN(date_start) FROM myTable WHERE date_start > m.date_start)
) tmp ON tmp.secondDate = m.date_start;
And here is an SQL Fiddle example.
It looks something like this, i dont have environment to test this select but i think it should work
SELECT t1.date_start, t1.date_end, t1.amount, (t1.amount - t2.amount) as perc_change
FROM table1 t1
JOIN table1 t2 ON t1.id = t2.id + 1 // here your join to
//get next row and calculate this perc_change field
You case use the mysql var system :
SELECT date_start, date_end, IF(#last IS NOT NULL,ammount - #last , '' ) as perc_change, #last := amount as amount
From table
ORDER BY date_start;
the var #last is set at each passage, so column order between perc_change and amount is important
Related
I have a set of data that lists when a User changes a Product, we look at who changed it, when, the old cost and new cost, and the percentage price difference.
I want to use a group by, where statement, or case to group by and exclude products that filters out changes were the change occurred in the same day and resulted in the original price staying.
So the situation I want to exclude would look like this:
| product | Changed By | Old Price | New Price | % diff | Day Changed |
|----------|------------|-----------|-----------|--------|-------------|
| blue hat | me | 94.00 | 95.00 | 1.05 | 2016-11-28 |
| blue hat | me | 95.00 | 94.00 | 1.05 | 2016-11-28 |
Any ideas how to do this with MySql?
Here is a working version for anyone who wants to see this done using subqueries, where's, and group by's.
This query looks at the changes to an Item's cost by a User for the span of 1 day, where it pulls in all the results from "yesterday". It lists all the changes for that day one asc and one desc and compares the price changes that way. If they are the same from the oldest change to the newest change of that say then it is exempted.
SELECT
us.Name,
it.Name,
pal.CS_PA_Line_Supplier__c as supplier_name,
newest.NewValue as new_value,
oldest.OldValue as old_value,
((newest.NewValue - oldest.OldValue) / oldest.OldValue) * 100 as Percentage
FROM
(
SELECT Id, Name, KNDY4__Item__c, CS_PA_Line_Supplier__c
FROM KNDY4__Contract_Line__c
) pal
LEFT JOIN
(
SELECT *
FROM
(
SELECT
ParentId,
CreatedById,
CreatedDate,
Field,
NewValue
FROM KNDY4__Contract_Line__History
WHERE CreatedDate >= CURDATE() - INTERVAL 1 DAY
AND CreatedDate < CURDATE()
AND Field='KNDY4__Negotiated_Price__c'
ORDER BY CreatedDate DESC
) cd
GROUP BY ParentId
) newest
ON newest.ParentId=pal.Id
LEFT JOIN
(
SELECT * FROM (
SELECT
ParentId,
OldValue
FROM KNDY4__Contract_Line__History
WHERE CreatedDate >= CURDATE() - INTERVAL 1 DAY
AND CreatedDate < CURDATE()
AND Field='KNDY4__Negotiated_Price__c'
ORDER BY CreatedDate ASC
) cd
GROUP BY ParentId
) oldest
ON oldest.ParentId=pal.Id
LEFT JOIN
(
SELECT Id, Name
FROM User
) us
ON us.Id = newest.CreatedById
LEFT JOIN
(
SELECT Id,Name
FROM KNDY4__Item__c
) it
ON it.Id=pal.KNDY4__Item__c
WHERE newest.ParentId IS NOT NULL
AND oldest.OldValue IS NOT NULL
AND newest.NewValue != oldest.OldValue
GROUP BY pal.KNDY4__Item__c, pal.CS_PA_Line_Supplier__c
ORDER BY it.Name ASC
I need help figuring this out.
At the moment I have this query:
SELECT Table1.Column1 AS Company
,SUM(DATEDIFF(Day, Table2.Column2, Table1.Column3)) AS DiffDate
FROM Table1
INNER JOIN Table2 ON Table1.Column6 = Table2.Column6
AND Table1.Column5 = Table2.Column5
GROUP BY Company
The result of this is...
Company DiffDate
company1 8
But this is not what I want because there are values that are the combinations of column5 and column6 that are the same. See below.
Company | Column6 | Column5 | DiffDate
Company1 | 5782 | 10 | 2
Company1 | 5782 | 10 | 2
Company1 | 5782 | 20 | 2
Company1 | 5782 | 30 | 2
So the result I'm after is 6, not 8.
I tried using SELECT DISTINCT but that doesn't do anything.
Thank you in advance.
edit 2016-02-09
I've made the following query in SSMS.
;WITH cte AS (SELECT DISTINCT bestlevdat, bestradnr, bestnr FROM dbo.bpa)
SELECT dbo.bp.ftgnr AS Företagsnr,SUM(DATEDIFF(Day,cte.bestlevdat, dbo.bp.bestberlevdat)) AS Diff_Bekräftat_dat,
SUM(DATEDIFF(Day,cte.bestlevdat, dbo.bp.bestbeglevdat)) AS Diff_Önskat_dat, COUNT(dbo.bp.bestradnr) AS AntalRader, SUM(CASE WHEN datediff(day, cte.bestlevdat, dbo.bp.bestberlevdat) < - 0 THEN 1 ELSE 0 END) AS Antal_avvikande_rader, ROUND((COUNT(dbo.bp.bestradnr) - SUM(CASE WHEN datediff(day, cte.bestlevdat, dbo.bp.bestberlevdat) < - 0 THEN 1.0 ELSE 0.0 END)) * 100 / COUNT(dbo.bp.bestradnr), 1) AS Levsäk
FROM dbo.bp INNER JOIN
cte ON dbo.bp.bestnr = cte.bestnr AND dbo.bp.bestradnr = cte.bestradnr
WHERE (YEAR(dbo.bp.bestberlevdat) = '2015')
GROUP BY dbo.bp.ftgnr
ORDER BY AntalRader DESC
This works a charm. But when I create a new view and paste this query there and try to save I get an error on the ";" again. If I remove ";" in SSMS I get error "The multi-part identifier "cte.bestlevdat" could not be bound" and also converts my query to the following:
WITH cte AS (SELECT DISTINCT bestlevdat, bestradnr, bestnr
FROM dbo.bpa AS bpa_1)
SELECT TOP (100) PERCENT dbo.bp.ftgnr AS Företagsnr, SUM(DATEDIFF(Day, cte_1.bestlevdat, dbo.bp.bestberlevdat)) AS Diff_Bekräftat_dat, SUM(DATEDIFF(Day,
cte_1.bestlevdat, dbo.bp.bestbeglevdat)) AS Diff_Önskat_dat, COUNT(dbo.bp.bestradnr) AS AntalRader, SUM(CASE WHEN datediff(day, cte.bestlevdat,
dbo.bp.bestberlevdat) < - 0 THEN 1 ELSE 0 END) AS Antal_avvikande_rader, ROUND((COUNT(dbo.bp.bestradnr) - SUM(CASE WHEN datediff(day, cte.bestlevdat,
dbo.bp.bestberlevdat) < - 0 THEN 1.0 ELSE 0.0 END)) * 100 / COUNT(dbo.bp.bestradnr), 1) AS Levsäk
FROM dbo.bp INNER JOIN
cte AS cte_1 ON dbo.bp.bestnr = cte_1.bestnr AND dbo.bp.bestradnr = cte_1.bestradnr
WHERE (YEAR(dbo.bp.bestberlevdat) = '2015')
GROUP BY dbo.bp.ftgnr
ORDER BY AntalRader DESC
Sorry if I've missed something obvious, but I'm pretty new with querys and this is my first attempt on "View".
Sounds like you want to do something like you want the distinct rows from Table2, and to join/aggregate them from Table1, so using a CTE may help, so something like:
;WITH cte AS (
SELECT DISTINCT
Column2,
Column5,
Column6
FROM Table2
)
SELECT
Table1.Column1 AS Company,
SUM(DATEDIFF(Day, cte.Column2, Table1.Column3)) AS DiffDate
FROM Table1
INNER JOIN cte
ON Table1.Column6 = cte.Column6
AND Table1.Column5 = cte.Column5
GROUP BY Company
Suppose I have some data like:
id status activity_date
--- ------ -------------
101 R 2014-01-12
101 Mt 2014-04-27
101 R 2014-05-18
102 R 2014-02-19
Note that for rows with id = 101 we have activity between 2014-01-12 to 2014-04-26 and 2014-05-18 to current date.
Now I need to select that data where status = 'R' and the date is the most current date as of a given date, e.g. if I search for 2014-02-02, I would find the status row created on 2014-01-12, because that was the status that was still valid at the time for entity ID 101.
If I understand correctly:
Step 1: Convert the start and end date rows into columns. For this, you must join the table with itself based on this criteria:
SELECT
dates_fr.id,
dates_fr.activity_date AS date_fr,
MIN(dates_to.activity_date) AS date_to
FROM test AS dates_fr
LEFT JOIN test AS dates_to ON
dates_to.id = dates_fr.id AND
dates_to.status = 'Mt' AND
dates_to.activity_date > dates_fr.activity_date
WHERE dates_fr.status = 'R'
GROUP BY dates_fr.id, dates_fr.activity_date
+------+------------+------------+
| id | date_fr | date_to |
+------+------------+------------+
| 101 | 2014-01-12 | 2014-04-27 |
| 101 | 2014-05-18 | NULL |
| 102 | 2014-02-19 | NULL |
+------+------------+------------+
Step 2: The rest is simple. Wrap the query inside another query and use appropriate where clause:
SELECT * FROM (
SELECT
dates_fr.id,
dates_fr.activity_date AS date_fr,
MIN(dates_to.activity_date) AS date_to
FROM test AS dates_fr
LEFT JOIN test AS dates_to ON
dates_to.id = dates_fr.id AND
dates_to.status = 'Mt' AND
dates_to.activity_date > dates_fr.activity_date
WHERE dates_fr.status = 'R'
GROUP BY dates_fr.id, dates_fr.activity_date
) AS temp WHERE '2014-02-02' >= temp.date_fr and ('2014-02-02' < temp.date_to OR temp.date_to IS NULL)
+------+------------+------------+
| id | date_fr | date_to |
+------+------------+------------+
| 101 | 2014-01-12 | 2014-04-27 |
+------+------------+------------+
SQL Fiddle
You can try
select id, status, activity_date
from TABLE
where status = "R" and activity_date = "2014-02-02"
where TABLE is name of your table
I think you need following ans
SELECT id,MAX(CAST(ACTIVITY_DATE AS date),MIN(CAST (ACTIVITY_DATE AS date)
FROM Table_Name WHERE CAST('2014-02-02' AS date)
BETWEEN MIN(CAST (ACTIVITY_DATE AS date) AND MAX(CAST(ACTIVITY_DATE AS date)
AND Status='R'
GROUP BY id
Try this:
select * from yourtable
where status='R' and activity_date= '2014-02-02'
You can make a query to effectively give you the most status as of a date, e.g.
SELECT
id,
substr(max(concat(activity_date, status)),11) as status,
max(activity_date) as activity_date
FROM table
WHERE activity_date <= '2014-02-02'
GROUP by id;
Then, similar to Salman's answer, you can use this result inside another query and look for all those results with a status of 'R'
SELECT * from (
SELECT
id,
substr(max(concat(activity_date, status)),11) as status,
max(activity_date) as activity_date
FROM table
WHERE activity_date <= '2014-02-02'
GROUP by id
) AS temp WHERE temp.status = 'R';
Edit: Rather than use the questionable method of sorting the statuses, you could identify the relevant maximum record with a sub-query, so the original query would become
SELECT join1.* FROM table AS join1
INNER JOIN (
SELECT id, max(activity_date) as max_activity_date
FROM table
WHERE activity_date < '2014-02-02'
GROUP BY id
) AS join2
ON join1.id = join2.id AND join1.activity_date = join2.max_activity_date;
and the full query
SELECT * from (
SELECT join1.* FROM table AS join1
INNER JOIN (
SELECT id, max(activity_date) as max_activity_date
FROM table
WHERE activity_date < '2014-02-02'
GROUP BY id
) AS join2
ON join1.id = join2.id AND join1.activity_date = join2.max_activity_date
) AS temp WHERE temp.status = 'R';
try the following
SELECT *
FROM your_relation
WHERE status='R'
AND activity_data="2014-02-02"
I completely agree with Salman's response, the table could be designed in a fashion that allows for greater query accuracy and extensibility. However, the question asked, with regards to a query selecting information based on status and date range can be expressed as.
SELECT * FROM Table_1
WHERE ((status = 'R')
AND ((activity_date BETWEEN '2014-01-12' AND '2014-04-26')
OR activity_date > CONVERT(DATETIME, '2014-05-17')))
This will select all data with a status of 'R' and will use the BETWEEN operator for the range desired; moreover, the conversion of the final operator is because the expression is evaluated as a mathematical expression and requires explicit conversion.
I have a table similar to this:
+---------------------+---------------------+
| from | to |
+---------------------+---------------------+
| 2014-07-01 01:00:00 | NULL |
+---------------------+---------------------+
| 2015-08-01 02:00:00 | 2015-10-01 02:00:00 |
+---------------------+---------------------+
| 2015-09-01 03:00:00 | 2015-10-01 03:00:00 |
+---------------------+---------------------+
And i need to know the next datetime in the future, which should be: 2015-08-01 02:00:00
i'm looking for the "correct" answer, ideally without subqueries or joins.
Edit:
Assuming that from is always before to for each row, you don't even need LEAST:
SELECT MIN(IF(`from` > NOW(), `from`, `to`))
FROM `yourtable`
WHERE `from` > NOW()
OR `to` > NOW();
This works because one of from or to have to be > NOW() or the WHERE eliminates that row. So in procedural pseudo code this is like
x = ("from" where "from" is > now) and
("to" where "from" is < now and "to" > now)
min(x)
Is there always a certain amount of time between those two dates? When it is always the same it shouldn't be to hard, because you can use something like this:
Update 'table'
Set to = from + INTERVAL 1 DAY
Obviously you can change the number of days that fits for your query.
I would start by writing a query to get all rows with a null to column, as well as a query that gets the minimum date from each row, and then join them together on the condition that the minimum row date is later than the date in the first table:
SELECT m.*, t.dateCol
FROM myTable m
JOIN(
SELECT LEAST(fromCol, toCol) AS dateCol
FROM myTable) t ON t.dateCol > m.fromCol AND m.toCol IS NULL;
Once you have that, you can group by fromCol and get the minimum dateCol as the next date:
SELECT m.fromCol, MIN(t.dateCol) AS nextDate
FROM myTable m
JOIN(
SELECT LEAST(fromCol, toCol) AS dateCol
FROM myTable) t ON t.dateCol > m.fromCol AND m.toCol IS NULL
GROUP BY m.fromCol;
The way that would look in an update is something like this:
UPDATE myTable m
JOIN(
SELECT m.fromCol, MIN(t.dateCol) AS nextDate
FROM myTable m
JOIN(
SELECT LEAST(fromCol, toCol) AS dateCol
FROM myTable) t ON t.dateCol > m.fromCol AND m.toCol IS NULL
GROUP BY m.fromCol) t ON t.fromCol = m.fromCol
SET m.toCol = t.nextDate;
Here is an SQL Fiddle example.
You can get the next closest date in each column by using
SELECT MIN(from) AS MinFrom, MIN(to) as MinTo FROM TABLENAME
Getting the smallest of both is a little harder and requires nesting, but it's doable.
SELECT MIN(COM.Dates) FROM (
SELECT from AS Dates FROM TABLENAME
UNION
SELECT to FROM TABLENAME
) AS COM
EDIT: Realized that only one overarching MIN was needed.
I have a table with various orders in it:
ID | Date | etc...
1 | 2013-01-01 | etc
2 | 2013-02-01 | etc
3 | 2013-03-01 | etc
4 | 2013-04-01 | etc
5 | 2013-05-01 | etc
6 | 2013-06-01 | etc
7 | 2013-06-01 | etc
8 | 2013-03-01 | etc
9 | 2013-04-01 | etc
10 | 2013-05-01 | etc
I want a query that ends wit the result:
overallTotal | totalThisMonth | totalLastMonth
10 | 2 | 1
But I want to do this in one query! I am trying to find a way to use subqueries to do this. SO far I have:
SELECT * from (
SELECT count(*) as overallTotal from ORDERS
)
How can I combine this with other subqueries so I can get the totals in one query?
UPDATE
Original question was for MySQL, but I need it for Firebird now.
With conditional sums you can do it (MySQL syntax):
select
count(*) as overallTotal,
sum(if(Month(Date)+12*Year(Date)=Month(GetDate())+12*Year(GetDate()), 1, 0))
as totalThisMonth
sum(if(Month(Date)+12*Year(Date)=Month(GetDate())+12*Year(GetDate())-1, 1, 0))
as totalThisMonth;
from mytable
Use the month+12*year formula to avoid the problem with year change.
UPDATE
With Firebird the same applies, you only have to replace Month(x) with EXTRACT (MONTH FROM x), Year(x) with EXTRACT (YEAR FROM x) and Getdate() with CURRENT_TIME. This will look ugly, so I won't put it here, but you could easily do it yourself.
Posubly use SUM of the result of IF statements, with the IF checking that it is a relevant date.
SELECT COUNT(*),
SUM(IF(YEAR(Date) = YEAR(CURDATE()) AND MONTH(Date) = MONTH(CURDATE()), 1, 0))
SUM(IF((YEAR(Date) = YEAR(CURDATE()) AND MONTH(Date) = MONTH(CURDATE()) - 1) OR (YEAR(Date) = YEAR(CURDATE()) - 1 AND MONTH(Date) = 12 AND MONTH(CURDATE()) = 1), 1, 0))
from ORDERS
Complexity is caused by coping with a change of year when looking for the previous month
I see you are already using sub queries, so why not do something like the following,
SELECT
(SELECT count(*) from ORDERS) as overallTotal,
(SELECT count(*) from ORDERS where Date between ... and ...) as totalThisMonth,
(SELECT count(*) from ORDERS where Date between ... and ...) as overallTotal
SELECT
(SELECT COUNT(*) FROM C AS total) AS T,
(SELECT COUNT(*) AS thisMonth FROM C WHERE MONTH(d) = 6) AS A,
(SELECT COUNT(*) AS lastMonth FROM C WHERE MONTH(d) = 5) AS B;
Please notice "month" are "hard coded" -- but it shouldn't be too difficult to extract the "current month" at application level. If you really need to, you could do it at SQL level with something like that:
SELECT
(SELECT COUNT(*) FROM C AS total) AS T,
(SELECT COUNT(*) AS thisMonth FROM C WHERE MONTH(d) = MONTH(NOW()) ) AS A,
(SELECT COUNT(*) AS lastMonth FROM C WHERE MONTH(d) = (MONTH(NOW())+11)%12) ) AS B;