41
| time                | company | quote |
+---------------------+---------+-------+
| 0000-00-00 00:00:00 | GOOGLE  |    40 |
| 2012-07-02 21:28:05 | GOOGLE  |    60 |
| 2012-07-02 21:28:51 | SAP     |    60 |
| 2012-07-02 21:29:05 | SAP     |    20 |

How do I do a lag on this table in MySQL to print the difference in quotes, for example:

GOOGLE | 20
SAP    | 40  
Randell
  • 6,112
  • 6
  • 45
  • 70
javanx
  • 678
  • 1
  • 5
  • 13
  • Are there only two per `company`? or is it variable? – Michael Berkowski Jul 03 '12 at 01:58
  • I see you have two companies here, but are there only ever two rows per company? If so you can use `MAX() - MIN()` aggregates trivially. If there are more than 2 rows per `company`, it is more complicated. – Michael Berkowski Jul 03 '12 at 02:01
  • I just need the latest two timestamp.. may be there are lot of entries for the same company but i just need to take the latest two time stamp and print the diff of quotes – javanx Jul 03 '12 at 02:02
  • If a company is represented by only one row, would you like to return that company in the results? If so, what difference should be returned for it? – Andriy M Jul 03 '12 at 08:01
  • And finally, what particular problem is this question about: how to return last two rows per company or how to calculate the difference between two rows? I don't think you should mix these two problems in a single question. You will likely get better quality answers if you ask two separate questions (in case you really need to solve both problems). – Andriy M Jul 03 '12 at 08:04
  • 1
    In your example, why isn't the result negative for one of the companies? Google goes from 40 up to 60 whereas SAP goes from 60 down to 20. http://sqlfiddle.com/#!2/b62e1/1/0 Or do you only want the absolute movement irrespective of direction (in which case take [`ABS(delta)`](http://dev.mysql.com/doc/en/mathematical-functions.html#function_abs))? – eggyal Jul 03 '12 at 08:48
  • Thanks a lot for your help.I just want to present the delta for example it 20 for google ab -40 for Sap, but now its doing the reverse – javanx Jul 03 '12 at 15:06
  • Why, eggyal's suggestion returns those results exactly. What is the issue? (By the way, @eggyal, you should probably make that an answer.) – Andriy M Jul 03 '12 at 16:04
  • @AndriyM: I'm quite sure that there must be a better way than that (which involves a full table scan), but I'm struggling to see it. I suppose one could take the group maximum twice and get something like http://sqlfiddle.com/#!2/e28e7/1/0 ... but that's so ugly and involves accessing the table 5 times -- can it really be the best option? I was leaving it open in the hope that someone else might come along with something much more insightful! – eggyal Jul 03 '12 at 16:35

3 Answers3

60

This is my favorite MySQL hack.

This is how you emulate the lag function:

SET @quot=-1;
select time,company,@quot lag_quote, @quot:=quote curr_quote
  from stocks order by company,time;
  • lag_quote holds the value of previous row's quote. For the first row @quot is -1.
  • curr_quote holds the value of current row's quote.

Notes:

  1. order by clause is important here just like it is in a regular window function.
  2. You might also want to use lag for company just to be sure that you are computing difference in quotes of the same company.
  3. You can also implement row counters in the same way @cnt:=@cnt+1

The nice thing about this scheme is that is computationally very lean compared to some other approaches like using aggregate functions, stored procedures or processing data in application server.

EDIT:

Now coming to your question of getting result in the format you mentioned:

SET @quot=0,@latest=0,company='';
select B.* from (
select A.time,A.change,IF(@comp<>A.company,1,0) as LATEST,@comp:=A.company as company from (
select time,company,quote-@quot as change, @quot:=quote curr_quote
from stocks order by company,time) A
order by company,time desc) B where B.LATEST=1;

The nesting is not co-related so not as bad (computationally) as it looks (syntactically) :)

Let me know if you need any help with this.

