SQL Performance issue when using CASE - sql

I have a SP that returns quite a lot of data every second and is shown in a grid. I'm now trying to reduce bandwidth and thinking of returning only columns currently shown in my grid.
This is of course simplified and minimized but basically what I had was the following SP:
SELECT
[Animals].[AnimalID] AS [AnimalID],
[Animals].[name] AS [AnimalName],
[Foods].[DisplayName] AS [Food],
[Animals].[Age] AS [AnimalAge],
[Animals].[AmountOfFood] AS [AmountOfFood]
What I’m currently trying is to pass a TVP of the names of the fields (#fields) currently shown on the grid and returning only required fields as such:
SELECT
[Animals].[AnimalID] AS [AnimalID],
[Animals].[name] AS [AnimalName],
CASE
WHEN ('Food' in (select * from #fields))
THEN [Foods].[DisplayName]
END AS [Food],
CASE
WHEN ('AnimalAge' in (select * from #fields))
THEN [Animals].[Age]
END AS [AnimalAge],
CASE
WHEN ('AmountOfFood' in (select * from #fields))
THEN [Animals].[AmountOfFood]
END AS [AmountOfFood]
The problem I'm facing is that (as could be expected) my SP went from taking ~200 ms to taking ~1 sec
Is there any way to maybe rewrite this so that it doesn’t kill us?
My kingdom for a foreach!!!

In SQL Server, you can also do this with dynamic SQL. Something like:
declare #sql nvarchar(max);
select #sql = (select ', '+
(case when FieldName = 'Food' then 'Foods.DisplayName'
when FieldName = 'AnimalAge' then 'Animals.Age'
. . .
end)
from #fields
for xml path ('')
);
select #sql = 'select [Animals].[AnimalID] AS [AnimalID], [Animals].[name] AS [AnimalName]'+#sql+RESTOFQUERY;
exec(#sql);

I'd try to convert the stored procedure into Table-Valued function, and make your grid select only required columns from it.
So your function would still select
SELECT
[Animals].[AnimalID] AS [AnimalID],
[Animals].[name] AS [AnimalName],
[Foods].[DisplayName] AS [Food],
[Animals].[Age] AS [AnimalAge],
[Animals].[AmountOfFood] AS [AmountOfFood]
If the client only selected for example select * AnimalID, Age from myfunction(..), only these columns would be transferred to the client.

Related

Apply like function on an array is SQL Server

I am getting array from front end to perform filters according that inside the SQL query.
I want to apply a LIKE filter on the array. How to add an array inside LIKE function?
I am using Angular with Html as front end and Node as back end.
Array being passed in from the front end:
[ "Sports", "Life", "Relationship", ...]
SQL query is :
SELECT *
FROM Skills
WHERE Description LIKE ('%Sports%')
SELECT *
FROM Skills
WHERE Description LIKE ('%Life%')
SELECT *
FROM Skills
WHERE Description LIKE ('%Relationship%')
But I am getting an array from the front end - how to create a query for this?
In SQL Server 2017 you can use OPENJSON to consume the JSON string as-is:
SELECT *
FROM skills
WHERE EXISTS (
SELECT 1
FROM OPENJSON('["Sports", "Life", "Relationship"]', '$') AS j
WHERE skills.description LIKE '%' + j.value + '%'
)
Demo on db<>fiddle
As an example, for SQL Server 2016+ and STRING_SPLIT():
DECLARE #Str NVARCHAR(100) = N'mast;mode'
SELECT name FROM sys.databases sd
INNER JOIN STRING_SPLIT(#Str, N';') val ON sd.name LIKE N'%' + val.value + N'%'
-- returns:
name
------
master
model
Worth to mention that input data to be strictly controlled, since such way can lead to SQL Injection attack
As the alternative and more safer and simpler approach: SQL can be generated on an app side this way:
Select * from Skills
WHERE (
Description Like '%Sports%'
OR Description Like '%Life%'
OR Description Like '%Life%'
)
A simple map()-call on the words array will allow you to generate the corresponding queries, which you can then execute (with or without joining them first into a single string).
Demo:
var words = ["Sports", "Life", "Relationship"];
var template = "Select * From Skills Where Description Like ('%{0}%')";
var queries = words.map(word => template.replace('{0}', word));
var combinedQuery = queries.join("\r\n");
console.log(queries);
console.log(combinedQuery);

SQL Stored procedures if statement inside select

sorry if this will be a dumb question, it's first time i'm working with SQL stored procedures. The question is simple - how can I declare if statement inside SELECT? To show what I mean, here is a code I currently have:
INSERT INTO #trAll
SELECT tr.EventDateTime
,tr.OrderId
,'EX'
,round(tr.Prod01_CompB_QuantityPV_LTR + tr.Prod01_Fuel_QuantityPV_LTR,0)
,tr.eAD
,tr.OperatorName
--
,cmr.ConsigneeName
,cmr.ConsigneeCompanyCode
,cmr.SenderName
,cmr.SenderCompanyCode
FROM vwTransaction AS tr
inner join tbOrderDetailCMR as cmr
on tr.orderid = cmr.OrderId
WHERE tr.EventDateTime BETWEEN #From
AND #to
AND ProductName = 'BIODIESELZ'
And this is what i need (hope you will understand what I mean):
INSERT INTO #trAll
SELECT tr.EventDateTime
,tr.OrderId
,'EX'
,round(tr.Prod01_CompB_QuantityPV_LTR + tr.Prod01_Fuel_QuantityPV_LTR+ (if (tr.Prod01_AdditiveA>0) THEN tr.Prod01_AdditiveA ELSE tr.Prod01_AdditiveB),0)
,tr.eAD
,tr.OperatorName
--
,cmr.ConsigneeName
,cmr.ConsigneeCompanyCode
,cmr.SenderName
,cmr.SenderCompanyCode
FROM vwTransaction AS tr
inner join tbOrderDetailCMR as cmr
on tr.orderid = cmr.OrderId
WHERE tr.EventDateTime BETWEEN #From
AND #to
AND ProductName = 'BIODIESELZ'
In short what I need is to add AdditiveA if its value is bigger than 0, else AdditiveB inside select statement.
By the syntax you're using I assume that this is T-SQL (Microsoft SQL-Server):
You can not use IF inside a statement like that. Use CASE here, like this:
...
,round(tr.Prod01_CompB_QuantityPV_LTR + tr.Prod01_Fuel_QuantityPV_LTR+
(CASE
WHEN (tr.Prod01_AdditiveA>0) THEN tr.Prod01_AdditiveA
ELSE tr.Prod01_AdditiveB
END),0)
...
EDIT: In fact, now that I read it, Ashwin Nairs answer is even better. He uses the IIF command, which is less "intrusive". You might want to look at and/or accept his answer.
Assuming you're using sql-server, try using iif instead
INSERT INTO #trAll
SELECT tr.EventDateTime
,tr.OrderId
,'EX'
,round(tr.Prod01_CompB_QuantityPV_LTR + tr.Prod01_Fuel_QuantityPV_LTR+ iif(tr.Prod01_AdditiveA>0, tr.Prod01_AdditiveA, tr.Prod01_AdditiveB),0)
,tr.eAD
,tr.OperatorName
--
,cmr.ConsigneeName
,cmr.ConsigneeCompanyCode
,cmr.SenderName
,cmr.SenderCompanyCode
FROM vwTransaction AS tr
inner join tbOrderDetailCMR as cmr
on tr.orderid = cmr.OrderId
WHERE tr.EventDateTime BETWEEN #From
AND #to
AND ProductName = 'BIODIESELZ'

Using the results of a select sub query as the columns to select in the main query. Injection?

I have a table that contains a column storing sql functions, column names and similar snippets such as below:
ID | Columsql
1 | c.clientname
2 | CONVERT(VARCHAR(10),c.DOB,103)
The reason for this is to use selected rows to dynamically create results from the main query that match spreadsheet templates. EG Template 1 requires the above client name and DOB.
My Subquery is:
select columnsql from CSVColumns cc
left join Templatecolumns ct on cc.id = ct.CSVColumnId
where ct.TemplateId = 1
order by ct.columnposition
The results of this query are 2 rows of text:
c.clientname
CONVERT(VARCHAR(10),c.DOB,103)
I would wish to pass these into my main statement so it would read initially
Select(
select columnsql from CSVColumns cc
left join Templatecolumns ct on cc.id = ct.CSVColumnId
where ct.TemplateId = 1
order by ct.columnposition
) from Clients c
but perform:
select c.clientname, CONVERT(VARCHAR(10),c.DOB,103) from clients c
to present a results set of client names and DOBs.
So far my attempts at 'injecting' are fruitless. Any suggestions?
You can't do this, at least not directly. What you have to do is, in a stored procedure, build up a varchar/string containing a complete SQL statement; you can execute that string.
declare #convCommand varchar(50);
-- some sql to get 'convert(varchar(10), c.DOB, 103) into #convCommand.
declare #fullSql varchar(1000);
#fullSql = 'select c.clientname, ' + #convCommand + ' from c,ients c;';
exec #fullSql
However, that's not the most efficient way to run it - and when you already know what fragment you need to put into it, why don't you just write the statement?
I think the reason you can't do that is that SQL Injection is a dangerous thing. (If you don't know why please do some research!) Having got a dangerous string into a table - e.g 'c.dob from clients c;drop table clients;'- using the column that contains the data to actually execute code would not be a good thing!
EDIT 1:
The original programmer is likely using a C# function:
string newSql = string.format("select c.clientname, {0} from clients c", "convert...");
Basic format is:
string.format("hhh {0} ggg{1}.....{n}, s0, s1,....sn);
{0} in the first string is replaced by the string at s0; {1} is replaces by tge string at s1, .... {n} by the string at sn.
This is probably a reasonable way to do it, though why is needs all the fragments is a bit opaque. You can't duplicate that in sql, save by doing what I suggest above. (SQL doesn't have anything like the same string.format function.)

Querying XML colum for values

I have a SQL Server table with an XML column, and it contains data something like this:
<Query>
<QueryGroup>
<QueryRule>
<Attribute>Integration</Attribute>
<RuleOperator>8</RuleOperator>
<Value />
<Grouping>OrOperator</Grouping>
</QueryRule>
<QueryRule>
<Attribute>Integration</Attribute>
<RuleOperator>5</RuleOperator>
<Value>None</Value>
<Grouping>AndOperator</Grouping>
</QueryRule>
</QueryGroup>
</Query>
Each QueryRule will only have one Attribute, but each QueryGroup can have many QueryRules. Each Query can also have many QueryGroups.
I need to be able to pull all records that have one or more QueryRule with a certain attribute and value.
SELECT *
FROM QueryBuilderQueries
WHERE [the xml contains any value=X where the attribute is either Y or Z]
I've worked out how to check a specific QueryRule, but not "any".
SELECT
Query
FROM
QueryBuilderQueries
WHERE
Query.value('(/Query/QueryGroup/QueryRule/Value)[1]', 'varchar(max)') like 'UserToFind'
AND Query.value('(/Query/QueryGroup/QueryRule/Attribute)[1]', 'varchar(max)') in ('FirstName', 'LastName')
You can use two exist(). One to check the value and one to check Attribute.
select Q.Query
from dbo.QueryBuilderQueries as Q
where Q.Query.exist('/Query/QueryGroup/QueryRule/Value/text()[. = "UserToFind"]') = 1 and
Q.Query.exist('/Query/QueryGroup/QueryRule/Attribute/text()[. = ("FirstName", "LastName")]') = 1
If you really want the like equivalence when you search for a Value you can use contains().
select Q.Query
from dbo.QueryBuilderQueries as Q
where Q.Query.exist('/Query/QueryGroup/QueryRule/Value/text()[contains(., "UserToFind")]') = 1 and
Q.Query.exist('/Query/QueryGroup/QueryRule/Attribute/text()[. = ("FirstName", "LastName")]') = 1
According to http://technet.microsoft.com/pl-pl/library/ms178030%28v=sql.110%29.aspx
"The XQuery must return at most one value"
If you are quite certain that for example your XML has let's say maximum 10 QueryRules you could maybe use WHILE to loop everything while droping your results into temporary table?
maybe below can help you anyway
CREATE TABLE #temp(
Query type)
DECLARE #i INT
SET #i = 1
WHILE #i >= 10
BEGIN
INSERT INTO #temp
SELECT
Query
FROM QueryBuilderQueries
WHERE Query.value('(/Query/QueryGroup/QueryRule/Value)[#i]', 'varchar(max)') LIKE 'UserToFind'
AND Query.value('(/Query/QueryGroup/QueryRule/Attribute)[#i]', 'varchar(max)') IN ('FirstName', 'LastName')
#i = #i + 1
END
SELECT
*
FROM #temp
It's a pity that the SQL Server (I'm using 2008) does not support some XQuery functions related to string such as fn:matches, ... If it supported such functions, we could query right inside XQuery expression to determine if there is any. However we still have another approach. That is by turning all the possible values into the corresponding SQL row to use the WHERE and LIKE features of SQL for searching/filtering. After some experiementing with the nodes() method (used on an XML data), I think it's the best choice to go:
select *
from QueryBuilderQueries
where exists( select *
from Query.nodes('//QueryRule') as v(x)
where LOWER(v.x.value('(Attribute)[1]','varchar(max)'))
in ('firstname','lastname')
and v.x.value('(Value)[1]','varchar(max)') like 'UserToFind')

MySQL IN Operator

http://pastebin.ca/1946913
When i write "IN(1,2,4,5,6,7,8,9,10)" inside of the procedure, i get correct result but when i add the id variable in the "IN", the results are incorrect. I made a function on mysql but its still not working, what can i do?
Strings (broadly, variable values) don't interpolate in statements. vKatID IN (id) checks whether vKatID is equal to any of the values listed, which is only one: the value of id. You can create dynamic queries using PREPARE and EXECUTE to interpolate values:
set #query = CONCAT('SELECT COUNT(*) AS toplam
FROM videolar
WHERE vTarih = CURDATE() AND vKatID IN (', id, ') AND vDurum = 1;')
PREPARE bugun FROM #query;
EXECUTE bugun;
You could use FIND_IN_SET( ) rather than IN, for example:
SELECT COUNT(*) AS toplam
FROM videolar
WHERE vTarih = CURDATE()
AND FIND_IN_SET( vKatID, id ) > 0
AND vDurum = 1
Sets have limitations - they can't have more than 64 members for example.
Your id variables is a string (varchar) not an array (tuple in SQL) ie you are doing the this in (in java)
String id = "1,2,3,4,5,6,7"
you want
int[] ids = {1,2,3,4,5,6,7}
So in your code
set id = (1,2,3,4,5,6,7,8,9,10)
I cannot help you with the syntax for declaring id as I don't know. I would suggest to ensure the code is easily updated create a Table with just ids and then change your stored procedure to say
SELECT COUNT(*) AS toplam
FROM videolar
WHERE vTarih = CURDATE() AND vKatID IN (SELECT DISTINCT id FROM idtable) AND vDurum = 1;
Hope this helps.