Calculating distance using geometry of x and y location in SQL - sql

I'm using SQL Server and I need to calculate the distance between the x and y of a frame and the previous x and y of a frame where the day, team, and member are all the same. Currently, I have this code that works but doesn't accomplish what I need. I'm getting every distance permutation of the x and y location where the day, team, and member are all the same.
I need help to incorporate frames into the query so that I get the N+1 Frame x and y location minus the N Frame x and y location.
CREATE TABLE TestTable (
Day int NULL,
Frame int NULL,
Team int NULL,
Member int NULL,
x float NULL,
y float NULL
);
Insert into a Values
(1, 1, 1, 1, 1486.64, 2017.55),
(1, 1, 1, 2, 1754.55, 1495.81),
(1, 1, 2,1, 2049.15, 876.349),
(1, 2, 1, 1, 1707.59, 1171.22),
(1, 2, 1, 2, 1432.56, 1459.99),
(1, 2, 2, 1, 1470.27, 1086.22),
(1, 3, 1, 1, 3639.19, 1281.36),
(1, 3, 1, 2, 2751.37, 976.348),
(1, 3, 2, 1, 2496.69, 1283.29),
(1, 4, 1, 1, 2347.26, 984.255),
(1, 4, 1, 2, 2044.92, 711.154),
(1, 4, 2, 1, 2473.65, 1816.23);
Select A.Day, A.Frame, A.Team, A.Member,
GEOMETRY::Point(A.[x], A.[y], 0).STDistance(GEOMETRY::Point(B.[x], B.[y], 0)) As Distance
From a A
Join a B
ON A.Day = B.Day and A.Team = B.Team and A.Member = B.Member
I also may deal with NULL x and y values so if it's possible to add this to the query too.
Where A.x IS NOT NULL and A.y IS NOT NULL
Ultimately I want to track the distance of every member throughout the day, frame by frame.Later, I'll add up each member's total distance for the day.

;WITH CTE1 AS
(
SELECT
[day], team, member, frame, x, y,
LAG(x) OVER (PARTITION BY [day], team, member ORDER BY frame) AS PervFrameX,
LAG(y) OVER (PARTITION BY [day], team, member ORDER BY frame) AS PervFrameY
FROM
TestTable
WHERE
X IS NOT NULL AND Y IS NOT NULL
),
CTE2 AS
(
SELECT
[day], team, member, frame, x, y, PervFrameX, PervFrameY,
IIF(PervFrameX IS NULL OR PervFrameY IS NULL, 0,
GEOMETRY::Point(x, y, 0).STDistance(GEOMETRY::Point(PervFrameX, PervFrameY, 0))) As Distance
FROM
CTE1
)
SELECT
*,
SUM(Distance) OVER (PARTITION BY [day], team, member) AS MemberTotalDistance,
SUM(Distance) OVER (PARTITION BY [day]) AS DailyTotalDistance
FROM
CTE2
ORDER BY
[day], team, member, frame
CTE1 and CTE2 are used to improve readability of the query.
Output:
day team member frame x y PervFrameX PervFrameY Distance MemberTotalDistance DailyTotalDistance
1 1 1 1 1486.64 2017.55 NULL NULL 0.000 4135.086 8812.698
1 1 1 2 1707.59 1171.22 1486.64 2017.55 874.696 4135.086 8812.698
1 1 1 3 3639.19 1281.36 1707.59 1171.22 1934.738 4135.086 8812.698
1 1 1 4 2347.26 984.255 3639.19 1281.36 1325.652 4135.086 8812.698
1 1 2 1 1754.55 1495.81 NULL NULL 0.000 2483.257 8812.698
1 1 2 2 1432.56 1459.99 1754.55 1495.81 323.976 2483.257 8812.698
1 1 2 3 2751.37 976.348 1432.56 1459.99 1404.695 2483.257 8812.698
1 1 2 4 2044.92 711.154 2751.37 976.348 754.586 2483.257 8812.698
1 2 1 1 2049.15 876.349 NULL NULL 0.000 2194.355 8812.698
1 2 1 2 1470.27 1086.22 2049.15 876.349 615.750 2194.355 8812.698
1 2 1 3 2496.69 1283.29 1470.27 1086.22 1045.167 2194.355 8812.698
1 2 1 4 2473.65 1816.23 2496.69 1283.29 533.438 2194.355 8812.698

Related

How to make new data is a function of last data

