Adjust values of individual rows based upon order then roll up - sql

OK - Let me explain this. I have a series of rows - the rows have a "weight" value and a "qty" and "rate" - others but am concerned with these for now...
What I need to do is to split the rows - which I have done in the below, based on the qty - so for a row that has a qty > 1 - a row is replicated for each qty value.
So, with them all split now - I need to determine which has the highest "weight" - then I need to keep that rate at 100%. All others will be rate/2.
Then - lastly - I need to roll the rows back up - so all like IDs are collapsed with aggregated rates - so back to single rows with sumed rates
/*
--create numbers table if don't already have one...
select top 1000000 row_number() over(order by t1.number) as N
into dbo.Numbers
from master..spt_values t1
cross join master..spt_values t2
*/
DECLARE #table TABLE
(
id int IDENTITY(1,1) ,
code varchar(10) ,
codeStatus varchar ,
qty int ,
rate money ,
codeWeight float ,
comment varchar(100)
)
INSERT INTO #table SELECT '12345' , 'T' , 3 , 375.86 , 5.6589 , NULL
INSERT INTO #table SELECT '45678' , 'T' , 2 , 2 , 4.0000 , NULL
INSERT INTO #table SELECT '11223' , 'T' , 1 , 2 , 3.0000 , NULL
SELECT t.id ,
t.code ,
t.qty ,
t.rate ,
t.codeWeight ,
n.N
FROM #table t
JOIN dbo.Numbers n on n.N <= t.qty
ORDER BY id ,
N

Here's a way to do the break down without needing the numbers table:
http://sqlfiddle.com/#!6/93b09/1
with cte as
(
SELECT id, code, qty, rate, codeWeight, qty N
FROM myTable t
union all
select id, code, qty, rate, codeWeight, N - 1
from cte
where N > 1
)
SELECT id, code, qty, rate, codeWeight, N
FROM cte
ORDER BY id, N
If I've understood the next steps correctly, then this should do what you're after...
http://sqlfiddle.com/#!6/93b09/4
declare #temp table(id int, code varchar(10), qty int, rate money, codeWeight float, N int)
;with cte as
(
SELECT id, code, qty, rate, codeWeight, qty N
FROM myTable t
union all
select id, code, qty, rate, codeWeight, N - 1
from cte
where N > 1
)
insert #temp
SELECT id, code, qty, rate, codeWeight, N
FROM cte
ORDER BY id, N
update t
set rate = t.rate / 2.0
from #temp t
inner join myTable m on m.id = t.id
and t.qty < m.qty
select id, code, max(qty) qty, sum(rate) rate, codeWeight
from #temp
group by id, code, codeWeight
Update
Based on discussion in comments, this SQL should do everything you need:
http://sqlfiddle.com/#!6/93b09/13
select id
, code
, codeStatus
, qty
, case (row_number() over (order by codeWeight desc))
when 1 then rate + case when qty > 0 then qty-1 else 0 end * rate / 2.0
else qty * rate / 2.0
end rate
, codeWeight
, comment
from myTable
i.e. the item with the greatest weight value returns a rate made up of one unit at the full rate, plus the remaining units at half price.
All other items are given rates which are their full quantity at half price.

Related

Select random rows till the sum of column reaches the target in sql server

