SQL: Order randomly when inserting objects to a table - sql

I have an UDF that selects top 6 objects from a table (with a union - code below) and inserts it into another table. (btw SQL 2005)
So I paste the UDF below and what the code does is:
selects objects for a specific city and add a level to those (from table Europe)
union that selection with a selection from the same table for objects that are from the same country and add a level to those
From the union, selection is made to get top 6 objects, order by level, so the objects from the same city will be first, and if there aren't any available, then objects from the same country will be returned from the selection.
And my problem is, that I want to make a random selection to get random objects from table Europe, but because I insert the result of my selection into a table, I can't use order by newid() or rand() function because they are time-dependent, so I get the following errors:
Invalid use of side-effecting or time-dependent operator in 'newid' within a function.
Invalid use of side-effecting or time-dependent operator in 'rand' within a function.
UDF:
ALTER FUNCTION [dbo].[Objects] (#id uniqueidentifier)
RETURNS #objects TABLE
(
ObjectId uniqueidentifier NOT NULL,
InternalId uniqueidentifier NOT NULL
)
AS
BEGIN
declare #city varchar(50)
declare #country int
select #city = city,
#country = country
from Europe
where internalId = #id
insert #objects
select #id, internalId from
(
select distinct top 6 [level], internalId from
(
select top 6 1 as [level], internalId
from Europe N4
where N4.city = #city
and N4.internalId != #id
union select top 6 2 as [level], internalId
from Europe N5
where N5.countryId = #country
and N5.internalId != #id
) as selection_1
order by [level]
) as selection_2
return
END
If you have fresh ideas, please share them with me.
(Just please, don't suggest to order by newid() or to add a column rand() with seed DateTime (by ms or sthg), because that won't work.)

Perhaps you could take advantage of the guids by adding a position parameter to your inputs and then passing in a randomly generated value and then ordering by Substring(internalID, #Random,1)

I found a good solution myself and I thought it might be handy to share it :)
DECLARE #seed1 int
DECLARE #seed2 int
SET #seed1 = DATEPART(SECOND,GETDATE())
SET #seed2 = DATEPART(MILLISECOND,GETDATE())
SELECT TOP 10 [Column1], [Column2]
FROM [TABLE]
ORDER BY ROW_NUMBER() OVER (ORDER BY [KeyColumn]) * seed2 % seed1
I think it's simple enough and it's quite handy

What version of the database server are you using?
In SQL Server 2005 you can use rand with getdate as seed, and the function becomes indeterministic.
In earlier versions you can't have indeterministic functions, and you would have to use a stored procedure instead.

Related

Anything like template literals while writing a search query in SQL?

I am writing a stored procedure to get a particular value.
declare #num int
set #num = (SELECT Id
FROM [sometable]
WHERE Name like '%today%')
-- returns #num = 1
Select Value
FROM [anothertable]
where name like 'days1'
In the last line of the query I want to add "1" or any other number after 'days', depending on the variable #num.
How can I do it, sort of like how we use template literals in Javascript, using the ${} syntax but in SQL?
You can just use the first query as a sub-query of the second:
select [Value]
from anothertable
where [name] = Concat('days', (select Id from sometable where [Name] like '%today%'));

T-SQL get substring

I am looking to get an order number from a column named KEY_Ref, this ref column have various contents, but some rows look like this
LINE_NO=15^ORDER_NO=176572^RELEASE_NO=1^
Now I am interested in getting the value for ORDER_NO (176572 in this case)
How would I (In SQL Server) go about getting this (Or other) value from the main string
The logic is always
key1=value1^key2=value2^key3=value3^
You can use string_split():
select t.*, s.orderno
from t outer apply
(select stuff(s.value, 1, 9, '') as orderno
from string_split(t.key_ref, '^') s
where s.value like 'ORDER_NO=%'
) s;
Here is a db<>fiddle.
this is going to be a bit lengthy answer however if your SQL server version doesn't support string_split function you may use this.
declare #str varchar(100) = 'LINE_NO=15^ORDER_NO=176572^RELEASE_NO=1^'
declare #substr1 varchar(50) = substring(#str,charindex('^',#str)+1,len(#str))
declare #substr2 varchar(50) = substring(#substr1,charindex('=',#substr1)+1,charindex('^',#substr1)-charindex('=',#substr1)-1)
select #substr2 as 'order number'
the final variable will produce the desired value and you must merge the above queries to a single query that can fetch the value from the table in a single select statement.
this will work only if the pattern doesn't deviate from the one you've mentioned.

How to select from text list randomly?

