How can I randomly assign one user to another in T-SQL? - tsql

I'm trying to write a sproc that will take a table of users and assign them to each other randomly with no duplicates and no one assigned to themselves.
The user table has an UserId column and a Username column. I have a separate table for storing the IDs. That table has a UserId column and an AssignedUserId column.
I used this snippet for randomly sorting rows:
SELECT ABS(CAST(CAST(NEWID() AS VARBINARY) AS INT)) AS [RandomNumber]
When the code above is part of a select, it works fine. I'm just having a hard time thinking as a data set. I can work the problem row-by-row or in C#, but I'm hoping there's a SQL guru that can show me how to do this in a nice, efficient query.
Thanks!

It can be done, but not good practice on large tables, but with your table being so small this will work great.
DECLARE #A TABLE (A1 INT, A2 INT)
INSERT INTO #A VALUES(1, 1)
INSERT INTO #A VALUES(2, 2)
INSERT INTO #A VALUES(3, 3)
INSERT INTO #A VALUES(4, 4)
INSERT INTO #A VALUES(5, 5)
SELECT * FROM #A ORDER BY NEWID()
Result1:
3 3
1 1
4 4
5 5
2 2
Result2:
2 2
3 3
4 4
5 5
1 1
Result3:
4 4
3 3
1 1
5 5
2 2
Result4:
3 3
5 5
1 1
4 4
2 2
Result5:
5 5
4 4
1 1
3 3
2 2

