SQL Conditional join inside a function - sql

This question has been asked many times on SO but I never quite found the answer to it, they are mostly solutions to avoid the problem altogether.
I'm working with SQL MS and I'm trying to build a query inside a function (for security reasons) that will either return a table or it's unnested version by country.
meaning that the function should either be
SELECT * FROM SALES AS S
or
SELECT
S.*,
C.Country,
C.CountryPercentage * S.AmountWithouthVAT as CountryValue
FROM SALES AS S
INNER JOIN CountryAllocation AS C ON S.CountryAllocationID = C.CountryAllocationID
(the fact that this join will make a single row into many rows is why I don't simply use the above one. And the reason why I don't make the join outside the function is because the person running the function will not have access to either of the tables. Also note that because of the way permissions in SQL Server work a dynamic query will require permission evaluation, meaning that is not a feasible option unless I'm to develop a structure around certificates)
So, now I got 2 problems:
The output table might or might not have the columns Country and CountryValue causing problems when defining the output type of the function
The actual way to have a function parameter to switch between the 2 versions of the table.
I've got a solution, but this code pains my eyes to look upon:
CREATE FUNCTION [dbo].[fn_I_view] (#Type int)
RETURNS #OutTable TABLE
(
SaleID int,
AmountWithouthVAT decimal(18, 2),
Country varchar(50),
AlocationPercentage decimal(18, 2)
)
AS
BEGIN
WITH
Out1 AS
(
SELECT
S.*,
NULL as Country,
NULL as AlocationPercentage
FROM Sales AS S
WHERE #Type = 1
),
Out2 AS
(
SELECT
S.*,
C.Country,
C.CountryPercentage * S.AmountWithouthVAT as CountryValue
FROM SALES AS S
INNER JOIN CountryAllocation AS C ON S.CountryAllocationID = C.CountryAllocationID
WHERE #Type = 2
)
INSERT INTO #OutTable
SELECT * FROM Out1
UNION ALL
SELECT * FROM Out2
RETURN
END
GO
so, I can't exactly fix the first problem, only worked around it by making SELECT * from [INV].[fn_I_ViewAllMyInvoices](1) still return those 2 extra columns with NULL and I didn't fix the second problem either, as I'm calculating both queries when I only needed 1 of them (and as you can expect this is a demo code, the real deal is way more complex)
Is there any way to improve this code?/solve the problem in a different way? performance, readability as well as maintenance improvements are all welcome

You don't need to calculate both. Just do:
BEGIN
IF #type = 1
BEGIN
INSERT INTO #OutTable
SELECT S.*, NULL as Country, NULL as AlocationPercentage
FROM Sales s;
END;
ELSE
BEGIN
INSERT INTO #OutTable
SELECT S.*, C.Country, C.CountryPercentage * S.AmountWithouthVAT as CountryValue
FROM SALES S JOIN
CountryAllocation C
ON S.CountryAllocationID = C.CountryAllocationID;
END;
RETURN;
END;

Related

Is there a better way to write this gross SQL?

So I'm creating a query for a report that could have several optional filters. I've only included client and station here to keep it simple. Each of these options could be an include or an exclude and could contain NULL, 1, or multiple values. So I split the varchar into a table before joining it to the query.
This test takes about 15 minutes to execute, which... just won't do :p Is there a better way? We have similar queries written with dynamic sql, and I was trying to avoid that, but maybe there's no way around it for this?
DECLARE
#ClientsInc VARCHAR(10) = 'ABCD, EFGH',
#ClientsExc VARCHAR(10) = NULL,
#StationsInc VARCHAR(10) = NULL,
#StationsExc VARCHAR(10) = 'SomeStation'
SELECT *
INTO #ClientsInc
FROM dbo.StringSplit(#ClientsInc, ',')
SELECT *
INTO #ClientsExc
FROM dbo.StringSplit(#ClientsExc, ',')
SELECT *
INTO #StationsInc
FROM dbo.StringSplit(#StationsInc, ',')
SELECT *
INTO #StationsExc
FROM dbo.StringSplit(#StationsExc, ',')
SELECT [some stuff]
FROM media_order mo
LEFT JOIN #ClientsInc cInc WITH(NOLOCK) ON cInc.Value = mo.client_code
LEFT JOIN #ClientsExc cExc WITH(NOLOCK) ON cExc.Value = mo.client_code
LEFT JOIN #StationsInc sInc WITH(NOLOCK) ON sInc.Value = mo.station_name
LEFT JOIN #StationsExc sExc WITH(NOLOCK) ON sExc.Value = mo.station_name
WHERE ((#ClientsInc IS NOT NULL AND cInc.Value IS NOT NULL)
OR (#ClientsExc IS NOT NULL AND cExc.Value IS NULL)
)
AND ((#StationsInc IS NOT NULL AND sInc.Value IS NOT NULL)
OR (#StationsExc IS NOT NULL AND sExc.Value IS NULL)
)
First of all, I always tend to mention Erland Sommarskog's Dynamic Search Conditions in such cases.
However, you already seem to be aware of the two options: one is dynamic SQL. The other is usually the old trick and (#var is null or #var=respective_column). This trick, however, works only for one value per variable.
Your solution indeed seems to work for multiple values. But in my opinion, you are trying too hard to avoid dynamic sql. Your requirements are complex enough to guarantee it. And remember, usually, dynamic sql is harder for you to code, but easier for the server in complex cases - and this one certainly is. Making a performance guess is always risky, but I would guess an improvement in this case.
I would use exists and not exists:
select ...
from media_order mo
where
(
#ClientsInc is null
or exists (
select 1
from string_split(#ClientsInc, ',')
where value = mo.client_code
)
)
and not exist (
select 1
from string_split(#ClientsExc, ',')
where value = mo.client_code
)
and (
#StationsInc is null
or exists (
select 1
from string_split(#StationsInc, ',')
where value = mo.station_name
)
)
and not exist (
select 1
from string_split(#StationsExc, ',')
where value = mo.station_name
)
Notes:
I used buil-in function string_split() rather than the custom splitter that you seem to be using. It is available in SQL Server 2016 and higher, and returns a single column called value. You can change that back to your customer function if you are running an earlier version
as I understand the logic you want, "include" parameters need to be checked for nullness before using exists, while it is unnecessary for "exclude" variables

How to use Join with like operator and then casting columns

I have 2 tables with these columns:
CREATE TABLE #temp
(
Phone_number varchar(100) -- example data: "2022033456"
)
CREATE TABLE orders
(
Addons ntext -- example data: "Enter phone:2022033456<br>Thephoneisvalid"
)
I have to join these two tables using 'LIKE' as the phone numbers are not in same format. Little background I am joining the #temp table on the phone number with orders table on its Addons value. Then again in WHERE condition I am trying to match them and get some results. Here is my code. But my results that I am getting are not accurate. As its not returning any data. I don't know what I am doing wrong. I am using SQL Server.
select
*
from
order_no as n
join
orders as o on n.order_no = o.order_no
join
#temp as t on t.phone_number like '%'+ cast(o.Addons as varchar(max))+'%'
where
t.phone_number = '%' + cast(o.Addons as varchar(max)) + '%'
You can not use LIKE statement in the JOIN condition. Please provide more information on your tables. You have to convert the format of one of the phone field to compile with other phone field format in order to join.
I think your join condition is in the wrong order. Because your question explicitly mentions two tables, let's stick with those:
select *
from orders o JOIN
#temp t
on cast(o.Addons as varchar(max)) like '%' + t.phone_number + '%';
It has been so long since I dealt with the text data type (in SQL Server), that I don't remember if the cast() is necessary or not.
Instead of trying to do everything in a single top-level query, you should apply a transformation projection to your orders table and use that as a subquery, which will make the query easier to understand.
Using the CHARINDEX function will make this a lot easier, however it does not support ntext, you will need to change your schema to use nvarchar(max) instead - which you should be doing anyway as ntext is deprecated, fortunately you can use CONVERT( nvarchar(max), someNTextValue ), though this will reduce performance as you won't be able to use any indexes on your ntext values - but this query will run slowly anyway.
SELECT
orders2.*,
CASE WHEN orders2.PhoneStart > 0 AND orders2.PhoneEnd > 0 THEN
SUBSTRING( orders2.Addons, orders2.PhoneStart, orders2.PhoneEnd - orders2.PhoneStart )
ELSE
NULL
END AS ExtractedPhoneNumber
FROM
(
SELECT
orders.*, -- never use `*` in production, so replace this with the actual columns in your orders table
CHARINDEX('Enter phone:', Addons) AS PhoneStart,
CHARINDEX('<br>Thephoneisvalid', AddOns, CHARINDEX('Enter phone:', Addons) ) AS PhoneEnd
FROM
orders
) AS orders2
I suggest converting the above into a VIEW or CTE so you can directly query it in your JOIN expression:
CREATE VIEW ordersWithPhoneNumbers AS
-- copy and paste the above query here, then execute the batch to create the view, you only need to do this once.
Then you can use it like so:
SELECT
* -- again, avoid the use of the star selector in production use
FROM
ordersWithPhoneNumbers AS o2 -- this is the above query as a VIEW
INNER JOIN order_no ON o2.order_no = order_no.order_no
INNER JOIN #temp AS t ON o2.ExtractedPhoneNumber = t.phone_number
Actually, I take back my previous remark about performance - if you add an index to the ExtractedPhoneNumber column of the ordersWithPhoneNumbers view then you'll get good performance.

large group of variable IDs

I have a working query that contains a large number of variable IDs. Rather than copying and pasting in each ID whenever I need to run a new query, I was wondering if there was a way to create a stored procedure out of the query below and pass in a group of IDs?
Here is the query. The IDs change all the time, so I'm trying to figure out a way of doing this easier but I'm not having much luck.
I thought about using a cursor in a stored procedure and just passing each ID, but that seems cumbersome and inefficient.
SELECT gm.geoId, T.number As surveyID, 0 as SpeciesCount
FROM (
VALUES (1994328036),(1994328037),(1994328038),(1994328039),(1994328040),(1994328041),(1994328042),(1994328043),
(1994328044),(1994328045),(1994328046),(1994328047),(1994328048),(1994328049),(1994328050),(1994328051),
(1994328052),(1994328053),(1994328054),(1994328055)
) AS T(number)
CROSS JOIN dbo.groupBiology gm
You can create a table-valued function (TVF) like this:
CREATE FUNCTION tvf_GetIDs ()
RETURNS
#output TABLE ( data int )
AS
BEGIN
INSERT INTO #output (data) VALUES
(1994328036),(1994328037),(1994328038),(1994328039),
(1994328040),(1994328041),(1994328042),(1994328043),
(1994328044),(1994328045),(1994328046),(1994328047),
(1994328048),(1994328049),(1994328050),(1994328051),
(1994328052),(1994328053),(1994328054),(1994328055)
RETURN
END
GO
then use this function wherever the IDs are required, e.g.
SELECT *
FROM Customers AS c
INNER JOIN (SELECT * FROM tvf_GetIDs()) t ON c.CustID = t.data
You only need to update the TVF whenever the IDs change.

SQL IN() operator with condition inside

I've got table with few numbers inside (or even empty): #states table (value int)
And I need to make SELECT from another table with WHERE clause by definite column.
This column's values must match one of #states numbers or if #states is empty then accept all values (like there is no WHERE condition for this column).
So I tried something like this:
select *
from dbo.tbl_docs docs
where
docs.doc_state in(iif(exists(select 1 from #states), (select value from #states), docs.doc_state))
Unfortunately iif() can't return subquery resulting dataset. I tried different variations with iif() and CASE but it wasn't successful. How to make this condition?
select *
from dbo.tbl_docs docs
where
(
(select count(*) from #states) > 0
AND
docs.doc_state in(select value from #states)
)
OR
(
(select count(*) from #states)=0
AND 1=1
)
Wouldn't a left join do?
declare #statesCount int;
select #statesCount = count(1) from #states;
select
docs.*
from dbo.tbl_docs docs
left join #states s on docs.doc_state = s.value
where s.value is not null or #statesCount = 0;
In general, whenever your query contains sub-queries, you should stop for five minutes, and think hard about whether you really need a sub-query at all.
And if you've got a server capable of doing that, in many cases it might be better to preprocess the input parameters first, or perhaps use constructs such as MS SQL's with.
select *
from dbo.tbl_docs docs
where exists (select 1 from #states where value = doc_state)
or not exists (select 1 from #state)

Sql Server IN Clause Issue

Writing a stored procedure that will have multiple input parameters. The parameters may not always have values and could be empty. But since the possibility exists that each parameter may contain values I have to include the criterion that utilizing those parameters in the query.
My query looks something like this:
SELECT DISTINCT COUNT(*) AS SRM
FROM table p
WHERE p.gender IN (SELECT * FROM Fn_SplitParms(#gender)) AND
p.ethnicity IN (SELECT * FROM Fn_SplitParms(#race)) AND
p.marital_status IN (SELECT * FROM Fn_SplitParms(#maritalstatus))
So my problem is if #gender is empty(' ') the query will return data where gender field is empty when I really want to just ignore p.gender all together. I don't want to have to accomplish this task using IF/ELSE conditional statements because they would be too numerous.
Is there any way to use CASE with IN for this scenario? OR
Is there other logic that I'm just not comprehending that will solve this?
Having trouble finding something that works well...
Thanks!
Use or:
SELECT DISTINCT COUNT(*) AS SRM
FROM table p
WHERE
(p.gender IN (SELECT * FROM Fn_SplitParms(#gender)) OR #gender = '')
AND (p.ethnicity IN (SELECT * FROM Fn_SplitParms(#race)) OR #race = '')
AND (p.marital_status IN (SELECT * FROM Fn_SplitParms(#maritalstatus)) OR #maritalstatus = '')
You might also want to consider table-valued parameters (if using SQL Server 2008 and up) - these can sometimes make the code simpler, since they are treated as tables (which in your case, may be empty) and you can join - plus no awkward split function required.