SQL Selecting All columns based on max value of two column - sql-server

I have a table like this
PkReferencesID FkReferencesID DocID FKHRSEmployeeID AssignedDate AssignedTime FKActivityID
40855 NULL 8530 13 13920625 0829 1
40856 40855 8530 438 13920625 0829 1
40857 40856 8530 13 13920625 0829 2
40858 40857 8530 438 13920625 0830 1
40859 40858 8530 NULL 13920625 0830 1
NULL NULL NULL NULL NULL NULL NULL
How i can select rows with DocID = 8530 and FKActivityID = 1 and Maximum AssignedDate and Maximum AssignedTime for per FKHRSEmployeeID?
Note:the result should be one row for per FKHRSEmployeeID
Any help would be appriciated. Thanks
The result must be like this
PkReferencesID FkReferencesID DocID FKHRSEmployeeID AssignedDate AssignedTime FKActivityID
40855 NULL 8530 13 13920625 0829 1
40858 40857 8530 438 13920625 0830 1

The easiest way to do this is with row_number():
select t.*
from (select t.*,
row_number() over (partition by FKHRSEmployeeID
order by AssignedDate desc, AssignedTime desc
) as seqnum
from t
where DocID = 8530 and FKActivityID = 1
) t
where seqnum = 1

Query 2 will retrieve the information you're asking for:
Query1: This query gets the maximum assigned date/time for each employee.
SELECT FKHRSEmployeeID, MAX(AssignedDate & AssignedTime)
FROM Table
WHERE T.DocID=8530
AND T.FKActivityID=1
GROUP BY FKHRSEmployeeID;
Query2: This query uses the former one to retrieve only the matching records.
SELECT T.*
FROM Table AS T
INNER JOIN Query1 AS Q
ON (T.Date=LEFT(Q.MaxDateTime,8))
AND (T.Time=RIGHT(Q.MaxDateTime,4))
AND (T.FKHRSEmployeeID=Q.FKHRSEmployeeID)
WHERE T.DocID=8530
AND T.FKActivityID=1;
This query by itself doesn't guarantee you'll only get one record per employee, unless the combination FKHRSEmployeeID/AssignedDate/AssignedTime is unique in the table. You can ensure this by creating a unique index based on those columns.

Related

SQL Check to see if any value in a list exists in another table