In SQL server I have a table just like the following table, original, and I want to update where Index ID>3
and the principle is lastaccmulated*2 + movement.
For example
where Index ID =3 accumulated = 8 * 2 + 2 =18
I tried the lag function but it can only be used in select, which means I cannot finish in one update.
Is there any sharp function to make this happen?
Table orginal
IndexID
accumulated
movement
1
5
2
2
8
2
3
0
2
4
0
2
5
0
2
Table what I want after update
IndexID
accumulated
movement
1
5
2
2
8
2
3
18
2
4
38
2
5
78
2
Just like above mention, it went wrong when I use lag function.
Try this:
DROP TABLE IF EXISTS #YOUR_TABLE
SELECT
id,
accumulated,
movement
INTO #YOUR_TABLE
FROM (
VALUES
(1, 5, 2),
(2, 8, 2),
(3, 0, 2),
(4, 0, 2),
(5, 0, 2),
(6, 0, 2)
) src (id, accumulated, movement)
;WITH
CALCULATION AS (
SELECT
id,
2 * accumulated + movement as accumulated
FROM #YOUR_TABLE
WHERE id = 2
UNION ALL
SELECT
yt.id,
2 * c.accumulated + yt.movement as accumulated
FROM CALCULATION c
JOIN #YOUR_TABLE yt ON yt.id = c.id + 1
)
UPDATE yt SET
yt.accumulated = c.accumulated
FROM #YOUR_TABLE yt
JOIN CALCULATION c ON
c.id = yt.id
WHERE
yt.id >= 3
OPTION (MAXRECURSION 0) -- To prevent recursion limitiations
SELECT * FROM #YOUR_TABLE
We are using recursive CTE here. Before UNION ALL we give values for step zero, after we have calculation based on previous step (yt.id = c.id + 1).

Count all rows while not counting any row after a negative value

