Stored Procedure is setting variable to value from wrong row - sql

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

Related

Why is it not possible to set a column's alias dynamically?

I have a stored procedure and I want to pass a column alias as a parameter, something like:
SELECT u.userLoginName AS #columnName
FROM -- some JOINs
WHERE -- some conditions
where #columnName can be one of two options and it is set before the SELECT statement according to some condition.
I already know that it can be done only by dynamic SQL, but I don't understand why?
I know that the Order of execution of a Query is: FROM and JOINs -> WHERE -> GROUP BY and only then SELECT.
So if at this point I already got the result set, i.e, the finale table, why can't I just rename the column name as #columnName? What happens in the background that I miss?
This may answer your question.
A SQL result set is conceptually just like a table: it has well defined rows and columns and no ordering unless created with an explicit order by.
A SQL query is processed in two phases: it is compiled and optimized, then it is run. (Happily some databases are now starting to provide dynamic optimization as well, but the queries still go through the compilation phase.)
All information about the result set needs to be known during the compilation phase -- and that includes the resulting column names and column types. Dynamic names would prevent this from happening. They would only be known during the execution phase.
Note that this applies to parameters as identifier as well. Parameters are substituted at the beginning of the execution phase.
This is not a limitation of any particular database. It applies to all of them. I suspect that some more modern databases are implemented in a way that would allow for more dynamic naming, but I don't know of any databases that actually implement it except through dynamic SQL.
It is possible but you have to use a dynamic query.
Let assume we have the following table
Create table #TBL ([Months] VARCHAR(3), Value INT)
INSERT INTO #TBL values
('Jan',20),('Feb',12),('Jan',15),('Mar',25),
('Feb',18),('Jan',9),('Mar',10),('Jan',19)
GO
And I want to dynamically set the columns name using variable. I can use the bellow code
DECLARE #M VARCHAR(10)='Months',
#T VARCHAR(10)='Total'
-- Dynamic query to get the column name
DECLARE #qry VARCHAR(MAX)
SET #qry = 'SELECT [Months] AS '+#M+',
sum(Value) as '+#T+'
FROM #TBL
group by [Months]
DROP TABLE #TBL'
EXEC (#qry)
Note the query itself have to be dynamic

Sql Server 2008 r2 Using a WHILE loop inside a function

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.

SQL Server + String format follow by an incrementing number solution

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.

Should I write a whole procedure for each database table.column I update separately?

I have an application that uses AJAX liberally. I have several places where a single database column is being updated for the record the user is actively editing.
So far I've been creating separate stored procedures for each AJAX action... so I've got UPDATE_NAME, UPDATE_ADDRESS, UPDATE_PHONE stored procedures.
I was just wondering if there's a better way to continue utilizing stored procedures, but without creating one for each column.
I'd like to avoid reflecting upon a string parameter which specifies the column, if possible. I.e. I know I could have an UPDATE_COLUMN procedure which takes as one of its parameters the column name. This kind of gives me the willies, but if that's the only way to do it then I may give it some more considering. But not all columns are of the same data type, so that doesn't seem like a silver bullet.
Consider writing a single update procedure that accepts several columns and uses DEFAULT NULL for all columns that are not mandatory (as suggested by others).
Using NVL in the update will then only update the columns you provided. the only problem with this approach is, that you can't set a value to NULL.
PROCEDURE update_record (
in_id IN your_table.id%TYPE,
in_name IN your_table.name%TYPE DEFAULT NULL,
in_address IN your_table.address%TYPE DEFAULT NULL,
in_phone IN your_table.phone%TYPE DEFAULT NULL,
in_...
) AS
BEGIN
UPDATE your_table
SET name = NVL( in_name, name ),
address = NVL( in_address, address),
phone = NVL( in_phone, phone ),
...
WHERE id = in_id;
END update_record;
You can call it with named parameters then:
update_record( in_id => 123, in_address => 'New address' );
This allows you to update several columns at once when necessary.
I would say to stop using stored procedures for activities that simple, there is no justification to create so many small procedures for every single column in the database. You are much better off with dynamic sql (with parameters) for that.
Create a procedure that can update every column, but only updates columns for which you pass a non-null parameter
CREATE PROCEDURE spUpdateFoo (#fooId INT, #colA INT, #colB VARCHAR(32), #colC float)
AS
update Foo set colA = ISNULL(#colA, colA),
colB = ISNULL(#colB, colB),
colC = ISNULL(#colC, colC)
where fooId = #fooId
Note that this doesn't work if you want to be able to explicitly set null values through your procedure, but you could choose a different value to specify a non-change (-1, etc) with a little more complexity.
It doesn't hurt to do what you are doing, but it could get a little crazy if you continue that path. One thing you can do is create one stored procedure and assign NULL values as default parameters to all your fields that you are updating. So when you call the sproc from your app, if a parameter is given a value that value will be used in the update, otherwise the parameter will take a null value.
Then you can do a check in the sproc IF #Parameter IS NOT NULL ...
If you find yourself ever only needing to update just one field and you do not want to create one central sproc and pass nulls, then use Octavia's solution right below mine and write a simple update procedure.

How to make a view column NOT NULL

I'm trying to create a view where I want a column to be only true or false. However, it seems that no matter what I do, SQL Server (2008) believes my bit column can somehow be null.
I have a table called "Product" with the column "Status" which is INT, NULL. In a view, I want to return a row for each row in Product, with a BIT column set to true if the Product.Status column is equal to 3, otherwise the bit field should be false.
Example SQL
SELECT CAST( CASE ISNULL(Status, 0)
WHEN 3 THEN 1
ELSE 0
END AS bit) AS HasStatus
FROM dbo.Product
If I save this query as a view and look at the columns in Object Explorer, the column HasStatus is set to BIT, NULL. But it should never be NULL. Is there some magic SQL trick I can use to force this column to be NOT NULL.
Notice that, if I remove the CAST() around the CASE, the column is correctly set as NOT NULL, but then the column's type is set to INT, which is not what I want. I want it to be BIT. :-)
You can achieve what you want by re-arranging your query a bit. The trick is that the ISNULL has to be on the outside before SQL Server will understand that the resulting value can never be NULL.
SELECT ISNULL(CAST(
CASE Status
WHEN 3 THEN 1
ELSE 0
END AS bit), 0) AS HasStatus
FROM dbo.Product
One reason I actually find this useful is when using an ORM and you do not want the resulting value mapped to a nullable type. It can make things easier all around if your application sees the value as never possibly being null. Then you don't have to write code to handle null exceptions, etc.
FYI, for people running into this message, adding the ISNULL() around the outside of the cast/convert can mess up the optimizer on your view.
We had 2 tables using the same value as an index key but with types of different numerical precision (bad, I know) and our view was joining on them to produce the final result. But our middleware code was looking for a specific data type, and the view had a CONVERT() around the column returned
I noticed, as the OP did, that the column descriptors of the view result defined it as nullable and I was thinking It's a primary/foreign key on 2 tables; why would we want the result defined as nullable?
I found this post, threw ISNULL() around the column and voila - not nullable anymore.
Problem was the performance of the view went straight down the toilet when a query filtered on that column.
For some reason, an explicit CONVERT() on the view's result column didn't screw up the optimizer (it was going to have to do that anyway because of the different precisions) but adding a redundant ISNULL() wrapper did, in a big way.
All you can do in a Select statement is control the data that the database engine sends to you as a client. The select statement has no effect on the structure of the underlying table. To modify the table structure you need to execute an Alter Table statement.
First make sure that there are currently no nulls in that bit field in the table
Then execute the following ddl statement:
Alter Table dbo.Product Alter column status bit not null
If, otoh, all you are trying to do is control the output of the view, then what you are doing is sufficient. Your syntax will guarantee that the output of the HasStatus column in the views resultset will in fact never be null. It will always be either bit value = 1 or bit value = 0. Don't worry what the object explorer says...