Generating a list of random numbers, summing to a fixed amount using SQL - sql

This question is about generating N random numbers whose sum is M(constant) using SQL.
For example we have a number M=9.754. We need 10 random numbers whose sum is 9.754.
It should be done in SQL Server environment.
Can anybody help me?

While the #Squirrel answer is interesting but numbers here is more random
here is the code:
DECLARE #s INT=1,
#k FLOAT=0,
#final FLOAT=9.917,
#sum FLOAT =0,
#min FLOAT=1,
#max FLOAT=9.917
BEGIN
WHILE (#sum <> #final)
BEGIN
WHILE (#s <= 10)
BEGIN
SET #k =
(
SELECT ROUND(RAND(CHECKSUM(NEWID())) * (#max - #min) + #min,3)
);
PRINT (CONCAT('random: ',#k));
IF(#sum+#k <=#final)
SET #sum+=#k;
SET #max=#final-#sum;
PRINT (CONCAT('computed sum: ',#k));
IF(#max>1) SET #min=1 ELSE SET #min=0;
IF(#sum=#final)
BREAK;
SET #s = #s + 1;
SET #k = #k + 0;
END;
PRINT (CONCAT('final', #final))
PRINT (CONCAT('sum', #sum))
IF(#sum<>#final)--force stop if after 10 try the sum not match with final
BEGIN
PRINT(CONCAT('final random number:',#final-#sum))
SET #sum=#final;
END;
SET #s=0;
IF(#sum=#final)
BEGIN
PRINT('****************************DONE****************************')
BREAK;
END
END;
PRINT ('end');
END;

Interesting requirement.
Query below uses tally table / number table to generate 10 random numbers after that find the ratio. Final query check for case where sum of the numbers is not equal to #m and make the adjustment on the biggest number.
declare #m decimal(10,3) = 9.754,
#n int = 10;
with
-- using recursive CTE to generate a number table
numbers as
(
select n = 1
union all
select n = n + 1
from numbers
where n < #n
),
-- generate random positive numbers using newid()
-- Note : 100 is chosen arbitrary
num as
(
select n = abs(checksum(newid())) % 100
from numbers
),
-- calculate the ratio
ratio as
(
select r,
rn = row_number() over (order by r desc),
sum_r = sum(r) over()
from
(
select r = convert(decimal(10,3), n * 1.0 / sum(n) over() * #m)
from num
) r
)
-- sum(r) may not equal to #m due to rounding
-- find the difference and adjust it to the biggest r
select r, rn, sum_r,
adj_r = r + case when rn = 1 then #m - sum_r else 0 end,
sum_adj_r = sum(r + case when rn = 1 then #m - sum_r else 0 end) over()
from ratio
dbfiddle demo

Related

T-SQL Recursive Division

How do I perform recursive division in SQL?
I know the basic premise is
declare #i = 0
declare #testValue bigint = FLOOR(#num/#dem)
while #testValue>0
begin
set #num = #num - #testValue*#dem
set #testValue = FLOOR(#num/#dem)
set #i = #i + 1
end
, but I do not know how to do this in an efficient manner.
Your query can be rewritten as the following:
Declare #Num BigInt = 10000,
#Dem BigInt = 50
;With Cte As
(
Select Floor(#num / #dem) TestValue, 1 As Iteration
Union All
Select Floor((TestValue * #Dem) / #Dem) As TestValue, Iteration + 1
From Cte
Where TestValue > 0
)
Select *
From Cte
But there's a problem with the design of it. It multiplies the value back to the division result, and divides again, resulting in an infinite loop.
I believe this is what you were trying to do:
Declare #Num BigInt = 10000,
#Dem BigInt = 50
;With Cte As
(
Select Floor(#num / #dem) TestValue, 1 As Iteration
Union All
Select Floor(TestValue / #Dem) As TestValue, Iteration + 1
From Cte
Where TestValue > 0
)
Select *
From Cte
Which, in this case gives the following results:
TestValue Iteration
200 1
4 2
0 3

How to select only armstrong numbers from the list?

I want is to select Armstrong numbers from the list below list I have searched of solution of this question bu unable to find in SQL-Server:
Numbers
121
113
423
153
541
371
I am sure most of you know what's the Armstrong number and how to calculate though I am describing is for the simplicity : sum of the cubes of its digits is equal to the number itself i.e.
1*1*1 + 5*5*5 + 3*3*3 = 153
3*3*3 + 7*7*7 + 1*1*1 = 371
Please help me on this as I am also trying but seeking for quick solution. It will be very helpful to me. Thanks in advance.
Obviously static processing during each query is not correct approach but we can create function like this and
create function dbo.IsArmstrongNumber(#n int)
returns int as
begin
declare #retValue int = 0
declare #sum int = 0
declare #num int = #n
while #num > 0
begin
set #sum += (#num%10) * (#num%10) * (#num%10)
set #num = #num/10
end
IF #sum = #n
set #retValue = 1
return #retValue
end
Pre-processing and selecting in IN clause is better
select * from #Numbers where dbo.IsArmstrongNumber(n) = 1
select 153 x into #temp;
insert #temp values(371);
insert #temp values(541);
with cte as (select x, substring(cast(x as nvarchar(40)) ,1,1) as u, 1 as N FROM #temp
union all
select x, substring(cast(x as nvarchar(40)),n+1,1) as u , n+1 from cte where len(cast(x as nvarchar(40))) > n
)
select x from cte group by x having SUM(POWER(cast(u as int),3)) = x
drop table #temp;
here is the mark 2 - you can change the #ORDER to explore power of 4,5 etc
declare #order int = 3;
declare #limit int = 50000;
with nos as (select 1 no
union all
select no + 1 from nos where no < #limit),
cte as (select no as x, substring(cast(no as nvarchar(40)) ,1,1) as u, 1 as N FROM nos
union all
select x, substring(cast(x as nvarchar(40)),n+1,1) as u , n+1 from cte where len(cast(x as nvarchar(40))) > n
)
select x from cte group by x having SUM(POWER(cast(u as int),#order)) = x
option (maxrecursion 0);
This is a quick mod to my sum of digits UDF
Declare #Table table (Numbers int)
Insert into #Table values
(121),
(113),
(423),
(153),
(541),
(371)
Select * from #Table where [dbo].[udf-Stat-Is-Armstrong](Numbers)=1
Returns
Numbers
153
371
The UDF
CREATE Function [dbo].[udf-Stat-Is-Armstrong](#Val bigint)
Returns Bit
As
Begin
Declare #RetVal as bigint
Declare #LenInp as bigint = len(cast(#Val as varchar(25)))
;with i AS (
Select #Val / 10 n, #Val % 10 d
Union ALL
Select n / 10, n % 10
From i
Where n > 0
)
Select #RetVal = IIF(SUM(power(d,#LenInp))=#Val,1,0) FROM i;
Return #RetVal
End
You can use the following to find Armstrong numbers using Sql functions:
WITH Numbers AS(
SELECT 0 AS number UNION ALL SELECT number + 1 FROM Numbers WHERE number < 10000)
SELECT number AS ArmstrongNumber FROM Numbers
WHERE
number = POWER(COALESCE(SUBSTRING(CAST(number AS VARCHAR(10)),1,1),0),3)
+ POWER(COALESCE(SUBSTRING(CAST(number AS VARCHAR(10)),2,1),0),3)
+ POWER(COALESCE(SUBSTRING(CAST(number AS VARCHAR(10)),3,1),0),3)
OPTION(MAXRECURSION 0)

Complicated SQL while loop

I am trying to create a while loop in SQL and it seems kind of complex. Here's what I need it to achieve:
Iterate through a single VARCHAR string (ex. '123')
If the nth character is in an even position in the string (ex. 2nd, 4th .... letter in the string), it must be added(SUM) to a base variable (Let's assume #z)
If the nth character is in an odd position in the string (ex. 1st, 3rd .... letter in the string), it must be multiplied by 2. If this newly generated value (Let's assume #y) is less than 10, it must be added(SUM) to the base variable (Still the same assumed #z). If #y is greater than 10, we need to subtract 9 from #y before adding(SUM) it to #z
After iterating through the entire string, this should return a numeric value generated by the above process.
Here is what I've done so far, but I'm stuck now (Needless to say, this code does not work yet, but I think I'm heading in the right direction):
DECLARE #x varchar(20) = '12345'
DECLARE #p int = len(#x)
WHILE #p > 0
SELECT #x =
stuff(#x, #p, 1,
case when CONVERT(INT,substring(#x, #p, 1)) % 2 = 0 then CONVERT(INT, #x) + CONVERT(INT,substring(#x, #p, 1))
end), #p -= 1
RETURN #x;
PS. The input will always be 100% numeric values, but it is formatted as VARCHAR when I recieve it.
UPDATE
The expected result for the sample string is 15
You can do this without using a loop. Here is a solution using Tally Table:
DECLARE #x VARCHAR(20) = '12345'
DECLARE #z INT = 0 -- base value
;WITH E1(N) AS( -- 10 ^ 1 = 10 rows
SELECT 1 FROM(VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))t(N)
),
E2(N) AS(SELECT 1 FROM E1 a CROSS JOIN E1 b), -- 10 ^ 2 = 100 rows
E4(N) AS(SELECT 1 FROM E2 a CROSS JOIN E2 b), -- 10 ^ 4 = 10,000 rows
CteTally(N) AS(
SELECT TOP(LEN(#x)) ROW_NUMBER() OVER(ORDER BY(SELECT NULL))
FROM E4
),
CteChars(N, num) AS(
SELECT
t.N, CAST(SUBSTRING(#x, t.N, 1) AS INT)
FROM CteTally t
WHERE t.N <= LEN(#x)
)
SELECT
SUM(
CASE
WHEN N % 2 = 0 THEN num
WHEN num * 2 < 10 THEN num * 2
ELSE (num * 2) - 9
END
) + #z
FROM CteChars
The CTEs up to CteTally generates a list of number from 1 to LEN(#x). CteChars breaks #x character by character into separate rows. Then the final SELECT does a SUM based on the conditions.
OUTPUT : 15
Check below if it helps you
DECLARE #x varchar(20) = '12345'
DECLARE #p int = 1
DECLARE #result bigint=0;
DECLARE #tempval int =0;
WHILE #p <= len(#x)
BEGIN
SET #tempval = CONVERT(INT,substring(#x, #p, 1));
if(#p%2 = 1)
BEGIN
SET #tempval = #tempval * 2;
IF(#tempval >= 10) SET #tempval = #tempval - 9;
END
SET #result = #result + #tempval;
SET #p = #p + 1;
END;
PRINT #result;--This is the result
RETURN #x;
DECLARE #x INT = 12345
DECLARE #p int = len(#x)
DECLARE #z INT =0
PRINT #p%2
SET #x=#x/10
PRINT #x
WHILE #p > 0
BEGIN
IF(#p%2 = 0)
BEGIN
SET #z=#z+#x%10
SET #p=#p-1
SET #x=#x/10
END
ELSE
BEGIN
SET #z=#z+(2*(#x%10))
SET #p=#p-1
SET #x=#x/10
IF(#x>=10)
BEGIN
SET #x=(#x/10+#x%10)
END
END
END
SELECT #z
The while loop does not seem necessary here.
This can be achieved with a CTE that will split the string and a case statement:
DECLARE #x varchar(20) = '12345';
with split(id, v) as (
select 0, cast(0 as tinyint)
union all
select id+1, cast(SUBSTRING(#x, id+1, 1) as tinyint)
From split
Where id+1 <= len(#x)
)
Select Result = SUM(
Case When id % 2 = 0 then v
When v < 5 then v*2
Else (v*2)-9
End
)
From split
output = 15

SQL Data Sampling

We have had a request to provide some data to an external company.
They require only a sample of data, simple right? wrong.
Here is their sampling criteria:
Total Number of records divided by 720 (required sample size) - this gives sampling interval (if result is a fraction, round down to next whole number).
Halve the sampling interval to get the starting point.
Return each record by adding on the sampling interval.
EXAMPLE:
10,000 Records - Sampling interval = 13 (10,000/720)
Starting Point = 6 (13/2 Rounded)
Return records 6, 19 (6+13), 32 (19+13), 45 (32+13) etc.....
Please can someone tell me how (if) something like this is possible in SQL.
If you have use of ROW_NUMBER(), then you can do this relatively easily.
SELECT
*
FROM
(
SELECT
ROW_NUMBER() OVER (ORDER BY a, b, c, d) AS record_id,
*
FROM
yourTable
)
AS data
WHERE
(record_id + 360) % 720 = 0
ROW_NUMBER() gives all your data a sequential identifier (this is important as the id field must both be unique and NOT have ANY gaps). It also defines the order you want the data in (ORDER BY a, b, c, d).
With that id, if you use Modulo (Often the % operator), you can test if the record is the 720th record, 1440th record, etc (because 720 % 720 = 0).
Then, if you offset your id value by 360, you can change the starting point of your result set.
EDIT
After re-reading the question, I see you don't want every 720th record, but uniformly selected 720 records.
As such, replace 720 with (SELECT COUNT(*) / 720 FROM yourTable)
And replace 360 with (SELECT (COUNT(*) / 720) / 2 FROM yourTable)
EDIT
Ignoring the rounding conditions will allow a result of exactly 720 records. This requires using non-integer values, and the result of the modulo being less than 1.
WHERE
(record_id + (SELECT COUNT(*) FROM yourTable) / 1440.0)
%
((SELECT COUNT(*) FROM yourTable) / 720.0)
<
1.0
declare #sample_size int, #starting_point int
select #sample_size = 200
select top (#sample_size) col1, col2, col3, col4
from (
select *, row_number() over (order by col1, col2) as row
from your_table
) t
where (row % ((select count(*) from your_table) / #sample_size)) - (select count(*) from your_table) / #sample_size / 2) = 0
It's going to work in SQL Server 2005+.
TOP (#variable) is used to limit rows (where condition because of integers rounding might not be enough, may return more rows then needed) and ROW_NUMBER() to number and order rows.
Working example: https://data.stackexchange.com/stackoverflow/query/62315/sql-data-sampling below code:
declare #tab table (id int identity(1,1), col1 varchar(3), col2 varchar(3))
declare #i int
set #i = 0
while #i <= 1000
begin
insert into #tab
select 'aaa', 'bbb'
set #i = #i+1
end
declare #sample_size int
select #sample_size = 123
select ((select count(*) from #tab) / #sample_size) as sample_interval
select top (#sample_size) *
from (
select *, row_number() over (order by col1, col2, id desc) as row
from #tab
) t
where (row % ((select count(*) from #tab) / #sample_size)) - ((select count(*) from #tab) / #sample_size / 2) = 0
SQL server has in-built function for it.
SELECT FirstName, LastName
FROM Person.Person
TABLESAMPLE (10 PERCENT) ;
You can use rank to get a row-number. The following code will create 10000 records in a table, then select the 6th, 19th, 32nd, etc, for a total of 769 rows.
CREATE TABLE Tbl (
Data varchar (255)
)
GO
DECLARE #i int
SET #i = 0
WHILE (#i < 10000)
BEGIN
INSERT INTO Tbl (Data) VALUES (CONVERT(varchar(255), NEWID()))
SET #i = #i + 1
END
GO
DECLARE #interval int
DECLARE #start int
DECLARE #total int
SELECT #total = COUNT(*),
#start = FLOOR(COUNT(*) / 720) / 2,
#interval = FLOOR(COUNT(*) / 720)
FROM Tbl
PRINT 'Start record: ' + CAST(#start as varchar(10))
PRINT 'Interval: ' + CAST(#interval as varchar(10))
SELECT rank, Data
FROM (
SELECT rank()
OVER (ORDER BY t.Data) as rank, t.Data AS Data
FROM Tbl t) q
WHERE ((rank + 1) + #start) % #interval = 0

SQL Query to split number in a given interval

I have a number (say 100,000). I want to split this number between given intervals of 15,000 and 19,900. I expect the results to be between 15,000 and 19,900. I need an SQL Query/Function to do this task.
Example:
If I need to split 100,000 between intervals of 15,000 and 19,900 the results should be like this:
15100.00
16750.00
19365.22
18254.85
15987.65
14542.28
Please see that the sum of all these values is equal to 100,000
Thanks in advance.
If the last selected number does not need to be within the range you specified (as per your example), following would return some random numbers summing to the total given.
DECLARE #LowBound FLOAT
DECLARE #HighBound FLOAT
DECLARE #Total FLOAT
SET #LowBound = 15000
SET #HighBound = 19900
SET #Total = 100000
;WITH q AS (
SELECT [Value] = #LowBound + (#HighBound - #LowBound) * RAND(CHECKSUM(NEWID()))
, [RunningTotal] = CAST(0.00 AS FLOAT)
UNION ALL
SELECT [Value] = #LowBound + (#HighBound - #LowBound) * RAND(CHECKSUM(NEWID()))
, [RunningTotal] = CAST((q.[RunningTotal] + [Value]) AS FLOAT)
FROM q
WHERE q.[RunningTotal] < #Total
)
SELECT CASE WHEN [RunningTotal] < #Total
THEN [Value]
ELSE [Value] - ([RunningTotal] - #Total)
END AS Result
FROM (
SELECT [Value]
, [RunningTotal] = [Value] + [RunningTotal]
FROM q
) q
WHERE [RunningTotal] - [Value] <= #Total