I have a table t with:
PLACE
LOCATION
TS
ID
AMOUNT
GOING_IN
GOING_OUT
1
10
2020-10-01
1
100
10
0
1
10
2020-10-02
1
110
5
-50
1
10
2020-10-03
1
75
0
-100
1
10
2020-10-04
1
-25
30
0
1
10
2020-10-05
1
5
0
0
1
10
2020-10-06
1
5
38
-300
1
10
2020-10-07
1
-257
0
0
1
10
2020-10-01
2
1
10
0
1
10
2020-10-02
2
11
0
-12
1
10
2020-10-03
2
-1
0
-100
1
10
2020-10-04
2
-101
0
0
2
20
2020-11-15
1
18
20
0
2
20
2020-11-16
1
38
0
0
2
20
2020-11-15
3
-9
20
-31
2
20
2020-11-16
3
-20
0
0
So due to SAP legacy stuff some logistic data is mangled which may lead to negative inventory.
To check how severe the error is I need to count for each PLACE, LOCATION, ID
the number of rows that have a positive AMOUNT AND which do not have a negative AMOUNT before
the number of rows that have a negative AMOUNT AND any positive AMOUNT that has a negative AMOUNT anywhere before
As you can see in my table there are (for PLACE=1, LOCATION=10, ID=1) 3 rows with a positive AMOUNT without any negative AMOUNT before. But then there is a negative AMOUNT and some positive AMOUNTS afterwards --> those 4 rows should not be counted for COUNT_CORRECT but should count for COUNT_WRONG.
So in this example table my query should return:
PLACE
LOCATION
TOTAL
COUNT_CORRECT
COUNT_WRONG
RATIO
1
10
11
5
6
0.55
2
20
4
2
2
0.5
My code so far:
CREATE OR REPLACE TABLE ANALYTICS.t (
PLACE INT NOT NULL
, LOCATION INT NOT NULL
, TS DATE NOT NULL
, ID INT NOT NULL
, AMOUNT INT NOT NULL
, GOING_IN INT NOT NULL
, GOING_OUT INT NOT NULL
, PRIMARY KEY(PLACE, LOCATION, ID, TS)
);
INSERT INTO ANALYTICS.t
(PLACE, LOCATION, TS, ID, AMOUNT, GOING_IN, GOING_OUT)
VALUES
(1, 10, '2020-10-01', 1, 100, 10, 0)
, (1, 10, '2020-10-02', 1, 110, 5, -50)
, (1, 10, '2020-10-03', 1, 75, 0, -100)
, (1, 10, '2020-10-04', 1, -25, 30, 0)
, (1, 10, '2020-10-05', 1, 5, 0, 0)
, (1, 10, '2020-10-06', 1, 5, 38, 300)
, (1, 10, '2020-10-07', 1, -257, 0, 0)
, (1, 10, '2020-10-04', 2, 1, 10, 0)
, (1, 10, '2020-10-05', 2, 11, 0, -12)
, (1, 10, '2020-10-06', 2, -1, 0, -100)
, (1, 10, '2020-10-07', 2, -101, 0, 0)
, (2, 20, '2020-11-15', 1, 18, 12, 0)
, (2, 20, '2020-11-16', 1, 30, 0, 0)
, (2, 20, '2020-11-15', 3, -9, 20, -31)
, (2, 20, '2020-11-16', 3, -20, 0, 0)
;
Then
SELECT PLACE
, LOCATION
, SUM(CASE WHEN AMOUNT >= 0 THEN 1 ELSE 0 END) AS 'COUNT_CORRECT'
, SUM(CASE WHEN AMOUNT < 0 THEN 1 ELSE 0 END) AS 'COUNT_WRONG'
, ROUND((SUM(CASE WHEN AMOUNT < 0 THEN 1 ELSE 0 END) / COUNT(AMOUNT)) * 100, 2) AS 'ratio'
FROM t
GROUP BY PLACE, LOCATION
ORDER BY PLACE, LOCATION
;
But I don't know how I can filter for "AND which do not have a negative AMOUNT before" and counting by PLACE, LOCATION, ID as an intermediate step.
Any help appreciated.
I'm not sure if I understand your question correctly, but the following gives you the number of rows before the first negative amount per (place, location) partition.
The subselect computes the row numbers of all rows with a negative amount. Then we can select the minimum of this as the first row with a negative amount.
SELECT
place,
location,
COUNT(*) - NVL(MIN(pos) - 1, COUNT(*)) AS COUNT_WRONG,
COUNT(*) - local.COUNT_WRONG AS COUNT_CORRECT,
ROUND(local.COUNT_WRONG / COUNT(*),2) AS RATIO
FROM
( SELECT
amount,
place,
location,
CASE
WHEN amount < 0
THEN ROW_NUMBER() over (
PARTITION BY
place,
location
ORDER BY
"TIMESTAMP")
ELSE NULL
END pos -- Row numbers of rows with negative amount, else NULL
FROM
t)
GROUP BY
place,
location;
I have edited the query. Please let me know if this works.
ALL_ENTRIES query has all the row numbers for the table t partitioned by place,location and ID and ordered by timestamp.
TABLE1 is used to compute the first negative entry. This is done by joining with ALL_ENTRIES and selecting the minimum row number where amount < 0.
TABLE2 is used to compute the last correct entry. Basically ALL_ENTRIES is joined with TABLE1 with the condition that the row numbers should be lesser than the row number in TABLE1. This will give us the row number corresponding to the last correct entry.
TABLE1 and TABLE2 are joined with ALL_ENTRIES to calculate the max row number, which gives the total entries.
In the final select statement I have used case when statement to account for IDs where there are no negative amount values. In those scenarios all the entries should be correct. Hence, the max row number is considered for those cases.
WITH ALL_ENTRIES AS (
SELECT
PLACE,
LOCATION,
ID,
TIMESTAMP,
AMOUNT,
ROW_NUMBER() OVER(PARTITION BY PLACE,LOCATION,ID ORDER BY TIMESTAMP) AS 'ROW_NUM'
FROM t)
SELECT
PLACE,
LOCATION,
ID,
TOTAL,
COUNT_CORRECT,
TOTAL - COUNT_CORRECT AS COUNT_WRONG,
COUNT_CORRECT / TOTAL AS RATIO
FROM
(SELECT
ae.PLACE,
ae.LOCATION,
ae.ID,
MAX(ae.ROW_NUM) as TOTAL,
MAX (CASE WHEN table2.LAST_CORRECT_ENTRY IS NULL THEN ae.ROW_NUM ELSE table2.LAST_CORRECT_ENTRY END) AS COUNT_CORRECT,
FROM
ALL_ENTRIES ae
LEFT JOIN
(SELECT
ae.PLACE,
ae.LOCATION,
ae.ID,
MAX(ae.ROW_NUM) as LAST_CORRECT_ENTRY
FROM
ALL_ENTRIES ae
INNER JOIN
( SELECT
t.PLACE,
t.LOCATION,
t.ID, MIN(ae.ROW_NUM) as FIRST_NEGATIVE_ENTRY
FROM t t
INNER JOIN
ALL_ENTRIES ae ON t.PLACE = ae.PLACE
AND t.LOCATION = ae.LOCATION
AND t.ID = ae.ID
AND t.TIMESTAMP = ae.TIMESTAMP
AND t.AMOUNT = ae.AMOUNT
AND ae.AMOUNT < 0
GROUP BY t.PLACE, t.LOCATION
) table1
ON ae.PLACE = table1.PLACE
AND ae.LOCATION = table1.LOCATION
AND ae.ID = table1.ID
AND ae.ROW_NUM < table1.FIRST_NEGATIVE_ENTRY
GROUP BY ae.PLACE, ae.LOCATION, ae.ID
) table2
ON ae.PLACE = table2.PLACE
AND ae.LOCATION = table2.LOCATION
AND ae.ID = table2.ID
GROUP BY ae.PLACE, ae.LOCATION, ae.ID
)

