Freight per delivery - sql

I am a beginner in SQL and struggle with a little issue where I hope you can help me with. What I want to achieve: I want to calculate freight costs per delivery which is depending on the route and the weight.
For this I have one table (shipments 3350) where all shipments for a certain period are included, so it contains delivery number, route, weight, etc. The table shipments I want to join with table freight rates as I want to calculate the freight costs per delivery. Table freight rates includes basically the different routes, weight categories and the price (one route can have different costs based on the weight being shipped). Moreover it is to consider that the table shipments is not clean and I need to remove duplicates for deliveries (Delivery numbers can pop up several times which should not be the case)
This is what I did. Basically I have created 2 CTEs which I joined afterards. The outcome looks promising. However I have one issue I struggle with. As mentioned price is depending on the route and the correspondent weight. However each route has different freight rates depending on the weight. I.e. route abc, weight within 0 and 5kg 5€, >5kg but <10kg 10€ and so on. Hence, the query should identify correct freight costs based on route and weight information to be found on the delivery. Sometimes this fails (wrong freight costs being selected) and I have no clue what needs to be changed. Hence my question is whether there is something obviously wrong in my code which prevents me from getting correct freight costs?
With CTE1 as
(
Select row_number() over (Partition by [Delivery] order by [Delivery]) as ROWID
,[Delivery]
,[Total Weight]
,[CTY]
,[Route]
,[Shipment]
,[SearchTerm]
,[Shipment route]
,[Shipping Conditions]
from [BAAS_PowerBI].[dbo].[Shipments 3350 ]
)
, CTE2 as
(select * from(
select [route],[Lower Scale quantity limit],[Upper scale quantity limit],[Amount],[sales org]
from [BAAS_PowerBI].[dbo].[RM35_freight rates 27112018 test]
)x where x.[sales org]=3350)
Select * from CTE1
left join CTE2
on [CTE1].[route] = [CTE2].[route]
where [Total Weight] <[Upper scale quantity limit] and [Total Weight] >=[Lower Scale quantity limit] and ROWID=1
You can see from the pictures that the query has selected the wrong weight category. It should have selected the category 0-10Kg and not 30-55Kg

where [Total Weight] < [Upper scale quantity limit]
and [Total Weight] >= [Lower Scale quantity limit]
didn't work, because your columns were strings, and for strings '10' is smaller than '2' for instance, because '1' is smaller than '2' in the character table (ANSI, ASCII, UNICODE, well, whatever it is).
But there is another issue with your WHERE clause: it renders your outer join a mere inner join. Here is why:
With CTE1
[Delivery] [Route] [Total Weight]
A X 6
B X 60
C Y 6
and CTE2
[Route] [Lower Scale quantity limit] [Upper scale quantity limit]
X 1 10
X 11 20
This statement:
select *
from cte1
left join cte2 on cte1.route = cte2.route
leads to
[Delivery] [Route] [Total Weight] [Lower Scale quantity limit] [Upper scale quantity limit]
A X 6 1 10
A X 6 11 20
B X 60 1 10
B X 60 11 20
C Y 6 null null
and the WHERE clause
where [Total Weight] < [Upper scale quantity limit]
and [Total Weight] >= [Lower Scale quantity limit]
reduces this to:
[Delivery] [Route] [Total Weight] [Lower Scale quantity limit] [Upper scale quantity limit]
A X 6 1 10
as only this one joined row matches the condition. This result is exactly the same as you would get with an inner join.
What you really want instead is an not an outer join that joins all route matches and even keeps routes that have no match (which is what left join cte2 on cte1.route = cte2.route does), but an outer join that joins all route/range matches and even keeps routes/totals that have no matching route/range:
select *
from cte1
left join cte2 on cte1.route = cte2.route
and [Total Weight] < [Upper scale quantity limit]
and [Total Weight] >= [Lower Scale quantity limit]
[Delivery] [Route] [Total Weight] [Lower Scale quantity limit] [Upper scale quantity limit]
A X 6 1 10
B X 60 null null
C Y 6 null null
Here you join every CTE1 row with their matching CTE2 row or with a dummy CTE2 row consisting of nulls when there is no match in CTE2.
(ROWID=1 belongs in the WHERE clause by the way, as this has nothing to do with which CTE2 rows to join to CTE1, but merely says which CTE1 rows you want to consider. If you mistakenly put ROWID=1 in the ON clause, too, you would suddenly select all CTE1 rows, but only look for CTE2 matches for those with ROWID=1.)
In short: When you outer join a table, put all its join criteria in the ON clause.