I need the random rows to be displayed till the target reaches
it should reach the target if there is any possible combination is there.
Id price
1 20
2 30
3 40
4 15
5 10
If the target is 30 it should return with
id price
1 20
5 10
or
id price
2 30
If the target exceeds the sum or no combinations --120 in this case it should return till last maximum
Id price
1 20
2 30
3 40
4 15
5 10
If the target is less than the sum --5 in this case it should return nothing
Id price
what i tried
select t.*(select q.*,sum(q.price) over(order by newid())as total from Orders q)t
where t.total<=45
i want rows till the exact target is reached
if there is no possible combinations of getting the exact target then only it has to return till the nearest value to the target
this query is giving me the rows not exactly all the time
I took your plain text data and turned it into reproducible DDL/DML:
DECLARE #table TABLE (ID INT, Price DECIMAL(10,4))
INSERT INTO #table (ID, Price) VALUES
(1, 20), (2, 30), (3, 40), (4, 15), (5, 10)
It's really helpful to have this in your question.
This was an interesting challenge to solve, I'm not sure I interpreted your requirement exactly, but here's what I understood:
Select a random row, check the new running total against a value, and if it's less get another random (but distinct!) row and check again. Repeat until the first row which puts the running total over the check value.
;WITH MinMax AS (
SELECT MIN(ID) AS minID, MAX(ID) AS maxID
FROM #table
)
SELECT ID, Price
FROM (
SELECT a.ID, a.Price, a.rn, SUM(Price) OVER (ORDER BY rn) AS rTotal
FROM (
SELECT t.ID, t.Price, ROW_NUMBER() OVER (ORDER BY RAND(CONVERT(VARBINARY,NEWID(),1))) AS rn
FROM (VALUES (
RAND(CONVERT(VARBINARY,NEWID(),1))
)
) A(Rnd1)
CROSS APPLY (SELECT minID, maxID FROM MinMax) mm
CROSS APPLY (SELECT TOP 100 1 AS z FROM sys.system_objects a CROSS APPLY sys.system_objects) z
LEFT OUTER JOIN #table t
ON ROUND(((maxID - minID) * Rnd1 + minID), 0) = t.ID
GROUP BY t.ID, t.Price
) a
) a
WHERE rTotal < = 50
So here we use some voodoo to find rows, and put them into a random order. Next we apply the windowed function ROW_NUMBER to give is a column to preserve that order. Next we can use the windowed function SUM to get a running total, of those rows, in that random order and finally we can compare the running total of each row, in that order, to the check value.
Example output:
ID
Price
2
30.0000
4
15.0000
ID
Price
5
10.0000
1
20.0000
4
15.0000
I aggre with #siggemannen that you should do this for very small populations.
Anyway is a nice excercise, here I'm addressing the 4 scenarios.
If SUM(Price) < TargetPrice return all
IF MIN(Price) < TargetPrice return nothing
Calculate all posible combinations of possible SUM(Price) between different rows. IF there is one combination equals to TargetPrice THEN return it. ELSE return all
So the code look like:
DECLARE #table TABLE (ID INT, Price money )--DECIMAL(10,4))
INSERT INTO #table (ID, Price) VALUES
(1, 20), (2, 30), (3, 40), (4, 15), (5, 10)
DECLARE #TargetPrice Money = 30
IF #TargetPrice > (SELECT SUM(Price) FROM #table) --If the target exceeds the sum
BEGIN
SELECT *
FROM #table
END
ELSE IF #TargetPrice < (SELECT MIN(Price) FROM #table) --If the target is less than the sum
BEGIN
SELECT TOP 0 *
FROM #table
END
ELSE
BEGIN
;WITH cte AS ( -- find all possible combinations recursively
SELECT id, Price , CONVERT(VARCHAR(MAX),Id) IdLst
FROM #table
UNION ALL
SELECT t.id, cte.Price + t.Price as Price, cte.IdLst + '|' + CONVERT(VARCHAR(MAX),t.Id) IDLst
FROM cte
INNER JOIN #table t
on cte.ID < t.ID
), EqCombinations AS ( -- keep only the first one that matches the target price
SELECT *, ROW_NUMBER() OVER (ORDER BY IDLst) rn
FROM cte
WHERE cte.Price = #TargetPrice
), Eq AS ( -- pick the first one
SELECT t.*
FROM EqCombinations
CROSS APPLY (SELECT * FROM STRING_SPLIT(IDLst, '|') parts) p
INNER JOIN #table t
ON p.value = t.ID
WHERE rn = 1
)
SELECT * , 'eq' why FROM Eq -- if the target was matched
-- else return all
UNION ALL SELECT * , 'all' why FROM #table WHERE NOT EXISTS(SELECT 1 FROM Eq)
END

How to get minimum 3 records per a group?

I have 3 columns in SalesCart table as follows,
I need to get minimum 3 records per Item as follows,
How to do that?
I guess we can use simply Row_Number() -
declare #testtable TABLE
(
ItemCode NVARCHAR(30),
Customer VARCHAR(10),
Amount INT
)
INSERT INTO #testtable
VALUES
('A-001','A', 25000)
,('A-001','B', 15000)
,('A-001','C', 12000)
,('A-001','D', 12500)
,('A-001','E', 20000)
,('A-002','C', 3000)
,('A-002','X', 2250)
,('A-002','Y', 3750)
,('A-002','D', 3100)
select *
from #testtable
select *
from
(
select *, ROW_number() over (PARTITION BY ItemCode ORDER BY ItemCode ) as Number
from #testtable
) t
where t.Number < 4
You can also try this and you can increase or decrease number based on your requirement dynamically.
DECLARE #top INT;
SET #top = 3;
;WITH grp AS
(
SELECT ItemCode, Customer, Amount,
rn = ROW_NUMBER() OVER
(PARTITION BY ItemCode ORDER BY ItemCode DESC)
FROM itemTable
)
SELECT ItemCode, Customer, Amount
FROM grp
WHERE rn <= #top
ORDER BY ItemCode DESC;