How to get conditional SUM?

I am trying to get a conditional sum based on another column. For example, suppose I have this dataset:
ID Date Type Total
-----------------------
5 12/16/2019 0 7
5 12/16/2019 1 0
5 12/17/2019 0 7
5 12/17/2019 1 7
5 12/18/2019 0 7
5 12/18/2019 1 0
5 12/19/2019 0 7
5 12/19/2019 1 7
5 12/20/2019 0 7
5 12/20/2019 1 7
5 12/23/2019 0 7
5 12/24/2019 0 7
5 12/25/2019 0 7
5 12/26/2019 0 7
5 12/27/2019 0 7
If there is a type of 1 then I only want that data for that data, else if there is only 0 then I want that data for that date.
So for 12/16/2019 I would want the value 0. For 12/23/2019 - 12/27/2019 I would want the value 7.
You can use row_number() :
select t.*
from (select t.*, row_number() over (partition by id, date order by type desc) as seq
from table t
) t
where seq = 1;
A simple ROW_NUMBER can handle this quite easily. I changed some of the column names because reserved words are just painful to work with.
declare #Something table
(
ID int
, SomeDate Date
, MyType int
, Total int
)
insert #Something values
(5, '12/16/2019', 0, 7)
, (5, '12/16/2019', 1, 0)
, (5, '12/17/2019', 0, 7)
, (5, '12/17/2019', 1, 7)
, (5, '12/18/2019', 0, 7)
, (5, '12/18/2019', 1, 0)
, (5, '12/19/2019', 0, 7)
, (5, '12/19/2019', 1, 7)
, (5, '12/20/2019', 0, 7)
, (5, '12/20/2019', 1, 7)
, (5, '12/23/2019', 0, 7)
, (5, '12/24/2019', 0, 7)
, (5, '12/25/2019', 0, 7)
, (5, '12/26/2019', 0, 7)
, (5, '12/27/2019', 0, 7)
select ID
, SomeDate
, MyType
, Total
from
(
select *
, RowNum = ROW_NUMBER()over(partition by SomeDate order by MyType)
from #Something
) x
where x.RowNum = 1
You can do this with simple aggregation . . . well, and case:
select id, date, max(type),
coalesce(max(case when type = 1 then total end),
max(total)
) as total
from t
group by id, date;
This formulation is assuming that you have only types 0 and 1 and at most one of each type on each day for a given id.

Running Sum that resets to 0 on each new cluster of consecutives

