I hope someone can help.
Essentially I have a table which contains fixed size data bundles such as: 50gb, 100gb, 250gb and 1000gb. There are more bundles than this but this is to show examples.
Essentially I want to create something where I pass it a number such as 1250gb and it will give me a list of which bundle sizes make up this 1250gb bundle from the table mentioned above.
How would I go about doing something like this?
There is a restriction to this subset sum problem, namely that we are looking for the minimal required bundles. This allows us to start looking from the biggest to the smallest bundle under the target size (just like handing back cash change, you start from the largest note and work your way down to the smallest coin).
Sample data
Added an extra bundle with size 2000 for demonstration purposes.
create table bundles
(
sizeGB int
);
insert into bundles (sizeGB) values
(50),
(100),
(250),
(1000),
(2000);
Solution
Set target size #targetGB = 1550 to have an example with repeated bundles
and excluding bundles that are too big.
Define an initial value #sumGB = 0 to increment as we go.
Select the biggest bundle #sumPartGB that we can add to #sumGB
and stay within the #targetGB size limit.
Store that part of the sum in a result table #result.
Increment #sumGB with the selected bundle.
Repeat as long as #sumGB < #targetGB.
In code:
declare #targetGB int = 1550;
declare #sumGB int = 0;
declare #result table
(
sizeGB int
);
while #sumGB < #targetGB
begin
declare #sumPartGB int;
select top 1 #sumPartGB = b.sizeGB
from bundles b
where b.sizeGB + #sumGB <= #targetGB
order by b.sizeGB desc;
insert into #result (sizeGB) values (#sumPartGB);
set #sumGB += #sumPartGB;
end
select r.sizeGB as sumParts
from #result r
order by r.sizeGB desc;
Result
sumParts
--------
1000
250
250
50
Calling this algorithm could be done through a table-valued function (= function that returns a table). How you store or wrap this algorithm ultimately depends on your application.
Define function
create function getBundles(#targetGB int)
returns #result table (sizeGB int)
as
begin
declare #sumGB int = 0;
while #sumGB < #targetGB
begin
declare #sumPartGB int;
select top 1 #sumPartGB = b.sizeGB
from bundles b
where b.sizeGB + #sumGB <= #targetGB
order by b.sizeGB desc;
insert into #result (sizeGB) values (#sumPartGB);
set #sumGB += #sumPartGB;
end
return;
end;
Call function
select r.sizeGB as sumParts
from getBundles(1550) r
order by r.sizeGB desc;
Fiddle to see everything in action.
I am trying to generating unique card number from following function. I put my query inside a while loop to prevent duplicate card number but still I am getting duplicate numbers.
Anyone can help me?
Create FUNCTION GetCardNumber ()
RETURNS varchar(20)
AS
BEGIN
Declare #NewID varchar(20);
Declare #NewID1 varchar(36) ;
Declare #Counter int = 0;
While(1=1)
Begin
Set #NewID1 = (SELECT [MyNewId] FROM Get_NewID);
Set #NewID = '2662464' + '823' + '001' +right(YEAR(GETUTCDATE()),2) +(left(convert(varchar,ABS(CAST(CAST(#NewID1 AS VARBINARY(5)) AS bigint))),5));
Set #Counter = (Select count(*) from ContactTBL where ContactMembershipID = #NewID);
If #Counter = 0
BEGIN
BREAK;
END
End
return #newID
END
Go
Update : I am getting MyNewID from View:
CREATE VIEW Get_NewID
AS
SELECT NEWID() AS MyNewID
GO
Many thanks in advance.
Won't this just return the same value every time you run it? I can't see anywhere where you're incrementing anything, or getting any kind of value that would give you unique values each time. You need to do something that changes the value each time, for example using the current exact date and time.
You're returning varchar(20) in line 2. To get your 'unique' NewId, you're doing this:
Set #NewId = (13 digit constant value) + (last 2 digits of current year) +
left(
convert(varchar,
ABS(CAST
(CAST(#NewID1 AS VARBINARY(5)) AS bigint)
)
)
,5)
which leaves you only 5 characters of uniqueness! This is almost certainly the issue. An easy fix may be increase the characters you return on line 2 e.g. RETURNS varchar(30)
What you're doing is unnecessarily complicated, and I think there is an element of overprotecting against potential duplicate values. This line is very suspect:
Set #NewID = '2662464' + '823' + '001' +right(YEAR(GETUTCDATE()),2) +(left(convert(varchar,ABS(CAST(CAST(#NewID1 AS VARBINARY(5)) AS bigint))),5));
The maximum for bigint is 2^63-1, so casting your 5-byte VARBINARY to a bigint could result in an overflow, which may also cause an issue.
I'm not sure exactly what you're trying to achieve, but you need to simplify things and make sure you have more scope for unique values!
Set #NewID1 = (SELECT [MyNewId] FROM Get_NewID);
always return the same result (if no other changes)
Set #NewID = '2662464' + '823' + '001' +right(YEAR(GETUTCDATE()),2) +(left(convert(varchar,ABS(CAST(CAST(#NewID1 AS VARBINARY(5)) AS bigint))),5));
as result #New_ID will be the same also
I'm running an update script to obfuscate data and am occasionally experiencing the arithmetic overflow error message, as in the title. The table being updated has 260k records and yet the update script will need to be run several times to produce the error. Although it's so rare I can't rely on the code until it's fixed as it's a pain to debug.
Looking at other similar questions, this is often resolved by changing the data type e.g from INT to BIGINT either in the table or in a calculation. However, I can't see where this could be required. I've reduced the script to the below as I've managed to pin point it to the update of one column.
A function is being called by the update and I've included this below. I suspect that, due to the randomness of the error, the use of the NEW_ID function could be causing it but I haven't been able to re-create the error when just running this part of the function multiple times. The NEW_ID function can't be used in functions so it's being called from a view, also included below.
Update script:
UPDATE dbo.Addresses
SET HouseNumber = CASE WHEN LEN(HouseNumber) > 0
THEN dbo.fn_GenerateRandomString (LEN(HouseNumber), 1, 1, 1)
ELSE HouseNumber
END
NEW_ID view and random string function
CREATE VIEW dbo.vw_GetNewID
AS
SELECT NEWID() AS New_ID
CREATE FUNCTION dbo.fn_GenerateRandomString (
#stringLength int,
#upperCaseBit bit,
#lowerCaseBit bit,
#numberBit bit
)
RETURNS nvarchar(100)
AS
BEGIN
-- Sanitise string length values.
IF ISNULL(#stringLength, -1) < 0
SET #stringLength = 0
-- Generate a random string from the specified character sets.
DECLARE #string nvarchar(100) = ''
SELECT
#string += c2
FROM
(
SELECT TOP (#stringLength) c2 FROM (
SELECT c1 FROM
(
VALUES ('A'),('B'),('C')
) AS T1(c1)
WHERE #upperCaseBit = 1
UNION ALL
SELECT c1 FROM
(
VALUES ('a'),('b'),('c')
) AS T1(c1)
WHERE #lowerCaseBit = 1
SELECT c1 FROM
(
VALUES ('0'),('1'),('2'),('3'),('4'),('5'),('6'),('7'),('8'),('9')
) AS T1(c1)
WHERE #numberBit = 1
)
AS T2(c2)
ORDER BY (SELECT ABS(CHECKSUM(New_ID)) from vw_GetNewID)
) AS T2
RETURN #string
END
Addresses table (for testing):
CREATE TABLE dbo.Addresses(HouseNumber nchar(32) NULL)
INSERT Addresses(HouseNumber)
VALUES ('DSjkmf jkghjsh35hjk h2jkhj3h jhf'),
('SDjfksj3548 ksjk'),
(NULL),
(''),
('2a'),
('1234567890'),
('An2b')
Note: only 7k of the rows in the addresses table have a value entered i.e. LEN(HouseNumber) > 0.
An arithmetic overflow in what is otherwise string-based code is confounding. But there is one thing that could be causing the arithmetic overflow. That is your ORDER BY clause:
ORDER BY (SELECT ABS(CHECKSUM(New_ID)) from vw_GetNewID)
CHECKSUM() returns an integer, whose range is -2,147,483,648 to 2,147,483,647. Note the absolute value of the smallest number is 2,147,483,648, and that is just outside the range. You can verify that SELECT ABS(CAST('-2147483648' as int)) generates the arithmetic overflow error.
You don't need the checksum(). Alas, you do need the view because this logic is in a function and NEWID() is side-effecting. But, you can use:
ORDER BY (SELECT New_ID from vw_GetNewID)
I suspect that the reason you are seeing this every million or so rows rather than every 4 billion rows or so is because the ORDER BY value is being evaluated multiple times for each row as part of the sorting process. Eventually, it is going to hit the lower limit.
EDIT:
If you care about efficiency, it is probably faster to do this using string operations rather than tables. I might suggest this version of the function:
CREATE VIEW vw_rand AS SELECT rand() as rand;
GO
CREATE FUNCTION dbo.fn_GenerateRandomString (
#stringLength int,
#upperCaseBit bit,
#lowerCaseBit bit,
#numberBit bit
)
RETURNS nvarchar(100)
AS
BEGIN
DECLARE #string NVARCHAR(255) = '';
-- Sanitise string length values.
IF ISNULL(#stringLength, -1) < 0
SET #stringLength = 0;
DECLARE #lets VARCHAR(255) = '';
IF (#upperCaseBit = 1) SET #lets = #lets + 'ABC';
IF (#lowerCaseBit = 1) SET #lets = #lets + 'abc';
IF (#numberBit = 1) SET #lets = #lets + '0123456789';
DECLARE #len int = len(#lets);
WHILE #stringLength > 0 BEGIN
SELECT #string += SUBSTRING(#lets, 1 + CAST(rand * #len as INT), 1)
FROM vw_rand;
SET #stringLength = #stringLength - 1;
END;
RETURN #string
END;
As a note: rand() is documented as being exclusive of the end of its range, so you don't have to worry about it returning exactly 1.
Also, this version is subtly different from your version because it can pull the same letter more than once (and as a consequence can also handle longer strings). I think this is actually a benefit.
I am asked to generate custom ID values for primary key columns. The query is as follows,
SELECT * FROM SC_TD_GoodsInward WHERE EntityId = #EntityId
SELECT #GoodsInwardId=IIF((SELECT COUNT(*) FROM SC_TD_GoodsInward)>0, (Select 'GI_'+RIGHT('00'+CONVERT(varchar,datepart(YY,getdate())),2)+RIGHT('00'+CONVERT(varchar,datepart(MM,getdate())),2)+RIGHT('00'+CONVERT(varchar,datepart(DD,getdate())),2)+'_'+CONVERT(varchar,#EntityId)+'_'+(SELECT RIGHT('0000'+CONVERT(VARCHAR,CONVERT(INT,RIGHT(MAX(GoodsInwardId),4))+1),4) from SC_TD_GoodsInward)), (SELECT 'GI_'+RIGHT('00'+CONVERT(varchar,datepart(YY,getdate())),2)+RIGHT('00'+CONVERT(varchar,datepart(MM,getdate())),2)+RIGHT('00'+CONVERT(varchar,datepart(DD,getdate())),2)+'_'+CONVERT(varchar,#EntityId)+'_0001'))
Here the SC_TD_GoodsInward is a table, GoodsInwardId is the value to be generated. I am getting the desired outputs too. Examples.
GI_131118_1_0001
GI_131212_1_0002
GI_131212_1_0003
But, the above condition fails when the last digits reach 9999. I simulated the query and the results were,
GI_131226_1_9997
GI_140102_1_9998
GI_140102_1_9999
GI_140102_1_0000
GI_140102_1_0000
GI_140102_1_0000
GI_140102_1_0000
GI_140102_1_0000
After 9999, it goes to 0000 and does not increment thereafter. So, in the future, I will eventually run into a PK duplicate error. How can i recycle the values so that after 9999, it goes on as 0000, 0001 ... etc. What am I missing in the above query?
NOTE: Please consider the #EntityId value to be 1 in the query.
I am using SQL SERVER 2012.
Before giving a solution for the question few points on your question:
As the Custom primary key consists of mainly three parts Date(140102), physical location where transaction takes place (entityID), 4 place number(9999).
According to the design on a single date in a single physical location there cannot be more than 9999 transactions -- My Solution will also contain the same limitation.
Some points on my solution
The 4 place digit is tied up to the date which means for a new date the count starts from 0000. For Example
GI_140102_1_0001,
GI_140102_1_0002,
GI_140102_1_0003,
GI_140103_1_0000,
GI_140104_1_0000
Any way the this field will be unique.
The solution compares the latest date in the record to the current date.
The Logic:
If current date and latest date in the record matches
Then it increments 4 place digit by the value by 1
If the current date and the latest date in the record does not matched
The it sets the 4 place digit by the value 0000.
The Solution: (Below code gives out the value which will be the next GoodsInwardId, Use it as per requirement to fit in to your solution)
declare #previous nvarchar(30);
declare #today nvarchar(30);
declare #newID nvarchar(30);
select #previous=substring(max(GoodsInwardId),4,6) from SC_TD_GoodsInward;
Select #today=RIGHT('00'+CONVERT(varchar,datepart(YY,getdate())),2)
+RIGHT('00'+CONVERT(varchar,datepart(MM,getdate())),2)+RIGHT('00'+CONVERT(varchar,datepart(DD,getdate())),2);
if #previous=#today
BEGIN
Select #newID='GI_'+RIGHT('00'+CONVERT(varchar,datepart(YY,getdate())),2)
+RIGHT('00'+CONVERT(varchar,datepart(MM,getdate())),2)+RIGHT('00'+CONVERT(varchar,datepart(DD,getdate())),2)
+'_'+CONVERT(varchar,1)+'_'+(SELECT RIGHT('0000'+
CONVERT(VARCHAR,CONVERT(INT,RIGHT(MAX(GoodsInwardId),4))+1),4)
from SC_TD_GoodsInward);
END
else
BEGIN
SET #newID='GI_'+RIGHT('00'+CONVERT(varchar,datepart(YY,getdate())),2)
+RIGHT('00'+CONVERT(varchar,datepart(MM,getdate())),2)+RIGHT('00'+CONVERT(varchar,datepart(DD,getdate())),2)
+'_'+CONVERT(varchar,1)+'_0000';
END
select #newID;
T-SQL to create the required structure (Probable Guess)
For the table:
CREATE TABLE [dbo].[SC_TD_GoodsInward](
[EntityId] [int] NULL,
[GoodsInwardId] [nvarchar](30) NULL
)
Sample records for the table:
insert into dbo.SC_TD_GoodsInward values(1,'GI_140102_1_0000');
insert into dbo.SC_TD_GoodsInward values(1,'GI_140101_1_9999');
insert into dbo.SC_TD_GoodsInward values(1,'GI_140101_1_0001');
**Its a probable solution in your situation although the perfect solution would be to have identity column (use reseed if required) and tie it with the current date as a computed column.
You get this problem because once the last 4 digits reach 9999, 9999 will remain the highest number no matter how many rows are inserted, and you are throwing away the most significant digit(s).
I would remodel this to track the last used INT portion value of GoodsInwardId in a separate counter table (as an INTEGER), and then MODULUS (%) this by 10000 if need be. If there are concurrent calls to the PK generator, remember to lock the counter table row.
Also, even if you kept all the digits (e.g. in another field), note that ordering a CHAR is as follows
1
11
2
22
3
and then applying MAX() will return 3, not 22.
Edit - Clarification of counter table alternative
The counter table would look something like this:
CREATE TABLE PK_Counters
(
TableName NVARCHAR(100) PRIMARY KEY,
LastValue INT
);
(Your #EntityID might be another candidate for the counter PK column.)
You then increment and fetch the applicable counter on each call to your custom PK Key generation PROC:
UPDATE PK_Counters
SET LastValue = LastValue + 1
WHERE TableName = 'SC_TD_GoodsInward';
Select
'GI_'+RIGHT('00'+CONVERT(varchar,datepart(YY,getdate())),2)
+RIGHT('00'+CONVERT(varchar,datepart(MM,getdate())),2)
+RIGHT('00'+CONVERT(varchar,datepart(DD,getdate())),2)+'_'
+CONVERT(varchar,#EntityId)+'_'
+(SELECT RIGHT('0000'+ CONVERT(NVARCHAR, LastValue % 10000),4)
FROM PK_Counters
WHERE TableName = 'SC_TD_GoodsInward');
You could also modulo the LastValue in the counter table (and not in the query), although I believe there is more information about the number of records inserted by leaving the counter un-modulo-ed.
Fiddle here
Re : Performance - Selecting a single integer value from a small table by its PK and then applying modulo will be significantly quicker than selecting MAX from a SUBSTRING (which would almost certainly be a scan)
DECLARE #entityid INT = 1;
SELECT ('GI_'
+ SUBSTRING(convert(varchar, getdate(), 112),3,6) -- yymmdd today DATE
+ '_' + CAST(#entityid AS VARCHAR(50)) + '_' --#entity parameter
+ CASE MAX(t.GI_id + 1) --take last number + 1
WHEN 10000 THEN
'0000' --reset
ELSE
RIGHT( CAST('0000' AS VARCHAR(4)) +
CAST(MAX(t.GI_id + 1) AS VARCHAR(4))
, 4)
END) PK
FROM
(
SELECT TOP 1
CAST(SUBSTRING(GoodsInwardId,11,1) AS INT) AS GI_entity,
CAST(SUBSTRING(GoodsInwardId,4,6) AS INT) AS GI_date,
CAST(RIGHT(GoodsInwardId,4) AS INT) AS GI_id
FROM SC_TD_GoodsInward
WHERE CAST(SUBSTRING(GoodsInwardId,11,1) AS INT) = #entityid
ORDER BY gi_date DESC, rowTimestamp DESC, gi_id DESC
) AS t
This should take the last GoodInwardId record, ordered by date DESC and take its numeric "id". Then add + 1 to return the NEW id and combine it with today's date and the #entityid you passed. If >9999, start again from 0000.
You need a timestamp type column tho, to order two inserted in the same date + same transaction time. Otherwise you could get duplicates.
I have simplified the answer even more and arrived with the following query.
IF (SELECT COUNT(GoodsInwardId) FROM SC_TD_GoodsInward WHERE EntityId = #EntityId)=0
BEGIN
SELECT #GoodsInwardId= 'GI_'+RIGHT('00'+CONVERT(varchar,datepart(YY,getdate())),2)+
RIGHT('00'+CONVERT(varchar,datepart(MM,getdate())),2)+
RIGHT('00'+CONVERT(varchar,datepart(DD,getdate())),2)+'_'+
CONVERT(varchar,#EntityId)+'_0001'
END
ELSE
BEGIN
SELECT * FROM SC_TD_GoodsInward WHERE EntityId = #EntityId AND CONVERT(varchar,CreatedOn,103) = CONVERT(varchar,GETDATE(),103)
SELECT #GoodsInwardId=IIF(##ROWCOUNT>0,
(Select 'GI_'+
RIGHT('00'+CONVERT(varchar,datepart(YY,getdate())),2)+
RIGHT('00'+CONVERT(varchar,datepart(MM,getdate())),2)+
RIGHT('00'+CONVERT(varchar,datepart(DD,getdate())),2)+'_'+
CONVERT(varchar,#EntityId)+'_'+
(SELECT RIGHT('0000'+CONVERT(VARCHAR,CONVERT(INT,RIGHT(MAX(GoodsInwardId),4))+1),4) from SC_TD_GoodsInward WHERE CONVERT(varchar,CreatedOn,103) = CONVERT(varchar,GETDATE(),103))),
(SELECT 'GI_'+RIGHT('00'+CONVERT(varchar,datepart(YY,getdate())),2)+
RIGHT('00'+CONVERT(varchar,datepart(MM,getdate())),2)+
RIGHT('00'+CONVERT(varchar,datepart(DD,getdate())),2)+'_'+
CONVERT(varchar,#EntityId)+'_0001'))
END
select * from SC_TD_GoodsInward
We have a program that can pull information from a database that we use for shipping. The way it works is it uses an ODBC driver to pull from our database, so that when we type in "order number 5" into the shipping program it will also pull the matching address, phone number, etc.
The problem is that the database contains only numbers for the orders, however the program that contains the database which we use for inventory management prints our labels with the order number in the format TK123456. I need to figure out how to make SQL interpret the order number as just numbers when inputted, so basically cut the TK off the start.
SELECT RXFILL.RXFILL_ID, RXMAIN.RX_NUMBER, PATIENT.FIRSTNAME, PATIENT.LASTNAME,
SHIPADDRESS1, SHIPADDRESS2, SHIPCITY, SHIPSTATE, SHIPZIP, EMAIL
FROM RXFILL
LEFT JOIN RXMAIN ON RXFILL.RXMAIN_ID = RXMAIN.RXMAIN_ID
LEFT JOIN PATIENT ON RXMAIN.PATIENT_ID = PATIENT.PATIENT_ID
WHERE RXFILL_ID=$ORDERNUMBER
If I am understanding it correctly the $ORDERNUMBER is what needs to be adjusted to not include letters. However the program does specify the final line must be in the format WHERE [field name]=$ORDERNUMBER.
How can this be done?
If you only want this to be solved in SQL and not in the calling application, and you know that the first two characters of $ORDERNUMBER will always be 'TK', then you can easily solve it by taking a substring of $ORDERNUMBER starting at the third character... i.e.
WHERE RXFILL_ID=SUBSTRING($ORDERNUMBER, 2).
That syntax might not be exact, since you haven't divulged your DBMS type and each DBMS implements SUBSTRING in whatever way they want.
If you share more info about the calling application which sets $ORDERNUMBER, I'm sure it would be better to make the change there.
I have write a function for this.
CREATE Function SelectNumbersFromString(#str varchar(max))
Returns varchar(max) as
BEGIN
Declare #cchk char(5);
Declare #len int ;
Declare #aschr int;
SET #len = ( SElect len(#str) );
Declare #count int
SET #count = 1
DECLARE #ans varchar(max)
SET #ans = ''
While #count <= #len
BEGIN
SET #cchk = ( select Substring(#str,#count,1) );
SET #aschr = ( select ASCII(#cchk) );
IF #aschr in ( 49,50,51,52,53,54,55,56,57,58 )
BEGIN
SET #ans = #ans + CHAR(#aschr)
END
SET #count = #count + 1;
END
RETURN #ans;
END
TESTED
SELECT SelectNumbersFromString('abc3deef5ff6') will return 356
From http://wfjanjua.blogspot.com/2012/07/add-numbers-from-stringvarchar-in-tsql.html