tags:

views:

932

answers:

5

How to get a value from previous result row of a SELECT statement

If we have a table called cardevent and has rows [ID(int) , Value(Money) ] and we have some rows in it, for example

ID --Value

1------70 
1------90
2------100
2------150
2------300 
3------150 
3------200 
3-----250 
3-----280

so on...

How to make one Query that get each row ID,Value and the previous Row Value in which data appear as follow

ID --- Value ---Prev_Value

1 ----- 70 ----------  0 
1 ----- 90 ---------- 70
2 ----- 100 --------  90 
2 ------150 -------- 100
2 ------300 -------- 150
3 ----- 150 -------- 300 
3 ----- 200 -------- 150 
3 ---- 250 -------- 200 
3 ---- 280 -------- 250

so on.

i make the following query but it's so bad in performance in huge amount of data

SELECT cardevent.ID, cardevent.Value, 
      (SELECT F1.Value
       FROM cardevent as F1 
       where F1.ID = (SELECT  Max(F2.ID)
                      FROM cardevent as F2 
                      WHERE F2.ID < cardevent.ID)
      ) AS Prev_Value
FROM cardevent

So can anyone help me to get the best solution for such a problem ?

A: 

Your query looks OK, but I suspect the performance problem is because it's an O(n2) algorithm. An outer select for the rows, and an inner select that looks for the biggest row less than the current one. Even with indexing, this can take some time to get the max values.

You could try the following query instead, as it's possible you can gain performance from indexing. I'm not sure your query is being optimized using a join (take a look at the plan for your query and make sure it's using the proper indexes). This is still technically O(n2), but indexing could cut this significantly:

SELECT e.ID, e.Value, prev.value 
FROM cardevent AS e
LEFT JOIN cardevent AS prev ON prevevent.ID = (SELECT MAX(e2.ID)
                                               FROM cardevent AS e2
                                               WHERE e2.ID < e.ID);

Another way might be to use a stored procedure that makes one pass through and would be O(n) instead. Pseudocode:

initialize prev_value = 0

foreach row in cardevent:
    Return the row //row.ID, row.value, @prev_value
    Set prev_value = row.value //for the next time

You'll have to see which one ends up being faster.


Also, if you only need these values in a client application and are returning every row all the time, you can push this calculation to the client instead. One additional pass through the returned rows will give the previous value:

for i=0 to returnedRows.length - 1:
    Set prev_value = (i == 0 ? 0 : returnedRows[i-1].value)
lc
This is a set operation, iteration over cursor will always be slower than well thought-out query.
Michał Chaniewski
USING ACCESS 2003 DATABASE, HOW CAN I MAKE THIS QUERY
Gopal
@Michal Chaniewski - I don't know if I'd say always. In either case the data will have already been retrieved for the previous row, so if the OP was experiencing poor performance, I suggested another way of doing things. Additionally, I find it odd the OP's query doesn't produce nearly identical plans as the explicit join, so I didn't suggest it at first (now edited).
lc
+2  A: 

Try something like this:

SELECT C.ID, C.Value, COALESCE(C1.Value, 0)
FROM cardevent C
    LEFT JOIN cardevent C1
    ON C1.Id = (SELECT MAX(id) FROM cardevent C2 where C2.Id < C.Id)
Michał Chaniewski
+1. This seems to be the fastes of all given solutions.
Lieven
It seems to compile to the same execution plan as MicSim's solution - and it makes sense if you look closely. I'd say both are ok.
Michał Chaniewski
I didn't look close enough at the actual execution plans, only at the percentages given by SQL Server. All solutions are working afaik, main difference is execution time and readability. Yours is fastest (or as fast as MicSim's solution) and most readable. Mine is... well, not so good <g>
Lieven
+1 for the use of COALESCE. Its not often I see this used but its a very useful command in SQL to know
kevchadders
Though COALESCE doesn't work in MS Access, just SQL Server.
MicSim
+3  A: 

This one should work fine for both databases:

SELECT cardevent.ID, cardevent.Value, 
  (SELECT TOP 1 F1.Value
   FROM cardevent as F1
   WHERE F1.ID < cardevent.ID
   ORDER BY F1.ID DESC
   ) AS Prev_Value
FROM cardevent

Update: Assuming that ID is not unique, but that the combination of ID and Value is unique (you must specify what imposes your ordering on the table, so one knows what is the previous row), you must use this query:

select cardevent.ID, cardevent.Value, 
    (select TOP 1 F1.Value from cardevent as F1
     where (F1.ID < cardevent.ID) or (F1.ID = cardevent.ID and F1.Value < cardevent.Value)
     order by F1.ID DESC, F1.Value DESC
    ) AS Prev_Value
