SQL Server : set a row value based on a condition - sql

I don't know what would be the appropriate title for this problem, but here is what I need to accomplish
Here is my dataset:
State TimeInState
--------------------------
1 20
3 0
4 5
8 2
5 10
1 18
3 30
12 2
2 0
What I want is another column in here, lets say FooID. What FooID is a int value that will remain same until the state is 1 again.
So the dataset would look like this:
State TimeInState FooID
------------------------------------------
1 20 1
3 0 1
4 5 1
8 2 1
5 10 1
1 18 2
3 30 2
12 2 2
2 0 2
So if there was another row at the end with State=1 then FooID will be 3 until the next state is changed.
How can I accomplish this in T-SQL?
Thanks in advance.

If you have some way of ordering rows (like an ID of sorts), then here is an example of how you could do something like this:
DECLARE #T TABLE (ID INT IDENTITY(1, 1), State INT, TimeInState INT)
INSERT #T (State, TimeInState)
VALUES (1, 20), (3, 0), (4, 5), (8, 2), (5, 10), (1, 18)
, (3, 30), (12, 2), (2, 0), (1, 1), (1, 1), (2, 1);
WITH CTE AS (
SELECT *
, ROW_NUMBER() OVER (ORDER BY CASE WHEN State = 1 THEN 0 ELSE 1 END, ID) RN
FROM #T
)
SELECT State, TimeInState, Foo.FooID
FROM CTE T
CROSS APPLY (SELECT MAX(RN) FooID FROM CTE WHERE State = 1 AND ID <= T.ID) Foo
ORDER BY ID;
But if you don't have the data ordered in some way already, then I don't think you can ensure the result set will sort the data in the way you want to sort it.

Related

Update column with a dynamic sequence with Row_number()

I tried to update in MSSQL a column(Y) of a table(A) with with an ascending sequence that resets itself when the value of another column(X) of the same table changes.
Table A at the beginning:
id
X
Y
1
1
1
2
1
1
3
2
1
4
2
1
5
2
1
6
3
1
As it should be after the script:
id
X
Y
1
1
1
2
1
2
3
2
1
4
2
2
5
2
3
6
3
1
I tried with row_number() but in the loop, it modify all the rows :
With a counter and variable to increment:
UPDATE dbo.A
SET "Y" = #MyInc
FROM (
SELECT ROW_NUMBER() OVER ( "Id" ASC) AS row_num_Id
, Id
, X
, Y
FROM dbo.A) AS sub
WHERE row_num_Id = #MyCounter;
This will give you the results you want
CREATE TABLE #T (
Id INT NOT NULL,
X INT NOT NULL,
Y INT NOT NULL
)
INSERT INTO #T(Id, X, Y)
VALUES
(1, 1, 1),
(2, 1, 1),
(3, 2, 1),
(4, 2, 1),
(5, 2, 1),
(6, 3, 1);
GO
WITH WithRowNumbers AS (
SELECT
Id,
X,
ROW_NUMBER() OVER (PARTITION BY X ORDER BY Id) As RowNumber
FROM #T
)
UPDATE T
SET Y = WRN.RowNumber
FROM WithRowNumbers AS WRN
INNER JOIN #T AS T ON T.Id = WRN.Id
SELECT * FROM #T
Or as #CharlieFace mentions you can simplify even more, as the CTE is like a view of the original table.
UPDATE T
SET Y = T.RowNumber
FROM WithRowNumbers AS T;

Select rows using group by and in each group get column values based on highest of another column value