Dojo
  • 5,374
  • 4
  • 49
  • 79
  • I am getting error. DDL and DML statements are not allowed in the query panel for MySQL; only SELECT statements are allowed. Put DDL and DML in the schema panel. – javanx Jul 03 '12 at 17:51
  • Though the error does not indicate this, try enabling "allowMultiQueries". This is a connector paramenter. For JDBC connector, see: http://dev.mysql.com/doc/refman/5.1/en/connector-j-reference-configuration-properties.html . Are you able to run it successfully from MySQL client? – Dojo Jul 03 '12 at 17:59
  • You can also try executing the two statements individually but in the same session. – Dojo Jul 03 '12 at 18:02
  • 14
    @javanx Hi, I'm the author of SQL Fiddle. The error message you mention was actually a bug in the way I was handling certain types of MySQL queries. Thanks to your message here, I recognized it as such and have worked out a solution that fixes it (see here, for example: http://sqlfiddle.com/#!2/4f8a1/2). Thanks! – Jake Feasel Jul 04 '12 at 05:03
  • This worked for me! But I don't understand the mechanism. Could you explain? – David Rubinger Jan 15 '19 at 21:07
  • `@`symobol indicates that it is session variable. So `@quot` is a variable. `SET @quot=0` initializes it to 0. Imagine the rest of the query like a for loop that is iterrating through the result set. whenever you do `@quot:=x`, the value of current row's x column is copied into `@quot`. What is the difference between `SET @quot=something` and `@quot:=something` (notice the colon)? I don't know, that's how they want the syntax to be. When you use `SET` it does not expect a colon, when you just do `@variable_name = something`, it needs a colon before equal sign. so its `@quot:=something`. – Dojo Jan 18 '19 at 18:07
  • Note the order colums/operations `@quot lag_quote, @quot:=quote curr_quote`. First I just read the value of `@quot` and name the column `lag_quote`. `@quot` is still holding the previously assigned value, i.e the previous row's `quote` value as we have not yet executed `@quot:=quote` for the current row. Then we do `@quot:=quote` whose output is the updated value of `@quot` i.e quote from the current row. And we name this column column as `curr_quote` – Dojo Jan 18 '19 at 18:20
  • 2
    **NOTE** You have to keep the data type of the variable consistent with the type of data you are trying to lag. EG, if you want to accumulate a FLOAT field, you must initialize your variable as `@quot=0.0`, otherwise it will not work. Took me a minute to figure this out! – quickshiftin Nov 15 '19 at 16:40
13

From MySQL 8.0 and above there is no need to simulate LAG. It is natively supported,

Window Function :

Returns the value of expr from the row that lags (precedes) the current row by N rows within its partition. If there is no such row, the return value is default. For example, if N is 3, the return value is default for the first two rows. If N or default are missing, the defaults are 1 and NULL, respectively.

SELECT
     company,
     quote,
     LAG(quote) OVER(PARTITION BY company ORDER BY time) AS prev_quote
FROM tab;

DBFiddle Demo

Community
  • 1
  • 1
Lukasz Szozda
  • 162,964
  • 23
  • 234
  • 275
9

To achieve the desired result, first you need to find the last and next to last timestamps for each company. It is quite simple with the following query:

SELECT c.company, c.mts, max(l.ts) AS lts
  FROM (SELECT company, max(ts) AS mts FROM cq GROUP BY company) AS c
  LEFT JOIN cq l
    ON c.company = l.company AND c.mts > l.ts
 GROUP BY c.company, c.mts;

Now you have to join this subquery with the original table to get the desired results:

SELECT c.company, l.quote, coalesce(l1.quote, 0),
       (l.quote - coalesce(l1.quote, 0)) AS result
  FROM (SELECT c.company, c.mts, max(l.ts) AS lts
      FROM (SELECT company, max(ts) AS mts FROM cq GROUP BY company) AS c
      LEFT JOIN cq l
        ON c.company = l.company AND c.mts > l.ts
     GROUP BY c.company, c.mts) AS c
  LEFT JOIN cq AS l ON l.company = c.company AND l.ts = c.mts
  LEFT JOIN cq AS l1 ON l1.company = c.company AND l1.ts = c.lts;

You can observe results on SQL Fiddle.

This query is using only standard SQL capabilities and should work on any RDBMS.

vyegorov
  • 21,787
  • 7
  • 59
  • 73
  • Good answer, especially for people preparing for tech interviews since they often discourage using window functions. – Evan Zamir Apr 19 '21 at 13:13