I have trouble using table type as parameter in stored procedure.
This is my table type:
CREATE TYPE [dbo].[ TableTypeMyValue] AS TABLE(
[MyValue] [nvarchar](50) NULL
)
GO
I need #MyValue to be passed from c#:
#BCID nvarchar(20),
#MyValue TableTypeMyValue READONLY
AS
BEGIN
SET NOCOUNT ON
SELECT IIF (EXISTS (SELECT bcc.BCID, ocv.CAID, ocv.CAValue
FROM BCTWO AS ocv
INNER JOIN BCONE AS bcc ON bcc.MyValue = ocv.MyValue
WHERE ocv.MyValue = #MyValue AND bcc.[Part of the key] = 1 AND bcc.BCID = #BCID), 1, NULL)
END
This is error that i got:
`Must declare the table variable #MyValue`.
I think that I have some problem with table type parameter. I'm not using it correctly. Can someone help ?
Thanks in advance!
You need to treat #CharacteristicValue as a table, not the same as a scalar variable. You should probably join to it and have ocv.CharacteristicValue = #CharacteristicValue instead be an ON condition that references the CharacteristicValue column within the table.
You're trying to use a table type parameter like a scalar parameter, specially:
ocv.CharacteristicValue = #CharacteristicValue
You need to either use a JOIN or an EXISTS clause. For example:
WHERE EXISTS (SELECT 1
FROM #CharacteristicValue CV
WHERE CV.CharacteristicValue = ocv.CharacteristicValue)
Related
This is my initial PL/SQL code :
TYPE VarcharArray IS TABLE OF VARCHAR2(100) INDEX BY BINARY_INTEGER;
and i use it in the following code :
PROCEDURE Create(inFatherId IN VARCHAR2, inBarcode IN VarcharArray, inItemId IN VarcharArray)
IS
myCount NUMBER(38);
sampleId_FromDb NUMBER(38);
itemId_FromDb NUMBER(38);
BEGIN
myCount := inBarcode.COUNT;
FOR i IN 1..myCount
LOOP
SELECT ITEM.Id INTO itemId_FromDb FROM ITEM WHERE FatherId = inFatherId AND CampaignItemId = inItemId(i);
SELECT SAMPLE_SEQUENCE.NEXTVAL INTO sampleId_FromDb FROM DUAL;
INSERT INTO CAMPAIGN_SAMPLES(Id, Barcode, ItemId) VALUES(sampleId_FromDb, inBarcode(i), itemId_FromDb);
END LOOP;
END;
I've seen that the array type can be translated into MS SQL with Table-Valued Parameters, however how can i iterate in a similar fashion so that i include in the iteration the thee operations ?
In the current PL/SQL implementation i send up to 50.000 elements in the array and the performance is decent. I would desire something similar also in MS SQL.
There's no need to be looping and inserting one row at a time. That's just a way to make your code slower. Since tables don't have any order in them, you need to add one column to define the order. Your type would be like this:
CREATE TYPE VarcharArray AS TABLE(ID int, Item VARCHAR(100));
Then, you can rewrite your procedure as a single INSERT statement.
CREATE PROCEDURE SomeProcedure(
#FatherId AS VARCHAR, --This might need a length or will be defaulted to length 1
#Barcode AS VarcharArray READONLY,
#ItemId AS VarcharArray READONLY
)
AS
INSERT INTO CAMPAIGN_SAMPLES(Id, Barcode, ItemId)
SELECT NEXT VALUE FOR SAMPLE_SEQUENCE,
bc.Item,
i.Id
FROM ITEM i
JOIN #ItemId ii ON i.CampaignItemId = ii.Item
JOIN #Barcode bc ON ii.ID = bc.ID
WHERE i.FatherId = #FatherId;
You could also create a table with both values and prevent any ordering problems that could occur.
CREATE TYPE BarcodeItems AS TABLE(Item VARCHAR(100), Barcode VARCHAR(100));
GO
CREATE PROCEDURE SomeProcedure(
#FatherId AS VARCHAR, --This might need a length or will be defaulted to length 1
#BarcodeItems AS BarcodeItems READONLY
)
AS
INSERT INTO CAMPAIGN_SAMPLES(Id, Barcode, ItemId)
SELECT NEXT VALUE FOR SAMPLE_SEQUENCE,
bi.Item,
i.Id
FROM ITEM i
JOIN #BarcodeItems bi ON i.CampaignItemId = bi.Item
WHERE i.FatherId = #FatherId;
I have a stored procedure in a program that is not performing well. Its truncated version follows. The MyQuotes table has an IDENTITY column called QuoteId.
CREATE PROCEDURE InsertQuote
(#BinderNumber VARCHAR(50) = NULL,
#OtherValue VARCHAR(50))
AS
INSERT INTO MyQuotes (BinderNumber, OtherValue)
VALUES (#BinderNumber, #OtherValue);
DECLARE #QuoteId INT
SELECT #QuoteId = CONVERT(INT, SCOPE_IDENTITY());
IF #BinderNumber IS NULL
UPDATE MyQuotes
SET BinderNumber = 'ABC' + CONVERT(VARCHAR(10),#QuoteId)
WHERE QuoteId = #QuoteId;
SELECT #QuoteId AS QuoteId;
I feel like the section where we derive the binder number from the scope_identity() can be done much, much, cleaner. And I kind of think we should have been doing this in the C# code rather than the SQL, but since that die is cast, I wanted to fish for more learned opinions than my own on how you would change this query to populate that value.
The following update avoids needing the id:
UPDATE MyQuotes SET
BinderNumber = 'ABC' + CONVERT(VARCHAR(10), QuoteId)
WHERE BinderNumber is null;
If selecting QuoteId as a return query is required then using scope_identity() is as good a way as any.
Dale's answer is better, however this can be useful way too:
DECLARE #Output TABLE (ID INT);
INSERT INTO MyQuotes (BinderNumber, OtherValue) VALUES (#BinderNumber, #OtherValue) OUTPUT inserted.ID INTO #Output (ID);
UPDATE q SET q.BinderNumber = 'ABC' + CONVERT(VARCHAR(10),o.ID)
FROM MyQuotes q
INNER JOIN #Output o ON o.ID = q.ID
;
Also, if BinderNumber is always linked to ID, it would be better to just create computed column
AS 'ABC' + CONVERT(VARCHAR(10),ID)
I'm writing a stored procedure with quite a lot of expensive work to do that may or may not take a filter parameter. Doing the filtering is itself quite expensive, and the table being filtered is large. I just tried to change the inner filtering function so throw an error if called with invalid parameters, as a warning to developers not to use it that way.
BUT - If I call my outer test function with NULL, it works as I'd expect, not calling the inner function and not throwing the error. If I call my outer test function with a variable with the VALUE of NULL, then it calls the filter function with a null parameter, and throws the error, even thought the code only says to call the function when the value is not null.
What's going on here?
Much simplified example:
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[MyTable]') AND type in (N'U')) DROP TABLE MyTable
GO
CREATE TABLE MyTable (Pk int, Field int)
GO
INSERT INTO MyTable VALUES (1, 1)
INSERT INTO MyTable VALUES (2, 4)
INSERT INTO MyTable VALUES (3, 9)
INSERT INTO MyTable VALUES (4, 16)
GO
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[FilterRows]') AND type in (N'FN', N'IF', N'TF', N'FS', N'FT')) DROP FUNCTION FilterRows
GO
CREATE FUNCTION FilterRows(#searchParameter int)
RETURNS #Pks TABLE
(
Pk int
)
AS
BEGIN
IF (#searchParameter IS null)
BEGIN
-- This is bad news. We don't want to be here with a null search, as the only thing we can do is return every row in the whole table
-- RAISERROR ('Avoid calling FilterRows with no search parameter', 16, 1)
-- we can't raise errors in functions!
-- Make it divide by zero instead then
INSERT INTO #Pks SELECT Pk FROM MyTable WHERE 1/0 = 1
END
ELSE
BEGIN
INSERT INTO #Pks SELECT Pk FROM MyTable WHERE Field > #searchParameter
END
RETURN
END
GO
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[OuterFunction]') AND type in (N'FN', N'IF', N'TF', N'FS', N'FT')) DROP FUNCTION OuterFunction
GO
CREATE FUNCTION OuterFunction(#searchParameter int)
RETURNS TABLE AS
RETURN
SELECT *
FROM
MyTable
WHERE
(#SearchParameter IS NULL) OR (#searchParameter IS NOT NULL AND Pk IN (SELECT Pk FROM dbo.FilterRows(#searchParameter)))
GO
SELECT * FROM dbo.OuterFunction(2) -- Returns filtered values
SELECT * FROM dbo.OuterFunction(null) -- returns everything, doesn't call FilterRows
DECLARE #x int = null
SELECT * FROM dbo.OuterFunction(#x) -- WTF! Throws error!
The difference when a value null is passed than when constant null is passed is the same difference between using (is Null) and (= null)
#var = null -- considered as false
#var is null -- considered as unknown
for more details : SQL is null and = null
so if you want to make behavior of both (calling constant null & pass Null value) is the same, use the following tricky although I don't prefer this one.
Alter FilterRows function to be
IF (#searchParameter = null)
--IF (#searchParameter is null)
Note: sorry for typing this answer here, it is supposed to be comment instead of answer, the rule is "You must have 50 reputation to comment" and I have only 22 :(
I think what's going on is that in
SELECT * FROM MyTable WHERE (#SearchParameter IS NULL) OR
(#searchParameter IS NOT NULL AND Pk IN (SELECT Pk FROM dbo.FilterRows(#searchParameter)))
The query analyzer can see that the subquery
(SELECT Pk FROM dbo.FilterRows(#searchParameter))
does not depend on any values from MyTable. As it's constant for all rows, it runs that subquery first, in order to join MyTable to the results. So it executes it before evaluating the WHERE clause where it tests whether #searchParameter IS NULL or not.
When #searchParameter is just "NULL" and not a variable with value NULL, then the analyzer can short-circuit the whole where clause in the execution plan and so knows not to pre-calculate the subquery.
Or, something like that.
I Waint run code in SQL SERVER
ALTER FUNCTION return_table (#table nvarchar(250))
RETURNS TABLE
AS
RETURN
(
SELECT * FROM #table
)
none using PROCEDURE . THANK FOR HELP
The only way to make this work is truly horrible for both aesthetic and (probably) performance reasons. It also assumes that all tables that might be passed as parameters have the same structure:
ALTER FUNCTION return_table (#table sysname)
RETURNS TABLE
AS
RETURN
(
SELECT * FROM TableA where #table = 'TableA'
UNION ALL
SELECT * FROM TableB where #table = 'TableB'
UNION ALL
SELECT * FROM TableC where #table = 'TableC'
/* add more UNIONs and SELECTs for each table that you want to use */
)
A function requires an explicit definition of the return value type, e.g. the columns being returned. The SQL statement you provided will not work in such a manner for the following reasons:
ALTER FUNCTION return_table (#table nvarchar(250)) -- You have declared a parameter of type table, this is not the right way of doing this
RETURNS TABLE -- you haven't defined the structure of the table being returned, this needs explicitly defining
AS
RETURN
(
SELECT * FROM #table -- using SELECT * is bad practice as the structure of the table may change and then conflict with the structure of the table being returned
)
The first part of the problem is declaring a parameter of type TABLE; this question has good examples on how to get around doing this. Quick summary: you need to declare the table as a type before passing in the type as the parameter to your function:
CREATE TYPE MyTableParameterDefinition AS TABLE (
[ColumnName] NVARCHAR(250) NULL
)
This type can then be passed as a parameter to your function:
CREATE FUNCTION myFunctionName (
#TableVariable MyTableParameterDefinition READONLY
)...--INSERT CODE HERE--
I'm uncertain whether you can use a similar method for the return type and would suggest against this given the implication of the contract defined by the function. Better practice would be to define the structure of the table being returned and explicitly select the columns from the table:
ALTER FUNCTION return_table (
#table MyTableParameterDefinition
)
RETURNS TABLE
(
-- Explicitly define columns returned by the function
[ColumnName] NVARCHAR(250) NULL
)
AS
RETURN
(
SELECT
[ColumnName]
FROM
#table
)
Error message:
Warning : SQM1014: Unable to extract function 'dbo.ProductFamilyIndex_EN' from SqlServer. Null or empty full-text predicate.
function defined as:
CREATE FUNCTION [dbo].[ProductFamilyIndex_EN]
(
#topn int,
#keywords nvarchar(4000)
)
RETURNS TABLE
AS
RETURN
(
select top (#topn) ProductFamilyID
from (
select pf.ProductFamilyID, t.[RANK] as _rank
from containstable(ProductFamily, (Name_EN), #keywords, LANGUAGE 'English', #topn) t
inner join ProductFamily pf on(pf.ProductFamilyID=t.[KEY])
union all
select p.ProductID as ProductFamilyID, t.[RANK] as _rank
from containstable(Product, (LongDescription_EN, ShortDescription_EN), #keywords, LANGUAGE 'English', #topn) t
inner join Product p on(p.ProductID=t.[KEY] and p.ProductFamilyID is null and p.Deleted is null)
) t
group by ProductFamilyID
order by max(_rank) desc
)
don't get confused by the union inside - that just means that a product without a family is a family on its own.
tried to give default values to the parameters:
#topn int = 1000,
#keywords nvarchar(4000) = 'test'
with the same result.
Using .NET 3.5 and sql2008.
As you mentioned, SQLMetal needs a return type.
Another way to solve this, is to explicitly set your default inside the stored procedure:
SET #topn = COALESCE(#topn, 1000)
Throw that before the SELECT statement to insure that any NULL parameters will return a valid value.
This is useful not only for SQLMetal, but for anyone who uses this function.
Reposting my own answer properly so I can close this thread.
Problem solved.
Apparently sqlmetal runs the function to figure out the return type, but insists on supplying null parameter instead of default, which seems like sqlmetal's bug.
Way to work around it is to declare return type explicitly:
alter function [dbo].[ProductFamilyIndex_EN] (#topn int, #keywords nvarchar(4000))
returns #t table (ProductFamilyID int not null)
as begin
...
return end