Apply SQL Function to a Table Column Values - sql-server

I have a Transact SQL function SimpleSplit, which splits a string according to the delimiter. I can use it as follows:
DECLARE #DelimitedString NVARCHAR(128)
SET #DelimitedString = 'Processor,RAM,SSD,Ethernet'
SELECT * FROM [dbo].[SimpleSplit](#DelimitedString, ',')
This results in:
Processor
RAM
SSD
Ethernet
As expected.
I now have a Table called PROD_TABLE with a column Descr. I would like to apply this function to each value in column Descr. I attempted the following and it does not work:
SELECT p.[ID], p.[Descr]
FROM [Amazon].[dbo].[PROD_TABLE] p
OUTER APPLY [dbo].[SimpleSplit](p.Descr, '-') d
In the output I only see the ID and the Descr Columns i.e. no results from the SimpleSplit function. However, if I attempt
SELECT *
FROM [Amazon].[dbo].[PROD_TABLE] p
OUTER APPLY [dbo].[SimpleSplit](p.Descr, '-') d
I see the results of the SimpleSplit function in the last column. Why does this query apply the function, but the previous query does not?
Answer
Thanks to mr.Rebands answer below, I realized that I needed to name the results. Hence * worked, but to explicitly name the columns I needed to do something like:
SELECT p.[ID], p.[Descr], d.[Data]
FROM [Amazon].[dbo].[PROD_TABLE] p
OUTER APPLY [dbo].[SimpleSplit](p.[Descr], '-') d

Your function returns a table - what is the column name of the SimpleSplit result table? You will have to include that column name in your select statement.
OUTER APPLY is applied but the results are not selected.

Related

Combine NVARCHAR column with the result of a SUM in SQL Server

How can I combine a NVARCHAR column with the result of a SUM in SQL Server?
I have tried
dbo.tblCurrencies.strCurrencySymbol + dbo.tblItems.dcUnitPrice * dbo.tblItems.intItemQuantity
Which tells me:
Error converting data type nvarchar to numeric
So I tried
dbo.tblCurrencies.strCurrencySymbol + CAST(SUM(dbo.tblItems.dcUnitPrice * dbo.tblItems.intItemQuantity) AS NVARCHAR)
AND
CAST(dbo.tblCurrencies.strCurrencySymbol + SUM(dbo.tblItems.dcUnitPrice * dbo.tblSalesOrderItems.intItemQuantity) AS NVARCHAR)
Both tell me
Cannot use an aggregate or a subquery in an expression used for the group by list of a group by clause.
Any more information you need just let me know I'll put it up.
EDIT:
The query
SELECT dbo.tblItems.fkOrder, dbo.tblCurrencies.strCurrencySymbol + SUM(dbo.tblItems.dcUnitPrice * dbo.tblItems.intItemQuantity) AS TotalPrice
FROM dbo.tblCurrencies RIGHT OUTER JOIN
dbo.tblOrders ON dbo.tblCurrencies.pkCurrency = dbo.tblOrders.fkPaysInCurrency RIGHT OUTER JOIN
dbo.tblItems ON dbo.tblOrders.pkOrder = dbo.tblItems.fkOrder
GROUP BY dbo.tblItems.fkOrder
EDIT 2:
Ok I managed to solve it by adding dbo.tblCurrencies.strCurrencySymbol into the group by and using CONCAT()
My query now looks like this:
SELECT dbo.tblItems.fkOrder, { fn CONCAT(dbo.tblCurrencies.strCurrencySymbol, CAST(SUM(dbo.tblItems.dcUnitPrice * dbo.tblItems.intItemQuantity) AS NVARCHAR(10))) } AS TotalPrice
FROM dbo.tblCurrencies RIGHT OUTER JOIN
dbo.tblOrders ON dbo.tblCurrencies.pkCurrency = dbo.tblOrders.fkPaysInCurrency RIGHT OUTER JOIN
dbo.tblItems ON dbo.tblOrders.pkOrder = dbo.tblItems.fkOrder
GROUP BY dbo.tblItems.fkOrder, dbo.tblCurrencies.strCurrencySymbol
It also works without the CONCAT()
SELECT dbo.tblItems.fkOrder, dbo.tblCurrencies.strCurrencySymbol + CAST(SUM(dbo.tblItems.dcUnitPrice * dbo.tblItems.intItemQuantity) AS NVARCHAR(10)) AS TotalPrice
FROM dbo.tblCurrencies RIGHT OUTER JOIN
dbo.tblOrders ON dbo.tblCurrencies.pkCurrency = dbo.tblOrders.fkPaysInCurrency RIGHT OUTER JOIN
dbo.tblItems ON dbo.tblOrders.pkOrder = dbo.tblItems.fkOrder
GROUP BY dbo.tblItems.fkOrder, dbo.tblCurrencies.strCurrencySymbol
Not sure which is better though?
In SQL Server there's a concept named Data Type Precedence. It's there in cases such as these when two different data types need to be combined because there needs to be a method to decide on what the data type of the final output will be.
In your case you're combining NVARCHAR and NUMERIC data types and as NUMERIC has the higher precedence the approach it takes is to try and convert your text data into a number which it can't do, hence the error.
If you need the conversion to run in a way other than defined in the normal precedence order then you need to explicitly make the conversion yourself with either CAST or CONVERT. In your case you need to multiply the needed numbers, convert those to text and then concatenate the currency symbol.
Subsequent errors come from using fields that are neither within an aggregate or specified in the Group By of your select statement. Adding you currency field in there should resolve the other issue assuming you can't have one order with multiple currencies involved.
Putting it all together gives you the following:
SELECT
Orders.fkOrder,
Currencies.strCurrencySymbol
+ CAST(SUM(Items.dcUnitPrice * Items.intItemQuantity) AS varchar(50)) AS TotalPrice
FROM dbo.tblItems AS Items
LEFT OUTER JOIN dbo.tblOrders AS Orders
ON Items.fkOrder = Orders.pkOrder
LEFT OUTER JOIN dbo.tblCurrencies AS Currencies
ON Orders.fkPaysInCurrency = Currencies.pkCurrency
GROUP BY Orders.fkOrder, Currencies.strCurrencySymbol
Based on the query fragment that you have posted, I would try something along the lines of
dbo.tblCurrencies.strCurrencySymbol +
CAST(SUM(dbo.tblItems.dcUnitPrice * dbo.tblSalesOrderItems.intItemQuantity) AS NVARCHAR(x))
Points to note: the cast to NVARCHAR is done on the sum part only
x is the length of the NVARCHAR string that you wish to cast it to (make sure that it is wide enough to hold the largest value that the sum can return).
Using the schema name prior to the column name is deprecated.

Optimizing SQL Function

I'm trying to optimize or completely rewrite this query. It takes about ~1500ms to run currently. I know the distinct's are fairly inefficient as well as the Union. But I'm struggling to figure out exactly where to go from here.
I am thinking that the first select statement might not be needed to return the output of;
[Key | User_ID,(User_ID)]
Note; Program and Program Scenario are both using Clustered Indexes. I can provide a screenshot of the Execution Plan if needed.
ALTER FUNCTION [dbo].[Fn_Get_Del_User_ID] (#_CompKey INT)
RETURNS VARCHAR(8000)
AS
BEGIN
DECLARE #UseID AS VARCHAR(8000);
SET #UseID = '';
SELECT #UseID = #UseID + ', ' + x.User_ID
FROM
(SELECT DISTINCT (UPPER(p.User_ID)) as User_ID FROM [dbo].[Program] AS p WITH (NOLOCK)
WHERE p.CompKey = #_CompKey
UNION
SELECT DISTINCT (UPPER(ps.User_ID)) as User_ID FROM [dbo].[Program] AS p WITH (NOLOCK)
LEFT OUTER JOIN [dbo].[Program_Scenario] AS ps WITH (NOLOCK) ON p.ProgKey = ps.ProgKey
WHERE p.CompKey = #_CompKey
AND ps.User_ID IS NOT NULL) x
RETURN Substring(#UserIDs, 3, 8000);
END
There are two things happening in this query
1. Locating rows in the [Program] table matching the specified CompKey (#_CompKey)
2. Locating rows in the [Program_Scenario] table that have the same ProgKey as the rows located in (1) above.
Finally, non-null UserIDs from both these sets of rows are concatenated into a scalar.
For step 1 to be efficient, you'd need an index on the CompKey column (clustered or non-clustered)
For step 2 to be efficient, you'd need an index on the join key which is ProgKey on the Program_Scenario table (this likely is a non-clustered index as I can't imagine ProgKey to be PK). Likely, SQL would resort to a loop join strategy - i.e., for each row found in [Program] matching the CompKey criteria, it would need to lookup corresponding rows in [Program_Scenario] with same ProgKey. This is a guess though, as there is not sufficient information on the cardinality and distribution of data.
Ensure the above two indexes are present.
Also, as others have noted the second left outer join is a bit confusing as an inner join is the right way to deal with it.
Per my interpretation the inner part of the query can be rewritten this way. Also, this is the query you'd ideally run and optimize before tacking the string concatenation part. The DISTINCT is dropped as it is automatic with a UNION. Try this version of the query along with the indexes above and if it provides the necessary boost, then include the string concatenation or the xml STUFF approaches to return a scalar.
SELECT UPPER(p.User_ID) as User_ID
FROM
[dbo].[Program] AS p WITH (NOLOCK)
WHERE
p.CompKey = #_CompKey
UNION
SELECT UPPER(ps.User_ID) as User_ID
FROM
[dbo].[Program] AS p WITH (NOLOCK)
INNER JOIN [dbo].[Program_Scenario] AS ps WITH (NOLOCK) ON p.ProgKey = ps.ProgKey
WHERE
p.CompKey = #_CompKey
AND ps.User_ID IS NOT NULL
I am taking a shot in the dark here. I am guessing that the last code you posted is still a scalar function. It also did not have all the logic of your original query. Again, this is a shot in the dark since there is no table definitions or sample data posted.
This might be how this would look as an inline table valued function.
ALTER FUNCTION [dbo].[Fn_Get_Del_User_ID]
(
#_CompKey INT
) RETURNS TABLE AS RETURN
select MyResult = STUFF(
(
SELECT distinct UPPER(p.User_ID) as User_ID
FROM dbo.Program AS p
WHERE p.CompKey = #_CompKey
group by p.User_ID
UNION
SELECT distinct UPPER(ps.User_ID) as User_ID
FROM dbo.Program AS p
LEFT OUTER JOIN dbo.Program_Scenario AS ps ON p.ProgKey = ps.ProgKey
WHERE p.CompKey = #_CompKey
AND ps.User_ID IS NOT NULL
for xml path ('')
), 1, 1, '')
from dbo.Program

Select IDs which belongs ONLY to the list passed as parameter

Let's start from data:
DECLARE #Avengers TABLE ([Hero] varchar(32), [Preference] varchar(32));
INSERT INTO #Avengers VALUES
('Captain_America','gingers'),('Captain_America','blondes'),
('Captain_America','brunettes'),('Hulk','gingers'),('Hulk','blondes'),
('Hawkeye','gingers'),('Hawkeye','brunettes'),('Iron_Man','blondes'),
('Iron_Man','brunettes'),('Thor','gingers'),('Nick_Fury','blondes');
Now I would like to pass a #Preferences as a list of [Preference] (either comma separated or single column table parameter) without knowing how many parameters I am going to get and based on this to select [Hero] who prefers exactly these #Preferences as provided in parameter (list), by that I mean if I am after 'blondes' and 'gingers' then I am after 'Hulk' only
(NOT 'Captain_America' who prefers 'blondes', 'gingers' and 'brunettes').
I would like to get something like:
SELECT [Hero]
FROM #Avengers
WHERE *IS_ASSIGNED_ONLY_TO_THE_LIST*([Preference]) = #Preference
Well, I think I overcomplicated my code, but it works.
SELECT a.Hero, COUNT(*), MIN(p.N)
FROM #Avengers a
LEFT JOIN ( SELECT *, COUNT(*) OVER() N
FROM #Preferences) p
ON a.Preference = p.Preference
GROUP BY a.Hero
HAVING COUNT(*) = MIN(p.N)
AND COUNT(*) = COUNT(p.Preference)
;
I'm using #Preferences as a table.

Using Table-Valued Functions in SQL Server

I'm sorry for I'm not very good at English.
My T-SQL function:
create function sumofOrder(#thang int, #nam int)
returns table
as
return
SELECT
Sales.SalesOrderDetail.SalesOrderID,
Sales.SalesOrderHeader.OrderDate,
SUM(Sales.SalesOrderDetail.OrderQty * Sales.SalesOrderDetail.UnitPrice) AS SubTotal
FROM
Sales.SalesOrderDetail
INNER JOIN
Sales.SalesOrderHeader ON Sales.SalesOrderDetail.SalesOrderID = Sales.SalesOrderHeader.SalesOrderID
GROUP BY
Sales.SalesOrderDetail.SalesOrderID,
Sales.SalesOrderHeader.OrderDate
HAVING
(SUM(Sales.SalesOrderDetail.OrderQty * Sales.SalesOrderDetail.UnitPrice) > 70000)
AND YEAR(Sales.SalesOrderHeader.OrderDate) = #nam
AND MONTH(Sales.SalesOrderHeader.OrderDate) = #thang
And now, I want to use it for filtering by #thang and #nam and not any specific values.
Example of a scalar function:
select
[DepartmentID], [Name], dbo.cau1([DepartmentID]) as 'tongnhanvien'
from
[HumanResources].[Department]
Thank you everyone!
ps: my idea is: select * from [dbo].[sumofOrder](MONTH(Sales.SalesOrderHeader.OrderDate), YEAR(Sales.SalesOrderHeader.OrderDate))
Table-Valued User-Defined Functions
simply select from it:
select * from sumofOrder(thangValue, namValue)
Also Using APPLY
The APPLY operator allows you to invoke a table-valued function for
each row returned by an outer table expression of a query. The
table-valued function acts as the right input and the outer table
expression acts as the left input. The right input is evaluated for
each row from the left input and the rows produced are combined for
the final output. The list of columns produced by the APPLY operator
is the set of columns in the left input followed by the list of
columns returned by the right input.
EDIT
SELECT f.*
FROM Sales.SalesOrderHeader s
CROSS APPLY [dbo].[sumofOrder](month(s.OrderDate), year(s.OrderDate)) f

Execution of ISNULL in SQL Server

Here is how i am using ISNULL condition to check for student address.
It works fine but how ISNULL function treat the null codition i.e the second parameter which is display if first condition is null.
Will it calculate Value for second parameter when first condition is not null?
select
...
...
(CASE
WHEN st.ADDRESS='Y' THEN st.LOCATION
ELSE
ISNULL(
(SELECT TOP 1 STDLOC.LOCATION FROM STDLOC
INNER JOIN COMLOC ON STKLOC.LOCATION=COMLOC.CODE AND COMLOC.ADDRESS='Y'
WHERE STDLOC.ZIBCODE=st.ZIBCODE)
,(SELECT TOP 1 COMLOC.LOCATION FROM COMLOC COMLOC.ZIBCODE=st.ZIBCODE))
END
) AS STDUDENTLOCATION
FROM STUDENT st
Both queries inside the ISNULL will be executed, even if the first query will return a value.
Here is a simple test I've made:
Create and populate sample table:
DECLARE #T AS TABLE
(
Col int
)
INSERT INTO #T Values(1),(2)
SELECT ISNULL(
(SELECT TOP 1 Col FROM #T ORDER BY Col DESC),
(SELECT TOP 1 Col FROM #T ORDER BY Col )
)
Execution plan image:
As you can clearly see, the execution plan includes both queries.
I also was looking for an answer. After some reading I came out with my own way to check it.
Deviding by zero will give an error, so we can try:
SELECT ISNULL( (SELECT TOP 1 object_id FROM sys.columns), 5 / 0)
This will give correct result. BUT
SELECT ISNULL( (SELECT TOP 0 object_id FROM sys.columns), 5 / 0)
It will throw an error, because result of first query gives NULL so it tries the second query which fails
ISNULL is a T-SQL specific function that will use the specified second parameter as the return value if the first parameter is NULL(https://msdn.microsoft.com/en-us/library/ms184325.aspx).
Use COALESCE function if you want to return the first non-null value from multiple arguments, and this is a standard function that is supported by all types of relational databases.
This POST provide a good answer for the question:
Is Sql Server's ISNULL() function lazy/short-circuited?

Resources