I'm trying to build SQL function that I can use as a default value for a column. The function is about selecting an avatar image path randomly if the user didn't assign an image.
I have tried to but a completely wrong example to just approach the image not the solution
what I need to do is something like this
select top 1 from "avatar1,png, avatar2,png, avatar3.png, avatar4.png, avatar5.png" order by rand();
and I will convert it to a function like this
CREATE FUNCTION dbo.ReturnAvatar()
RETURNS nvarchar(100)
AS
BEGIN
DECLARE #ret nvarchar(100);
SET #ret = (select top 1 from "avatar1,png, avatar2,png, avatar3.png, avatar4.png, avatar5.png" as tbl order by rand());
RETURN #ret;
END;
this is just to explain the idea that I'm not able to apply. I don't know if SQL server has something like this or not.
Here is one way:
CREATE VIEW getNewID AS SELECT newid() as new_id
CREATE FUNCTION dbo.ReturnAvatar()
RETURNS nvarchar(100)
AS
BEGIN
DECLARE #ret nvarchar(100);
SET #ret = (SELECT TOP 1 value
FROM
STRING_SPLIT('avatar1.png,avatar2.png,avatar3.png,avatar4.png,avatar5.png', ',')
ORDER BY (SELECT new_id FROM getNewID));
RETURN #ret;
END;
Note that your current CSV string of filenames does not seem proper, because comma does not indicate the start of the extension in either Windows or Linux. So, I have assumed dot everywhere. In addition, if you want to use STRING_SPLIT, you may only split on a single character. Therefore, I assume that comma will be the delimiter here.
You do not need to create a table at all. Simply put the number inside your string and choose the number randomly:
select 'avatar'+str(round(rand()*5+1,0))+'.png'
would be fine.
Put that into your function and you are all set.
rand() produces 0..1(excl.) so you can simply multiply it by 5 and add 1 to get your range of 1...5
Demo: http://sqlfiddle.com/#!18/9eecb/82866
Documentation:
ROUND ( numeric_expression , length [ ,function ] )
STR ( float_expression [, length [, decimal]])
rand(seed)
So essentially you could boil it down to:
select 'avatar'+ltrim(str(rand()*5+1,20,0))+'.png'
with
ltrim(string) taking care of the space
create function dbo.ReturnAvatar(#uid uniqueidentifier, #avatars int = 10)
returns varchar(100)
as
begin
return ('avatar' + cast(abs(checksum(#uid)) % isnull(abs(#avatars), 10)+1 as varchar(100)) + '.png')
end
go
create table myusers
(
username varchar(50),
theavatar varchar(100) default( dbo.ReturnAvatar(newid(), default))
);
insert into myusers(username)
select top (10000) 'user' + cast(row_number() over(order by(select null)) as varchar(50))
from master.dbo.spt_values as a
cross join master.dbo.spt_values as b;
go
select theavatar, count(*)
from myusers
group by theavatar;
go
drop table myusers;

Make SQL SERVER evaluate clauses in a certain order

Take the following table as an instance:
CREATE TABLE TBL_Names(Name VARCHAR(32))
INSERT INTO TBL_Names
VALUES ('Ken'),('1965'),('Karen'),('2541')
sqlfiddle
Executing following query throws an exception:
SELECT [name]
FROM dbo.tblNames AS tn
WHERE [name] IN ( SELECT [name]
FROM dbo.tblNames
WHERE ISNUMERIC([name]) = 1 )
AND [name] = 2541
Msg 245, Level 16, State 1, Line 1 Conversion failed when converting
the varchar value 'Ken' to data type int.
While the following query executes without error:
SELECT [name]
FROM dbo.tblNames AS tn
WHERE ISNUMERIC([name]) = 1
AND [name] = 2541
I know that this is because of SQL Server Query Optimizer's decision. but I am wondering if there is any way to make sql server evaluate clauses in a certain order. this way, in the first query,the first clause filters out those Names that are not numeric so that the second clause will not fail at converting to a number.
Update: As you may noticed, the above query is just an instance to exemplify the problem. I know the risks of that implicit conversion and appreciate those who tried to warn me of that. However my main question is how to change Optimizer's behavior of evaluating clauses in a certain order.
There is no "direct" way of telling the engine to perform operations in order. SQL isn't an imperative language where you have complete control of how to do things, you simply tell what you need and the server decides how to do it itself.
For this particular case, as long as you have [name] = 2541, you are risking a potential conversion failure since you are comparing a VARCHAR column against an INT. Even if you use a subquery/CTE there is still room for the optimizer to evaluate this expression first and try to convert all varchar values to int (thus failing).
You can evade this with workarounds:
Correctly comparing matching data types:
[name] = '2541'
Casting [name] to INT beforehand and only whenever possible and on a different statement, do the comparison.
DECLARE #tblNamesInt TABLE (nameInt INT)
INSERT INTO #tblNamesInt (
nameInt)
SELECT
[nameInt] = CONVERT(INT, [name])
FROM
dbo.tblNames
WHERE
TRY_CAST([name] AS INT) IS NOT NULL -- TRY_CAST better than ISNUMERIC for INT
SELECT
*
FROM
#tblNamesInt AS T
WHERE
T.nameInt = 2351 -- data types match
Even an index hint won't force the optimizer to use an index (that's why it's called a hint), so we have little control on how it gets stuff done.
There are a few mechanics that we know are evaluated in order and we can use to our advantage, such as the HAVING expressions will always be computed after grouping values, and the grouping always after WHERE conditions. So we can "safely" do the following grouping:
DECLARE #Table TABLE (IntsAsVarchar VARCHAR(100))
INSERT INTO #Table (IntsAsVarchar)
VALUES
('1'),
('2'),
('20'),
('25'),
('30'),
('A') -- Not an INT!
SELECT
CASE WHEN T.IntsAsVarchar < 15 THEN 15 ELSE 30 END,
COUNT(*)
FROM
#Table AS T
WHERE
TRY_CAST(T.IntsAsVarchar AS INT) IS NOT NULL -- Will filter out non-INT values first
GROUP BY
CASE WHEN T.IntsAsVarchar < 15 THEN 15 ELSE 30 END
But you should always avoid writing code that implies implicit conversions (like T.IntsAsVarchar < 15).
Try like this
SELECT [name]
FROM #TBL_Names AS tn
WHERE [name] IN ( SELECT [name]
FROM #TBL_Names
WHERE ISNUMERIC([name]) = 1 )
AND [name] = '2541'
2)
AND [name] = convert(varchar,2541 )
Since You are storing name as varchar(32) varchar will accept integer datatype values also called precedence value
What about:
SELECT *
FROM dbo.tblNames AS tn
WHERE [name] = convert(varchar, 2541)
Why do you need ISNUMERIC([name]) = 1) since you only care about the value '2541'?
You can try this
SELECT [name]
FROM dbo.TBL_Names AS tn
WHERE [name] IN ( SELECT [name]
FROM dbo.TBL_Names
WHERE ISNUMERIC([name]) = 1 )
AND [name] = '2541'
You need to just [name] = 2541 to [name] = '2541'. You are missing ' (single quote) with name in where condition.
You can find the live demo Here.
Honestly, I wouldn't apply the implicit cast to your column [name], it'll make the query non-SARGable. Instead, convert the value of your input (or pass it as a string)
SELECT [name]
FROM dbo.TBL_Names tn
WHERE [name] = CONVERT(varchar(32),2541);
If you "must", however, wrap [name] (and suffer performance degradation) then use TRY_CONVERT:
SELECT [name]
FROM dbo.TBL_Names tn
WHERE TRY_CONVERT(int,[name]) = 2541;

Replace Any Occurrence of "P" in String With A Value From Another Table

I have a column, sort_order in a table that contains a string of numbers, a delimiter and some P values:
1150||P||1168||1144||1149||1147||1164||1152||P||1148||1162||1163||P||1156||1157||1154||
I would like to replace any P values in this string with another value from the event_tile_id column of another table.
So far I've drafted this SQL below with no luck. What changes can I make to this Query to get the effect I need?
`SELECT sort_order,
(
REPLACE(sort_order,'P',
(SELECT TOP 1 event_tile_id
FROM daily_email_sales_today)
)
)
as sort_order
FROM daily_email_preview`
Removed "default_SaleID" from Query. Replace should now have 4 arguments.
This is how I would do it.
Since you don't have any joins, why not do a simpler update query using a static value?
DECLARE #update VARCHAR(100)
SET #update = (SELECT TOP 1 event_tile_id FROM daily_email_sales_today)
update daily_email_preview
SET sort_order = replace(sort_order,'P', #update)
Or even,
update daily_email_preview
SET sort_order = replace(sort_order,'P', '<new value>')
Assuming you are using SQL Server.
Along the same thought process as #Eric_Hauenstein if you are running this in a TSQL process:
declare #rSTR as varchar(50)
SELECT TOP 1 #rSTR = event_tile_id FROM daily_email_sales_toda
SELECT sort_order, REPLACE(sort_order,'P', #rSTR) as sort_order
FROM daily_email_preview