Related

Cumulative tiered rate calculation in SQL Server for DML UPDATE/INSERT trigger?

Essentially, using SQL Server, I want to take the "Gross Amt" from the current table below (which is derived from a computed column upon INSERT or UPDATE) and then have that "Gross Amt" run through the "Tiered Table" to derive the "Total A $" in the desired output table.
I figured this would likely need to be done with a trigger (maybe a function?) since this calculation would happen upon INSERT or UPDATE and because the conditional logic could be incorporated into it since there are different tier tables with different Min/Max values and percentage thresholds for different tiers.
The example below is, of course, cumulative, and functions like marginal income tax rates, the first 10000 is at 90% (for Total A), the second tier calculates the 19999 at 60%, the third 69999 at 40%, and so on, etc. There are other regions with different tiers that are just simple lookup reference values.
Tiered table:
RegionID
TierNo
Min
Max
Total A
Total B
3
1
0
10000
.90
.10
3
2
10001
30000
.60
.40
3
3
30001
100000
.40
.60
3
4
100001
500000
.40
.60
3
5
500001
999999999999
.20
.80
Current table sample:
TransID
RegionID
GrossAmt
Total A %
Total A $
Net Amt
100001
3
125000
Desired output:
TransID
RegionID
GrossAmt
Total A %
Total A $
Net Amt
100001
3
125000
0.47
59000
66000
Any ideas or guidance would be extremely helpful and appreciated.
Firstly, your tiers aren't quite right. For example, the first one begins at 0 and ends at 10000, but the next one starts at 10001, leaving anything between 10000 and 10001 unaccounted for. Instead, have your tiers abutting each other, and pick your intervals carefully using > AND <=.
Secondly, this doesn't need a trigger at all. You should just calculate this on the fly when you need it. Create a view or an inline Table Valued Function if you need to.
It looks like a fairly simple grouped join, which you can do neatly using CROSS APPLY. You just need to calculate how much to multiply for each tier: the lower of GrossAmt or Max, subtracting Min
SELECT
c.*,
[Total A %] = t.Total / c.GrossAmt,
[Total A $] = t.Total,
[Net Amt] = c.GrossAmt - t.Total
FROM CurrentData c
CROSS APPLY (
SELECT
Total = SUM((v.ActualMax - t.Min) * t.[Total A])
FROM Tiers t
CROSS APPLY (VALUES(
CASE WHEN c.GrossAmt < t.Max THEN c.GrossAmt ELSE t.Max END
)) v(ActualMax)
WHERE c.GrossAmt > t.Min
) t;
db<>fiddle
You can do this as an inline Table Valued Function if you want.
CREATE FUNCTION dbo.GetTieredTotal (#GrossAmt decimal(18,9))
RETURNS TABLE
AS RETURN
SELECT
Total = SUM((CASE WHEN #GrossAmt < t.Max THEN #GrossAmt ELSE t.Max END - t.Min) * t.[Total A])
FROM Tiers t
WHERE #GrossAmt > t.Min
;
You can then change the main query to
SELECT
c.*,
[Total A %] = t.Total / c.GrossAmt,
[Total A $] = t.Total,
[Net Amt] = c.GrossAmt - t.Total
FROM CurrentData c
CROSS APPLY dbo.GetTieredTotal (c.GrossAmt) t;

Tracking LIFO Orders in SQL

I am trying to map inventory using LIFO to determine the dates the orders initially arrived in the inventory to the day that they leave. However, the inventory can go from positive to negative.
For example:
Day 1: purchase 1,000 units; (inventory 1,000 units)
Day 2: purchase 1,000 units; (inventory 2,000 units)
Day 3: sell 500 units; (inventory 1,500 units)
Day 4: purchase 2,000 units; (inventory 3,500 units)
Day 5: sell 3,000 units; (inventory 500 units)
Day 6: sell 10,000 units; (inventory -9,500 units)
I will need to know that Day 5 units come from a minimum date of day 1 and maximum date of day 4. Is there any way to do this in SQL?
UPDATE #TEMP_ORDERS_STEP_2
SET CUMULATIVE_UNITS = UNITS
, REMAINING_UNITS = UNITS
, Min_Inventory_Date = 'n/a'
, Max_Inventory_Date = 'n/a'
WHERE Row_ID = 1
AND CUMULATIVE_SHARES IS NULL
--(30609 row(s) affected)
SELECT DateId, OrderID, ProductCode, ProductType, Units, Row_ID, Inventory, CUMULATIVE_UNITS, Min_Inventory_Date, Max_Inventory_Date
FROM #TEMP_ORDERS_STEP_2 A
JOIN (SELECT * FROM #TEMP_ORDERS_STEP_2 WHERE REMAINING_UNITS IS NOT NULL) B
ON A.ProductCode = B.ProductCode AND A.ProductType = B.ProductType AND A.Row_ID = B.Row_ID + 1
WHERE A.CUMULATIVE_SHARES IS NULL
I guess you want something like this
with hist as (select *
from (
values (1 , 1000 , 0),
(2 , 1000 , 0),
(3 , 0 , 500),
(4 , 2000 , 0),
(5 , 0 , 3000),
(6 , 0 , 10000)
) as V (day, buy, sell)),
stock as (
select day,
sum(buy) over(partition by 0 order by day ROWS UNBOUNDED PRECEDING)
- sum(sell) over(partition by 0 order by day ROWS UNBOUNDED PRECEDING) as stock
from hist),
stock_with_max_min_days as (
select s.day, s.stock,
FIRST_VALUE(s2.day) over(partition by s.day order by s2.stock asc ROWS UNBOUNDED PRECEDING) min_previous_day,
FIRST_VALUE(s2.day) over(partition by s.day order by s2.stock desc ROWS UNBOUNDED PRECEDING) max_previous_day
from stock s
left outer join stock s2
on s.day > s2.day)
select day, stock, min_previous_day, max_previous_day
from stock_with_max_min_days
group by day, stock, min_previous_day, max_previous_day
you can see a working demo in this fiddle:
https://dbfiddle.uk/?rdbms=sqlserver_2017&fiddle=76c61fbd3bcc1a0c048587601ee2b1c0

Somar um valor total e retornar muitas linhas

I have two tables (order header and order lines). I want to add the order lines, group by month and year, but it brings me all the rows of all the orders. Have you any way to sum it up?
This is probably because I'm doing one calculation per line.
I need to do it per line because the stock is from a few different places.
For example, one stock is from the back street and another stock is from the other block.
If I use the total discount for the sales order, it doubles the same order if the inventory comes from different places.
and if I use the calculation to make the discount per line, it will bring line by line (for sure).
Was there any other possibility?
I tried with variable, but I can not pass more than one value on the same variable.
SELECT
MONTH(X.DOCDATE) MES,
YEAR(X.DOCDATE) ANO,
(100 - X.DiscPrcnt)* SUM(LineTotal) /100 as 'TOTAL'
FROM RDR1 INNER JOIN ORDR X ON RDR1.DocEntry = X.DocEntry
WHERE X.CANCELED <> 'Y'
AND X.DocTotal > 0
AND X.DocDate BETWEEN '20140101' AND '20190630'
AND OcrCode IN ('EXT', 'EXT-JD')
Group by
X.DOCDATE,
X.DiscPrcnt
ORDER BY X.DOCDATE
I would like it to be:
Month YEAR Total
1 2014 5000
2 2014 7000
I imagine you want to be grouping by month and year rather than the full date. Also, I think you may not want a separate row for each discount percent value right?
How about this query?:-
SELECT
MONTH(X.DOCDATE) MES,
YEAR(X.DOCDATE) ANO,
SUM((100 - X.DiscPrcnt)* X.LineTotal/100) as 'TOTAL'
FROM RDR1 INNER JOIN ORDR X ON RDR1.DocEntry = X.DocEntry
WHERE X.CANCELED <> 'Y'
AND X.DocTotal > 0
AND X.DocDate BETWEEN '20140101' AND '20190630'
AND OcrCode IN ('EXT', 'EXT-JD')
Group by
YEAR(X.DOCDATE),
MONTH(X.DOCDATE)
ORDER BY
YEAR(X.DOCDATE),
MONTH(X.DOCDATE)

How can I SUM these column fields from a complex SELECT statement together?

I created this long SELECT statement to gather data to gauge performance over a monthly period for specific areas for a customer.
I am at the point now where I have the three areas needed for grading performance, but I have to add them all together to designate a combined grade.
enter image description here
I have so far been unable to do so though. When I use SELECT CAST I get the below error:
SELECT CAST('Average Percentage Minutes Above 800ms Per Device' AS int),
'Average Percentage of Minutes of Packet Loss',
'Average Percentage of Total Down Minutes Per Device'
AS Total
FROM
(SELECT
(SELECT DISTINCT CUSTOMER
FROM NodesCustomProperties WHERE CUSTOMER like '%Example%') AS 'Area',---query goes on and on
I get the error:
Conversion failed when converting the varchar value 'Average Percentage Minutes Above 800ms Per Device' to data type int
When I use SUM I get the error:
"Operand data type varchar is invalid for sum operator" I tried to convert the selected data to INT, but this failed.
Below is the entire query, with some specific data changed for confidentiality.
========
(SELECT
(SELECT DISTINCT CUSTOMER
FROM NodesCustomProperties WHERE CUSTOMER like '%Example%') AS 'Area',
(SELECT COUNT(*)*2/43200*100.0 AS INT
FROM Nodes n
INNER JOIN dbo.ResponseTime REP
ON n.NodeID = REP.NodeID
INNER JOIN dbo.NodesCustomProperties NCP
ON NCP.NodeID=n.NodeID
WHERE REP.AvgResponseTime > 800
AND ncp.CUSTOMER like '%Example%'
AND REP.DateTime > DateADD(DAY,-30, Current_TimeStamp))/(SELECT COUNT(*) as 'Total Area Devices'
FROM [SolarWindsOrion].[dbo].[NodesCustomProperties]
WHERE CUSTOMER like '%Example%')
'Average Percentage Minutes Above 800ms Per Device'
,
(SELECT COUNT(*)*2/43200*100.00 AS INT
FROM ResponseTime_Detail REP
INNER JOIN NodesCustomProperties NCP on REP.NodeID=NCP.NodeID
WHERE NCP.CUSTOMER like '%Example%'
AND DATETIME >=DATEADD(DAY,-30,GETDATE())
AND PercentLoss>5 AND Availability <>0) /(SELECT COUNT(*) as 'Total Area Devices'
FROM [SolarWindsOrion].[dbo].[NodesCustomProperties]
WHERE CUSTOMER like '%Example%')
as 'Average Percentage of Minutes of Packet Loss'
,
SUM(OutageDurationInMinutes)/(SELECT COUNT(*) AS INT
FROM [SolarWindsOrion].[dbo].[NodesCustomProperties]
WHERE CUSTOMER like '%Example%')
/43200.00*100.00
as 'Average Percentage of Total Down Minutes Per Device' from
(SELECT
StartTime.EventTime AS Down_Event_time,
(SELECT TOP 1
EventTime
FROM Events AS Endtime
WHERE EndTime.EventTime >= StartTime.EventTime
AND EndTime.EventType = 5
AND EndTime.NetObjectType = 'N'
AND EndTime.NetworkNode = StartTime.NetworkNode
AND EventTime IS NOT NULL
ORDER BY EndTime.EventTime)
AS UpEventTime,
Nodes.Caption, StartTime.Message, DATEDIFF(Mi, StartTime.EventTime,(SELECT TOP 1 EventTime
FROM Events AS Endtime
INNER JOIN NodesCustomProperties NCP on Nodes.NodeID=NCP.NodeID
WHERE EndTime.EventTime > StartTime.EventTime AND EndTime.EventType = 5 AND EndTime.NetObjectType = 'N'
AND EndTime.NetworkNode = StartTime.NetworkNode
ORDER BY EndTime.EventTime))
AS OutageDurationInMinutes
FROM Events StartTime INNER JOIN Nodes ON StartTime.NetworkNode = Nodes.NodeID
INNER JOIN NodesCustomProperties NCP on Nodes.NodeID=NCP.NodeID
WHERE (StartTime.EventType = 1) AND NCP.CUSTOMER like '%Example%'
) as uptimetable
WHERE outageDurationInMinutes IS NOT NULL
AND UpEventTime >= DATEADD(DAY,-30,GETDATE())
OR
outageDurationInMinutes IS NOT NULL
AND UpEventTime >= DATEADD(DAY,-30,GETDATE()))
Use square brackets instead of single quotes when using names that don't adhere to the naming conventions for regular database identifiers.
e.g. [Average Percentage Minutes Above 800ms Per Device]
Avoid using special characters, spaces, and leading numbers in database names, table names, column names, and aliases. When they do occur, wrap them in square brackets.
Reference:
Database Identifiers - docs

sum divided values problem (dealing with rounding error)

I've a product that costs 4€ and i need to divide this money for 3 departments.
On the second column, i need to get the number of rows for this product and divide for the number of departments.
My query:
select
department, totalvalue,
(totalvalue / (select count(*) from departments d2 where d2.department = p.product))
dividedvalue
from products p, departments d
where d.department = p.department
Department Total Value Divided Value
---------- ----------- -------------
A 4 1.3333333
B 4 1.3333333
C 4 1.3333333
But when I sum the values, I get 3,999999. Of course with hundreds of rows i get big differences...
Is there any chance to define 2 decimal numbers and round last value? (my results would be 1.33 1.33 1.34)
I mean, some way to adjust the last row?
In order to handle this, for each row you would have to do the following:
Perform the division
Round the result to the appropriate number of cents
Sum the difference between the rounded amount and the result of the division operation
When the sum of the differences exceeds the lowest decimal place (in this case, 0.01), add that amount to the results of the next division operation (after rounding).
This will distribute fractional amounts evenly across the rows. Unfortunately, there is no easy way to do this in SQL with simple queries; it's probably better to perform this in procedural code.
As for how important it is, when it comes to financial applications and institutions, things like this are very important, even if it's only by a penny, and even if it can only happen every X number of records; typically, the users want to see values tie to the penny (or whatever your unit of currency is) exactly.
Most importantly, you don't want to allow for an exploit like "Superman III" or "Office Space" to occur.
With six decimals of precision, you would need about 5,000 transactions to notice a difference of one cent, if you round the final number to two decimals. Increasing the number of decimals to an acceptable level would eliminate most issues, i.e. using 9 decimals you would need about 5,000,000 transactions to notice a difference of a cent.
Maybe you can make a forth row that will be Total - sum(A,B,C).
But it depends on what you want to do, if you need exact value, you can keep fractions, else, truncate and don't care about the virtual loss
Also can be done simply by adding the rounding difference of a particular value to the next number to be rounded (before rounding). This way the pile remains always the same size.
Here's a TSQL (Microsoft SQL Server) implementation of the algorithm provided by Martin:
-- Set parameters.
DECLARE #departments INTEGER = 3;
DECLARE #totalvalue DECIMAL(19, 7) = 4.0;
WITH
CTE1 AS
(
-- Create the data upon which to perform the calculation.
SELECT
1 AS Department
, #totalvalue AS [Total Value]
, CAST(#totalvalue / #departments AS DECIMAL(19, 7)) AS [Divided Value]
, CAST(ROUND(#totalvalue / #departments, 2) AS DECIMAL(19, 7)) AS [Rounded Value]
UNION ALL
SELECT
CTE1.Department + 1
, CTE1.[Total Value]
, CTE1.[Divided Value]
, CTE1.[Rounded Value]
FROM
CTE1
WHERE
Department < #departments
),
CTE2 AS
(
-- Perform the calculation for each row.
SELECT
Department
, [Total Value]
, [Divided Value]
, [Rounded Value]
, CAST([Divided Value] - [Rounded Value] AS DECIMAL(19, 7)) AS [Rounding Difference]
, [Rounded Value] AS [Calculated Value]
FROM
CTE1
WHERE
Department = 1
UNION ALL
SELECT
CTE1.Department
, CTE1.[Total Value]
, CTE1.[Divided Value]
, CTE1.[Rounded Value]
, CAST(CTE1.[Divided Value] + CTE2.[Rounding Difference] - ROUND(CTE1.[Divided Value] + CTE2.[Rounding Difference], 2) AS DECIMAL(19, 7))
, CAST(ROUND(CTE1.[Divided Value] + CTE2.[Rounding Difference], 2) AS DECIMAL(19, 7))
FROM
CTE2
INNER JOIN CTE1
ON CTE1.Department = CTE2.Department + 1
)
-- Display the results with totals.
SELECT
Department
, [Total Value]
, [Divided Value]
, [Rounded Value]
, [Rounding Difference]
, [Calculated Value]
FROM
CTE2
UNION ALL
SELECT
NULL
, NULL
, SUM([Divided Value])
, SUM([Rounded Value])
, NULL
, SUM([Calculated Value])
FROM
CTE2
;
Output:
You can plug in whatever numbers you want at the top. I'm not sure if there is a mathematical proof for this algorithm.