Creating a loop to suggest username+number - SQL Server - sql

I've been trying to create a procedure to implement in user creation. The procedure is meant to see if the username is already taken, in which case it has to print an alternative username. The alternate needs to be constructed from the username+lowest number that is not already taken.
For example, if I tried "Ofek" and both "Ofek" and "Ofek1" are taken, I should receive "Ofek2".
So far this is what I have:
ALTER PROCEDURE [dbo].[SuggestUsername]
#username varchar(10)
AS
BEGIN
DECLARE #name varchar(10)
IF #username IN (SELECT User_Name FROM Players WHERE User_Name = #username)
WHILE (#name IN (SELECT User_Name FROM Players WHERE User_Name = #username))
SET #name = (CONCAT(#username, +1))
PRINT (CONCAT('Username already taken. Please choose a different one, or use: ', #name))
END
For some reason, #name comes up as empty whenever I tried running it. Is there a better way to do it? if not, what's wrong with my current code?
Thanks in advance!
edit
I've decided to give up this idea and try to implement it differently (with a count of how many times #username already appears as the additional number). Thanks for all who tried to help.

Since you mention that this is just "for a class" in your comment and I have time to spend because of COVID, here is a trick.
CREATE VIEW MyView
AS
SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) N
FROM (VALUES (0), (0), (0), (0), (0), (0), (0), (0), (0)) T(A) CROSS JOIN
(VALUES (0), (0), (0), (0), (0), (0), (0), (0), (0)) TT(B) CROSS JOIN
(VALUES (0), (0), (0), (0), (0), (0), (0), (0), (0)) TTT(C) CROSS JOIN
(VALUES (0), (0), (0), (0), (0), (0), (0), (0), (0)) TTTT(D);
-- 6561 number
CREATE TABLE Users
(
UserName VARCHAR(10) UNIQUE
);
INSERT Users VALUES
('Ofek'), ('Ofek1');
CREATE PROCEDURE MyProc
#UserName VARCHAR(10)
AS
BEGIN
WITH CTE(N, UN) AS
(
SELECT 0, #UserName
UNION ALL
SELECT N, CONCAT(#UserName, N)
FROM MyView
)
INSERT Users(UserName)
SELECT TOP 1 UN
FROM CTE
WHERE NOT EXISTS(SELECT 1 FROM Users WHERE UserName = UN)
ORDER BY N;
END
EXEC MyProc 'Ofek';
SELECT * FROM Users;
You can see how it's working on a db<>fiddle
I won't recommend this as a real solution or a way to work with on a real database, instead you can add a UNIQUE constraint or using EXISTS() to notify the user if the name is already taken without suggesting.

Related

Looping in SQL Server from 1 to 60

I have a table T1 as below
I need to copy the data from T1 to another table called T2. T2 has an additional column called 'Month' and each record from T1 needs to be copied to T2 60 times, with Month value ranging from 1 to 60.
I have been trying something like this and need the MONTH value to be taken dynamically , like a loop from 1 to 60. Could someone help please? Thank you
INSERT INTO T2
SELECT PRODUCT, CUSTOMER, 1 as MONTH
FROM T1
We can use a cross join approach:
WITH months AS (
SELECT n = v2.n * 10 + v1.n
FROM (VALUES (0), (1), (2), (3), (4), (5), (6), (7), (8), (9)) v1(n)
CROSS JOIN (VALUES (0), (1), (2), (3), (4), (5), (6)) v2(n)
)
INSERT INTO T2 (Product, Customer, Month)
SELECT t1.Product, t1.Customer, m.n
FROM table1 t1
CROSS JOIN months m
WHERE m.n BETWEEN 1 AND 60;
CROSS JOIN to a tally, with the values 1 to 60. -- Thanks #Larnu for the answer.
I used stored procedures in MySQL:
DELIMITER $$
CREATE PROCEDURE auto_insert()
BEGIN
DECLARE i1 INT DEFAULT 1;
WHILE i1 <= 60 DO
INSERT INTO T2 SELECT *, i1 FROM T1;
SET i1 = i1 + 1;
END WHILE;
END $$
DELIMITER ;
CALL auto_insert;

How to select values with a column alias?

If I do this in SQLite3:
SELECT *
FROM (VALUES (1), (2), (3)) AS "tbl.col"
WHERE "tbl"."col" = 1
I get: no such column: tbl.col
What is the correct way?
Thanks!
I think you are looking for this:
SELECT *
FROM (VALUES (1), (2), (3)) AS tbl(col)
WHERE tbl.col = 1;
Note that when you escape an identifier (using "tbl.col"), then that is one name that has a period in it. Not two names.
EDIT:
I would have expected the above to work, but it doesn't in SQLite. One alternative is to use a CTE:
with tbl(col) as (
VALUES (1), (2), (3)
)
SELECT *
FROM tbl
where tbl.col = 1
I found this question which answers my question.
SELECT "tbl"."" AS "col"
FROM (VALUES (1), (2), (3)) AS "tbl"
Basically, in a VALUES the first column name is an empty string "", the second is ":1", third is ":2" and so forth...
Hope this helps somebody else.

SQL sectioning/averaging based on different timetag/timestamps and user-chosen input (T-SQL)

I've got the following problem that I would like to cope with - I have a SQL dataset that I would like to section (e.g. like this one):
OldTimetag OldValue
2012-05-03 12:47:00 5
2012-05-03 13:00:00 1.3
2012-05-03 13:21:00 7
2012-05-03 14:56:00 5
2012-05-03 14:57:00 0.3
.... ....
Now, I want to section (and/or average) the data based on a user-chosen interval - into new timetags, e.g. every 15 minutes with the first timetag as starting point, i.e.:
NewTimetag NewValue
2012-05-03 12:47:00 4.507
2012-05-03 13:02:00 1.3
.... ....
The main constraint is that the value next to the timetag is always valid, until the next timetag appears. So the value of 5 at timetag 2012-05-03 12:47:00 is valid for the next 13 minutes until 13:00:00. The value for the first 15 minutes from 12:47:00 would be (13*5+2*1.3)/15 = 4.507. In the next 15 minutes, at 13:02:00 the value is simply equal to 1.3... (and so on)
I've come so long, that it is a good idea to make an "artificial table" first, to later join it with the old table. I'm generating that table by:
DECLARE #intStart datetime, #intEnd datetime
SELECT #intStart =min(OldTimetag), #intEnd = MAX(OldTimetag)
FROM OldTable
where OldTimetag between '2012-05-03 12:47:00' and '2012-05-03 14:57:00'
Declare #ArtificalTable table (NewTimeTag datetime, NewValue Float)
Declare #MinuteSlicer Int
Set #MinuteSlicer = 15
Insert #Hallo Select #intStart, null
While ( #intStart < #intEnd ) BEGIN
Insert #ArtificalTable
Select DATEADD(mi,#MinuteSlicer, #intStart), Null
Set #intStart = DATEADD(mi,#MinuteSlicer,#intStart)
If #intEnd <= DATEADD(mi,#MinuteSlicer,#intStart)
Break
End
This gives me an output like:
NewTimetag NewValue
2012-05-03 12:47:00 Null
2012-05-03 13:02:00 Null
.... ....
However, I'm having problems with the next step, how to join the tables correctly - can anyone give me a hint?
Here is one way of doing it.
Sample Data:
declare #data table(OldTimetag datetime2, OldValue numeric(5,2));
Insert into #data(OldTimetag, OldValue) Values
('2012-05-03 12:47:00', 5)
, ('2012-05-03 13:00:00', 1.3)
, ('2012-05-03 13:21:00', 7)
, ('2012-05-03 14:56:00', 5)
, ('2012-05-03 14:57:00', 0.3);
Your custom range size in minutes:
declare #mins int = 15;
List is used to quickly compute an ordered list of number from 0 to n Where n <= to the number of minutes between the first and the last OldTimetag.
With list(n) as (
Select top(Select 1+DATEDIFF(minute, min(OldTimetag), max(OldTimetag)) From #data)
ROW_NUMBER() over(order by (select 1))-1
From (
Select 1 From (values(1), (1), (1), (1), (1), (1), (1), (1), (1), (1)) as x1(n)
Cross Join (values(1), (1), (1), (1), (1), (1), (1), (1), (1), (1)) as x2(n)
Cross Join (values(1), (1), (1), (1), (1), (1), (1), (1), (1), (1)) as x3(n)
) as x(n)
)
Select NewTimetag = DATEADD(minute, #mins*(l.n/#mins), MIN(r.startTime)), NewValue = AVG(d.oldValue)
From list l
Cross Join (Select startTime = min(OldTimetag) From #data) as r
Cross Apply (Select maxTimetag = MAX(OldTimetag) From #data Where OldTimetag <= DATEADD(minute, n, startTime)) as mx
Inner Join #data d on d.OldTimetag = mx.maxTimetag
Group By l.n/#mins
Cross Join is used to mix each number from the ordered list with the first OldTimetag from your data.
Cross Apply is used to get the nearest OldTimetag before each minute created with the Cross Join.
Inner Join then matches the nearest OldTimetag with your data in order to retrieved oldValue.
Select only have to calculate the average for each range on #mins minutes and its NewTimetag.
It works well for a range of up to 1000 minutes between the min and max OldTimetag. If you need to go beyond this limit, you can add a a 4th line in the list CTE:
Cross Join (values(1), (1), (1), (1), (1), (1), (1), (1), (1), (1)) as x4(n) => up to 10.000
Cross Join (values(1), (1), (1), (1), (1), (1), (1), (1), (1), (1)) as x5(n) => up to 100.000
...
One way is to determine the intervals (an interval is generated if it contains at least one timestamp), augment the time table with the next timestamp and then calculate the averages for each such interval by intersecting the intervals with the time table.
IF OBJECT_ID('tempdb..#values') IS NOT NULL DROP TABLE #values
CREATE TABLE #values (pk int identity, time datetime, value numeric(10,4))
INSERT INTO #values VALUES ('2012-05-03 12:47:00', 5)
INSERT INTO #values VALUES ('2012-05-03 13:00:00', 1.3)
INSERT INTO #values VALUES ('2012-05-03 13:21:00', 7)
INSERT INTO #values VALUES ('2012-05-03 14:56:00', 5)
INSERT INTO #values VALUES ('2012-05-03 14:57:00', 0.3)
DECLARE #timeSpanMinutes int SET #timeSpanMinutes=15
DECLARE #startTime datetime, #endTtime datetime
SELECT #startTime=MIN(time) FROM #values
SELECT #endTtime =DATEADD(MINUTE,(DATEDIFF(MINUTE,#startTime,MAX(time))
/#timeSpanMinutes+1)*#timeSpanMinutes, #startTime) FROM #values -- MAX(time) multiple
SELECT intervals.start
, SUM(value*(DATEDIFF(MINUTE -- minutes in intersection of [start,end] and [time,next]
, CASE WHEN time<start THEN start ELSE time END -- Maximum(time,start)
, CASE WHEN next<DATEADD(MINUTE,#timeSpanMinutes,intervals.start) THEN next
ELSE DATEADD(MINUTE,#timeSpanMinutes,intervals.start) END -- Minimum(next,end)
)*1.0/#timeSpanMinutes)) as average
FROM
(SELECT DISTINCT DATEADD(MINUTE, (DATEDIFF(MINUTE,#startTime,time)
/#timeSpanMinutes)*#timeSpanMinutes, #startTime) AS start
FROM #values -- round start to multiple of #timeSpanMinutes
UNION SELECT DISTINCT DATEADD(MINUTE,#timeSpanMinutes+(DATEDIFF(MINUTE,#startTime,time)
/#timeSpanMinutes)*#timeSpanMinutes, #startTime)
FROM #values -- union distinct with same as above but shifted with #timeSpanMinutes
) intervals -- intervals start time (end is calculated as start + #timeSpanMinutes)
INNER JOIN
(SELECT v.*,ISNULL((SELECT MIN(time) FROM #values WHERE time>v.time),#endTtime) as next
FROM #values v -- add next column to #values
) vals
ON vals.next>=intervals.start and vals.time<=DATEADD(MINUTE,#timeSpanMinutes,start)
WHERE intervals.start<>#endTtime
GROUP BY intervals.start
ORDER BY intervals.start

How to insert N rows of default values into a table

I have a table containing an identity column as well as a column representing the creation date:
CREATE TABLE dbo.OrderStatus
(
OrderStatusId int IDENTITY(1, 1) NOT NULL,
CreationDate datetime NOT NULL default GETDATE(),
CONSTRAINT PK_OrderStatus PRIMARY KEY(OrderStatusId)
)
Since the identity column generates a value by itself and the CreationDate is always going to be the current date (GETDATE()), I can add a row thanks to DEFAULT VALUES:
INSERT INTO dbo.OrderStatus DEFAULT VALUES;
But what can I do if I want to add, let's say, three records?
Current solution (edited some input since it didn't make any sense)
For now, in order to do what I want, I add several rows with VALUES:
INSERT INTO dbo.OrderStatus (CreationDate)
VALUES (GETDATE()),
(GETDATE()),
(GETDATE())
Although, I'd prefer to know the equivalent of INSERT INTO .. DEFAULT VALUES for multiple rows, in case that I add another column with a default value later on.
Is there a way to insert N rows into a table with DEFAULT VALUES or in a similar way?
An easier way is:
insert dbo.OrderStatus default values
go 500
this will insert 500 rows of default values.
You can use your original definition and just use a while loop, for example
DECLARE #OrderStatus TABLE
(
OrderStatusId int IDENTITY(1, 1) NOT NULL,
CreationDate datetime NOT NULL DEFAULT GETDATE()
--CONSTRAINT PK_OrderStatus PRIMARY KEY(OrderStatusId) -- this can be uncommented if creating a real table.
)
DECLARE #i int = 0;
WHILE #i < 100 -- insert 100 rows. change this value to whatever you want.
BEGIN
INSERT #OrderStatus DEFAULT VALUES
SET #i = #i + 1;
END
SELECT * FROM #OrderStatus
Here's how to do it using a recursive CTE:
;with cteNums(n) AS
(
SELECT 1
UNION ALL
SELECT n + 1
FROM cteNums WHERE n < 100 -- how many times to iterate
)
INSERT #OrderStatus
SELECT * FROM cteNums
Just note that for the CTE you'd have to specify OPTION(MAXRECURSION ...) if it's greater than 100. Also note that even though you're selecting a list of numbers from the CTE, they don't actually get inserted into the table.
The Tally Table method can insert large sets of multiple rows, providing the tally table is big enough. This Tally table will handle up to 1000 entries.
WITH Tally (n) AS
(
-- 1000 rows
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) a(n)
CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) b(n)
CROSS JOIN (VALUES(0),(0),(0),(0),(0),(0),(0),(0),(0),(0)) c(n)
)
--SELECT * FROM Tally;
Create Table #temp (id int, d datetime, GUID uniqueidentifier, str1 nvarchar(1), number int)
insert into #temp
select n, getdate(), newid(), 'a', 101 from tally
where N<=100 -- THIS IS WHERE YOU INDICATE HOW MANY ROWS
select * from #temp
Set up a trigger when a new row is CREATEd:
https://msdn.microsoft.com/en-us/library/ms189799.aspx

Get nth weekday from XDate in SQL Server

I have to get date repeatedly for every N months.
I have XDate to Start from.
I want the nth week's mth weekday's date.
N is say 2 - I have to get for every 2 month
XDate is suppose tomorrow's date. So, Starting from tomorrow
m is 7 - So, get date of every Saturday
n is 2 - of second week.
I could not even think for start point for this complex logic.
Any suggestion how should I start - pseudo code
Thanks in advance,
First, this is where a calendar table comes in handy. The following code creates a table called calendar and populates it with dates starting in 2000. It also has a column called NthWeekdayInMonth. For example, if you look at the entries for 1/29/05 through 1/31/05 you'll see that this column is set to a 5 because those were the 5th Saturday, Sunday, and Monday of the month.
CREATE TABLE Calendar
(
[Date] date NOT NULL,
[NthWeekdayInMonth] int,
CONSTRAINT PK_Calendar
PRIMARY KEY CLUSTERED ([Date])
WITH FILLFACTOR = 100
)
;WITH cte AS
(
SELECT
DATEADD(d, (a.Number * 256) + b.Number, '01/01/2000') AS [Date]
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)
)
INSERT INTO Calendar
SELECT
[Date],
ROW_NUMBER() OVER (PARTITION BY YEAR([Date]), MONTH([Date]), DATEPART(dw, [Date]) ORDER BY [Date]) FROM cte
ORDER BY
[Date]
GO
Now that we have a calendar table the rest is fairly straightforward. I did deviate from your design in one respect but you should be able to adjust it if needed. In my implementation, the starting date is literally the first date that should be returned. So a starting date of 1/11/2014, looking every 2 months would return:
2014-01-11
2014-03-08
2014-05-10
2014-07-12
By passing the first date the code can figure out what day of the week it was and what week of the month. Passing those values in is redundant. The test code is below...
DECLARE #startDate date
DECLARE #everyNMonths int
DECLARE #numResults int
DECLARE #nthAppearanceOfDay int
SET #startDate = '01/11/2014' -- First occurence is on this date
SET #everyNMonths = 2 -- Skip every n months
SET #numResults = 4 -- Max # of results to return
-- Figure out which x-day of the month this is. For example, if the starting
-- date is 1/11/2014 that was the second Saturday so this will be set to 2.
SELECT #nthAppearanceOfDay = NthWeekdayInMonth FROM calendar WHERE [date] = #startDate
-- Use a CTE to get all the months involved in this calculation
;WITH candidateMonths AS (
SELECT
1 AS [resultnum], #startDate AS [date]
UNION ALL
SELECT resultnum + 1, DATEADD(month, #everyNMonths, [date]) FROM candidateMonths
WHERE resultnum + 1 <= #numResults
)
-- Now evaluate every date for each of the candidate months. If the day of week matches
-- that of the start date AND it is the Nth occurrence of that day of week in the month
-- include it
SELECT
c.[Date]
FROM
candidateMonths cm
INNER JOIN calendar c ON ( (YEAR(c.[Date]) = YEAR(cm.[Date])) AND (MONTH(c.[Date]) = MONTH(cm.[Date])))
WHERE
(DATEPART(dw, c.[date]) = DATEPART(dw, #startDate)) -- Same day of week
AND
(c.NthWeekdayInMonth = #nthAppearanceOfDay) -- Same week of month
I've been experimenting with the following code:
SELECT * FROM dbo.NthWeekday(GETDATE(), 1, 1);
SELECT * FROM dbo.NthWeekday(GETDATE(), 1, -1);
Where 1 is Sunday and 7 is Saturday regardless of the ##DATEFIRST setting. A positive value for n (or 0) will return the Next Nth Weekday while a negative value for n returns the Previous Nth Weekday.
I don't fully understand what you want but if I gathered correctly: just getting the Nth Weekday is not enough. You want to do this repeatedly for X months as well. This is the tentative code I'd use:
DECLARE #date DATE = GETDATE();
DECLARE #numMonths INT = -5
DECLARE #weekday INT = 1;
DECLARE #n INT = 2;
SELECT C.D
FROM dbo.RangeSmallInt(0, #numMonths - SIGN(#numMonths)) A
CROSS APPLY ( -- MonthBegin
SELECT DT = DATEADD(m, DATEDIFF(m, 0, #date) + A.N, 0)
) B
CROSS APPLY dbo.NthWeekday(B.DT, #weekday, #n) C;
Results: 2014-12-14
2015-01-11
2015-02-08
2015-03-08
2015-04-12
Which you could wrap in a table-valued function much like I have done with NthWeekday and RangeSmallInt. The RangeSmallInt function call can be replaced with a numbers table, tally CTE, or whatever terminology/style you're comfortable with.
How it works:
We start by generating a set of numbers beginning with 0 because we want the function to be inclusive. (#numMonths - SIGN(#numMonths)) handles the addition or subtraction of 1/0 from #numMonths based on the sign of #numMonths. This ensures that the proper range of integers (in the above case: 0 through -4) are generated for our next trick.
Once we have a range of integers to work with we can use them to offset the date. In this case we want to find out the beginning of the month for X months. If we had a function that could return the Nth Month Begin Date then we would simply pass the integers we already have to the function and get out the dates we want. So that's exactly what we do using CROSS APPLY.
Now that we have the beginning of the month for X months solved all we need to do is apply our NthWeekday function to these dates.
Nth Weekday:
CREATE FUNCTION dbo.NthWeekday (
#date DATE = NULL
, #weekday INT = NULL
, #n INT = 1
)
RETURNS TABLE
AS
RETURN (
SELECT D = CASE SIGN(#n)
WHEN -1 THEN DATEADD(d, -(DATEPART(dw, #date) + ##DATEFIRST - #weekday) % 7 + ((#n + 1) * 7), #date)
ELSE DATEADD(d, (#weekday - DATEPART(dw, #date) + ##DATEFIRST) % 7 + ((#n - SIGN(#n)) * 7), #date)
END
);
RangeSmallInt:
-- Generate a range of up to 65,536 contiguous BIGINTS
CREATE FUNCTION dbo.RangeSmallInt (
#num1 BIGINT = NULL
, #num2 BIGINT = NULL
)
RETURNS TABLE
AS
RETURN (
WITH Numbers(N) AS (
SELECT N FROM(VALUES
(1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 16
, (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 32
, (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 48
, (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 64
, (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 80
, (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 96
, (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 112
, (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 128
, (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 144
, (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 160
, (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 176
, (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 192
, (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 208
, (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 224
, (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 240
, (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1), (1) -- 256
) V (N)
)
SELECT TOP (
CASE
WHEN #num1 IS NOT NULL AND #num2 IS NOT NULL THEN ABS(#num1 - #num2) + 1
ELSE 0
END
)
N = ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) + CASE WHEN #num1 <= #num2 THEN #num1 ELSE #num2 END - 1
FROM Numbers A
, Numbers B
WHERE ABS(#num1 - #num2) + 1 < 65537
);