I have a voucher format stored which reads something like this:
[VOUTYPECODE][ISBIRTHDAY][ISREUSABLE][STARTD][ENDD]VT555 + (RunningNo)
Referring to the query portion below,
#VouFormatLastNum is a string format represented like this:
'VouT001012012010420120704VT555181' derived by querying the respective [] bracketed items from the voucher format as seen above. Now, I am using 'VT555' as a blocker which is stored in #VouFormatCore (derived by detaching all bracketed items) to retrieve the running no. of the previous issued voucher which is '181' so that I can add a +1 to that running no. for my next voucher.
Everything works fine until I change my voucher format's with a different blocker other than VT555. Apparently, the last voucher will still have VT555 but the new #VouFormatCore has been changed to some other value hence I can no longer perform the query below to retrieve the running no.
I have been trying to think of ways to get by this problem. Anyway care to share a solution? Thanks.
--#VouFormatLastNum = 'VouT001012012010420120704VT555181' (This is obtained by querying the Top 1 voucher from the voucher table ordered by issue date.)
Declare #position as int
Set #position = (select len(#VouFormatLastNum) - charindex(reverse(#VouFormatCore),reverse(#VouFormatLastNum)) +1)
--#VouFormatLastNum will now contain the next running no. after selecting the substring below:
Set #VouFormatLastNum = (select substring(#VouFormatLastNum, #position+1,len(#VouFormatLastNum)-#position) + 1)
select #VouFormatLastNum
Set #NextVoucher= #var1 + #var2 + #var3 + #var4 + #var5 + #VouFormatCore + #VouFormatLastNum
Are you able to change the storage mechanism?
As you have 6 or 7 pieces of information, you should ideally have 6 or 7 fields in your storage table. The final string can then be reconstituted from those fields, whilst also making querying individual elements very simple...
I'll assume MS SQL Server from your example...
CREATE TABLE vouchers (
id INT IDENTITY(1,1),
VouTypeCode NCHAR(7) NOT NULL,
IsBirthday NCHAR(2) NOT NULL,
IsReusable NCHAR(2) NOT NULL,
StartD SMALLDATETIME NOT NULL,
EndD SMALLDATETIME NOT NULL,
PRIMARY KEY (id)
)
Now, to create a new voucher, just insert into the table, except for the id field, and a new id will be created for you. And you can create the whole voucher code from that record.
You can also, as necessary, change the data types to e more flexible/appropriate to their actual use. And add additional fields, such as a 'Blocker' field to allow different vouchers to have different blockers.
If you can't change the data structure, you need to be specific about the constraints on the data-format of the voucher codes. For example, I see you use REVERSE(), so can I assume that 'VT555' may sometimes appear earlier in the string? Will the running number always be a set length, or have a max/min length?
Without knowing the exact constraints it's not possible to write an algorithm that deals with multiple different blockers.
Options could be...
Check for other blockers if 'VT555' never appears CHARINDEX() = 0
Check for other blockers if CHARINDEX() > 8 (or some other value), as it appears to early in the string to count.
Scan the reversed string for the first non-numeric character. The alpha and numeric characters surrounding that point form the blocker. Iterate though all blockers until an appropriate match is found.
To be more specific in the answer, however, you need to be more specific about the problem space. Sorry.
Related
I have a product entry page through we keep on adding product entries in our database.
Product location wise there are 2 series. e.g., ABCTMP(Series(1-max)) and XYZ(Series(1-max)).
Table is having primary key constraint which is a combination of 4 columns. Out of 4, only one is giving an issue while increment series combination wise.
That first column is location wise product code as stated above and it is of data type char(20) as it stores values like ABCTMP01 and through classic asp code. We increment that last 01 value by addition of one into existing value.
Right now, facing issue when last value reaches 99 and turns to 100. It generates code 100 through code but unable to insert in database and giving this error that was due to existing entry in database.
Duplicate key part is same one which I mentioned above in subject/header. If I delete record from table of record no. 100 to check, it gives me proper record of 99 through above query and through above classic asp code, it generates next code as 99+1 = 100.
But when I again try to add next series record for 101, even through SQL mgt studio, it gives me below error.
Violation of PRIMARY KEY constraint 'PK'. Cannot insert duplicate key in object 'prdct_mst_tab'. The duplicate key value is (PWATERTMP100 , 006, Y, 01). The statement has been terminated.
Have tried by dropping constraint and changing size of data type char(20) to char(30) as there are dependencies on table. But not worked. Then, have tried by changing data type from char(30) to varchar(30), still not worked. Then again tried by manually
executing insert command
in SQL itself, but same error occurred for 101th record.
Before generating next series, there is select statement to check latest inserted record which will get incremented later.
For generating next record of 101,there select statement must show last inserted record of 100, but it's still giving 99th record and code is generating as 100 again and the error continues the same. I do not understand why it's not taking 100th record when I execute SELECt statement in SQL mgt studio. DataType of that PWATERTMP100 column is char(20).
Below my classic asp code for series generation and SQL 'SELECT top 1 *' statement for record count for location wise product.
select top 1 *
from prdct_mst_tab
where pmt_prdct_cd like 'PWATER%'
and pmt_umt_unit_cd='006'
AND PMT_CMT_CMPNY_CD='01'
order by pmt_prdct_cd desc
Classic ASP Code: -
If recordset.eof Then
getcode="ABCTMP01"
Else
getcode = clng(Mid(recordset("Column1"),10,20))
response.write("Hello" & getcode)
getcode = getcode +1
response.write("<br />Hello" & getcode)
getcode = "ABCTMP" & getcode
response.write("<br />Hello" & getcode)
End if
Below for adding generated product code in database table.
Sql is as below
select * from Table1
recordset.open sql,con,3,2
recordset.addnew
recordset("Column1")=getcode
recordset.update
recordset.close
Note : Values given above are sample one.
I want the record gets inserted even when it turns from 99 to 100, means code will become ABCTMP99 - ABCTMP100 and continue from series starting with 100 range(3 digits)like 100, 101, 102....
The problem is that the order by in this:
select top 1 * from prdct_mst_tab
where pmt_prdct_cd like 'PWATER%'
and pmt_umt_unit_cd='006'
AND PMT_CMT_CMPNY_CD='01'
order by pmt_prdct_cd desc
Does not do what you expect.
Try running this in management studio:
select * from prdct_mst_tab
where pmt_prdct_cd like 'PWATER%'
and pmt_umt_unit_cd='006'
AND PMT_CMT_CMPNY_CD='01'
order by pmt_prdct_cd desc
You'll see that the 100 appears before 99 because it is ordering it alphanumerically not numerically.
In fact you will also see that 10 appears before 9 - how did you ever get past this?
You have a fundamental design flaw. I will add to that by posing a solution which cements the design flaw in place and introduces new bugs. But it will give you a result.
One workaround is to do something this:
select
MAX(
CASE
WHEN ISNUMERIC(RIGHT(RTRIM(pmt_prdct_cd),3)) = 1
THEN RIGHT(RTRIM(pmt_prdct_cd),3)
ELSE '0' + RIGHT(RTRIM(pmt_prdct_cd),2)
END
) As LargestNumber
from prdct_mst_tab
where pmt_prdct_cd like 'PWATER%'
and pmt_umt_unit_cd='006'
AND PMT_CMT_CMPNY_CD='01'
What does this do?
It checks if the last three characters are a number. If it is it uses it.
If it isn't a number it grabs the last two characters and puts a zero in front.
Then it picks the largest number out of all of those.
note - this returns a number, it doesn't return the full product code. So you'll need to remove the ASP Mid code that tries to pull the number out.
This might work until you find some other data or case that you haven't mentioned yet. Like for example if there are trailing characters that aren't numeric. Or for when you need a four character number
Make no mistake - you have a fundamental design flaw and this just prolongs the issue, adds complexity, and introduces more bugs down the track
Some basic observations:
char is a bad data type for this
It has concurrency issues - if two requests call this at the same time (easily done from a web app), it returns the same number and they both try and insert a duplicate value
You should not be assigning and storing incrementing numbers like this. Just use an IDENTITY in the database.
I guess since you are using classic ASP, you are not in a situation that you can redesign this.
You need to decide whether you are going to patch this with something that will introduce new bugs or fix it properly.
Does each product code really need to be incremented within it's own domain like that? Is there any issue with having ABC01 then DEF02 then XYZ03?
EDIT:
I'm changing column datatype to Varchar, should suggestion work, answer will be upvoted
Full Story:
I receive data for a person with an associated temporary number for every person that is 5 digits long, I process this information and then send variables to a stored procedure that handles the inserting of this data. When sending the variables to the stored procedure I appear to be losing any prefixed 0's.
For example:
Number sent to stored Proc - Number actually inserted column
12345 - 12345
01234 - 1234
12340 - 12340
This only appears to be happening for numbers with a 0 in front. Meaning if I received:
00012 it would insert as 12
Is there a way where I could either update the column to always 0 pad to the left by a fixed number, meaning if we got 12 it would automatically make the value 00012.
OR
Is there a way to do this with the variable when its received by the stored procedure before the variable is inserted into the table.
Something along the lines of:
SET #zeroPaddedFixedNum = LeftPad(#numberRecieved, '0', 5);
Additionally, I now need to stop any more numbers from inserting and update all current incorrectly lengthed numbers. Any suggestions?
Perhaps it's just my Google ability that has failed but I have tried searching numerous pages.
For this, the column should be of varchar datatype. You can then do this
Insert into table(col)
select right('00000'+cast(#var as varchar(5)),5)
EDIT : To update existing data
Update table
set col=right('00000'+cast(col as varchar(5)),5)
where len(col)<5
As pointed out, you'll have to use VARCHAR(5) for your needs... But I would not change the columns type, if the values stored are numbers actually. Rather use one of the following, whenever you pass these values to your SP (You might use a computed column or a VIEW though).
Try
SELECT REPLACE(STR(YourNumber,5),' ','0');
The big advantage: In cases, where your number exceeds 5 digits, this would return *****. It is better to get an error than to get wrong numbers... Other approaches with RIGHT() might truncate your result unpredictably.
With SQL Server 2012 you should use FORMAT()
SELECT FORMAT(YourNumber,'00000')
I am writing a stored procedure to query a specific table. The behavior I am seeing is really weird though. First let me layout my table design and some sample queries, to make this easier to explain:
Table Structure:
All Rows for specific Reservation ID (Only 1 Row)
Notice the value for the FieldName column, this will be important below.
Stored Procedure being called (and result):
Basically, this stored procedure returns the specified price (Flight price, Hotel price, Flight markup, Hotel markup, etc, etc..) and for the specified currency. All values are inserted into the Reservations_CurrencyPrices table, so they're only calculated once ever and not again (for performance reasons).
When debugging, I can see a problem, but I do not understand WHY this is happening:
Notice the value of the #Result variable is in the Locals window. This SHOULD NOT be... it makes no sense, because at this point any value for 'TotalMarkupPrice' record has not been inserted and indeed the value of the #Result variable is being set to the price from the 'TotalPrice' record.. which you can see in one of the images above where it's the only row in the table. So it seems that even though I have specified the WHERE [FieldName] = #FieldName condition and the value of #FieldName is 'TotalMarkupPrice', it returns the value from the row for 'TotalPrice' instead. This makes no sense. What am I missing here?
I ran that query manually to see what the result would be and it correctly returns NULL, as you can see below:
Why, oh why is #Result being set to the wrong value? The WHERE clause is not being honored when inside the sproc, it seems.
In your screenshot your code is:
WHERE [FieldName] = FieldName
, not what you think:
WHERE [FieldName] = #FieldName
Effectively, you are not using the #FieldName parameter.
Same problem one line below:
AND [ToCurrency] = ToCurrency
Since it is easy to miss one # symbol, I prefer to name parameters like this:
#ParamFieldName
#ParamToCurrency
I read an answer that said you don't want to use WHILE loops in SQL Server. I don't understand that generalization. I'm fairly new to SQL so I might not understand the explanation yet. I also read that you don't really want to use cursors unless you must. The search results I've found are too specific to the problem presented and I couldn't glean useful technique from them, so I present this to you.
What I'm trying to do is take the values in a client file and shorten them where necessary. There are a couple of things that need to be achieved here. I can't simply hack the field values provided. My company has standard abbreviations that are to be used. I have put these in a table, Abbreviations. the table has the LongName and the ShortName. I don't want to simply abbreviate every LongName in the row. I only want to apply the update as long as the field length is too long. This is why I need the WHILE loop.
My thought process was thus:
CREATE FUNCTION [dbo].[ScrubAbbrev]
(#Field nvarchar(25),#Abbrev nvarchar(255))
RETURNS varchar(255)
AS
BEGIN
DECLARE #max int = (select MAX(stepid) from Abbreviations)
DECLARE #StepID int = (select min(stepid) from Abbreviations)
DECLARE #find varchar(150)=(select Longname from Abbreviations where Stepid=#stepid)
DECLARE #replace varchar(150)=(select ShortName from Abbreviations where Stepid=#stepid)
DECLARE #size int = (select max_input_length from FieldDefinitions where FieldName = 'title')
DECLARE #isDone int = (select COUNT(*) from SizeTest where LEN(Title)>(#size))
WHILE #StepID<=#max or #isDone = 0 and LEN(#Abbrev)>(#size) and #Abbrev is not null
BEGIN
RETURN
REPLACE(#Abbrev,#find,#replace)
SET #StepID=#StepID+1
SET #find =(select Longname from Abbreviations where Stepid=#stepid)
SET #replace =(select ShortName from Abbreviations where Stepid=#stepid)
SET #isDone = (select COUNT(*) from SizeTest where LEN(Title)>(#size))
END
END
Obviously the RETURN should go at the end, but I need to reset the my variables to the next #stepID, #find, and #replace.
Is this one of those times where I'd have to use a cursor (which I've never yet written)?
Generally, you don't want to use cursors or while loops in SQL because they only process a single row at a time, and thus perform very poorly. SQL is designed and optimized to process (potentially very large) sets of data, not individual values.
You could factor out the while loop by doing something like this:
UPDATE t
SET t.targetColumn = a.ShortName
FROM targetTable t
INNER JOIN Abbreviations a
ON t.targetColumn = a.LongName
WHERE LEN(t.targetColumn) > #maxLength
This is generalized and you will need to tweak it to fit your specific data model, but here's what's going on:
For every row in "targetTable", set the value of "targetColumn" (what you want to abbreviate) to the relevant abbreviation (found in Abbreviations.ShortName) iff: the current value has a standardized abbreviation (the inner join) and the current value is longer than desired (the where condition).
You'll need to add an integer parameter or local variable, #maxLength, to indicate what constitutes "too long". This query processes the target table all at once, updating the value in the target column for every eligible row, while a function will only find the abbreviation for a single item (the intersection of one row and one column) at a time.
Note that this won't do anything if the value is too long but doesn't have a standard abbreviation. Your existing code has this same limitation, so I assume this is desired behavior.
I also recommend making this a stored procedure rather than a function. Functions on SQL Server are treated as black boxes and can seriously harm performance, because the optimizer generally doesn't have a good idea of what they're doing.
First I apologize for the poor formatting here.
Second I should say up front that changing the table schema is not an option.
So I have a table defined as follows:
Pin varchar
OfferCode varchar
Pin will contain data such as:
abc,
abc123
OfferCode will contain data such as:
123
123~124~125
I need a query to check for a count of a Pin/OfferCode combination and when I say OfferCode, I mean an individual item delimited by the tilde.
For example if there is one row that looks like abc, 123 and another that looks like abc,123~124, and I search for a count of Pin=abc,OfferCode=123 I wand to get a count = 2.
Obviously I can do a similar query to this:
SELECT count(1) from MyTable (nolock) where OfferCode like '%' + #OfferCode + '%' and Pin = #Pin
using like here is very expensive and I'm hoping there may be a more efficient way.
I'm also looking into using a split string solution. I have a Table-valued function SplitString(string,delim) that will return table OutParam, but I'm not quite sure how to apply this to a table column vs a string. Would this even be worth wile pursuing? It seems like it would be much more expensive, but I'm unable to get a working solution to compare to the like solution.
Your like/% solution is open to a bug if you had offer codes other than 3 digits (if there was offer code 123 and 1234, searching for like '%123%' would return both, which is wrong). You can use your string function this way:
SELECT Pin, count(1)
FROM MyTable (nolock)
CROSS APPLY SplitString(OfferCode,'~') OutParam
WHERE OutParam.Value = #OfferCode and Pin = #Pin
GROUP BY Pin
If you have a relatively small table you can probably get away with this. If you are working with a large number of rows or encountering performance problems, it would be more effective to normalize it as RedFilter suggested.
using like here is very expensive and I'm hoping there may be a more efficient way
The efficient way is to normalize the schema and put each OfferCode in its own row.
Then your query is more like (although you may need to use an intersection table depending on your schema):
select count(*)
from MyTable
where OfferCode = #OfferCode
and Pin = #Pin
Here is one way to use like for this problem, which is standard for getting exact matches when searching delimited strings while avoiding the '%123%' matches '123' and '1234' problem:
-- Create some test data
declare #table table (
Pin varchar(10) not null
, OfferCode varchar(100) not null
)
insert into #table select 'abc', '123'
insert into #table select 'abc', '123~124'
-- Mock some proc params
declare #Pin varchar(10) = 'abc'
declare #OfferCode varchar(10) = '123'
-- Run the actual query
select count(*) as Matches
from #table
where Pin = #Pin
-- Append delimiters to find exact matches
and '~' + OfferCode + '~' like '%~' + #OfferCode + '~%'
As you can see, we're adding the delimiters to the searched string, and also the search string in order to find matches, thus avoiding the bugs mentioned by other answers.
I highly doubt that a string splitting function will yield better performance over like, but it may be worth a test or two using some of the more recently suggested methods. If you still have unacceptable performance, you have a few options:
Updated:
Try an index on OfferCode (or on a computed persisted column of '~' + OfferCode + '~'). Contrary to the myth that SQL Server won't use an index with like and wildcards, this might actually help.
Check out full text search.
Create a normalized version of this table using a string splitter. Use this table to run your counts. Update this table according to some schedule or event (trigger, etc.).
If you have some standard search terms, pre-calculate the counts for these and store them on some regular basis.
Actually, the LIKE condition is going to have much less cost than doing any sort of string manipulation and comparison.
http://www.simple-talk.com/sql/performance/the-seven-sins-against-tsql-performance/