I need to get latest field based on another field in group by
we have
Table "SchoolReview"
Id
SchoolId
Review
Point
1
1
rv1
8
2
1
rv2
7
3
2
rv3
4
4
2
rv4
7
5
3
rv5
2
6
3
rv6
8
I need to group by SchoolId and the inside group I need to get Review and Point from highest "Id" column.
I dont need "Id" coulmn but even if I get it for this solution its okay.
Result I am looking for shall look like this.
SchoolId
Review
Point
1
rv2
7
2
rv4
7
3
rv6
8
Any one experienced in MS SQL Server can help in this regard?
Using sample data from other answer
SELECT *
INTO #Data
FROM (VALUES
(1, 1, 'rv1', 8),
(2, 1, 'rv2', 7),
(3, 2, 'rv3', 4),
(4, 2, 'rv4', 7),
(5, 3, 'rv5', 2),
(6, 3, 'rv6', 8)
) v (Id, SchoolId, Review, Point)
SELECT S.SchoolId,
S.Review,
S.Point
FROM #Data S
INNER JOIN
(
SELECT Id = MAX(S1.Id),
S1.SchoolId
FROM #Data S1
GROUP BY SchoolId
) X ON X.Id = S.Id AND X.schoolId = S.SchoolId
ORDER BY X.SchoolId
;
output
You do not need to group the rows, you simply need to select the appropriate rows from the table. In this case, using ROW_NUMBER() is an option:
Table:
SELECT *
INTO Data
FROM (VALUES
(1, 1, 'rv1', 8),
(2, 1, 'rv2', 7),
(3, 2, 'rv3', 4),
(4, 2, 'rv4', 7),
(5, 3, 'rv5', 2),
(6, 3, 'rv6', 8)
) v (Id, SchoolId, Review, Point)
Statement:
SELECT SchoolId, Review, Point
FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY SchoolId ORDER BY Id DESC) AS Rn
FROM Data
) t
WHERE Rn = 1
Result:
SchoolId Review Point
---------------------
1 rv2 7
2 rv4 7
3 rv6 8

Distribute values to several rows in SQL Server

