I have a GPS app that saves data to the following table:
CREATE TABLE [dbo].[T_Tracking]
(
[id] [int] IDENTITY(1,1) NOT NULL,
[IMEI] [nvarchar](50) NULL,
[TrackTime] [datetime] NULL,
[Longitude] [nvarchar](50) NULL,
[Lattitude] [nvarchar](50) NULL,
[speed] [float] NULL,
[CarID] [int] NULL,
[Country] [nvarchar](50) NULL,
[City] [nvarchar](50) NULL,
[Area] [nvarchar](50) NULL,
[Street] [nvarchar](50) NULL,
[FullAddress] [nvarchar](150) NULL,
[Distance] [float] NULL
-- ...
)
I want to make a trip query pulling back start time & speed, and end time & speed.
This is my query:
SELECT id
, IMEI
, TrackTime as StartTime
, speed as StartSpeed
, CarID
, FullAddress
, (
SELECT TOP (1) TrackTime AS Expr1
FROM T_Tracking AS E2
WHERE (CarID = E1.CarID)
AND (id > E1.id)
AND (speed <5)
ORDER BY id desc
) AS StopTime
, (
SELECT TOP (1) speed AS Expr1
FROM T_Tracking AS E2
WHERE (CarID = E1.CarID)
AND (id > E1.id)
AND (speed <5)
ORDER BY id desc
) AS EndSpeed
FROM T_Tracking AS E1
WHERE (speed > 5)
order by id desc
It works fine, but to decide that it is the end if the trip the car should be stopping for 15 minutes (i.e. as the car might stop in traffic for a minute or 2, so we don't want that to count as the end of the trip).
How can I add this logic?
Additionally, I need to sum the distance field to get the trip total distance.
Sample Table Data:
The desired result is:
Notes:
the GPS get save a record every 30 sec
the car may be stopping in traffic so to decide it is the end of the trip it must be stopping for 15 min
stop not always speed=0 it will be speed<5 (Device accuracy / tolerance)
distance is the distance between the current point and the previous (distance in the 30 sec)
This is demo how you can get stops. For simplicity I used all INT to represent time and speed, the query can be easily adapted for DATETIME data type. The stop is logged if it's duration > 4.
SELECT CarID, stopped, grp
, min(TrackTime) stopStartTime
, max(TrackTime) + 1 - min(TrackTime) stopDuration
FROM (
SELECT id, TrackTime, CarID, stopped
, row_number() over (partition by CarID order by TrackTime) - row_number() over (partition by CarID, stopped order by TrackTime) grp
FROM (
SELECT id, TrackTime, CarID, CASE WHEN speed > 5 THEN 0 ELSE 1 END stopped
FROM (
-- Demo data
VALUES
(0,0,10, 2)
,(1,1,10, 8)
,(2,2,10, 8)
,(3,3,10, 4)
,(4,4,10, 4)
,(5,5,10, 4)
,(6,6,10, 4)
,(7,7,10, 4)
,(8,8,10, 4)
,(9,9,10, 8)
) T_Tracking (id, TrackTime, CarID, speed)
) g
) t
GROUP BY CarID, stopped, grp
HAVING max(TrackTime) + 1 - min(TrackTime) > 4
ORDER BY min(TrackTime)
Try this (untested):
select JourneyBounds.id
, JourneyBounds.IMEI
, JourneyBounds.TrackTime as StartTime
, JourneyBounds.speed as StartSpeed
, JourneyBounds.CarID
, JourneyBounds.FullAddress
, max(journey.TrackTime) StopTime
, max(case when journey.id = JourneyBounds.EndOfJourneyId then journey.speed else null end) EndSpeed
, JourneyBounds.Distance + sum(journey.Distance) TotalDistance
from (
select *
, (
select min(id)
from T_Tracking EndOfJourney
where EndOfJourney.Id > StartOfJourney.Id
and EndOfJourney.CarId = StartOfJourney.CarId
and EndOfJourney.speed < 5
--edit; car must have been stopped for 15 mins; so we need to check that the records after this stop confirm that (i.e. that the car does not move in that time)
and not exists (
select top 1 1
from T_Tracking WaitFifteenMins
where WaitFifteenMins.Id > EndOfJourney.Id
and WaitFifteenMins.TrackTime <= DateAdd(minute, 15, EndOfJourney.TrackTime)
and WaitFifteenMins.speed >= 5
)
--end of edit
) EndOfJourneyId
from T_Tracking StartOfJourney
where StartOfJourney.speed < 5
) JourneyBounds
inner join T_Tracking journey
on journey.CarId = JourneyBounds.CarId
and journey.id > JourneyBounds.Id
and journey.id <= JourneyBounds.EndOfJourneyId
group by JourneyBounds.id
, JourneyBounds.IMEI
, JourneyBounds.TrackTime
, JourneyBounds.speed
, JourneyBounds.CarID
, JourneyBounds.FullAddress
, JourneyBounds.Distance
having count(1) > 1
The JourneyBounds subquery gets all records with speed < 5 (i.e. the start records for any potential journeys). Additionally it pulls back the id of the first stop after that start (i.e. the first record with a greater id which has a speed of less than 5).
It then does an inner join pulling back all records for the same car which come after the start time, up to & including the end record for this journey. We then calculate the distance by summing all distances of the records on this journey.
The having count(1) > 1 on the end just says if the car's start record is immediately followed by a stop, we can assume it's not moved / there was no journey. Presumably we don't want those non-journeys in our results.
Related
A table looks like this :
CREATE TABLE [dbo].[HistDT](
[ID] [bigint] NULL,
[StartDtSK] [varchar](8) NULL,
[StartDt] [datetime] NULL,
[status] [nvarchar](30) NULL,
) ON [PRIMARY]
Example data set:
ID | StartDtSK | StartDt | Status |
1 20190520 20-05-2019 12:00:13 10
1 20190520 20-05-2019 10:00:00 5
1 20190414 14-04-2019 13:23:00 2
2 20190312 12-03-2019 10:03:00 10
2 20190308 08-03-2019 18:03:00 1
etc..
I need a query which will display the number of days spent in each status. That would be easy if the table i inherited had an end date. I would then calculate the datediff and pivot for column status values.
Maybe i should create a new table using ssis where i will add an EndDt column which will be the StartDt of the latest added Status.
But is there any way to do this without creating another table?
SQL Server 2008
This is not very pretty, and I haven't tested it for all use cases. I hope you can use it or find inspiration. I'm sure there is a better way :)
declare #table2 table (
[ID] [bigint] NULL,
[StartDtSK] [varchar](8) NULL,
[StartDt] [datetime] NULL,
[status] [nvarchar](30) NULL
)
insert into #table2
values
(1 , '20190520','2019-05-20 12:00:13','10'),
(1 , '20190520','2019-05-20 10:00:00','5'),
(1 , '20190414','2019-04-14 13:23:00','2'),
(2, '20190312', '2019-03-12 10:03:00', '10'),
(2 , '20190308', '2019-03-08 18:03:00', '1')
select *,DATEDIFF(dd,startdt,enddate) as TotalDAys from (
select x.ID,StartDtSK,Startdt,[Status],Enddate from (
select *,ROW_NUMBER() over(partition by id order by startdt) as rn from #table2
) x
cross apply ( select * from (select id,StartDt as Enddate,ROW_NUMBER() over(partition by id order by startdt) as rn2 from #table2 b
)f where (rn +1 = f.rn2 ) and x.id = f.id ) d
union all
select ID,StartDtSK,startdt,[Status],'9999-12-31' as Enddate from (
select *,ROW_NUMBER() over(partition by id order by startdt desc) as rn from #table2
)X where rn=1
)y
order by id,startdt
SQL Server 2008 without cross apply
This might be a bit more pretty :)
select *,DATEDIFF(dd,startdt,enddate) as TotalDAys from (
select x.ID,StartDtSK,Startdt,[Status],case when Enddate is null then '9999-12-31' else Enddate end as Enddate from (
select *,ROW_NUMBER() over(partition by id order by startdt) as rn from #table2
) x
left join (
select * from (select id,StartDt as Enddate,ROW_NUMBER() over(partition by id order by startdt) as rn2 from #table2 b
)f ) d on (rn +1 = d.rn2 ) and x.id = d.id
)y
SQL Server 2012 and above:
Is this what you want?
declare #table2 table (
[ID] [bigint] NULL,
[StartDtSK] [varchar](8) NULL,
[StartDt] [datetime] NULL,
[status] [nvarchar](30) NULL
)
insert into #table2
values
(1 , '20190520','2019-05-20 12:00:13','10'),
(1 , '20190520','2019-05-20 10:00:00','5'),
(1 , '20190414','2019-04-14 13:23:00','2')
select *,Datediff(dd,Startdt,Enddate) as TotalDays from (
select *,LAG(StartDt,1,'9999-12-31') over(partition by ID order by StartDT desc) as EndDate from #table2
)x
Insert a rule that handles current status (9999-12-31) date
Maybe LEAD function is usefull for your question.
Like this
IsNull(DateAdd(SECOND,-1,Cast(LEAD ([StartDt],1) OVER (PARTITION BY [status] ORDER BY [StartDt]) AS DATETIME)),getdate()) AS EndDate
Table Schema:
CREATE TABLE [dbo].[TblPriceDetails](
[PriceID] [int] IDENTITY(1,1) NOT NULL,
[VID] [int] NOT NULL,
TypeID int not null,
[RangeStart] [decimal](18, 3) NOT NULL,
[RangeEnd] [decimal](18, 3) NOT NULL,
[Price] [decimal](18, 2) NOT NULL,
[ExtraLoad] [decimal](18, 3) NULL,
[ExtraPrice] [decimal](18, 2) NULL
)
GO
Sample Data
Insert into dbo.TblPriceDetails values (1,1, 0,0.250,10,0,0)
Insert into dbo.TblPriceDetails values (1,1, 0.251,0.500,15,0.500,15)
Insert into dbo.TblPriceDetails values (1,1, 3,5,40,1,25)
GO
Insert into dbo.TblPriceDetails values (1,2, 0,0.250,15,0,0)
Insert into dbo.TblPriceDetails values (1,2, 0.251,0.500,20,0.500,20)
Insert into dbo.TblPriceDetails values (1,2, 3,5,50,1,30)
GO
Expected Output:
For VID = 1 and TypeID = 1 and a given value 0.300
As the input unit falls between RangeStart 0.251 and RangeEnd 0.500
the resultant price will be 15
For VID = 1 and TypeID = 1 and a given value 0.600
As per the data until 0.500 the price is 15 and for every extraLoad
of upto 0.500 its another 15. So the final price will be 30
For VID = 1 and TypeID = 1 and given value 1.500
As per the data until 0.500 the price is 15. For every extra 0.500
its another 15, so for the remaining 1 unit it would be 15 * 2. The
final price will be 45
For VID = 1 and TypeID = 1 and given value 5.5
As per the data until 5.000 the price is 40. For every extra 1 unit its another 25, so the final price will be 65
Need help in writing a query for this. Unlike my other questions I don't have a query yet to show what I have come up with till now. As of now I am not able to frame a logic and come up with a generic query for this.
It looks like you are looking to calculate postage price. The trick is to join on the RangeStart of the next weight tier. LEAD will help you do that:
;WITH
AdjustedPriceDetails AS
(
SELECT VID, TypeID, RangeStart, RangeEnd, Price, ExtraLoad, ExtraPrice
, ISNULL(LEAD(RangeStart, 1) OVER (PARTITION BY VID, TypeID ORDER BY RangeStart), 1000000) AS NextRangeStart
FROM TblPriceDetails
)
SELECT T.*
, A.Price + IIF(T.Value <= A.RangeEnd, 0, CEILING((T.Value - A.RangeEnd) / A.ExtraLoad) * A.ExtraPrice)
AS FinalPrice
FROM #TestData T
INNER JOIN AdjustedPriceDetails A ON A.RangeStart <= T.Value AND T.Value < A.NextRangeStart
Explanation:
LEAD(RangeStart, 1) OVER (PARTITION BY VID, TypeID ORDER BY RangeStart) gets the RangeStart of the next row that has the same VID and TypeID
You will eventually reach the highest weight tier. So ISNULL(..., 1000000) make this tier appear to end at 1M. The 1M is just a stand-in for infinity.
Edit: if you want to make this work with SQL Server 2008, change the CTE:
;WITH
tmp AS
(
SELECT VID, TypeID, RangeStart, RangeEnd, Price, ExtraLoad, ExtraPrice
, ROW_NUMBER() OVER (PARTITION BY VID, TypeID ORDER BY RangeStart) AS RowNumber
FROM TblPriceDetails
),
AdjustedPriceDetails AS
(
SELECT T1.VID, T1.TypeID, T1.RangeStart, T1.RangeEnd, T1.Price, T1.ExtraLoad, T1.ExtraPrice
, ISNULL(T2.RangeStart, 1000000) AS NextRangeStart
FROM tmp T1
LEFT JOIN tmp T2 ON T1.VID = T2.VID AND T1.TypeId = T2.TypeID AND T1.RowNumber + 1 = T2.RowNumber
)
If you wonder what #TestData is (you may not need it)
CREATE TABLE #TestData
(
VID int
, TypeID int
, Value float
)
INSERT INTO #TestData
( VID, TypeID, Value)
VALUES ( 1, 1, 0.3 )
, ( 1, 1, 0.6 )
, ( 1, 1, 1.5 )
, ( 1, 1, 5.5 )
Given the following tables:
CREATE TABLE [Contact]
(
[Id] INTEGER NOT NULL,
[Uri] CHARACTER VARYING(255) NOT NULL,
[CreatedOn] DATETIMEOFFSET NOT NULL
);
CREATE TABLE [Availability]
(
[Id] TINYINT NOT NULL,
[Name] CHARACTER VARYING(255) NOT NULL,
[CreatedOn] DATETIMEOFFSET NOT NULL
);
CREATE TABLE [ContactAvailability]
(
[Id] BIGINT NOT NULL,
[ContactId] INTEGER NOT NULL,
[AvailabilityId] INTEGER NOT NULL,
[CreatedOn] DATETIMEOFFSET NOT NULL
);
I am attempting to get a list of all of the contacts and the durations for which they have been in any of the availabilities for the current day.
The ContactAvailability table ends up having records such as:
(1, 1, 1, '01/01/2014 08:00:23.51 -07:00'),
(2, 1, 3, '01/01/2014 08:15:38.01 -07:00'),
(3, 1, 3, '01/01/2014 08:15:38.02 -07:00'),
(4, 2, 2, '01/01/2014 08:18:33.12 -07:00')
These records represent a Contact's transition from one Availability to another, and also from one Availability to the same. It is essentially a running status that is logged on an interval.
The query I have come up with only queries for a particular user and only gets a list of their availabilities for the current day, but it won't calculate how long the Contact has been in any Availability. I am not sure where to start when it comes to that.
This is that query:
SELECT [Contact].[Uri] AS [ContactUri],
[Availability].[Name] AS [AvailabilityName],
[ContactAvailability].[CreatedOn]
FROM [ContactAvailability]
INNER JOIN [Contact] ON [Contact].[Id] = [ContactAvailability].[ContactId]
INNER JOIN [Availability] ON [Availability].[Id] = [ContactAvailability].[AvailabilityId]
WHERE [Contact].[Uri] = 'sip:contact#example.com' AND
[ContactAvailability].[CreatedOn] >= '06/30/2014 00:00:00 -07:00' AND
[ContactAvailability].[CreatedOn] < '07/01/2014 00:00:00 -07:00'
You can use a Window Function in combination with a CTE.
I think this should work, not tested yet :) So you might have to change your column names.
with SourceTable
( ContactID, AvailabilityID, NewDate, OldDate)
as(
SELECT ContactAvailability.ContactID AS ContactID,
ContactAvailability.AvailabilityID AS AvailabilityID,
[ContactAvailability].[CreatedOn] As NewDate,
LAG(ContactAvailability.CreatedON) OVER (Partition By ContactAvailability.ContactID order by ContactAvailability.CreatedOn) as OldDate
FROM [ContactAvailability])
SELECT [Contact].[Uri] AS [ContactUri],
[Availability].[Name] AS [AvailabilityName],
SourceTable.OldDate as PreviousAvailabilityDate,
SourceTable.NewDate as CurrentAvailibilityDate,
SourceTable.NewDate - SourceTable.OldDate as DifferenceBetweenAvailability,
[ContactAvailability].[CreatedOn]
FROM SourceTable
INNER JOIN [Contact] ON [Contact].[Id] = SourceTable.[ContactId]
INNER JOIN [Availability] ON [Availability].[Id] = SourceTable.[AvailabilityId]
If you need to calculate the total time somebody has been in a certain availability (f.e. personA is in availability A then B then A again and then C) you will have to add another cte and partition on ContactAvailability.AvailabilityID and then make a sum of your calculated field.
CREATE TABLE [Transaction](
[TransactionID] [bigint] IDENTITY(1,1) NOT NULL,
[LocationID] [int] NOT NULL,
[KioskID] [int] NOT NULL,
[TransactionDate] [datetime] NOT NULL,
[TransactionType] [varchar](7) NOT NULL,
[Credits] [int] NOT NULL,
[StartingBalance] [int] NULL,
[EndingBalance] [int] NULL,
[SessionID] [int] NULL
);
Please refer to this fiddle for the sample data:
Link to SQL Fiddle
I'm trying to figure out if there is a way to assign a session number to a sequence of transactions in a single update.
A "Session" is defined as a number of deposits and purchases ending with a withdrawal. A Session has sequential transactions consisting of:
1 to n deposits (TransactionType = 'D'),
0 to n purchases (TransactionType = 'P') and
0 or 1 withdrawals (TransactionType = 'W')
With the same LocationID and KioskID. A session can end with a 0 balance or a withdrawal. First deposit with no session starts one. Only P transactions have balances. For D and W they are NULL.
LocationID, KioskID, SessionID must be unique.
I'm really hoping that there is a SQL way of doing this. I'd hate to have to loop through hundreds of millions of transactions to set sessions procedurally.
This should do it:
;WITH markSessions as
(
SELECT *,
CASE
WHEN TransactionType='W' THEN 1
WHEN TransactionType='P' And EndingBalance=0 THEN 1
ELSE 0 END As SessionEnd
FROM Transactions
)
SELECT *,
SUM(SessionEnd) OVER(PARTITION BY LocationID, KioskID ORDER BY TransactionID)
+ 1 - SessionEnd As SessionID
FROM markSessions
No triggers, cursors or client code needed.
If you actually want to set the SessionID in the table, then you'd use an UPDATE statement like this:
;WITH markSessions as
(
SELECT *,
CASE
WHEN TransactionType='W' THEN 1
WHEN TransactionType='P' And EndingBalance=0 THEN 1
ELSE 0 END As SessionEnd
FROM Transactions
)
UPDATE markSessions
SET SessionID = SUM(SessionEnd) OVER(PARTITION BY LocationID, KioskID ORDER BY TransactionID)
+ 1 - SessionEnd
I am unable to test it, but the following should take into account pre-existing SessionIDs
;WITH markSessions as
(
SELECT *,
CASE
WHEN TransactionType='W' THEN 1
WHEN TransactionType='P' And EndingBalance=0 THEN 1
ELSE 0 END As SessionEnd
FROM Transactions
)
UPDATE markSessions
SET SessionID = SUM(SessionEnd) OVER(PARTITION BY LocationID, KioskID ORDER BY TransactionID)
+ 1 - SessionEnd
+ COALESCE(MAX(SessionID) OVER (PARTITION BY LocationID, KioskID), 0)
WHERE SessionID Is NULL
Note that this will only work if all new rows (those without SessionIDs) have higher transaction IDs than the Pre-existing rows (those that already have SessionIDs). It definitely NOT work if new rows were added with TransactionIDs, lower than the highest TransactionID already assigned a SessionID.
If you may have that situation, then you likely will have to reassign the old TransactionIDs.
Basically I want to duplicate a row a variable number of times.
I have a table with the following structure:
CREATE TABLE [dbo].[Start](
[ID] [int] NOT NULL,
[Apt] [int] NOT NULL,
[Cost] [int] NOT NULL)
I want to duplicate each row in this table (Apt-1) times so in the end there will be #Apt rows. Moreover for each new row the value of Cost is decremented until it reaches 0. ID will be the same as there are no primary keys. If I have a record like this:
1 5 3
I need 4 new rows inserted in the same table and they should look like this
1 5 2
1 5 1
1 5 0
1 5 0
I have tried so far a lot of ways but I cannot make it work. Many thanks!
try this
DECLARE #Start TABLE (
[ID] [int] NOT NULL,
[Apt] [int] NOT NULL,
[Cost] [int] NOT NULL)
INSERT #Start (ID, Apt, Cost)
VALUES (1, 5, 3)
; WITH CTE_DIGS AS (
SELECT ROW_NUMBER() OVER(ORDER BY (SELECT 1)) AS rn
FROM master.sys.all_columns AS a
)
INSERT #Start (ID, Apt, Cost)
SELECT ID, Apt, CASE WHEN Cost - rn < 0 THEN 0 ELSE Cost - rn END
FROM #Start
INNER JOIN CTE_DIGS
ON Apt > rn
Try:
;with cte as
(select [ID], [Apt], [Cost], 1 counter from [Start]
union all
select [ID],
[Apt],
case sign([Cost]) when 1 then [Cost]-1 else 0 end [Cost],
counter+1 counter
from cte where counter < [Apt])
select [ID], [Apt], [Cost]
from cte