Ok, here's an alternate solution I came up with in the mean time. I decided that the users are already in the system in random order, so maybe I should try a decoder-ring style approach. So, this code generates a shift value and then shifts the user IDs around. The select statement does the "wrap-around" with a formula. It's not completely random and it could use some polish, but this is what I have. The users probably won't perceive the lack of randomness. Thanks for the help!
DECLARE #people TABLE
(
id INT,
name VARCHAR(50)
)
INSERT INTO #people VALUES (1,'Matthew')
INSERT INTO #people VALUES (2,'Mark')
INSERT INTO #people VALUES (3,'Luke')
INSERT INTO #people VALUES (4,'John')
INSERT INTO #people VALUES (5,'Doug')
INSERT INTO #people VALUES (6,'Jamie')
INSERT INTO #people VALUES (7,'John')
INSERT INTO #people VALUES (8,'Cameron')
INSERT INTO #people VALUES (9,'Emily')
INSERT INTO #people VALUES (10,'Tyler')
INSERT INTO #people VALUES (11,'Todd')
INSERT INTO #people VALUES (12,'Kathryn')
DECLARE #Random INT;
DECLARE #Upper INT;
DECLARE #Lower INT;
DECLARE #MaxId INT;
SET #Lower = 1 -- The lowest record ID
SELECT #Upper = (MAX(Id) / 2) FROM #people -- select random int that is somewhere between 1 and half of max ID for shift
SELECT #MaxId = MAX(Id) FROM #people -- select the largest ID in the table
SELECT #Random = ROUND(((#Upper - #Lower -1) * RAND() + #Lower), 0) -- calculate a random number to shift IDs off by
SELECT id, ((id + #Random) % #MaxId) + 1, #Random
from #people

Related

T-sql Concatenate variables for use in an Insert Into while loop

I'm trying to create a SQL statement that allows me to save time when creating a series of CTEs or temp tables that are all the same and only increment in certain places. I could do this with VBA, but I can't figure out if this is possible with SQL
I'm trying to run the following code
DECLARE #N as INT
DECLARE #POINTS as TABLE(ID int Not Null,n varchar(3) Not Null)
DECLARE #TABLENAME varchar(6)
Set #INC = 1
Set #N = 5
--WHILE #INC <= #N
--BEGIN
Declare #N1 INT
Declare #N2 INT
Declare #N3 INT
Declare #N4 INT
Declare #N5 INT
SET #N1=25
SET #N2=50
SET #N3=100
SET #N4=250
SET #N5=500
--END
WHILE #INC <= #N
BEGIN
INSERT INTO #POINTS(ID, n)
VALUES (#INC, CONCAT('#N',#INC))
SET #INC = #INC + 1
END
Select * from #POINTS
These are the results of the code after it runs:
ID n
1 #N1
2 #N2
3 #N3
4 #N4
5 #N5
Is it possible to have the values I set for each #Nx variable be inserted to the n column instead of the concatenated character values? If so, how? I haven't been able to find any results on someone trying to do this.
I can get around this problem by using outside tables, but I want to know if this is possible.
If you only have a small number of values values, you might as well write a single insert statement that will insert all 5 rows to the table:
INSERT INTO #POINTS(ID, n) VALUES
(1, '5'),
(2, '25'),
(3, '50'),
(4, '100'),
(5, '250'),
(6, '500');
If you have too many values to write like this, you can use a cte to generate an inline tally table, and use insert...select from that tally table:
DECLARE #Points as TABLE
(
ID int Not Null,
n varchar(3) Not Null
);
WITH E10(N) AS
(
SELECT 1
FROM (VALUES(0), (1), (2), (3), (4), (5), (6), (7), (8), (9))V(N)
), Tally(N) AS
(
-- Since all the numbers are multiples of 5,
-- there's no point of populating the tally
-- with numbers that aren't multiples of 5....
SELECT ROW_NUMBER() OVER(ORDER BY ##SPID) * 5
FROM E10 As Ten
CROSS JOIN E10 As Hundred
--Need more? Add more cross joins
-- each will multiply the number of rows by 10:
--CROSS JOIN E10 As Thousand
--CROSS JOIN E10 As [Ten Thousand] -- and so on
)
INSERT INTO #Points(ID, n)
SELECT ROW_NUMBER() OVER(ORDER BY N), CAST(N as varchar(3))
FROM Tally
WHERE N IN(5, 25, 50, 100, 250, 500);
SELECT *
FROM #Points;
Results:
ID n
1 5
2 25
3 50
4 100
5 250
6 500
Of course, if you already have a tally table, you don't need the cte...

SQL query to get the below output

Need help for query:
Id Name Balance
1 A 10
2 A -10
3 A 10
4 A 15
5 B 10.5
6 B -10.5
7 B 25
Requirement: I want to remove the balance with +ve and –ve values for each name. I am expecting the below output
Expected results: --
Id Name Balance
1 A 10
2 A 15
3 B 25
Can you please share me SQL query.
Here is one way to do it.
Create dummy data for testing
CREATE TABLE foo (Id INT, Name VARCHAR(50), Balance money);
INSERT INTO foo VALUES (1, 'A', 10);
INSERT INTO foo VALUES (2, 'A', -10);
INSERT INTO foo VALUES (3, 'A', 10);
INSERT INTO foo VALUES (4, 'A', 15);
INSERT INTO foo VALUES (5, 'B', 10.5);
INSERT INTO foo VALUES (6, 'B', -10.5);
INSERT INTO foo VALUES (7, 'B', 25);
1) Create a temp table that has one extra MatchFound column to record whether a match is found.
CREATE TABLE #TempTable(Id int, Name varchar(50), Balance decimal(10,2), MatchFound bit);
INSERT INTO #TempTable (Id, Name, Balance, MatchFound)
SELECT Id, Name, Balance, 0
FROM foo;
2) Loop through the data to find a match for each row. Update MatchFound column if a match is found.
DECLARE #Id INT, #Name VARCHAR(50), #Balance decimal(10,2), #MatchId INT;
DECLARE MyCursor CURSOR FOR SELECT Id, Name, Balance FROM #TempTable ORDER BY Id;
OPEN MyCursor
FETCH NEXT FROM MyCursor INTO #Id, #Name, #Balance
WHILE ##FETCH_STATUS = 0
BEGIN
IF EXISTS (SELECT 1 FROM #TempTable WHERE Id = #Id AND MatchFound = 0)
BEGIN
SET #MatchId = NULL
SELECT TOP 1 #MatchId = Id FROM #TempTable
WHERE Id <> #Id AND Name = #Name AND Balance = #Balance * -1 AND MatchFound = 0
IF #MatchId IS NOT NULL
UPDATE #TempTable SET MatchFound = 1 WHERE Id IN (#Id, #MatchId)
END
FETCH NEXT FROM MyCursor INTO #Id, #Name, #Balance
END
CLOSE MyCursor;
DEALLOCATE MyCursor;
3) Select and return rows that did not have a match
Select Id, Name, Balance From #TempTable Where MatchFound = 0
4) Clean up
drop table #TempTable

SQL Server : Update order, Why does it work and can I trust it?

I am using SQL Server 2012
There is a "magic query" I don't understand why it's working using a temporary column I am updating a table and let it use the previous values it already calculated.
It sets the rolMul to be a rolling multiplication of the item till now.
Can I trust this method?
Why does it work in the first place?
If I can't trust it what alternatives can I use?
-- Create data to work on
select * into #Temp from (
select 1 as id, null as rolMul ) A
insert into #temp select 2 as id, null as rolMul
insert into #temp select 3 as id, null as rolMul
insert into #temp select 4 as id, null as rolMul
insert into #temp select 5 as id, null as rolMul
------Here is the magic I don't understand why it's working -----
declare #rolMul int = 1
update #temp set #rolMul = "rolMul" = #rolMul * id from #temp
select * from #temp
-- you can see it did what I wanted multiply all the previous values
drop table #temp
What bothers me is:
Why does it work? can I trust it to work?
What about the order? If
the table was not ordered
select * into #Temp from (
select 3 as id, null as rolMul ) A
insert into #temp select 1 as id, null as rolMul
insert into #temp select 5 as id, null as rolMul
insert into #temp select 2 as id, null as rolMul
insert into #temp select 4 as id, null as rolMul
declare #rolMul int = 1
update #temp set #rolMul = "rolMul" = #rolMul * id from #temp
select * from #temp order by id
drop table #Temp
go
If I can't trust it what alternatives can I use?
As of SQL Server 2012, you can use an efficient rolling sum of logarithms.
WITH tempcte AS (
SELECT
id,
rolmul,
EXP(SUM(LOG(id)) OVER (ORDER BY id)) AS setval
FROM #Temp
)
UPDATE tempcte
SET rolmul = setval;
SQL Server 2012 introduces the OVER clause to the SUM function. Ole Michelsen shows with a brief example how this efficiently solves the running total problem.
The product law of logarithms says that the log of the product of two numbers is equal to the sum of the log of each number.
This identity allows us to use the fast sum to calculate multiplications at similar speed. Take the log before the sum and take the exponent of the result, and you have your answer!
SQL Server gives you LOG and EXP to calculate the natural logarithm (base e) and its exponential. It doesn't matter what base you use as long as you are consistent.
The updatable common table expression is necessary because window expressions can't appear in the SET clause of an update statement.
The query is reliably correct for small numbers of rows, but will overflow very quickly. Try 64 rows of 2 and you'll bust the bigint!
In theory this should product the correct result as long as the ids are unique. In practice, I think your set of ids will always be small :-)