I need help with SQL Server on how to distribute a row value to several rows with the same id. To illustrate,
Id = ProductInventoryCode
Qty = QuantityInStock
ForDistribution:
Id | Qty | TotalNoOfBranchesWithId
---+--------+-------------------------
1 | 40 | 2
2 | 33 | 3
3 | 21 | 2
A table that will receive the distributed values
Id | BranchCode | Qty | QtyFromForDistributionTable
-------------------------------------------------------
1 101 13 20
1 102 8 20
2 101 10 11
2 102 2 10
2 103 3 12
3 101 1 11
3 102 12 10
As much as possible the distribution should be near equal for each id and branches.
I got something like below, but somewhat got confused and lost path.
with rs as
(
select
r.*, cume.cumequantity,
coalesce(s.shipped, 0) AS shipped
from
tmpForDistribution r
cross apply
(SELECT SUM([QuantityInStock]) AS cumequantity
FROM tmpForDistribution r2
WHERE r2.ProductInventoryCode = r.ProductInventoryCode) cume
left join
(SELECT ProductInventoryCode, COUNT(ProductInventoryCode) AS shipped
FROM tmpDistributed s
GROUP BY s.ProductInventoryCode) s ON r.ProductInventoryCode = s.ProductInventoryCode
)
select
rs.ProductInventoryCode, rs.cumequantity, rs.QuantityInStock,
***"how to distribute"***
from rs
I'm currently using SQL Server 2008
Here's a sample screen output
The upper result is 145 Branches, below we use to distribute the ForDistributionQty field which is 3130, I am ending up with a fraction (DistVal = 21.586) which is not correct for this problem, it should be a whole number such as 21, however, if its just 21, then 21 x 145 is just 3045 which is shy of 85 units.
Here we distribute the values, and then make a final "adjustment" to the record which has the largest quantity (arbitrary). But at the end of the day, the math works and the distributed values are square.
Note: Not sure why in your sample why ID 2 did not get an even distribution
Declare #Table table (Id int,BranchCode int,Qty int)
Insert Into #Table values
(1, 101, 13),
(1, 102, 8),
(2, 101, 10),
(2, 102, 2),
(2, 103, 3),
(3, 101, 1),
(3, 102, 12)
Declare #Dist table (ID int,Qty int)
Insert Into #Dist values
(1,40),
(2,33),
(3,49)
;with cte0 as (
Select A.*
,ToDist = cast(D.Qty as int)
,DistVal = cast(D.Qty as int)/C.Cnt
,RN = Row_Number() over (Partition By A.ID Order By cast(D.Qty as int)/C.Cnt Desc,A.Qty Desc)
From #Table A
Join (Select ID,Cnt=count(*) from #Table Group By ID) C on A.ID=C.ID
Join #Dist D on A.ID=D.ID )
, cte1 as (
Select ID,AdjVal=Sum(DistVal)-max(ToDist) From cte0 Group By ID
)
Select A.ID
,A.BranchCode
,A.Qty
,DistVal = DistVal - case when A.RN<=abs(AdjVal) then 1*sign(AdjVal) else 0 end
From cte0 A
Join cte1 B on (A.ID=B.Id)
Order By 1,2
Returns
ID BranchCode Qty DistVal
1 101 13 20
1 102 8 20
2 101 10 11
2 102 2 11
2 103 3 11
3 101 1 24
3 102 12 25
If you can tolerate decimal values, a subquery seems to give a better query plan (tested on SQL 2014, with some sensible keys in place, this avoids a table spool and some additional index scans):
Declare #Table table (Id int,BranchCode int,Qty int, primary key(id, branchcode))
Insert Into #Table values
(1, 101, 13),
(1, 102, 8),
(2, 101, 10),
(2, 102, 2),
(2, 103, 3),
(3, 101, 1),
(3, 102, 12)
Declare #Dist table (ID int primary key,Qty int)
Insert Into #Dist values
(1,40),
(2,33),
(3,21)
SELECT
t.id
,t.BranchCode
,t.Qty
,(d.Qty / CAST((SELECT COUNT(*) as cnt FROM #table t2 where t.id = t2.id) AS decimal(10,2))) as DistributedQty
FROM #Table t
INNER JOIN #Dist d
ON d.id = t.Id
outputs:
Id BranchCode Qty DistributedQty
1 101 13 20.00000000000
1 102 82 20.00000000000
2 101 10 11.00000000000
2 102 21 11.00000000000
2 103 31 11.00000000000
3 101 11 10.50000000000
3 102 12 10.50000000000
If you need DistributedQty to be an int and retain remainders then I can't think of a better solution than #John Cappelletti's, noting that uneven quantities may not be as exactly even as you might hope (e.g. 32 distributed by three would result in a 12/10/10 distribution instead of an 11/11/10 distribution).

Add column with row number

I want to add a column to my select showing a set of number from say 1 to 4.
Example:
Select * gives me
Id Transaction
1 10
2 11
3 12
4 13
5 14
6 15
I want to add a column called "Flow". The result should be like this.
Id Transaction Flow
1 10 1
2 11 2
3 12 3
4 13 4
5 14 1
6 15 2
In this example the flow is from 1-4. Could be 1-n.
No particular relation between Id and Flow is needed.
If you're using SQL Server or other DBMS that allows ROW_NUMBER, you could do this:
CREATE TABLE #Tbl(Id INT, [Transaction] INT);
INSERT INTO #Tbl VALUES
(1, 10), (2, 11), (3, 12), (4, 13), (5, 14), (6, 15);
DECLARE #N INT = 4;
SELECT *,
Flow = 1 + ((ROW_NUMBER() OVER(ORDER BY Id) - 1) % #N)
FROM #Tbl
DROP TABLE #Tbl;
If you are using mySql.
Query
set #r := 0;
select Id, `Transaction`,
#r := (#r % 4) + 1 as Flow
from your_table_name
order by Id;
Demo
EDIT
Following sql query can be used irrespective of rdbms.
Query
select *, (
select ((count(*) - 1) % 4) + 1 as Flow
from your_table_name t2
where t1.Id >= t2.Id
) as Flow
from your_table_name t1;

MIcrosoft SQL Server WHERE/ CASE clauses

I have a where statement that depends on an id and based off the id the next where is determined. EX: if ID = 1 the where statement should be a<= 3 and b between 4 and 7 if ID <> 1 the where statement should be a<= 4 and b between 5 and 7. Not sure how to do this. Tried a Case clause but had no luck.
Here is a sample table in tempdb with data.
-- Just a test
use tempdb;
go
-- Drop table
if object_id('test') > 0
drop table test
go
-- Create table
create table test
(
id int,
a int,
b int
);
-- Add data
insert into test values
(1, 3, 4),
(2, 4, 5),
(1, 4, 4),
(2, 5, 5),
(1, 3, 3),
(2, 4, 4);
-- Full table
select * from test;
Here is a solution using the CASE statement.
-- Show the data
select
*
from
test
where
(
case
when id = 1 and a <= 3 and b between 4 and 7 then 1
when id <> 1 and a <= 4 and b between 5 and 7 then 1
else 0
end
) = 1;
Something like:
where
(id = 1 and a <= 3 and b between 4 and 7) or
(id <> 1 and a <= 4 and b between 5 and 7)
Based on your requirements you just need to parenthetical WHERE statements with an OR:
...
WHERE (ID = 1 AND a <= 3 AND b BETWEEN 4 AND 7)
OR (ID <> 1 AND a<= 4 AND b BETWEEN 5 AND 7)