T-SQL: Efficient way to add up column values - sql

Now I'm sure this has been asked and superbly been answered on here. However, I am unable to find the answer since it touches many keywords.
I basically want to replace a table of the form:
Type amount param note
7 2 str1 NULL
42 12 str2 NULL
128 7 str3 samplenote
42 12 NULL NULL
101 4 str4 NULL
42 12 NULL NULL
7 1 str1 samplenote
128 2 str5 NULL
with a table like:
Type amount param note
7 3 str1 combined
42 36 NULL combined
128 9 NULL combined
101 4 str4 combined
In words, I seek to sum up the amount parameter based on its type while declaring param = NULL for all "unclear" fields. (param should be NULL when the param values of combined Types have more than one different content; else, param should have the original content.)
With my python background, I tackled this task with a for loop approach, iterating through the types, adding a new row for every type with summed up amount and note = 'combined', to then delete the remaining rows (see below). There has to be a more efficient way with some JOIN statement I'm sure. But how would that look like?
FYI, this is the solution I am working on (not functioning yet!):
/*** dbo.sourcetable holds all possible Type values ***/
CREATE PROCEDURE [sumup]
AS
BEGIN
DECLARE #i int = (SELECT TOP (1) Type FROM [dbo].[sourcetable] ORDER BY Type)
DECLARE #MaxType int = (SELECT TOP (1) Type FROM [dbo].[sourcetable] ORDER BY Type DESC)
DECLARE #sum int
BEGIN TRY
WHILE #i <= #MaxType
BEGIN
IF EXISTS (SELECT * FROM [dbo].[worktable] WHERE Type = #i)
BEGIN
SET #sum = (SELECT SUM(amount) FROM [dbo].[worktable] WHERE Type = #i)
BEGIN
WITH cte AS (SELECT * FROM [dbo].[worktable] WHERE Type = #i)
INSERT INTO [dbo].[worktable]
([Type]
,[amount]
,[param]
,[note]
SELECT
cte.Type
,#sum
,cte.param
,'combined'
FROM cte
END
DELETE FROM [dbo].[worktable] WHERE Type = #i AND ISNULL([note],'') <> 'combined'
END
SET #i = #i + 1
END
END TRY
BEGIN CATCH
-- some errorlogging code
END CATCH
END
GO

This can be achieved with a single select statement.
If you require your combined flag to only apply to where more than one row has been combined, add another case expression checking the result of either a count(1) for rows combined or count(distinct param) for unique param values combined:
declare #t as table(type int, amount int, param varchar(15), note varchar(15));
insert into #t values (7,2,'str1',NULL),(42,12,'str2',NULL),(128,7,'str3','samplenote'),(42,12,NULL,NULL),(101,4,'str4',NULL),(42,12,NULL,NULL),(7,1,'str1','samplenote'),(128,2,'str5',NULL);
select type
,sum(amount) as amount
,case when count(distinct isnull(param,'')) = 1
then max(param)
else null
end as param
,'combined' as note
from #t
group by type
order by type;
Output:
+------+--------+-------+----------+
| type | amount | param | note |
+------+--------+-------+----------+
| 7 | 3 | str1 | combined |
| 42 | 36 | NULL | combined |
| 101 | 4 | str4 | combined |
| 128 | 9 | NULL | combined |
+------+--------+-------+----------+

I am doing this way from keyboard, but this may work or be close to what you want
Select type , amount , iif( dc=1,p,null) param, 'combined' note
from
(
Select type, sum(amount) amount,
count(distinct Param) dc,max(Param) p
From ....
Group by type
) x

Here is a possible solution:
declare #tbl as table (
type int
,amount int
,param varchar(15)
,note varchar(15)
)
insert into #tbl values (7,2,'str1',NULL)
insert into #tbl values (42,12,'str2',NULL)
insert into #tbl values (128,7,'str3','samplenote')
insert into #tbl values (42,12,NULL,NULL)
insert into #tbl values (101,4,'str4',NULL)
insert into #tbl values (42,12,NULL,NULL)
insert into #tbl values (7,1,'str1','samplenote')
insert into #tbl values (128,2,'str5',NULL)
;WITH CTE AS (
SELECT
type
,SUM(AMOUNT) AS amount
,COUNT(DISTINCT ISNULL(param, 'dummy value')) AS ParamNo
,MAX(Param) AS Param
FROM #tbl
GROUP BY type
) SELECT
type
,amount
,CASE WHEN ParamNo = 1 THEN Param ELSE NULL END AS Param
,'combined' AS note
FROM CTE

This should work:
Select Type, sum(amount) as amount, count(distinct param)
, case when count(distinct param) = 1 then max(param) end as param,
'Combined' as note
From
mytable
Group By Type

Related

Inserting not calculated Id in table transact SQL

I have a stored procedure where I receive data as JSON from a API in C#. I insert the data into two tables like this:
INSERT INTO dbo.ServiceRequestHeader(SubscriptionId, CustomerAccountId, ModifiedBy)
OUTPUT Inserted.ServiceRequestHeaderId INTO #TempT
SELECT
SubscriptionId,
CustomerAccountId,
ModifiedBy
FROM
OpenJson(#JsonServiceRequest)
WITH
(SubscriptionId TinyInt,
CustomerAccountId Int)
SELECT #TempId = Id FROM #TempT
INSERT INTO dbo.ServiceRequest(ServiceRequestId, ServiceRequestHeaderId, SubscriptionId)
SELECT
#TempId, -- <= Here I need to modify the serviceRequestHeaderId
#TempId,
SubscriptionId
FROM
OpenJson(#JsonServiceRequest, '$.ServiceRequest')
WITH (SubscriptionId TinyInt,
...)
The thing is that the serviceRequestId is not a calculated field and it's a special case that depends on ServiceRequestHeaderId.
Example:
If ServiceRequestHeaderId = 1000 the ServiceRequestId would be 1000 001, 1000 002... N...
This is where I can't come with a way to do it
You can generate servicerequestids as given below. I am using FORMAT function with 000 for padding with 0 till 3 digits. If you want four digits, use 0000.
SELECT #TempId = Id FROM #TempT
INSERT INTO dbo.ServiceRequest(ServiceRequestId, ServiceRequestHeaderId, SubscriptionId)
SELECT
CONCAT(#TempId,FORMAT(ROW_NUMBER() OVER(ORDER BY (SELECT null)),'000')) AS ServiceRequestId, -- <= Here I need to modify the serviceRequestHeaderId
#TempId,
SubscriptionId
FROM
OpenJson(#JsonServiceRequest, '$.ServiceRequest')
WITH (SubscriptionId TinyInt,
...)
You will get something like below:
+------------------+
| ServiceRequestId |
+------------------+
| 1000001 |
| 1000002 |
| 1000003 |
+------------------+
Use a CTE to calculate a row number per request and then build the id from it e.g.
with MyCTE as (
select
SubscriptionId
-- Order by whatever makes business sense to you
, row_number() over (order by SubscriptionId) rn
from openjson(#JsonServiceRequest, '$.ServiceRequest')
with (
SubscriptionId tinyint,
...
)
)
insert into dbo.ServiceRequest (ServiceRequestId, ServiceRequestHeaderId, SubscriptionId)
-- Put whatever logic you like here to calculate a row number based id
select convert(varchar(4),#TempId) + ' ' + case when rn >= 100 then convert(varchar(3),rn) when rn > 10 then '0' + convert(varchar(2),rn) else '00' + convert(varchar(1),rn) end
, #TempId, SubscriptionId
from MyCTE;

SQL Server: how to update a column with a value that is in that column when another number in another column is >1

I have a table with the following data:
Part Comp level item_nbr
-------------------------------
abc ab 1 1
null cd 2 2
null ef 3 3
cde gh 1 4
null ij 2 5
null kl 3 6
null mn 4 7
I would like to update the nulls to the value in each level 1, so every level that is >1 is updated with the level one value.
Part Comp level
---------------------
abc ab 1
abc cd 2
abc ef 3
cde gh 1
cde ij 2
cde kl 3
cde mn 4
I am at a loss as to how to achieve this on a very large dataset. Any help would be greatly appreciated!
To explain another way,
part level
abc 1
2
3
Then the next row is populated with another part
efg 1
2
2
etc.
Further clarification:
I need the string"abc" to be filled down with the string "abc" while the column fields below are null. The next row has a string of efg and the following column fields below are null, again, those fields should be filled down with the value "efg" and so on.
The level field = 1 will always have a part number, but all the other levels report up to the level 1 part, so should be populated identically. And repeat.
Hope this makes sense.
Use an updatable CTE with window functions:
with toupdate as (
select t.*,
max(part) over (partition by itm_nbr_not_null) as new_part
from (select t.*,
max(case when part is not null then item_nbr end) over (order by item_nbr) as itm_nbr_not_null
from t
) t
)
update toupdate
set part = new_part
where part is null;
You can run the CTE to see what is happening.
well, from your question what I understand is, you need to update the null column's value until you get a not null value. and you want to continue it up to the last row of the table.
for that scenario, I created a stored procedure, where I read the value of every n-th cell if it is null I changing it with the prev. cell's value, when the cell was not null.
Approach:
create a temporary table/ table variable.
add an extra column, which is basically identity, which will help to rank the column.
iterate a loop until the maximum row is reached.
in each iteration, read the cell value for the i-th row
4.1 if it is not null put it in a temporary variable.
4.2 else, replace/update the i-th cell's value with the temporary variable
continue it, until you reached up to the last row of the table/table variable.
look at my following snippets:
create proc DemoPost
as
begin
declare #table table(serial_no int identity(1,1), name varchar(30), text varchar(30), level int)
insert #table
select Name, Text, Level from Demo
declare #max as int = (select max(serial_no) from #table)
--select #max
declare #i as int =0
declare #temp as varchar(30)
declare #text as varchar(30)
while #i < #max
begin
set #i = #i +1
set #temp = (select name from #table where serial_no = #i)
-- if #temp is not null, fetch its value, otherwise, update/replace it with
-- previously gotten not-null cell's value.
if #temp is not null
begin
set #text = (select name from #table where serial_no = #i)
end
else
begin
update #table
set name = #text where serial_no = #i
end
end
select name, text, level from #table
end
You can update it using temporary table according to the given scenario i thought item_nbr is unique in row Hope this will help
SELECT *
INTO #TEMP
FROM URTablehere
DECLARE #PRev VARCHAR(MAX)
WHILE ( SELECT COUNT(*)
FROM URTablehere
) > 0
BEGIN
DECLARE #ID INT
DECLARE #Part VARCHAR(MAX)
DECLARE #Num INT
SELECT TOP ( 1 )
#ID = level ,
#Part = Part ,
#Num = item_nbr
FROM #TEMP
IF ( #ID = 1 )
BEGIN
SELECT #PRev = #Part
END
IF ( #ID > 1
AND #Part IS NULL
)
BEGIN
UPDATE URTablehere
SET Part = #PRev
WHERE item_nbr = #Num
END
DELETE
FROM #TEMP WHERE item_nbr=#Num
END

String Split Ignore Last delimiter if no data

I am string splitting some values that are comma delimited into rows.
However some values have an extra comma on the end.
Example
Userid | Value
1 | A,B,C,D,
2 | F,H
Code
select value
from string_split('A,B,C,D,',',')
Current Output
UserId | Value
1 | A
1 | B
1 | C
1 | D
1 |
Is there any way to make the string split function ignore the final comma if no data follows it?
Desired Output
UserId | Value
1 | A
1 | B
1 | C
1 | D
Using MSSQL
Just add "WHERE" sentence like this:
select value
from string_split('A,B,C,D,',',')
WHERE value <> ''
STRING_SPLIT Function doesn't support for lower version of sql server so first create a function to split the given string and join the function with your select query.Here is below sample for your expected result.
Created User defined Function
CREATE FUNCTION [dbo].[Udf_StringSplit]
(
#Userid INT,
#Value VARCHAR(1000)
)
RETURNS #Result TABLE(
Userid INT,
Value VARCHAR(10)
)
AS BEGIN
DECLARE #Data AS TABLE
(
Userid INT,
Value VARCHAR(100)
)
INSERT INTO #Data(Userid,Value)
SELECT #Userid, #Value
INSERT INTO #Result(Userid,Value)
SELECT Userid,
Split.a.value('.','nvarchar(1000)') AS Value
FROM
(
SELECT Userid,
CAST('<S>'+REPLACE(#Value,',','</S><S>')+'</S>' AS XML) Value
FROM #Data
) AS A
CROSS APPLY Value.nodes('S') AS Split(a)
WHERE Userid=#Userid AND Split.a.value('.','nvarchar(1000)') <>''
RETURN
END
GO
Sample data table
DECLARE #Data AS TABLE(Userid INT , Value VARCHAR(100))
INSERT INTO #Data
SELECT 1,'A,B,C,D,' UNION ALL
SELECT 2,'F,H'
Sql script to get the expected result
SELECT d.Userid,
f.Value
FROM #Data d
CROSS APPLY [dbo].[Udf_StringSplit] (d.Userid,d.Value) AS f
WHERE d.Userid=1
GO
Result
Userid Value
------------
1 A
1 B
1 C
1 D

How to create loop based on value of row?

I have problem when I use my query bellow to have a looping inside the cursor.
data in table1 will be like this:
id | data
----|---------
A | 4
B | 2
C | 5
the result in table2 should be like this:
id | data
----|---------
A | 1
A | 1
A | 1
A | 1
B | 1
B | 1
C | 1
C | 1
C | 1
C | 1
C | 1
I have SQL query with cursor like this:
DECLARE #table2 table ( id VARCHAR(500), data INTEGER)
DECLARE Cur CURSOR FOR
SELECT id, data FROM table1
OPEN Cur
WHILE ( ##FETCH_STATUS = 0 )
BEGIN
DECLARE #LoopNum INTEGER
DECLARE #tempID VARCHAR(255)
DECLARE #tempDATA INTEGER
FETCH NEXT FROM Cur INTO #tempID, #tempDATA
set #LoopNum = 0
WHILE #LoopNum < #tempDATA
BEGIN
INSERT INTO table2 (id, data)
VALUES( #tempID, 1)
SET #LoopNum = #LoopNum + 1
END
END
CLOSE Cur
DEALLOCATE Cur
SELECT * FROM table2
but the query didn't work. is there something wrong with my query?
Thank you.
Use this query to the expected result.
CREATE TABLE #test
(id CHAR(1),data INT)
INSERT #test VALUES ('A',4)
INSERT #test VALUES('B',2)
INSERT #test VALUES('C',5);
SELECT s.id, 1 AS data
FROM #test s
INNER JOIN
master.dbo.spt_values t ON t.type='P'
AND t.number BETWEEN 1 AND s.data
Note: Refer this Why (and how) to split column using master..spt_values?
You actually don't need a loop
IF OBJECT_ID('TEMPDB..#TEMP') IS NOT NULL
DROP TABLE #TEMP
SELECT 'A' AS ID, 4 AS DATA
INTO #TEMP UNION
SELECT 'B', 2 UNION
SELECT 'C', 5
;WITH CTE AS
(
SELECT 1 AS NUMBER
UNION ALL
SELECT NUMBER + 1
FROM CTE
WHERE NUMBER < 100
)
SELECT T.ID, 1
FROM CTE C
INNER JOIN #TEMP T
ON C.NUMBER <= T.DATA
ORDER BY T.ID
Carefull that if you want ot generate a large set of numbers in the CTE it may become slower.
Use a Recursive CTE which will help you to loop through the records.
CREATE TABLE #test
(id CHAR(1),data INT)
INSERT #test
VALUES ('A',4),('B',2),('C',5);
WITH cte
AS (SELECT 1 AS da,id,data
FROM #test a
UNION ALL
SELECT da + 1,id,data
FROM cte a
WHERE da < (SELECT data
FROM #test b
WHERE a.id = b.id))
SELECT id,
1 AS data
FROM cte
ORDER BY id
i used two loops
1. for each row
2. for number for duplicate insert
SET NOCOUNT on;
DECLARE #t table(row int IDENTITY(1,1),id varchar(10),data int)
INSERT INTO #t
SELECT * from xyz
DECLARE #x table(id varchar(10),data int) --table to hold the new data
DECLARE #i int=(SELECT count (*) from xyz) --number of rows main table
DECLARE #y int --number of duplicate
DECLARE #p int=1 --number of rows
WHILE #i!=0 --loop until last row of main table
BEGIN
SET #y=(SELECT data FROM #t WHERE row=#p) --set #y for number of 'row duplicate'
WHILE #y!=0
BEGIN
INSERT INTO #x
SELECT id,1
FROM #t
WHERE row=#p
SET #y=#y-1
END
SET #p=#p+1
SET #i=#i-1
END
SELECT * FROM #x

Sequential numbers randomly selected and added to table

The SO Question has lead me to the following question.
If a table has 16 rows I'd like to add a field to the table with the numbers 1,2,3,4,5,...,16 arranged randomly i.e in the 'RndVal' field for row 1 this could be 2, then for row 2 it could be 5 i.e each of the 16 integers needs to appear once without repetition.
Why doesn't the following work? Ideally I'd like to see this working then to see alternative solutions.
This creates the table ok:
IF OBJECT_ID('tempdb..#A') IS NOT NULL BEGIN DROP TABLE #A END
IF OBJECT_ID('tempdb..#B') IS NOT NULL BEGIN DROP TABLE #B END
IF OBJECT_ID('tempdb..#C') IS NOT NULL BEGIN DROP TABLE #C END
IF OBJECT_ID('tempdb..#myTable') IS NOT NULL BEGIN DROP TABLE #myTable END
CREATE TABLE #B (B_ID INT)
CREATE TABLE #C (C_ID INT)
INSERT INTO #B(B_ID) VALUES
(10),
(20),
(30),
(40)
INSERT INTO #C(C_ID)VALUES
(1),
(2),
(3),
(4)
CREATE TABLE #A
(
B_ID INT
, C_ID INT
, RndVal INT
)
INSERT INTO #A(B_ID, C_ID, RndVal)
SELECT
#B.B_ID
, #C.C_ID
, 0
FROM #B CROSS JOIN #C;
Then I'm attempting to add the random column using the following. The logic is to add random numbers between 1 and 16 > then to effectively overwrite any that are duplicated with other numbers > in a loop ...
SELECT
ROW_NUMBER() OVER(ORDER BY B_ID) AS Row
, B_ID
, C_ID
, RndVal
INTO #myTable
FROM #A
DECLARE #rowsRequired INT = (SELECT COUNT(*) CNT FROM #myTable)
DECLARE #i INT = (SELECT #rowsRequired - SUM(CASE WHEN RndVal > 0 THEN 1 ELSE 0 END) FROM #myTable)--0
DECLARE #end INT = 1
WHILE #end > 0
BEGIN
SELECT #i = #rowsRequired - SUM(CASE WHEN RndVal > 0 THEN 1 ELSE 0 END) FROM #myTable
WHILE #i>0
BEGIN
UPDATE x
SET x.RndVal = FLOOR(RAND()*#rowsRequired)
FROM #myTable x
WHERE x.RndVal = 0
SET #i = #i-1
END
--this is to remove possible duplicates
UPDATE c
SET c.RndVal = 0
FROM
#myTable c
INNER JOIN
(
SELECT RndVal
FROM #myTable
GROUP BY RndVal
HAVING COUNT(RndVal)>1
) t
ON
c.RndVal = t.RndVal
SET #end = ##ROWCOUNT
END
TRUNCATE TABLE #A
INSERT INTO #A
SELECT
B_ID
, C_ID
, RndVal
FROM #myTable
If the original table has 6 rows then the result should end up something like this
B_ID|C_ID|RndVal
----------------
| | 5
| | 4
| | 1
| | 6
| | 3
| | 2
I don't understand your code, frankly
This will update each row with a random number, non-repeated number between 1 and the number of rows in the table
UPDATE T
SET SomeCol = T2.X
FROM
MyTable T
JOIN
(
SELECT
KeyCol, ROW_NUMBER() OVER (ORDER BY NEWID()) AS X
FROM
MyTable
) T2 ON T.KeyCol = T2.KeyCol
This is more concise but can't test to see if it works as expected
UPDATE T
SET SomeCol = X
FROM
(
SELECT
SomeCol, ROW_NUMBER() OVER (ORDER BY NEWID()) AS X
FROM
MyTable
) T
When you add TOP (1) (because you need to update first RndVal=0 record) and +1 (because otherwise your zero mark means nothing) to your update, things will start to move. But extremely slowly (around 40 seconds on my rather outdated laptop). This is because, as #myTable gets filled with generated random numbers, it becomes less and less probable to get missing numbers - you usually get duplicate, and have to start again.
UPDATE top (1) x
SET x.RndVal = FLOOR(RAND()*#rowsRequired) + 1
FROM #myTable x
WHERE x.RndVal = 0
Of course, #gbn has perfectly valid solution.
This is basically the same as the previous answer, but specific to your code:
;WITH CTE As
(
SELECT B_ID, C_ID, RndVal,
ROW_NUMBER() OVER(ORDER BY NewID()) As NewOrder
FROM #A
)
UPDATE CTE
SET RndVal = NewOrder
SELECT * FROM #A ORDER BY RndVal