Trigger to update rank dynamically

I want to write trigger for insert update and Delete. O have one table named (tbl_rank) which have primary key (ID).
ID Name Rank
1 A 1
2 B 2
3 C 3
4 D 4
5 E 5
Now I want to insert new rank but conditions are
1) if I enter 6 it will be 6
2) if I enter 7 it also should be 6 (I mean in sequence)
3) if I enter 2 than than entered rank will be 2 and 2 will be 3 and so on
For delete trigger
1) if I delete 5 the rank should be 1 to 4
2) if I delete 2 the rank would be rearranged and 3 should be 2 and 4 would be 3 and so on
for update trigger
1) if I update 3 to 5 than 4 would be 3 and 5 would be 4
2) if I update 5 to 3 than 3 would be 4 and 4 would be 5
I wrote insert and delete trigger its working fine but in update I am getting uneven result.
Can you not just have tbl_rank as a view then you don't need any triggers? To rank them in the view you can use a windowed function row_number() over (order by Id)
How is the initial update performed? If you know it is an update then you need to do a delete and insert just for the effected range. Eg changing 3 to 5. You delete records for 3 to 5 then insert those 3 records again with the different ids. An update statement essentially does this anyway
There is an assumption that id is not an auto-identity column.
CREATE TRIGGER trg_tbl_rank
ON tbl_rank
INSTEAD OF INSERT,DELETE,UPDATE
AS
BEGIN
SET NOCOUNT ON;
DECLARE #v_deleted_rank INT;
DECLARE #v_inserted_rank INT;
DECLARE #v_max_rank INT;
SELECT #v_deleted_rank = COALESCE(rank, 0) FROM deleted;
SELECT #v_inserted_rank = COALESCE(rank, 0) FROM inserted;
SELECT #v_max_rank = COALESCE(MAX(rank), 0) FROM tbl_rank;
IF #v_deleted_rank > 0
BEGIN
DELETE FROM tbl_rank
WHERE id = (SELECT id FROM deleted);
UPDATE tbl_rank
SET rank = rank - 1
WHERE rank > #v_deleted_rank;
END
IF #v_inserted_rank > 0
BEGIN
IF #v_inserted_rank <= #v_max_rank
BEGIN
UPDATE tbl_rank
SET rank = rank + 1
WHERE rank >= #v_inserted_rank;
INSERT INTO tbl_rank (id, name, rank)
SELECT id, name, #v_inserted_rank FROM inserted;
END
ELSE
INSERT INTO tbl_rank (id, name, rank)
SELECT id, name, #v_max_rank + 1 FROM inserted;
END
END
GO
Here are queries to test:
INSERT INTO tbl_rank (id, name, rank) VALUES (1, 'A', 1);
INSERT INTO tbl_rank (id, name, rank) VALUES (2, 'B', 2);
INSERT INTO tbl_rank (id, name, rank) VALUES (3, 'C', 3);
INSERT INTO tbl_rank (id, name, rank) VALUES (4, 'D', 4);
INSERT INTO tbl_rank (id, name, rank) VALUES (5, 'E', 5);
SELECT * FROM tbl_rank;
INSERT INTO tbl_rank (id, name, rank) VALUES (6, 'F', 7);
SELECT * FROM tbl_rank;
INSERT INTO tbl_rank (id, name, rank) VALUES (7, 'G', 2);
SELECT * FROM tbl_rank;
DELETE FROM tbl_rank WHERE rank = 7;
SELECT * FROM tbl_rank;
DELETE FROM tbl_rank WHERE rank = 2;
SELECT * FROM tbl_rank;
UPDATE tbl_rank SET rank = 5 WHERE rank = 3;
SELECT * FROM tbl_rank;
UPDATE tbl_rank SET rank = 3 WHERE rank = 5;
SELECT * FROM tbl_rank;
TRUNCATE TABLE tbl_rank;
I want to write trigger for insert update and Delete I have one table named(tbl_rank)which have primary key(ID)
Please post DDL, so that people do not have to guess what the keys, constraints, Declarative Referential Integrity, data types, etc. in your schema are. Learn how to follow ISO-11179 data element naming conventions and formatting rules. Temporal data should use ISO-8601 formats. Code should be in Standard SQL as much as possible and not local dialect.
This is minimal behavior on SQL forums. Putting “tbl_” on table name is a classic design flaw called “tbling” and the column names are violations of ISO-11179 rules, too. Now we have to guess at keys, data types, etc. Here is my guess and clean up.
CREATE TABLE Prizes
(prize_id INTEGER NOT NULL PRIMARY KEY,
prize_name CHAR(1) NOT NULL,
prize_rank INTEGER NOT NULL);
INSERT INTO Prizes
VALUES
(1, 'A', 1),
(2, 'B', 2),
(3, 'C', 3),
(4, 'D', 4),
(5, 'E', 5);
Why triggers? RDBMS has virtual tables and columns. This not a deck of punch cards or a magnetic tape file. A VIEW is always current and correct.
CREATE VIEW Prize_List
AS
SELECT prize_id, prize_name,
ROW_NUMBER() OVER (ORDER BY prize_id)
AS prize_rank
FROM Prizes;
But it might be better to drop the prize_id column completely and re-arrange the display order based on the prize_rank column:
CREATE TABLE Prizes
(prize_name CHAR(1) NOT NULL,
prize_rank INTEGER NOT NULL PRIMARY KEY);
Now use procedures to manipulate the table as needed.
CREATE PROCEDURE Swap_Prize_Ranks (#old_prize_rank INTEGER, #new_prize_rank INTEGER)
AS
UPDATE Prizes
SET prize_rank
= CASE prize_rank
WHEN #old_prize_rank
THEN #new_prize_rank
ELSE prize_rank + SIGN(#old_prize_rank - #new_prize_rank)
END
WHERE prize_rank BETWEEN #old_prize_rank AND #new_prize_rank
OR prize_rank BETWEEN #new_prize_rank AND #old_prize_rank;
When you want to drop a few rows, remember to close the gaps with this:
CREATE PROCEDURE Close_Prize_Gaps()
AS
UPDATE Prizes
SET prize_rank
= (SELECT COUNT (P1.prize_rank)
FROM Prizes AS P1
WHERE P1.prize_rank <= Prizes.prize_rank);

Run insert statement x number of times

I have two tables. One table A has n rows of data and the other table B is empty. I want to insert n rows into table B, 1 row for each row in table A. Table B will have a couple of fields from table A in it, including a foreign key from table A.
In the end I want one row in B for each row in A. To do this I used:
INSERT INTO B(Col1
,Col2
,Col3
,Col4
,Col5
);
SELECT 100
,25
,'ABC'
,1
,A.ID
FROM Auctions A
Now, I've put this code in a stored procedure and this SP takes an int param called NumInserts.
I want to insert n * NumInserts rows. So, if n is 10 and NumInserts is 5 I want to run this code 5 * 10 (50) times.
In other words for each row in table A I want to insert 5 rows in table B. How would I do that?
create procedure insert_into_b
#numInserts int
as
begin
while #numInserts > 0
begin
insert into b (id)
select id from a
set #numInserts = #numInserts - 1
end
end
exec insert_into_b 2
This is a hack and I wouldn't recommend using it in production or big volumes of data. However, in development quick-and-dirty scenarios I found it often useful:
Use GO \[count\] to execute a batch of commands a specified number of times.
Concretely, if you had a stored procedure called InsertAIntoB, you could run this in Management Studio:
exec InsertAIntoB
GO 10
(replace 10 with whatever NumInserts is)
I prefer to avoid looping when I can, just so I don't have to maintain some easily breakable and somewhat ugly loop structure in my stored procedure.
You could easily do this with a Numbers table, the CROSS APPLY statement, and your existing INSERT statement.
Given that your numbers table would look like this:
Number
======
0
1
2
...
Your SQL statement simply becomes:
INSERT INTO B
(
[Col1]
,[Col2]
,[Col3]
,[Col4]
,[Col5]
)
SELECT
100
,25
,'ABC'
,1
,a.ID
FROM
Auctions a
CROSS APPLY
Numbers n
WHERE
n.Number BETWEEN 1 AND #NumInserts
Numbers tables can be useful if use appropriately. If you're unfamiliar with them, here are a few resources and some pros/cons:
http://dataeducation.com/you-require-a-numbers-table/ (the code to create a numbers table in this article is shown below)
http://archive.msdn.microsoft.com/SQLExamples/Wiki/View.aspx?title=NumbersTable
https://dba.stackexchange.com/questions/11506/why-are-numbers-tables-invaluable
Maybe this solution is overkill if #NumInserts is always going to be a reasonably small number, but if you already have a Numbers table sitting around, you might as well take advantage of it!
UPDATE:
Here's a quick and dirty method to populate a numbers table from 0 to 65,535:
CREATE TABLE Numbers
(
Number INT NOT NULL,
CONSTRAINT PK_Numbers
PRIMARY KEY CLUSTERED (Number)
WITH FILLFACTOR = 100
)
GO
INSERT INTO Numbers
SELECT
(a.Number * 256) + b.Number AS Number
FROM
(
SELECT number
FROM master..spt_values
WHERE
type = 'P'
AND number <= 255
) a (Number),
(
SELECT number
FROM master..spt_values
WHERE
type = 'P'
AND number <= 255
) b (Number)
GO
Credit: http://dataeducation.com/you-require-a-numbers-table/
Create procedure DoitNTimes
#N integer = 1
As
Set NoCount On
While #N > 0 Begin
Insert B (Col1, Col2, Col3, Col4, Col5)
Select 100, 25, 'ABC', 1, A.ID
From Auctions A
-- -----------------------------------
Set #N -= 1
End
If using SQL Server 2005 or earlier replace the Set #N -= 1' withSet #N = #N-1`
and if you really want to avoid loop using T-SQL variables, then use a CTE, not a disk-based table:
Create procedure DoitNTimes
#N integer = 1
As
Set NoCount On
With nums(num) As
(Select #N Union All
Select num - 1
From nums
Where num > 1)
Insert B (Col1, Col2, Col3, Col4, Col5)
Select 100, 25, 'ABC', 1, A.ID
From Auctions A Full Join nums
Option(MaxRecursion 10000)
but of course, this is also still looping, just like any solution to this issue.
Very late answer but there is no need to loop and it's a little simpler than Corey's good answer;
DECLARE #n int = 10;
INSERT INTO B(Col1,Col2,Col3,Col4,Col5);
SELECT 100,25,'ABC',1,A.ID
FROM Auctions A
JOIN (SELECT TOP(#n) 1 [junk] FROM sys.all_objects) as copies ON 1 = 1
You could use any table in the join as long as it has the number of rows you'll need. You could also change "1 [junk]" to "ROW_NUMBER() OVER(ORDER BY object_id) [copyno]" if you wanted a copy number somewhere in the insert table.
Hopefully this will save someone a little work down the road...
Try this (on SQL server databases):
DECLARE #NumInserts SMALLINT = 3
INSERT INTO B (Col1, Col2, Col3, Col4, Col5)
SELECT 100, 25, 'ABC', 1, A.ID
FROM Auctions A
JOIN master.dbo.spt_values numbers ON numbers.number < #NumInserts
WHERE numbers.[type] = 'P'
Note: This will only work if #NumInserts is less than or equal to 2048
master.dbo.spt_values WHERE type = 'P' is just a built-in SQL Server table of numbers from 0 to 2047