I have a temp table that looks something like this:
Record DepartmentId PositionId EmployeeId StatusId CustomerId
1 Null Null Null 4
2 7 454 Null Null
3 Null 454 Null 3
3 Null Null Null Null 214
3 Null Null Null Null 100
3 Null Null Null Null 312
4 Null Null Null Null 357
I inserted the above into the temp table from tables that looked like this:
Record Table Record-to-Department Record-to-Position
Record Name Record DepartmentId Record PositionId
1 Red 2 7 2 454
2 blue 3 454
3 Green
4 Purple
Record-To-Status Record-To-Customer
Record StatusId Record CustomerId
1 4 3 214
3 3 3 100
3 312
4 357
I have an employee whose record looks something like this:
EmployeeId DepartmentId PositionId StatusId
342 7 454 4
Employee Customers:
EmployeeId CustomerId
342 357
342 95
342 720
In this scenario, it would return Record 1 (because it matches the StatusId), Record 2 (because it matches both the DepartmentId and the PositionId), but it would not return Record 3 because it only matches the PositionId and not the StatusId, and it would return RecordId 4 because one of the Employee CustomerIds matches the CustomerId on Record 4.
I got part of this answer on another question enter link description here (please forgive me I am new and trying to figure out how to ask everything I need to know), but I can't figure out how to handle the multi-records.
I tried selecting the Employees customer Id's into a table variable and then attempted to use the Coalesce like this:
Declare #Customers table(CustomerId int)
INSERT INTO #Customers(CustomerId)
SELECT DISTINCT S.CustomerId
FROM employee_Customers
Select * from tbl
WHERE
COAlesce(StatusId,#StatusId)=#StatusId AND
COALESCE(DepartmentId,#DepartmentId)=#DepartmentId AND
Coalesce(PositionId,#PositionId)=#PositionId AND
Coalesce(EmployeeCompanyId,#EmployeeCompanyId) = #EmployeeCompanyId AND
COALESCE((Select CustomerId from tbl_Requirement_to_Customer),(Select CustomerId from #Customers)) = (Select CustomerId from #Customers)
But I receive the error "Subquery Returned more than 1 value".
I have a possible solution you can try. I don't think it will be plug-and-play but hopefully you can adapt it to your situation. I am using just the data as presented in your temp table, your employee record and your Employee-customers correlation.
The basic logic is to join your temp table to the employee(s) using or condition, but then to get a count of populated values, and compare this count to a count of the number of matching values, which must be at least the first count and greater than zero.
This returns your desired output:
select t.*
from Temp t
left join emp e on e.DepartmentId=t.DepartmentId or e.PositionId=t.PositionId or e.EmployeeId=t.EmployeeId or e.StatusId=t.StatusId
outer apply (
select case when exists (
select * from EmployeeCustomers ec join emp e on e.EmployeeId=ec.EmployeeId where ec.CustomerID=t.CustomerId
) then 1 else 0 end CustomerIdMatch
)c
outer apply (
values (
Iif(t.departmentId is null,0,1) +
Iif(t.PositionId is null,0,1) +
Iif(t.EmployeeId is null,0,1) +
Iif(t.StatusId is null,0,1) +
c.CustomerIdMatch
))x(Cnt)
outer apply (
values (
Iif(t.departmentId=e.DepartmentId,1,0) +
Iif(t.PositionId=e.PositionId,1,0) +
Iif(t.EmployeeId=e.EmployeeId,1,0) +
Iif(t.StatusId=e.StatusId,1,0) +
c.CustomerIdMatch
))y(Cnt2)
where cnt2>=cnt and cnt2>0
See working DB<>Fiddle

How can I avoid sum multiple times while using join

I am now using the mssql with its sample database "adventureworks 2014", here I faced some problems with join and sum, here is the two table I used:
PurchaseOrderHeader:
PurchaseOrderID VendorID OrderDate TotalDue
1 1580 2011-04-16 00:00:00.000 222.1492
2 1496 2011-04-16 00:00:00.000 300.6721
3 1494 2011-04-16 00:00:00.000 9776.2665
4 1650 2011-04-16 00:00:00.000 189.0395
5 1654 2011-04-30 00:00:00.000 22539.0165
6 1664 2011-04-30 00:00:00.000 16164.0229
7 1678 2011-04-30 00:00:00.000 64847.5328
PurchaseOrderDetail:
PurchaseOrderID PurchaseOrderDetailID OrderQty ProductID
1 1 4 1
2 2 3 359
2 3 3 360
3 4 550 530
4 5 3 4
5 6 550 512
6 7 550 513
7 8 550 317
7 9 550 318
7 10 550 319
Here is the sql script:
CREATE TABLE PurchaseOrderHeader(
PurchaseOrderID INTEGER NOT NULL PRIMARY KEY
,VendorID INTEGER NOT NULL
,OrderDate VARCHAR(23) NOT NULL
,TotalDue NUMERIC(10,4) NOT NULL
);
INSERT INTO PurchaseOrderHeader(PurchaseOrderID,VendorID,OrderDate,TotalDue) VALUES (1,1580,'2011-04-16 00:00:00.000',222.1492);
INSERT INTO PurchaseOrderHeader(PurchaseOrderID,VendorID,OrderDate,TotalDue) VALUES (2,1496,'2011-04-16 00:00:00.000',300.6721);
INSERT INTO PurchaseOrderHeader(PurchaseOrderID,VendorID,OrderDate,TotalDue) VALUES (3,1494,'2011-04-16 00:00:00.000',9776.2665);
INSERT INTO PurchaseOrderHeader(PurchaseOrderID,VendorID,OrderDate,TotalDue) VALUES (4,1650,'2011-04-16 00:00:00.000',189.0395);
INSERT INTO PurchaseOrderHeader(PurchaseOrderID,VendorID,OrderDate,TotalDue) VALUES (5,1654,'2011-04-30 00:00:00.000',22539.0165);
INSERT INTO PurchaseOrderHeader(PurchaseOrderID,VendorID,OrderDate,TotalDue) VALUES (6,1664,'2011-04-30 00:00:00.000',16164.0229);
INSERT INTO PurchaseOrderHeader(PurchaseOrderID,VendorID,OrderDate,TotalDue) VALUES (7,1678,'2011-04-30 00:00:00.000',64847.5328);
CREATE TABLE PurchaseOrderDetail(
PurchaseOrderID INTEGER NOT NULL
,PurchaseOrderDetailID INTEGER NOT NULL PRIMARY KEY
,OrderQty INTEGER NOT NULL
,ProductID INTEGER NOT NULL
);
INSERT INTO PurchaseOrderDetail(PurchaseOrderID,PurchaseOrderDetailID,OrderQty,ProductID) VALUES (1,1,4,1);
INSERT INTO PurchaseOrderDetail(PurchaseOrderID,PurchaseOrderDetailID,OrderQty,ProductID) VALUES (2,2,3,359);
INSERT INTO PurchaseOrderDetail(PurchaseOrderID,PurchaseOrderDetailID,OrderQty,ProductID) VALUES (2,3,3,360);
INSERT INTO PurchaseOrderDetail(PurchaseOrderID,PurchaseOrderDetailID,OrderQty,ProductID) VALUES (3,4,550,530);
INSERT INTO PurchaseOrderDetail(PurchaseOrderID,PurchaseOrderDetailID,OrderQty,ProductID) VALUES (4,5,3,4);
INSERT INTO PurchaseOrderDetail(PurchaseOrderID,PurchaseOrderDetailID,OrderQty,ProductID) VALUES (5,6,550,512);
INSERT INTO PurchaseOrderDetail(PurchaseOrderID,PurchaseOrderDetailID,OrderQty,ProductID) VALUES (6,7,550,513);
INSERT INTO PurchaseOrderDetail(PurchaseOrderID,PurchaseOrderDetailID,OrderQty,ProductID) VALUES (7,8,550,317);
INSERT INTO PurchaseOrderDetail(PurchaseOrderID,PurchaseOrderDetailID,OrderQty,ProductID) VALUES (7,9,550,318);
INSERT INTO PurchaseOrderDetail(PurchaseOrderID,PurchaseOrderDetailID,OrderQty,ProductID) VALUES (7,10,550,319);
and here is my code:
select PurchaseOrderHeader.VendorID,
SUM(CASE WHEN Datename(year,PurchaseOrderHeader.OrderDate) = 2011 THEN PurchaseOrderHeader.TotalDue else 0 END) as "TotalPay IN 2011",
SUM(CASE WHEN Datename(year,PurchaseOrderHeader.OrderDate) = 2011 THEN PurchaseOrderDetail.OrderQty else 0 END) as "TotalOrder IN 2011"
from PurchaseOrderHeader
left join PurchaseOrderDetail on PurchaseOrderHeader.PurchaseOrderID = PurchaseOrderDetail.PurchaseOrderID
group by PurchaseOrderHeader.VendorID
order by VendorID
Here is what I got:
VendorID TotalPay IN 2011 TotalOrder IN 2011
1494 9776.2665 550
1496 601.3442 6
1580 222.1492 4
1650 189.0395 3
1654 22539.0165 550
1664 16164.0229 550
1678 194542.5984 1650
while I should expect:
VendorID TotalPay IN 2011 TotalOrder IN 2011
1494 9776.2665 550
1496 300.6721 6
1580 222.1492 4
1650 189.0395 3
1654 22539.0165 550
1664 16164.0229 550
1678 64847.5328 1650
This code will join two tables on PurchaseOrderID, and calculate the TotalDue grouped by vendorID. The problem is when I use join, where will be multiple rows from table PurchaseOrderDetail refered to one row in table PurchaseOrderHeader. In this example for vendor 1496 and 1678 there are two or three rows refer to one row in PurchaseDetailHeader. So it will be added two or three times. How should I avoid adding multiple times, thanks!
You can just take your SUM and divide by COUNT. Something like this.
select PurchaseOrderHeader.VendorID,
SUM(CASE WHEN Datename(year,PurchaseOrderHeader.OrderDate) = 2011 THEN PurchaseOrderHeader.TotalDue else 0 END) / COUNT(*) as "TotalPay IN 2011",
SUM(CASE WHEN Datename(year,PurchaseOrderHeader.OrderDate) = 2011 THEN PurchaseOrderDetail.OrderQty else 0 END) / COUNT(*) as "TotalOrder IN 2011"
from Purchasing.PurchaseOrderHeader
left join Purchasing.PurchaseOrderDetail on PurchaseOrderHeader.PurchaseOrderID = PurchaseOrderDetail.PurchaseOrderID
group by PurchaseOrderHeader.VendorID
order by VendorID
select h.VendorID,
SUM(CASE WHEN Datename(year,h.OrderDate) = 2011 THEN h.TotalDue else 0 END) as "TotalPay IN 2011",
SUM(CASE WHEN Datename(year,h.OrderDate) = 2011 THEN d.OrderQty else 0 END) as "TotalOrder IN 2011"
from PurchaseOrderHeader h
left join (
select t.PurchaseOrderID,
sum(t.OrderQty) as OrderQty
from PurchaseOrderDetail t
group by t.PurchaseOrderID
) d on d.PurchaseOrderID = h.PurchaseOrderID
group by h.VendorID
order by VendorID
The default way to avoid double counting is to use SUM(DISTINCT expr).
This does not always work well enough, as you do not want to sum distinct values, but want to sum distinct rows even when those rows share the same values.
The solution is to use a sub-query to sum the details on order number and then join the result. Then you have only one total per order id to join with the order lines:
SELECT PurchaseOrderHeader.VendorID,
SUM(PurchaseOrderHeader.TotalDue) AS "TotalPay IN 2011",
SUM(POD.Qty) AS "TotalOrder IN 2011"
FROM PurchaseOrderHeader
LEFT JOIN (
SELECT PurchaseOrderDetail.PurchaseOrderID, SUM(OrderQty) AS Qty
FROM PurchaseOrderDetail
GROUP BY PurchaseOrderDetail.PurchaseOrderID
) AS POD on PurchaseOrderHeader.PurchaseOrderID = POD.PurchaseOrderID
WHERE Datename(year,PurchaseOrderHeader.OrderDate) = 2011
GROUP BY PurchaseOrderHeader.VendorID
ORDER BY VendorID
Also I took the freedom to remove the CASE WHEN statement from the SUM() to the WHERE part of the query. In this case that should give you the same results with shorter code.
Lots of good answers, but I think they miss the bit where a vendor could have multiple purchase orders, and that throws off how the TotalOrder gets calculated. (Try a sample with multiple vendors with multiple orders with each order having multiple details.) Don't forget to check for possible NULL values!
Here, I use the subquery to calculate the TotalPay for each vendor for the year in question, and then join that back to the list of all vendors. (Threw in table aliases as well, for legibility.)
-- As a subquery
SELECT
hd.VendorID,
,sum(case
when year(hd.OrderDate) = 2011 then hd.TotalDue
else 0
end) as "TotalPay IN 2011"
,isnull(subQuery.TotaOrderIn2011, 0) as "TotalOrder IN 2011"
from PurchaseOrderHeader hd
left join (-- Calculate volume by vendor for 2011
select
hd.VendorID
,sum(OrderQty) TotalOrderIn2011
from PurchaseOrderHeader hd
inner join PurchaseOrderDetail dt
on hd.PurchaseOrderID = dt.PurchaseOrderID
where year(hd.OrderDate) = 2011
group by
hd.VendorID
) subQuery
on subQuery.VendorId = hd.VendorId
group by hd.VendorID
order by hd.VendorID

How to select specific records of groups based on criteria

I'm trying to group a set of data and for some of the fields I need to select a specific value based on the ttype, for example I have the following rows:
caseid age iss gcs ttype
00170 64 25 17 Transfer Out
00170 64 27 15 Transfer In
00201 24 14 40 Transfer In
If a caseID has ttype 'Transfer Out' I want to use the ISS and GCS values from this row, otherwise use the values from the 'Transfer In' row.
My desired output based on the above example would be:
caseid age iss gcs
00170 64 25 17
00201 24 14 40
My current select statement is:
select caseid, max(age), max(iss), max(gcs)
from Table1
group by caseid
Which I know is incorrect but how do I specify the values for ISS and GCS from a specific row?
Thanks
Edit - I will not always need to select from Row1, table below with expanded data:
caseid age iss gcs los ttype disdate
170 64 25 17 5 Transfer Out 2014-01-02 00:00:00.000
170 64 27 15 1 Transfer In 2014-01-04 00:00:00.000
201 24 14 40 4 Transfer In 2014-01-04 00:00:00.000
In this case, I want the max age and the ISS and GCS figure for row1 as before but I need to sum the LOS and select the disdate for row 2 (ie the latest date), so my output would be:
caseid age iss gcs los disdate
170 64 25 17 6 2014-01-04
201 24 14 40 4 2014-01-04
Is this possible?
You can use a CTE and ROW_NUMBER + Over-clause (edited acc. to your updated question):
WITH CTE AS
(
SELECT caseid, age, iss, gcs, los, ttype, disdate,
SumLos = SUM(los) OVER (PARTITION BY caseid),
LatestDisDate = MAX(disdate) OVER (PARTITION BY caseid),
rn = ROW_NUMBER() OVER (PARTITION BY caseid
ORDER BY CASE WHEN ttype = 'Transfer Out'
THEN 0 ELSE 1 END ASC, disdate ASC)
FROM dbo.Table1
)
SELECT caseid, age, iss, gcs, los = SumLos, disdate = LatestDisDate
FROM CTE
WHERE rn = 1
Demo
I think this is what you need -
;WITH CTE AS
(
SELECT case_id, age,iss,gcs, ROW_NUMBER () over (PARTITION BY ttype order by gcs DESC) Rn
from YOUR_TABLE_NAME
)
SELECT case_id,age,iss,gcs
from CTE where Rn =1

SQL Server: Lead/Lag analytic function across groups (and not within groups)

Sorry for the long post, but I have provided copy & paste sample data and a possible solution approach below. The relevant part of the question is in the upper part of the post (above the horizontal rule).
I have the following table
Dt customer_id buy_time money_spent
-------------------------------------------------
2000-01-04 100 11:00:00.00 2
2000-01-05 100 16:00:00.00 1
2000-01-10 100 13:00:00.00 4
2000-01-10 100 14:00:00.00 3
2000-01-04 200 09:00:00.00 10
2000-01-06 200 10:00:00.00 11
2000-01-06 200 11:00:00.00 5
2000-01-10 200 08:00:00.00 20
and want a query to get this result set
Dt Dt_next customer_id buy_time money_spent
-------------------------------------------------------------
2000-01-04 2000-01-05 100 11:00:00.00 2
2000-01-05 2000-01-10 100 16:00:00.00 1
2000-01-10 NULL 100 13:00:00.00 4
2000-01-10 NULL 100 14:00:00.00 3
2000-01-04 2000-01-06 200 09:00:00.00 10
2000-01-06 2000-01-10 200 10:00:00.00 11
2000-01-06 2000-01-10 200 11:00:00.00 5
2000-01-10 NULL 200 08:00:00.00 20
That is: I want for each costumer (customer_id) and each day (Dt) the next day the same customer has visited (Dt_next).
I have already one query that gives the latter result set (data and query enclosed below the horizontal rule). However, it involves a left outer join and two dense_rank aggregate functions. This approach seems a bit clumsy to me and I think that there should be a better solution. Any pointers to alternative solutions highly appreciated! Thank you!
BTW: I am using SQL Server 11 and the table has >>1m entries.
My query:
select
customer_table.Dt
,customer_table_lead.Dt as Dt_next
,customer_table.customer_id
,customer_table.buy_time
,customer_table.money_spent
from
(
select
#customer_data.*
,dense_rank() over (partition by customer_id order by customer_id asc, Dt asc) as Dt_int
from #customer_data
) as customer_table
left outer join
(
select distinct
#customer_data.Dt
,#customer_data.customer_id
,dense_rank() over (partition by customer_id order by customer_id asc, Dt asc)-1 as Dt_int
from #customer_data
) as customer_table_lead
on
(
customer_table.Dt_int=customer_table_lead.Dt_int
and customer_table.customer_id=customer_table_lead.customer_id
)
Sample data:
create table #customer_data (
Dt date not null,
customer_id int not null,
buy_time time(2) not null,
money_spent float not null
);
insert into #customer_data values ('2000-01-04',100,'11:00:00',2);
insert into #customer_data values ('2000-01-05',100,'16:00:00',1);
insert into #customer_data values ('2000-01-10',100,'13:00:00',4);
insert into #customer_data values ('2000-01-10',100,'14:00:00',3);
insert into #customer_data values ('2000-01-04',200,'09:00:00',10);
insert into #customer_data values ('2000-01-06',200,'10:00:00',11);
insert into #customer_data values ('2000-01-06',200,'11:00:00',5);
insert into #customer_data values ('2000-01-10',200,'08:00:00',20);
Try this query:
select cd.Dt
, t.Dt_next
, cd.customer_id
, cd.buy_time
, cd.money_spent
from (
select Dt
, LEAD(Dt) OVER (PARTITION BY customer_id ORDER BY Dt) AS Dt_next
, customer_id
from (
select distinct Dt, customer_id
from #customer_data
) t
) t
inner join #customer_data cd on t.customer_id = cd.customer_id and t.Dt = cd.Dt
Why field money_spent has float type? You may have problems with calculations. Convert it to decimal type.

Finding the difference within a SQL Server query and dividing by seconds elapsed between records

Hi I have a table of meter values in a SQL Server database, which contains a table with the following columns:
Timestamp, meterID, rawValue
I am trying to graph the water usage rate using a query and google charts, the problem is that I need to calculate the rate from the raw meter values which are updated every 15 to 30 minutes.
I want to run a query that returns the values for a specific water meter.
MeterID, Timestamp, (rawValue-previousRawValue)/(timestamp difference in seconds)
any help is much appreciated.
Edit 1: I have modified index definition to eliminate LookUp operator => fewer logical reads.
Edit 2: I have added the second solution based on quirky update method. Please read this article (Solving the Running Total and Ordinal Rank Problems) written by Jeff Moden.
First solution can be tested with SQL Server 2005/2008:
--Create test table
CREATE TABLE dbo.MeterValues
(
ID INT IDENTITY(1,1) PRIMARY KEY
,[Timestamp] DATETIME NOT NULL
,MeterID INT NOT NULL
,RawValue INT NOT NULL
);
CREATE UNIQUE INDEX IUN_MeterValues_MeterID_Timestamp
--SQL Server 2008
ON dbo.MeterValues (MeterID, [Timestamp])
INCLUDE (RawValue)
--SQL Server 2005
--ON dbo.MeterValues (MeterID, [Timestamp],RawValue)
--DROP INDEX dbo.MeterValues.IUN_MeterValues_MeterID_Timestamp
--Insert some values
INSERT dbo.MeterValues ([Timestamp], MeterID, RawValue)
SELECT '2011-01-01T00:00:00', 1, 100
UNION ALL
SELECT '2011-01-01T00:00:15', 1, 105
UNION ALL
SELECT '2011-01-01T00:00:30', 1, 102
UNION ALL
SELECT '2011-01-01T00:00:45', 1, 108
UNION ALL
SELECT '2011-01-01T00:01:00', 1, 109
UNION ALL
SELECT '2011-01-01T00:00:00', 2, 1000
UNION ALL
SELECT '2011-01-01T00:00:15', 2, 900
UNION ALL
SELECT '2011-01-01T00:00:30', 2, 1105
UNION ALL
SELECT '2011-01-01T00:00:45', 2, 1050
UNION ALL
SELECT '2011-01-01T00:01:00', 2, 910;
--Check test data
SELECT *
FROM dbo.MeterValues mv
ORDER BY mv.MeterID, mv.ID DESC;
--Solution
WITH ValuesWithRowNumber
AS
(
SELECT mv.MeterID
,mv.RawValue
,mv.[Timestamp]
,ROW_NUMBER() OVER(PARTITION BY mv.MeterID ORDER BY mv.[Timestamp] ASC) RowNum
FROM dbo.MeterValues mv
)
SELECT crt.MeterID
,crt.[Timestamp] AS CrtTimestamp
,prev.[Timestamp] AS PrevTimestamp
,crt.RawValue AS CrtRawValue
,prev.RawValue AS PrevRawValue
,(crt.RawValue - prev.RawValue)*1.00/DATEDIFF(SECOND, prev.[Timestamp], crt.[Timestamp]) Diff
,STR((crt.RawValue - prev.RawValue)*1.00/DATEDIFF(SECOND, prev.[Timestamp], crt.[Timestamp])*100, 10, 2)+'%' [Percent]
FROM ValuesWithRowNumber crt --crt=current
LEFT JOIN ValuesWithRowNumber prev ON crt.MeterID = prev.MeterID --prev=previous
AND crt.RowNum - 1 = prev.RowNum
ORDER BY crt.MeterID, crt.[Timestamp] DESC;
--By, by
DROP TABLE dbo.MeterValues;
Results:
MeterID CrtTimestamp PrevTimestamp CrtRawValue PrevRawValue Diff Percent
----------- ----------------------- ----------------------- ----------- ------------ --------------------------------------- -----------
1 2011-01-01 00:01:00.000 2011-01-01 00:00:45.000 109 108 0.0666666666666 6.67%
1 2011-01-01 00:00:45.000 2011-01-01 00:00:30.000 108 102 0.4000000000000 40.00%
1 2011-01-01 00:00:30.000 2011-01-01 00:00:15.000 102 105 -0.2000000000000 -20.00%
1 2011-01-01 00:00:15.000 2011-01-01 00:00:00.000 105 100 0.3333333333333 33.33%
1 2011-01-01 00:00:00.000 NULL 100 NULL NULL NULL
2 2011-01-01 00:01:00.000 2011-01-01 00:00:45.000 910 1050 -9.3333333333333 -933.33%
2 2011-01-01 00:00:45.000 2011-01-01 00:00:30.000 1050 1105 -3.6666666666666 -366.67%
2 2011-01-01 00:00:30.000 2011-01-01 00:00:15.000 1105 900 13.6666666666666 1366.67%
2 2011-01-01 00:00:15.000 2011-01-01 00:00:00.000 900 1000 -6.6666666666666 -666.67%
2 2011-01-01 00:00:00.000 NULL 1000 NULL NULL NULL
The second solution can/should work with SQL 2000/2005/2008 (please read "The RULES" section from Jeff Moden article):
--Create test table
CREATE TABLE dbo.MeterValues
(
MeterID INT NOT NULL
,[Timestamp] DATETIME NOT NULL
,RawValue INT NOT NULL
,Diff NUMERIC(10,3) NULL
,PRIMARY KEY CLUSTERED(MeterID,[Timestamp])
);
--Insert some values
INSERT dbo.MeterValues ([Timestamp], MeterID, RawValue)
SELECT '2011-01-01T00:00:00', 1, 100
UNION ALL
SELECT '2011-01-01T00:00:15', 1, 105
UNION ALL
SELECT '2011-01-01T00:00:30', 1, 102
UNION ALL
SELECT '2011-01-01T00:00:45', 1, 108
UNION ALL
SELECT '2011-01-01T00:01:00', 1, 109
UNION ALL
SELECT '2011-01-01T00:00:00', 2, 1000
UNION ALL
SELECT '2011-01-01T00:00:15', 2, 900
UNION ALL
SELECT '2011-01-01T00:00:30', 2, 1105
UNION ALL
SELECT '2011-01-01T00:00:45', 2, 1050
UNION ALL
SELECT '2011-01-01T00:01:00', 2, 910;
--Check test data
SELECT *
FROM dbo.MeterValues mv
ORDER BY mv.MeterID, mv.[Timestamp];
DECLARE #OldRawValue INT
,#Diff NUMERIC(10,3)
,#OldMeterID INT
,#OldTimestamp DATETIME;
PRINT '*****Star*****'
--Calculations
UPDATE dbo.MeterValues WITH(TABLOCKX)
SET #Diff = CASE WHEN #OldMeterID = MeterID THEN (RawValue - #OldRawValue)*1.00/DATEDIFF(SECOND,#OldTimeStamp,[TimeStamp]) END
,Diff = #Diff
,#OldRawValue = RawValue
,#OldMeterID = MeterID
,#OldTimestamp = [Timestamp]
OPTION(MAXDOP 1);
--Results
SELECT *
FROM dbo.MeterValues mv
ORDER BY mv.MeterID, mv.[Timestamp];
PRINT '*****Stop*****'
--By, by
DROP TABLE dbo.MeterValues;
Results:
MeterID Timestamp RawValue Diff
----------- ----------------------- ----------- ---------------------------------------
1 2011-01-01 00:01:00.000 109 0.067
1 2011-01-01 00:00:45.000 108 0.400
1 2011-01-01 00:00:30.000 102 -0.200
1 2011-01-01 00:00:15.000 105 0.333
1 2011-01-01 00:00:00.000 100 NULL
2 2011-01-01 00:01:00.000 910 -9.333
2 2011-01-01 00:00:45.000 1050 -3.667
2 2011-01-01 00:00:30.000 1105 13.667
2 2011-01-01 00:00:15.000 900 -6.667
2 2011-01-01 00:00:00.000 1000 NULL
Try this
Select a.metered,a.timestamp,
(a.rawValue-b.rawValue)/(a.timestamp-b.timestamp)
From meters A
Join (selec top 2 rawValue,Timestamp
From meters where metered = #meter
order by timestamp DESC) b
On b.timestamp <> a.timestamp and a.meterId=B.meterId
Added a DESC to the timestamp in the middle query. This will cause the most recent two timestamps to be returned, and then the JOIN will "filter out" the one that matches the current row from A
I made a few minor changes to both my query and #Bogdan's query to make them as similar as possible, then compared them. Bogdan's modified query is at the bottom of this post.
Stacked together in the same query, according to the SQL Server Query Execution Plan, mine is 53% of the query cost, and Bogdan's is 47%.
For the data set offered in the Bogdan's post:
My query: 6 scans and 27 logical reads
Bogdan's: 6 scans and 72 logical reads
I added values every 15secs up to 5mins for both meterID 1 and 2, for a total of 42 records, then reran the queries with SQL Server Profiler.
Where my query wins on reads, Bogdan's still wins on CPU and Duration.
CPU SCANS READS DURATION
--------------------------------------
Mine 47 22 313 249ms
Bogdan's 16 22 972 15ms
--------------------------------------
I'm making a few assumptions, like that your MeterID is an INT. Change that as necessary.
I'm also assuming since you want to run the query for a specific meter ID, that it will be passed in as a parameter to a stored procedure.
This should work on SQL Server 2005 and later.
I do a few things that might distract from the actual solution. The core logic is really within the WHILE loop.
CREATE PROCEDURE [dbo].[GetMeterResults]
#MeterID INT
AS
BEGIN
-- create a temp table to store results
CREATE TABLE #tempResults
(
MeterID INT,
[Timestamp] DATETIME,
Result FLOAT
)
DECLARE
#Timestamp DATETIME,
#RawValue INT,
#LastTimestamp DATETIME,
#LastRawValue INT,
#FirstRun BIT = 1
DECLARE cr CURSOR FAST_FORWARD FOR
SELECT
[Timestamp],
RawValue
FROM
YourTable
WHERE
MeterID = #MeterID
ORDER BY
[Timestamp]
OPEN cr
FETCH NEXT FROM cr INTO #Timestamp, #RawValue
WHILE (##FETCH_STATUS = 0)
BEGIN
IF (#FirstRun = 1)
BEGIN -- the first run
SELECT #FirstRun = 0 -- flip the bit for all future runs
END
ELSE -- all subsequent runs after the first
BEGIN
INSERT INTO
#tempResults
SELECT
#MeterID,
#Timestamp,
(#RawValue - #LastRawValue) * 1.00 / DATEDIFF(s, #LastTimestamp, #Timestamp)
END
-- save the current values for comparison on the next run
SELECT
#LastTimestamp = #Timestamp,
#LastRawValue = #RawValue
FETCH NEXT FROM cr INTO #Timestamp, #RawValue
END
CLOSE CR
DEALLOCATE CR
-- return the result set
SELECT
*
FROM
#tempResults
-- clean up the temp table
DROP TABLE #tempResults
END
GO
Bogdan's modified query that filters by MeterID for an apples-to-apples comparison with my query above:
DECLARE #MeterID INT = 1;
WITH ValuesWithRowNumber
AS
(
SELECT mv.MeterID
,mv.RawValue
,mv.[Timestamp]
,ROW_NUMBER() OVER(PARTITION BY mv.MeterID ORDER BY mv.[Timestamp] ASC) RowNum
FROM dbo.MeterValues mv
WHERE mv.MeterID = #MeterID
)
SELECT crt.MeterID
,crt.[Timestamp] AS CrtTimestamp
,prev.[Timestamp] AS PrevTimestamp
,crt.RawValue AS CrtRawValue
,prev.RawValue AS PrevRawValue
,(crt.RawValue - prev.RawValue)*1.00/DATEDIFF(SECOND, prev.[Timestamp], crt.[Timestamp]) Diff
FROM ValuesWithRowNumber crt --crt=current
JOIN ValuesWithRowNumber prev ON crt.RowNum - 1 = prev.RowNum
ORDER BY crt.[Timestamp];

Resources