I have tried and failed to adapt several running sum methods (remember I have to use SQL Server 2008, so it's a bit trickier than in 2012).
The goal is to have a running sum of Amount ordered by Date. Any time Category field changes value during that list, the sum should restart.
Table structure:
[Date], [Category], [Amount]
Example:
[Date], [Category], [Amount], [RunSumReset]
-------------------------------------------
1-Jan, catA, 10, 10
2-Jan, catA, 5, 15
3-Jan, catA, 15, 30
15-Jan, catB, 3, 3
1-Feb, catB, 6, 9
11-Feb, catA, 10, 10
12-Feb, catC, 2, 2
1-Apr, catA, 5, 5
Thanks so much for any slick tips or tricks
Using Version 2008 makes things a bit trickier since the window version of SUM with ORDER BY clause is not available.
One way to do it is:
WITH CTE AS (
SELECT [Date], Category, Amount,
ROW_NUMBER() OVER (ORDER BY [Date]) -
ROW_NUMBER() OVER (PARTITION BY Category
ORDER BY [Date]) AS grp
FROM mytable
)
SELECT [Date], Category, Amount, Amount + COALESCE(t.s, 0) AS RunSumReset
FROM CTE AS c1
OUTER APPLY (
SELECT SUM(c2.Amount)
FROM CTE AS c2
WHERE c2.[Date] < c1.[Date] AND
c1.Category = c2.Category AND
c1.grp = c2.grp) AS t(s)
ORDER BY [Date]
The CTE is used to calculate field grp that identifies islands of consecutive records having the same Category. Once Category changes, grp value also changes. Using this CTE we can calculate the running total the way it is normally done in versions prior to SQL Server 2012, i.e. using OUTER APPLY.
Select sum of amounts in current row and up to first row that has different category. In your case you will need to replace NULL with some min date that SQL Server supports, like '17530101':
DECLARE #t TABLE
(
category INT ,
amount INT ,
ordering INT
)
INSERT INTO #t
VALUES ( 1, 1, 1 ),
( 1, 2, 2 ),
( 1, 3, 3 ),
( 2, 4, 4 ),
( 2, 5, 5 ),
( 3, 6, 6 ),
( 1, 7, 7 ),
( 1, 8, 8 ),
( 4, 9, 9 ),
( 1, 10, 10 )
SELECT category ,
amount ,
( SELECT SUM(amount)
FROM #t
WHERE category = t.category
AND ordering <= t.ordering
AND ordering > ( SELECT ISNULL(MAX(ordering), 0)
FROM #t
WHERE category <> t.category
AND ordering < t.ordering
)
) AS sum
FROM #t t
ORDER BY t.ordering
Output:
category amount sum
1 1 1
1 2 3
1 3 6
2 4 4
2 5 9
3 6 6
1 7 7
1 8 15
4 9 9
1 10 10

Soccer league table standings with SQL

Perhaps a familiar table for many people. A soccer league table.
But, in this list there is one mistake, rank 4 and 5, are totally equal, so these teams should not be ranked 4 and 5, but 4 and 4, and then the ranking should continue with 6.
Ranking | Team | Points | Goals difference | Goals scored | Goals against
1 A 3 4 4 0
2 B 3 3 3 0
3 C 3 1 2 1
4 D 3 1 1 0
5 E 3 1 1 0
6 F 1 0 2 2
7 G 1 0 0 0
I have been trying to improve the MS SQL query that produces this table, by using a Common Table Expression and SELECT ROW_Number, but that never gives me the right result. Does anyone have a better idea?
You can do this easy by using the RANK() function.
declare #table as table
(
Team varchar(1),
Points int,
GoalsScored int,
GoalsAgainst int
)
insert into #table values ('A', 3, 4, 0),
('B', 3, 3, 0),
('C', 3, 2, 1),
('D', 3, 1, 0),
('E', 3, 1, 0),
('F', 1, 2, 2),
('G', 1, 0, 0)
select RANK() OVER (ORDER BY points desc, GoalsScored - GoalsAgainst desc, GoalsScored desc) AS Rank
,team
,points
,GoalsScored - GoalsAgainst as GoalsDifference
,GoalsScored
,GoalsAgainst
from #table
order by rank
Here is a possible solution. I'm not sure specifically how you are ranking so I've ranked based on Points DESC, Goals Diff DESC, Goals Scored DESC and Goals Against ASC.
;WITH
src AS (
SELECT Team, Points, GoalsDiff, GoalsScor, GoalsAga
FROM dbo.[stats]
)
,src2 AS (
SELECT Points, GoalsDiff, GoalsScor, GoalsAga
FROM src
GROUP BY Points, GoalsDiff, GoalsScor, GoalsAga
)
,src3 AS (
SELECT ROW_NUMBER() OVER (ORDER BY Points DESC, GoalsDiff DESC, GoalsScor DESC, GoalsAga) AS Ranking
,Points, GoalsDiff, GoalsScor, GoalsAga
FROM src2
)
SELECT src3.Ranking, src.Team, src.Points, src.GoalsDiff, src.GoalsScor, src.GoalsAga
FROM src
INNER JOIN src3
ON src.Points = src3.Points
AND src.GoalsDiff = src3.GoalsDiff
AND src.GoalsScor = src3.GoalsScor
AND src.GoalsAga = src3.GoalsAga
The basic approach I used is to select just the stats themselves then group them all. Once grouped then you can rank them and then join the grouped stats with ranking back to the original data to get your rank numbers against the teams. One way to think of it is that you are ranking the stats not the teams.
Hope this helps.