User-defined In-line Table-Valued Functions Called On Each Other In SQL Server 2008 - sql

I am using SQL Server 2008, and I am struggling with learning how to correctly call a User-defined In-line Table-Valued Function on a User-defined In-line Table-Valued Function (that is, since each expects a scalar or scalars as input and outputs a table, I want to learn how to correctly call one by passing it another table, whereupon each row is treated as its scalar inputs).
I posted a couple questions related to this recently, but I think I was not clear enough, and did not sufficiently encapsulate the problem to cleanly demonstrate it. I have now prepared the proper statements to provide anyone interested in helping the necessary tables, views, functions, and SELECT outputs to see the problem occur in front of them by executing the query below.
There are several ways I can phrase the core question, and from here and other forums, I can tell I have difficulty clearly expressing it. I am going to phrase it several ways here, but these are all meant to be the same question, phrased differently so people from different backgrounds can more easily understand me.
How do I correctly write the "imageFileNameFromAddress" function below so it works as intended; to wit, the intent is that it takes the same input as "bookAndPageFromAddress" and, using bookAndPageFromAddress and imageFileNameFromBookPage, passing the input to the first, then its output to the second, and returns the second's output?
Why does the third SELECT statement at the bottom below provide different results from the second one, and how do I fix the underlying function(s) to provide identical results, without repeating code from the other functions?
What is the correct syntax for the OUTER APPLY call in imageFileNameFromAddress so that its output is not null?
WARNING: The code below constructs the necessary tables, views, and functions to demonstrate the problem by dropping them first if they exist, so please please please check first to make sure you don't drop anything of your own! The final three SELECTS demonstrate the problem; the final two SELECTS should have identical output, but do not - the first one (of the final two, so the middle of the three) is a three row table of strings, and the final one is a one row table containing only a NULL.
USE [TOM_GIS]
GO
IF OBJECT_ID(N'[dbo].[constant]', N'U') IS NOT NULL
DROP TABLE [dbo].[constant]
CREATE TABLE [dbo].[constant]
(
ID INT IDENTITY(1,1) PRIMARY KEY CLUSTERED,
BOOK varchar(5),
PAGE varchar(5),
DocID numeric(8, 0)
)
INSERT INTO [dbo].[constant]
VALUES(' 4043',' 125', 576030)
GO
IF OBJECT_ID(N'[dbo].[images]', N'U') IS NOT NULL
DROP TABLE [dbo].[images]
CREATE TABLE [dbo].[images]
(
ID INT IDENTITY(1,1) PRIMARY KEY CLUSTERED,
DocID numeric(8, 0),
ImageID numeric(12,0)
)
INSERT INTO [dbo].[images] VALUES(576030, 1589666);
INSERT INTO [dbo].[images] VALUES(576030, 1589667);
INSERT INTO [dbo].[images] VALUES(576030, 1589668);
GO
IF OBJECT_ID(N'[dbo].[addressBookPage]', N'U') IS NOT NULL
DROP TABLE [dbo].[addressBookPage]
CREATE TABLE [dbo].[addressBookPage]
(
ID INT IDENTITY(1,1) PRIMARY KEY CLUSTERED,
PARCEL_ADDRESS nvarchar(50),
BOOK nchar(10),
PAGE nchar(10),
)
INSERT INTO [dbo].[addressBookPage]
VALUES('155 CENTER STREET','4043', '125')
GO
IF OBJECT_ID(N'[dbo].[vw_quindraco]') IS NOT NULL
DROP VIEW [dbo].[vw_quindraco]
GO
CREATE VIEW [dbo].[vw_quindraco]
AS
WITH files AS (SELECT RIGHT('00000000' + LTRIM(STR(c.DocID)), 8) AS PathInfo
,RIGHT('0000000000' + LTRIM(STR(i.ImageID)), 12) AS FileName
,ltrim(c.Book) as Book
,ltrim(c.Page) as Page
FROM [dbo].[constant] AS c INNER JOIN
[dbo].[images] AS i ON c.DocID = i.DocID)
SELECT 'Images/' + SUBSTRING(PathInfo, 1, 2) + '/' + SUBSTRING(PathInfo, 3, 2) + '/' + SUBSTRING(PathInfo, 5, 2)
+ '/' + RIGHT(PathInfo, 8) + '/' + FileName + '.tif' AS FullFileName
,Book
,Page
FROM files AS files_1
GO
IF OBJECT_ID(N'[dbo].[bookAndPageFromAddress]') IS NOT NULL
DROP FUNCTION [dbo].[bookAndPageFromAddress];
GO
CREATE FUNCTION [dbo].[bookAndPageFromAddress] (#address NVARCHAR(max))
RETURNS TABLE AS RETURN(
SELECT PARCEL_ADDRESS AS Address, Book, Page
FROM [dbo].[addressBookPage]
WHERE PARCEL_ADDRESS like '%' + #address + '%'
);
GO
IF OBJECT_ID(N'[dbo].[imageFileNameFromBookPage]') IS NOT NULL
DROP FUNCTION [dbo].[imageFileNameFromBookPage];
GO
CREATE FUNCTION [dbo].[imageFileNameFromBookPage] (#book nvarchar(max), #page nvarchar(max))
RETURNS TABLE AS RETURN(
SELECT i.FullFileName
FROM [dbo].[vw_quindraco] i
WHERE i.Book like #book
AND i.Page like #page
);
GO
IF OBJECT_ID(N'[dbo].[imageFileNameFromAddress]') IS NOT NULL
DROP FUNCTION [dbo].[imageFileNameFromAddress];
GO
CREATE FUNCTION [dbo].[imageFileNameFromAddress] (#address NVARCHAR(max))
RETURNS TABLE AS RETURN(
SELECT *
FROM [dbo].[bookAndPageFromAddress](#address) addresses
OUTER APPLY [dbo].[imageFileNameFromBookPage](addresses.Book, addresses.Page) foo
);
GO
SELECT Book,Page FROM [dbo].[bookAndPageFromAddress]('155 Center Street');
SELECT FullFileName FROM [dbo].[imageFileNameFromBookPage]('4043','125');
SELECT FullFileName FROM [dbo].[imageFileNameFromAddress]('155 Center Street')

You have your table fields as nchars, and you are using Like.
Because it's nchar, the value is padded with spaces to the declared length (10).
Because it's Like, the spaces are considered essential part of a match, whereas the equality operator, =, would ignore trailing spaces.
Because data types in the table and in the function parameters do not match, implicit conversions happen in the background, ultimately causing comparison to fail because of spaces.
Use = instead of Like inside imageFileNameFromBookPage to quickly fix it.
Better yet, use correct data types in all functions and views to avoid any conversions.

Related

Table Variable in SQL Server Function from Input Columns

I would like to create a function that returns a column based on input from three other columns. As temporary tables are not allowed within functions, is it possible to create a table variable from three input columns?
CREATE FUNCTION dbo.convert_value(
#CustomerID VARCHAR(MAX),
#CustomerValue VARCHAR(MAX),
#CustomerDescription VARCHAR(MAX)
)
RETURNS FLOAT
AS BEGIN
DECLARE #CustomerTable TABLE (
UniquePatientUID VARCHAR(MAX),
ResultValue VARCHAR(MAX),
PracticeDescription VARCHAR(MAX)
);
-- How can I insert #UniquePatientUID, #ResultValue and #PracticeDescription into #CustomerTable
END
The context of this question is that I have a SQL script that uses temporary tables and many UPDATE and ALTER TABLE statements, that I need to convert into a function. That script begins with the three columns mentioned, and adds a fourth column, Converted_Value, which is calculated with several hundred lines of code and manipulating temporary tables. Is there any hope here?
A table variable insert is really not different than a regular insert. Don't use temp tables. You can alter the table as well, or just declare it initially with that fourth column and allow it to be NULL.
INSERT INTO #CustomerTable (UniquePatientUID, ResultValue, PracticeDescription)
VALUES(#CustomerID, #CustomerValue, #CustomerDescription);
Don't forget to return the FLOAT.
Table Variable is a table so, you can just use INSERT INTO ... VALUES....
INSERT INTO #CustomerTable (UniquePatientUID,ResultValue,PracticeDescription )
VALUES
(#UniquePatientUID, #ResultValue , #PracticeDescription)
Unless you need a table variable for some specific reason, why not just work with the variables as a derived table expression? i.e.
;with inputs (UniquePatientUID, ResultValue, PracticeDescription) as
(
select #UniquePatientUID, #ResultValue, #PracticeDescription
)
select *
from inputs
Table variables fall out of scope after the function call, and you can't pass table types in or out of functions either. So really all a table variable does here is serve as a means of place keeping that's more familiar to SQL developers. But they're not free, which is the only reason I'm curious what your use case is.
If you don't need to return them as a set or something similar, you can just interact with the variables directly too.

How to create a function SQL that returns a string from a table?

How can I create a function like this?
function FN_something (#entrada char(50))
declare #consulta table
declare #notificacao varchar(50)
declare #multa float
declare #saida varchar(50)
set #consulta as = (select num_notificacao,num_multa from table where field = #entrada)
set #notificacao = #consulta.num_notificacao
set #multa = #consulta.num_multa
set #saida = "resultado: "+ #notificacao +";"+#multa
return #saida
Thanks in advance
I would not use a function... Scalar functions tend to be a real performance killer. Try to use something like this inline
SELECT 'resultado: '
+ ISNULL(CAST(t.num_notificacao AS VARCHAR(MAX)),'???')
+ ';'
+ ISNULL(CAST(t.num_multa AS VARCHAR(MAX)),'???')
FROM SomeTable AS t WHERE t.SomeField=#entrada;
If you need a function it was much better to use an inlined TVF (syntax without BEGIN...END and bind it into your query with CROSS APPLY.
Might be simplified:
If your columns are NOT NULL you can go without ISNULL()-function. If your columns are strings, you can do without CAST()... My code is defensive proramming :-D
Hint
If this is something you need more often, you might introduce a VIEW carrying this calculated column and use it instead of your table. You might include this value into your table as computed column as well...
UPDATE
Great, the VIEW you show in the comment is an inline TVF actually, which is very good!
My magic crystall ball tells me, that you might need something like this:
SELECT cl.*
,'resultado: ' + t.num_notificacao + ';' + t.num_multa AS CalculatedResult
FROM dbo.[CampoLivre876]('SomeParameter') AS cl
LEFT JOIN SomeOtherTable AS t ON cl.entrada=t.SomeField --should be only one related row per main row!
This will call the iTFV and join it to the other Table, where the two columns are living. I Assume, that the CampoLivre876-row knows its entrada key.
Hint 2:
If this works for you, you might include this approach directly into your existing iTVF.
UPDATE 2
You might try to change your function like here:
ALTER FUNCTION [dbo].[CampoLivre876] ()
RETURNS TABLE
RETURN
Select cl.mul_numero_notificacao + ';' + CAST(cl.mul_valor_multa as varchar(max)) AS ExistingColumn
,'resultado: ' + t.num_notificacao + ';' + CAST(t.num_multa AS varchar(max)) AS CalculatedResult
From Campo_Livre AS cl With(NoLock)
INNER JOIN SomeOtherTable AS t ON cl.entrada=t.SomeField;
This should read all lines in one go. Reading 1 row after the other is - in almost all cases - something really, really bad...
Here is an example of a function with correct SQL Server syntax:
create function FN_something (
#entrada char(50) -- should probably be `varchar(50)` rather than `char(50)`
) returns varchar(50)
begin
declare #saida varchar(50);
select #saida = 'resultado: ' + num_notificacao + ';' + num_multa
from table
where field = #entrada;
return #saida;
end;
Note: This assumes that the num_ columns are strings, not numbers. If they are numbers, you need to convert them or use concat().
EDIT:
A function really isn't appropriate for this. Probably the best solution is a computed column:
alter table t add something as (concat('resultado: ', num_notificacao, ';', num_multa);
Then you can get the value directly from the table. In earlier versions of SQL Server, you would use a view rather than computed column.

APPLY and Passing Arguments to Functions

Note: This is in SQL Server 2008.
I'm trying to use the APPLY operator to allow me to call user-defined in-line table-valued functions on each other in a sensible manner, but it doesn't seem to faithfully do so. I'm using OUTER APPLY, but CROSS APPLY does the same thing. Here's the relevant chunk:
SELECT addresses.Book,addresses.Page,foo.BookInput,foo.PageInput
FROM [dbo].bookPageFromAddress(#address) addresses
outer apply [dbo].[imageFileFromBookPage](addresses.Book, addresses.Page) foo
Nothing is bizarre:
GO
CREATE FUNCTION [dbo].imageFileFromBookPage (#book nvarchar(max), #page nvarchar(max))
RETURNS TABLE AS RETURN(
WITH ids AS (
SELECT right('00000000'+ltrim(str(c.DocID)),8) as VarString,
right('0000000000'+ltrim(str(i.ImageID)),12) as PathVar6
FROM [Resolution].[dbo].[Constant] c, [Resolution].[dbo].[Images] i
WHERE ltrim(c.[Book]) like #book
AND ltrim(c.[Page]) like #page
AND i.DocID = c.DocID
)
SELECT '/Images/' +
substring(ids.VarString,1,2)+'/' +
substring(ids.VarString,3,2)+'/' +
substring(ids.VarString,5,2)+'/' +
right(ids.VarString,8)+'/' +
PathVar6 + '.tif' as ImageLocation
,#book as BookInput, #page as PageInput
FROM ids
);
So, in essence,imageFileFromBookPage outputs its input to BookInput and PageInput. Here's the result of that outer apply on sample input:
Book 4043
Page 125
BookInput NULL
PageInput NULL
Note that Book and Page are strings, not integers; they just happen to be holding numeric characters here, but BookInput and PageInput are really NULL, not strings. I thought at first it was a typing issue; both Book and Page are nchar(10) in their original table, and both of my functions expect nvarchar(max) inputs. I tried CASTing the arguments as nvarchar(max) and got identical results, however.
Am I barking up the wrong tree? I'm not familiar with APPLY, but it sure seems like what I want, here. How do I get APPLY or something like it to actually pass along values to the next function?
EDIT: Modified code above to include more information.
Looking at the APPLY docs, shouldn;t the function be declared as:
CREATE FUNCTION [dbo].imageFileFromBookPage (#book nvarchar(max), #page nvarchar(max))
returns #Data Table
(
book nvartchar(max),
page nvarchar(max)
)
as
begin
insert into #Data
select #book, #page;
end
In other words, it's not a problem with APPLY, it's a problem that your function needs to return a Table.
Cheers -

SQL How to Split One Column into Multiple Variable Columns

I am working on MSSQL, trying to split one string column into multiple columns. The string column has numbers separated by semicolons, like:
190230943204;190234443204;
However, some rows have more numbers than others, so in the database you can have
190230943204;190234443204;
121340944534;340212343204;134530943204
I've seen some solutions for splitting one column into a specific number of columns, but not variable columns. The columns that have less data (2 series of strings separated by commas instead of 3) will have nulls in the third place.
Ideas? Let me know if I must clarify anything.
Splitting this data into separate columns is a very good start (coma-separated values are an heresy). However, a "variable number of properties" should typically be modeled as a one-to-many relationship.
CREATE TABLE main_entity (
id INT PRIMARY KEY,
other_fields INT
);
CREATE TABLE entity_properties (
main_entity_id INT PRIMARY KEY,
property_value INT,
FOREIGN KEY (main_entity_id) REFERENCES main_entity(id)
);
entity_properties.main_entity_id is a foreign key to main_entity.id.
Congratulations, you are on the right path, this is called normalisation. You are about to reach the First Normal Form.
Beweare, however, these properties should have a sensibly similar nature (ie. all phone numbers, or addresses, etc.). Do not to fall into the dark side (a.k.a. the Entity-Attribute-Value anti-pattern), and be tempted to throw all properties into the same table. If you can identify several types of attributes, store each type in a separate table.
If these are all fixed length strings (as in the question), then you can do the work fairly simply (at least relative to other solutions):
select substring(col, 1+13*(n-1), 12) as val
from t join
(select 1 as n union all select union all select 3
) n
on len(t.col) <= 13*n.n
This is a useful hack if all the entries are the same size (not so easy if they are of different sizes). Do, however, think about the data structure because semi-colon (or comma) separated list is not a very good data structure.
IF I were you, I would create a simple function that is dividing values separated with ';' like this:
IF EXISTS (SELECT * FROM sysobjects WHERE id = object_id(N'fn_Split_List') AND xtype IN (N'FN', N'IF', N'TF'))
BEGIN
DROP FUNCTION [dbo].[fn_Split_List]
END
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION [dbo].[fn_Split_List](#List NVARCHAR(512))
RETURNS #ResultRowset TABLE ( [Value] NVARCHAR(128) PRIMARY KEY)
AS
BEGIN
DECLARE #XML xml = N'<r><![CDATA[' + REPLACE(#List, ';', ']]></r><r><![CDATA[') + ']]></r>'
INSERT INTO #ResultRowset ([Value])
SELECT DISTINCT RTRIM(LTRIM(Tbl.Col.value('.', 'NVARCHAR(128)')))
FROM #xml.nodes('//r') Tbl(Col)
RETURN
END
GO
Than simply called in this way:
SET NOCOUNT ON
GO
DECLARE #RawData TABLE( [Value] NVARCHAR(256))
INSERT INTO #RawData ([Value] )
VALUES ('1111111;22222222')
,('3333333;113113131')
,('776767676')
,('89332131;313131312;54545353')
SELECT SL.[Value]
FROM #RawData AS RD
CROSS APPLY [fn_Split_List] ([Value]) as SL
SET NOCOUNT OFF
GO
The result is as the follow:
Value
1111111
22222222
113113131
3333333
776767676
313131312
54545353
89332131
Anyway, the logic in the function is not complicated, so you can easily put it anywhere you need.
Note: There is not limitations of how many values you will have separated with ';', but there are length limitation in the function that you can set to NVARCHAR(MAX) if you need.
EDIT:
As I can see, there are some rows in your example that will caused the function to return empty strings. For example:
number;number;
will return:
number
number
'' (empty string)
To clear them, just add the following where clause to the statement above like this:
SELECT SL.[Value]
FROM #RawData AS RD
CROSS APPLY [fn_Split_List] ([Value]) as SL
WHERE LEN(SL.[Value]) > 0

Filtering With Multi-Select Boxes With SQL Server

I need to filter result sets from sql server based on selections from a multi-select list box. I've been through the idea of doing an instring to determine if the row value exists in the selected filter values, but that's prone to partial matches (e.g. Car matches Carpet).
I also went through splitting the string into a table and joining/matching based on that, but I have reservations about how that is going to perform.
Seeing as this is a seemingly common task, I'm looking to the Stack Overflow community for some feedback and maybe a couple suggestions on the most commonly utilized approach to solving this problem.
I solved this one by writing a table-valued function (we're using 2005) which takes a delimited string and returns a table. You can then join to that or use WHERE EXISTS or WHERE x IN. We haven't done full stress testing yet, but with limited use and reasonably small sets of items I think that performance should be ok.
Below is one of the functions as a starting point for you. I also have one written to specifically accept a delimited list of INTs for ID values in lookup tables, etc.
Another possibility is to use LIKE with the delimiters to make sure that partial matches are ignore, but you can't use indexes with that, so performance will be poor for any large table. For example:
SELECT
my_column
FROM
My_Table
WHERE
#my_string LIKE '%|' + my_column + '|%'
.
/*
Name: GetTableFromStringList
Description: Returns a table of values extracted from a delimited list
Parameters:
#StringList - A delimited list of strings
#Delimiter - The delimiter used in the delimited list
History:
Date Name Comments
---------- ------------- ----------------------------------------------------
2008-12-03 T. Hummel Initial Creation
*/
CREATE FUNCTION dbo.GetTableFromStringList
(
#StringList VARCHAR(1000),
#Delimiter CHAR(1) = ','
)
RETURNS #Results TABLE
(
String VARCHAR(1000) NOT NULL
)
AS
BEGIN
DECLARE
#string VARCHAR(1000),
#position SMALLINT
SET #StringList = LTRIM(RTRIM(#StringList)) + #Delimiter
SET #position = CHARINDEX(#Delimiter, #StringList)
WHILE (#position > 0)
BEGIN
SET #string = LTRIM(RTRIM(LEFT(#StringList, #position - 1)))
IF (#string <> '')
BEGIN
INSERT INTO #Results (String) VALUES (#string)
END
SET #StringList = RIGHT(#StringList, LEN(#StringList) - #position)
SET #position = CHARINDEX(#Delimiter, #StringList, 1)
END
RETURN
END
I've been through the idea of doing an
instring to determine if the row value
exists in the selected filter values,
but that's prone to partial matches
(e.g. Car matches Carpet)
It sounds to me like you aren't including a unique ID, or possibly the primary key as part of values in your list box. Ideally each option will have a unique identifier that matches a column in the table you are searching on. If your listbox was like below then you would be able to filter for specifically for cars because you would get the unique value 3.
<option value="3">Car</option>
<option value="4">Carpret</option>
Then you just build a where clause that will allow you to find the values you need.
Updated, to answer comment.
How would I do the related join
considering that the user can select
and arbitrary number of options from
the list box? SELECT * FROM tblTable
JOIN tblOptions ON tblTable.FK = ? The
problem here is that I need to join on
multiple values.
I answered a similar question here.
One method would be to build a temporary table and add each selected option as a row to the temporary table. Then you would simply do a join to your temporary table.
If you want to simply create your sql dynamically you can do something like this.
SELECT * FROM tblTable WHERE option IN (selected_option_1, selected_option_2, selected_option_n)
I've found that a CLR table-valued function which takes your delimited string and calls Split on the string (returning the array as the IEnumerable) is more performant than anything written in T-SQL (it starts to break down when you have around one million items in the delimited list, but that's much further out than the T-SQL solution).
And then, you could join on the table or check with EXISTS.