from cardevent
MicSim
@MicSim - I used your query, but it Showing Error "At Most one record can be returned by the subquery" nothing display in prev_value field
Gopal
Output Showing like this....PERSONID CARDEVENTDATE Expr1002 Prev_Value#Name? #Name? #Name? #Name?#Name? #Name? #Name? #Name?#Name? #Name? #Name? #Name?#Name? #Name? #Name? #Name?
Gopal
Just tested above query with Access 2000 (don't have 2003 here) and it's working. Maybe you have extended/modified the query?
MicSim
Your query is working Fine, but Id is not different all the time for the value. Example. id, value - 1 100, 2 200, 2 400, 2 500, 3 100 3 200 so on.. So Result is.. 1 100 0, 2 200 100, 2 400 200, 2 500 400, 3 100 500, 3 200 100 so on.
Gopal
See update in posting.
MicSim
@MicSim - I used your updated query, but it Showing Error "At Most one record can be returned by the subquery"
Gopal
I hope, you used the lower one! :-) (it's working for me) If it's not working for you, then you have some more columns in your query or still other data which makes problems.
MicSim
Column using - select Id, name, titlename, value, min(time), max(time), (select TOP 1 F1.Value from cardevent as F1 where (F1.ID < cardevent.ID) or (F1.ID = cardevent.ID and F1.Value < cardevent.Value) order by F1.ID DESC, F1.Value DESC ) AS Prev_Value from table - But it showing "At Most one record can be returned by the subquery" - How can i make a query for all the mentioned column? Please.
Gopal
This is leading to nowhere... you can't use min(), max() functions in select without grouping by the other fields. You should really do a simple select and sort your data by ID and Value and then postprocess it in code.
MicSim
Here a query for your updated column info, but the result makes absolutely no sense to me. Sorry I can't help you better, but as in programming: vague requirements, wrong results. select ID, name, titlename, value, min(time), max(time), (select TOP 1 F1.Value from cardevent as F1 where (F1.ID < cardevent.ID) or (F1.ID = cardevent.ID and F1.Value < cardevent.Value) order by F1.ID DESC, F1.Value DESC ) AS Prev_Valuefrom cardeventgroup by ID, value, name, titlenameorder by ID, value, name, titlename
MicSim
Leave the min(time), Max(time) - remaining how can i take.
Gopal
This one works for me: SELECT ID, name, titlename, value, (select TOP 1 F1.Value from cardevent as F1 where (F1.ID < cardevent.ID) or (F1.ID = cardevent.ID and F1.Value < cardevent.Value) order by F1.ID DESC, F1.Value DESC ) AS Prev_ValueFROM cardevent
MicSim
@MICSIM: Now Your query is working Fine. Thanks. One more thingIn my query am selecting a VALUE BETWEEN using where condition. like where VALUE between 90 to 250 from the above mentioned table, But it displaying 1, 90, 150 so on.. because 150 is wrong because am selecting a value from 90 to 250, so before no value it should print 0 or null. but it printing 150. How can i make a condition for this? almost we got only one thing we have to do. Please
Gopal
If you want to have only some values, then just put the filter at the end of the query, after the last from. If it should apply to the before value, then also add it into the inner select where part as F1.Value.
MicSim
@Micsim: am new to access 2003, not cleared from the above comments. can you please how can i make inner select for my query. please.
Gopal
Please Can you edit your answer
Gopal
A: 

How does following perform?

SELECT F1.ID, F1.Value, F3.Value
FROM cardevent F1
     INNER JOIN (
        SELECT F1.ID, mID = MAX(F2.ID)
        FROM cardevent F1
             INNER JOIN cardevent F2 ON F2.ID < F1.ID
        GROUP BY F1.ID
      ) F2 ON F2.id = F1.ID
     INNER JOIN cardevent F3 ON F3.ID = F2.mID
UNION ALL
SELECT F1.ID, F1.Value, 0
FROM cardevent F1
     INNER JOIN (
        SELECT ID = MIN(ID) 
        FROM cardevent
      ) F2 ON F2.ID = F1.ID
ORDER BY 1
Lieven
A: 

I prefer TOP 1 instead of MAX(...), which IMHO should be also faster:

SELECT      C.ID, C.Value, COALESCE(C1.Value, 0)
FROM        cardevent C
LEFT JOIN   cardevent C1
        ON  C1.ID = (SELECT TOP 1 C2.ID FROM cardevent C2 WHERE C2.ID < C.ID ORDER BY C2.ID)
van
Using access 2003 database, is not accepting coalesce function
Gopal
@Gopal: I am not about the efficiency of the code in MSAccess, but IIF and ISNULL should help: "IIF(ISNULL(C1.Value), 0, C1.Value))"
van