aggregation according to different conditions on same column

I have a table #tbl like below, i need to write a query like if there are more than 3 records availble
for particular cid then avg(val of particular cid ) for particular cid should be dispalyed against each id and if there are less than
3 records availble for particular cid then avg(val of all records availble).
Please suggest.
declare #tbl table(id int, cid int, val float )
insert into #tbl
values(1,100,20),(2,100,30),(3,100,25),(4,100,31),(5,100,50),
(6,200,30),(7,200,30),(8,300,90)
Your description is not clear, but I believe you need windowed functions:
WITH cte AS (
SELECT *, COUNT(*) OVER(PARTITION BY cid) AS cnt
FROM #tbl
)
SELECT id, (SELECT AVG(val) FROM cte) AS Av
FROM cte
WHERE cnt <=3
UNION ALL
SELECT id, AVG(val) OVER(PARTITION BY cid) AS Av
FROM cte
WHERE cnt > 3
ORDER BY id;
DBFiddle Demo
EDIT:
SELECT id,
CASE WHEN COUNT(*) OVER(PARTITION BY cid) <= 3 THEN AVG(val) OVER()
ELSE AVG(val) OVER(PARTITION BY cid)
END
FROM #tbl
ORDER BY id;
DBFiddle Demo2
You can try with the following. First calculate the average for each Cid depending in it's number of occurences, then join each Cid with the Id to display all table.
;WITH CidAverages AS
(
SELECT
T.cid,
Average = CASE
WHEN COUNT(1) >= 3 THEN AVG(T.val)
ELSE (SELECT AVG(Val) FROM #tbl) END
FROM
#tbl AS T
GROUP BY
T.cid
)
SELECT
T.*,
C.Average
FROM
#tbl AS T
INNER JOIN CidAverages AS C ON T.cid = C.cid
Given the clarifications in comments, I am thinking this is the intention
declare #tbl table(id int, cid int, val float )
insert into #tbl
values(1,100,20),(2,100,30),(3,100,25),(4,100,31),(5,100,50),
(6,200,30),(7,200,30),(8,300,90);
select distinct
cid
, case
when count(*) over (partition by cid) > 3 then avg(val) over (partition by cid)
else avg (val) over (partition by 1)
end as avg
from #tbl;
http://dbfiddle.uk/?rdbms=sqlserver_2017&fiddle=fdf4c4457220ec64132de7452a034976
cid avg
100 31.2
200 38.25
300 38.25
There are a number of aspects of a query like this that when run at scale though are going to be pretty bad on the query plan, I'd want to test this at a larger scale and tune before using.
The description was not clear on what happened if it was exactly 3, it mentions 'more than 3' and 'less than 3' - within this code the 'more than' was used to determine which category it was in, and less than interpreted to mean 'less than or equal to 3'

SQL Server - How to filter rows based on matching rows?

I have a complex query that feeds into a simple temp table named #tempTBRB.
select * from #tempTBRB ORDER BY AccountID yields this result set:
In all cases, when there is only 1 row for a given AccountID, the row should remain, no problem. But whenever there are 2 rows (there will never be more than 2), I want to keep the row with SDIStatus of 1, and filter out SDIStatus of 2.
Obviously if I used a simple where clause like "WHERE SDIStatus = 1", that wouldn't work, because it would filter out a lot of valid rows in which there is only 1 row for an AccountID, and the SDIStatus is 2.
Another way of saying it is that I want to filter out all rows with an SDIStatus of 2 ONLY WHEN there is another row for the same AccountID. And when there are 2 rows for the same AccountID, there will always be exactly 1 row with SDIStatus of 1 and 1 row with SDIStatus of 2.
I am using SQL Server 2012. How is it done?
SELECT
AccountID
,MIN(SDIStatus) AS MinSDIStatus
INTO #MinTable
FROM #tempTBRB
GROUP BY AccountID
SELECT *
FROM #tempTBRB T
JOIN #MinTable M ON
T.AccountID = M.AccountID
AND T.SDIStatus = M.MinSDIStatus
DROP TABLE #MinTable
Here is a little test that worked for me. If you just add the extra columns in your SELECT statements, all should be well:
CREATE TABLE #Temp ( ID int, AccountID int, Balance money, SDIStatus int )
INSERT INTO #Temp ( ID, AccountID, Balance, SDIStatus ) VALUES ( 1, 4100923, -31.41, 2 )
INSERT INTO #Temp ( ID, AccountID, Balance, SDIStatus ) VALUES ( 2, 4132170, 0, 2 )
INSERT INTO #Temp ( ID, AccountID, Balance, SDIStatus ) VALUES ( 3, 4137728, 193.10, 1 )
INSERT INTO #Temp ( ID, AccountID, Balance, SDIStatus ) VALUES ( 4, 4137728, 0, 2 )
SELECT ID, AccountID, Balance, SDIStatus
FROM
(
SELECT ID, AccountID, Balance, SDIStatus,
row_number() over (partition by AccountID order by SDIStatus desc) as rn
FROM #Temp
) x
WHERE x.rn = 1
DROP TABLE #Temp
Yields the following:
ID AccountID Balance SDIStatus
1 4100923 -31.41 2
2 4132170 0.00 2
4 4137728 0.00 2
I guess you need a similar code, make the necessary changes according to your table structure
declare #tab table (ID INT IDENTITY (1,1),AccountID int,SDISTATUS int)
insert into #tab values(4137728,1),(4137728,2),(41377,1),(41328,2)
select * from
(select *, row_number()OVER(Partition by AccountID Order by SDISTATUS ) RN from #tab) T
where t.RN=1
Or
WITH CTE AS
(select *, row_number()OVER(Partition by AccountID Order by SDISTATUS ) RN from #tab)
select * from CTE where t.RN=1

Split a row on 2 or more rows depending on a column

I have a question
If I have one row that looks like this
|ordernumber|qty|articlenumber|
| 123125213| 3 |fffff111 |
How can I split this into three rows like this:
|ordernumber|qty|articlenumber|
| 123125213| 1 |fffff111 |
| 123125213| 1 |fffff111 |
| 123125213| 1 |fffff111 |
/J
You can use recursive CTE:
WITH RCTE AS
(
SELECT
ordernumber, qty, articlenumber, qty AS L
FROM Table1
UNION ALL
SELECT
ordernumber, 1, articlenumber, L - 1 AS L
FROM RCTE
WHERE L>0
)
SELECT ordernumber,qty, articlenumber
FROM RCTE WHERE qty = 1
SQLFiddleDEMO
EDIT:
Based on Marek Grzenkowicz's answer and MatBailie's comment, whole new idea:
WITH CTE_Nums AS
(
SELECT MAX(qty) n FROM dbo.Table1
UNION ALL
SELECT n-1 FROM CTE_Nums
WHERE n>1
)
SELECT ordernumber ,
1 AS qty,
articlenumber
FROM dbo.Table1 t1
INNER JOIN CTE_Nums n ON t1.qty >= n.n
Generating number from 1 to max(qty) and join table on it.
SQLFiddle DEMO
Here's a quick hack using an additional table populated with a number of rows suitable for the qty values you are expecting:
-- helper table
CREATE TABLE qty_splitter (qty int)
INSERT INTO qty_splitter VALUES (1)
INSERT INTO qty_splitter VALUES (2)
INSERT INTO qty_splitter VALUES (3)
INSERT INTO qty_splitter VALUES (4)
INSERT INTO qty_splitter VALUES (5)
....
-- query to produce split rows
SELECT t1.ordernumber, 1, t1.articlenumber
FROM table1 t1
INNER JOIN qty_splitter qs on t.qty >= qs.qty
You can do it using CTE
declare #t table (ordername varchar(50), qty int)
insert into #t values ('ord1',5),('ord2',3)
;with cte as
(
select ordername, qty, qty-1 n
from #t
union all
select ordername, qty, n-1
from cte
where n>0
)
select ordername,1
from cte
order by ordername
Also you can use option with master..spt_values system table.
SELECT t.ordernumber, o.qty, t.articlenumber
FROM dbo.SplitTable t CROSS APPLY (
SELECT 1 AS qty
FROM master..spt_values v
WHERE v.TYPE = 'P' AND v.number < t.qty
) o
However, for this purpose is preferable to use its own sequence table
See demo on SQLFiddle