Conditional statement in sql query on classic ASP Page - sql

I am fetching three values in my page named 123.asp
(123.asp?School=NewEnglishSchool&Batch=1&AcademicYear=2)
School = Request.QueryString("School")
Batch = Request.QueryString("Batch")
AcademicYear = Request.QueryString("AcademicYear")
I have a simple query in form like below
strSqlData="select * from MyTable where gender="male""
I need to create a query like below, if I found a value for Batch and AcademicYear except for School on the page via Request.QueryString means (123.asp?Batch=1&AcademicYear=2)
then I want to write a query something like this
strSqlData="select * from MyTable where gender="male"
if len(AcademicYear)<>o then
and AcademicYear='"&AcademicYear&"'
elseif len(Batch)<>o then
and Batch='"&Batch&"'
end if
elseif len(School)<>o then
and School='"&School&"'
end if "
I want to create something like above but in proper method without using case, because I have so many filters on the page like school, batch and Academicyear.

In VBScript you can use the ampersand (&) symbol to concatenate strings. Try this:
strSqlData="select * from MyTable where gender='male'"
if AcademicYear <> "" then
strSqlData= strSqlData & " and AcademicYear=" & AcademicYear
end if
if Batch <> "" then
strSqlData= strSqlData & " and Batch=" & Batch
end if
if School <> "" then
strSqlData= strSqlData & " and School=" & School
end if
You have three separate "and" clauses to append to your sql query. The conditional statements are independent of each other so you shouldn't be using elseif which is for different options within a single conditional statement. It's simpler to check whether or not a string isn't empty if stringname <> "" than using len, (and I doubt your conditional statements would work because you appear to be using a lower case letter "o" where you should be using a zero)
It's very easy to make mistakes when you're assembling sql queries like this. In testing it's often worth adding a line like Response.Write strSqlData before you try to execute it to check that the query is what you were intending
As other comments have suggested though, your code is vulnerable to an SQL injection attack. Urls which contain ".asp?" can almost be guaranteed to be hit by an ASPROX type attack sooner or later. Parameterised queries are the best means of guarding against this, but one quick approach if your querystring values are all numeric is to use cint() - eg
strSqlData= strSqlData & " and AcademicYear=" & cint(AcademicYear)
This will throw a type mismatch error if the querystring contains anything other than numbers, and the script will fall over before it attempts to execute your sql query.

Instead of creating a sql query in your asp page you may create a Store Procedure and call it on your ASP page with the required parameter from your Query String.
Here is the sample SP which I have created as per your asked scenario
CREATE PROCEDURE Sp_fetch_data #school VARCHAR(max),
#batch VARCHAR(max),
#academicYear VARCHAR(max)
AS
BEGIN
SET #school = (SELECT CASE
WHEN #school = '' THEN NULL
ELSE #school
END)
SET #batch = (SELECT CASE
WHEN #batch = '' THEN NULL
ELSE #batch
END)
SET #academicYear = (SELECT CASE
WHEN #academicYear = '' THEN NULL
ELSE #academicYear
END)
SELECT *
FROM mytable
WHERE gender = 'male'
AND school = #school
AND batch = #batch
AND academicyear = #academicYear
END
Now simply call this SP in your ASP page:
strSqlData = "sp_fetch_data'"&Request.QueryString("school")&"','"&Request.QueryString("batch")&"','"&Request.QueryString("academicYear")&"'"
EDIT:Execute Store Procedure with NULL data
ALTER PROCEDURE Sp_fetch_data #school VARCHAR(max) = NULL,
#batch VARCHAR(max) = NULL,
#academicYear VARCHAR(max) = NULL
AS
BEGIN
SELECT *
FROM mytable
WHERE gender = 'male'
AND ( #school = school
OR #school IS NULL )
AND ( #batch = batch
OR #batch IS NULL )
AND ( #academicYear = academicyear
OR #academicYear IS NULL )
END

You should think the problem only as a string creation. You have to create a string that will be a correct sql query.
The approach that you have tried wants to create a query that contains logical statements. Instead of that, you should write your C# code in a way to create a proper sql query.
For an example, let's take the first part of your query:
strSqlData="select * from MyTable where gender="male"
if len(AcademicYear)<>o then
and AcademicYear='"&AcademicYear&"'
You can easily transfer that for a proper C# solution something like that:
string strSqlData;
int AcademicYear = 1994;
int o = 2000;
string male = "male";
strSqlData="select * from MyTable where gender= " + male;
if (AcademicYear != o)
strSqlData += " and AcademicYear=" + AcademicYear.ToString();
Console.WriteLine(strSqlData);
Actually after you have fixed that part of your code, there will be some additional to do that you have to do. For example, your should use parametrized queries instead of hard coding an sql string.
Maybe you should find an example about building data access layer.

Related

Bit field in where clause

I have a situation where I want to search for a field in the where clause only if the bit variable is 1 else ignore it.
#Active bit = 0
select * from Foo where firstname = 'xyz' and
if(#active=1)
then search on the Active column else ignore the filtering on the Active column. How can I have that in a simple condition instead of checking each parameter seperately and then building the where clause
Just simple logic will usually suffice:
select * from Foo where firstname = 'xyz' and
(#Active = 0 or Active = <filter condition>)
For general advice about writing code for arbitrary search conditions, you could do worse than read Erland Sommarskog's Dynamic Search Conditions in T-SQL
it seems like Active is the Actual Column as well in your table.
using Case stmt you can make the search efficient as it will use appropriate indexes you may have on this table.
DECLARE #Active BIT=0
SELECT *
FROM Foo
WHERE firstname = 'a'
AND active = CASE WHEN #Active=1 THEN #Active ELSE Active END
How about:
DECLARE #Active bit
if #Active = 1
BEGIN
(select * from Foo where firstname = 'bar' and Active = --condition)
END
else
BEGIN
(select * from Foo where firstname = 'bar')
END
of course, something will have to set the value for #Active somewhere between the declaration and the if...else statement.
you can write this as below
select * from Foo where firstname = 'xyz' and (#isactive=0 or (some conditions))

SQL conditional statement to include operator

I am passing parameters into a stored procedure. The one parameter is a varchar(50) that can be a string like " > 5000" and " <= 10000".
Here is some of the code:
....
....
#colourid int = 0,
#regionid int = 0,
#sellingPrice varchar(50) = '-1'
AS
SELECT
....
....
WHERE
(dbo.tbl_Listings.fld_ColourID = CASE WHEN #colourid = 0 THEN dbo.tbl_Listings.fld_ColourID ELSE #colourid END)
AND (dbo.tbl_Listings.fld_RegionID = CASE WHEN #regionid = 0 THEN dbo.tbl_Listings.fld_RegionID ELSE #regionid END)
AND
How do I add #sellingPrice to the WHERE? I can't mimic how it was done for the int parameters because it's not always going to use =. I need to say "if selling price is not -1 then fld_SellingPrice #sellingPrice".
The only way you can achieve that is by using dynamic SQL, building up your query in a local variable and then executing it via (preferably) sp_executesql.
So something like
DECLARE #sql nvarchar(MAX)
SET #sql = 'SELECT .... WHERE ' + #sellingPrice
sp_executesql #sql
However, this really does open you up to the possibility of SQL injection, and therefore you have to either
a. Be very sure that the procedure will only be called by callers you trust fully
b. Add protection for badly formed parameters within your procedure, which is much harder than it sounds
c. Find a different way to approach the problem entirely.
If you know you are going to have a general set of comparisons to use, I would create a parameter per comparison in your SP and use them as needed. So your SP might have
#greaterThan int,
#lessThan int,
#equalTo int
Then in the SP you could do
if #greaterThan IS NULL
SELECT #greaterThan = MAX(field) FROM table -- or some arbitrary value that will always evaluate to true
if #lessThan IS NULL
SELECT #lessThan = MIN(field) FROM table
Then just use those in your WHERE clause. Otherwise, as posted, you're going to have to do dynamic SQL by building an SQL string with the pieces of the WHERE clause.
I would use a from and a to variable.
So when you want less than 5000, you set to variable = 5000 and leave from blank
....
....
#colourid int = 0,
#regionid int = 0,
#fromsellingprice int = 5000
#tosellingprice int = null
AS
SELECT
....
....
WHERE
(dbo.tbl_Listings.fld_ColourID = CASE WHEN #colourid = 0 THEN dbo.tbl_Listings.fld_ColourID ELSE #colourid END)
AND (dbo.tbl_Listings.fld_RegionID = CASE WHEN #regionid = 0 THEN dbo.tbl_Listings.fld_RegionID ELSE #regionid END)
AND
sellingPrice >= coalesce(#fromsellingprice, sellingprice)
and sellingPrice <= coalesce(#tosellingprice, sellingprice)
You can't do this directly in SQL - the parameter will not be parsed and interpreted as part of the query with predicates.
The only way to do this (passing in the operator) is to use dynamic SQL, which comes with its own pitfalls.
You may consider passing in a parameter for what operator to use and have a bunch of if sections for each supported parameter - this may be worse than dynamic SQL.

Efficiently Handling Multiple Optional Constraints in Where Clause

This is somewhat of a sequel to Slow Exists Check. Alex's suggestion works and successfully avoids code repetition, but I still end up with a second issue. Consider the example below (From AlexKuznetsov). In it, I have two branches to handle 1 contraint. If I had 2 optional constraints, I would end up with 4 branches. Basically, the number of branches increases exponentially with the number of constraints.
On the other hand, if I use a Multi-Statement Table-valued function or otherwise use temporary tables, the SQL query optimizer is not able to assist me, so things become slow. I am somewhat distrustful of dynamic SQL (and I've heard it is slow, too).
Can anyone offer suggestions on how to add more constraints without adding lots of if statements?
Note: I have previously tried just chaining x is null or inpo = #inpo together, but this is very slow. Keep in mind that while the inpo = #inpo test can be handled via some sort of indexing black magic, the nullity test ends up being evaluated for every row in the table.
IF #inpo IS NULL BEGIN
SELECT a,b,c
FROM dbo.ReuseMyQuery(#i1)
ORDER BY c;
END ELSE BEGIN
SELECT a,b,c
FROM dbo.ReuseMyQuery(#i1)
WHERE inpo = #inpo
ORDER BY c;
END
Variation Two: 2 constraints:
IF #inpo IS NULL BEGIN
IF #inpo2 IS NULL BEGIN
SELECT a,b,c
FROM dbo.ReuseMyQuery(#i1)
ORDER BY c;
END ELSE BEGIN
SELECT a,b,c
FROM dbo.ReuseMyQuery(#i1)
WHERE inpo2 = #inpo2
ORDER BY c;
END
END ELSE BEGIN
IF #inpo2 IS NULL BEGIN
SELECT a,b,c
FROM dbo.ReuseMyQuery(#i1)
WHERE inpo = #inpo
ORDER BY c;
END ELSE BEGIN
SELECT a,b,c
FROM dbo.ReuseMyQuery(#i1)
WHERE inpo = #inpo AND
inpo2 = #inpo2
ORDER BY c;
END
END
this is the best reference: http://www.sommarskog.se/dyn-search-2005.html
In such cases I use sp_executesql as described in Erland's article: Using sp_executesql
Whenever dynamic SQL is used, missing permissions may be a problem, so I have a real network account for unit testing, I add that account to the actual role, and I impersonate with that real account whenever I test dynamic SQL, as described here: Database Unit Testing: Impersonation
Here's a rough example. Modify the LIKE statements in the WHERE clause depending if you want "starts with" or "contains" or an exact match in your query.
CREATE PROCEDURE dbo.test
#name AS VARCHAR(50) = NULL,
#address1 AS VARCHAR(50) = NULL,
#address2 AS VARCHAR(50) = NULL,
#city AS VARCHAR(50) = NULL,
#state AS VARCHAR(50) = NULL,
#zip_code AS VARCHAR(50) = NULL
AS
BEGIN
SELECT [name],
address1,
address2,
city,
state,
zip_code
FROM my_table
WHERE ([name] LIKE #name + '%' OR #name IS NULL)
AND (address1 LIKE #address1 + '%' OR #address1 IS NULL)
AND (address2 LIKE #address2 + '%' OR #address2 IS NULL)
AND (city LIKE #city + '%' OR #city IS NULL)
AND (state LIKE #state + '%' OR #state IS NULL)
AND (zip_code LIKE #zip_code + '%' OR #zip_code IS NULL)
ORDER BY [name]
END
GO
Select blah from foo
Where (#inpo1 is null or #inpo1 = inpo1)
and (#inpo2 is null or #inpo2 = inpo2)
Apparently this is too slow. Interesting.
Have you considered code generation? Lengthy queries with lots of duplication is only an issue if it has to be maintained directly.
I realise your question may be purely academic, but if you have real world use cases have you considered only providing optimised queries for the most common scenarios?

creating SQL command to return match or else everything else

i have three checkboxs in my application. If the user ticks a combination of the boxes i want to return matches for the boxes ticked and in the case where a box is not checked i just want to return everything . Can i do this with single SQL command?
I recommend doing the following in the WHERE clause;
...
AND (#OnlyNotApproved = 0 OR ApprovedDate IS NULL)
It is not one SQL command, but works very well for me. Basically the first part checks if the switch is set (checkbox selected). The second is the filter given the checkbox is selected. Here you can do whatever you would normally do.
You can build a SQL statement with a dynamic where clause:
string query = "SELECT * FROM TheTable WHERE 1=1 ";
if (checkBlackOnly.Checked)
query += "AND Color = 'Black' ";
if (checkWhiteOnly.Checked)
query += "AND Color = 'White' ";
Or you can create a stored procedure with variables to do this:
CREATE PROCEDURE dbo.GetList
#CheckBlackOnly bit
, #CheckWhiteOnly bit
AS
SELECT *
FROM TheTable
WHERE
(#CheckBlackOnly = 0 or (#CheckBlackOnly = 1 AND Color = 'Black'))
AND (#CheckWhiteOnly = 0 or (#CheckWhiteOnly = 1 AND Color = 'White'))
....
sure. example below assumes SQL Server but you get the gist.
You could do it pretty easily using some Dynamic SQL
Lets say you were passing your checkboxes to a sproc as bit values.
DECLARE bit #cb1
DECLARE bit #cb2
DECLARE bit #cb3
DECLARE nvarchar(max) #whereClause
IF(#cb1 = 1)
SET #whereClause = #whereClause + ' AND col1 = ' + #cb1
IF(#cb2 = 1)
SET #whereClause = #whereClause + ' AND col2 = ' + #cb2
IF(#cb3 = 1)
SET #whereClause = #whereClause + ' AND col3 = ' + #cb3
DECLARE nvarchar(max) #sql
SET #sql = 'SELECT * FROM Table WHERE 1 = 1' + #whereClause
exec (#sql)
Sure you can.
If you compose your SQL SELECT statement in the code, then you just have to generate:
in case nothing or all is selected (check it using your language), you just issue non-filter version:
SELECT ... FROM ...
in case some checkboxes are checked, you create add a WHERE clause to it:
SELECT ... FROM ... WHERE MyTypeID IN (3, 5, 7)
This is single SQL command, but it is different depending on the selection, of course.
Now, if you would like to use one stored procedure to do the job, then the implementation would depend on the database engine since what you need is to be able to pass multiple parameters. I would discourage using a procedure with just plain 3 parameters, because when you add another check-box, you will have to change the SQL procedure as well.
SELECT *
FROM table
WHERE value IN
(
SELECT option
FROM checked_options
UNION ALL
SELECT option
FROM all_options
WHERE NOT EXISTS (
SELECT 1
FROM checked_options
)
)
The inner subquery will return either the list of the checked options, or all possible options if the list is empty.
For MySQL, it will be better to use this:
SELECT *
FROM t_data
WHERE EXISTS (
SELECT 1
FROM t_checked
WHERE session = 2
)
AND opt IN
(
SELECT opt
FROM t_checked
WHERE session = 2
)
UNION ALL
SELECT *
FROM t_data
WHERE NOT EXISTS (
SELECT 1
FROM t_checked
WHERE session = 2
)
MySQL will notice IMPOSSIBLE WHERE on either of the SELECT's, and will execute only the appropriate one.
See this entry in my blog for performance detail:
Selecting options
If you pass a null into the appropriate values, then it will compare that specific column against itself. If you pass a value, it will compare the column against the value
CREATE PROCEDURE MyCommand
(
#Check1 BIT = NULL,
#Check2 BIT = NULL,
#Check3 BIT = NULL
)
AS
SELECT *
FROM Table
WHERE Column1 = ISNULL(#Check1, Column1)
AND Column2 = ISNULL(#Check2, Column2)
AND Column3 = ISNULL(#Check3, Column3)
The question did not specify a DB product or programming language. However it can be done with ANSI SQL in a cross-product manner.
Assuming a programming language that uses $var$ for variable insertion on strings.
On the server you get all selected values in a list, so if the first two boxes are selected you would have a GET/POST variable like
http://url?colors=black,white
so you build a query like this (pseudocode)
colors = POST['colors'];
colors_list = replace(colors, ',', "','"); // separate colors with single-quotes
sql = "WHERE ('$colors$' == '') OR (color IN ('$colors_list$'));";
and your DB will see:
WHERE ('black,white' == '') OR (color IN ('black','white')); -- some selections
WHERE ('' == '') OR (color IN ('')); -- nothing selected (matches all rows)
Which is a valid SQL query. The first condition matches any row when nothing is selected, otherwise the right side of the OR statement will match any row that is one of the colors. This query scales to an unlimited number of options without modification. The brackets around each clause are optional as well but I use them for clarity.
Naturally you will need to protect the string from SQL injection using parameters or escaping as you see fit. Otherwise a malicious value for colors will allow your DB to be attacked.

SQL Precedence Matching

I'm trying to do precedence matching on a table within a stored procedure. The requirements are a bit tricky to explain, but hopefully this will make sense. Let's say we have a table called books, with id, author, title, date, and pages fields.
We also have a stored procedure that will match a query with ONE row in the table.
Here is the proc's signature:
create procedure match
#pAuthor varchar(100)
,#pTitle varchar(100)
,#pDate varchar(100)
,#pPages varchar(100)
as
...
The precedence rules are as follows:
First, try and match on all 4 parameters. If we find a match return.
Next try to match using any 3 parameters. The 1st parameter has the highest precedence here and the 4th the lowest. If we find any matches return the match.
Next we check if any two parameters match and finally if any one matches (still following the parameter order's precedence rules).
I have implemented this case-by-case. Eg:
select #lvId = id
from books
where
author = #pAuthor
,title = #pTitle
,date = #pDate
,pages = #pPages
if ##rowCount = 1 begin
select #lvId
return
end
select #lvId = id
from books
where
author = #pAuthor
,title = #pTitle
,date = #pDate
if ##rowCount = 1 begin
select #lvId
return
end
....
However, for each new column in the table, the number of individual checks grows by an order of 2. I would really like to generalize this to X number of columns; however, I'm having trouble coming up with a scheme.
Thanks for the read, and I can provide any additional information needed.
Added:
Dave and Others, I tried implementing your code and it is choking on the first Order by Clause, where we add all the counts. Its giving me an invalid column name error. When I comment out the total count, and order by just the individual aliases, the proc compiles fine.
Anyone have any ideas?
This is in Microsoft Sql Server 2005
I believe that the answers your working on are the simplest by far. But I also believe that in SQL server, they will always be full table scans. (IN Oracle you could use Bitmap indexes if the table didn't undergo a lot of simultaneous DML)
A more complex solution but a much more performant one would be to build your own index. Not a SQL Server index, but your own.
Create a table (Hash-index) with 3 columns (lookup-hash, rank, Rowid)
Say you have 3 columns to search on. A, B, C
For every row added to Books you'll insert 7 rows into hash_index either via a trigger or CRUD proc.
First you'll
insert into hash_index
SELECT HASH(A & B & C), 7 , ROWID
FROM Books
Where & is the concatenation operator and HASH is a function
then you'll insert hashes for A & B, A & C and B & C.
You now have some flexibility you can give them all the same rank or if A & B are a superior match to B & C you can give them a higher rank.
And then insert Hashes for A by itself and B and C with the same choice of rank... all the same number or all different... you can even say that a match on A is higher choice than a match on B & C. This solution give you a lot of flexibility.
Of course, this will add a lot of INSERT overhead, but if DML on Books is low or performance is not relevant you're fine.
Now when you go to search you'll create a function that returns a table of HASHes for your #A, #B and #C. you'll have a small table of 7 values that you'll join to the lookup-hash in the hash-index table. This will give you every possible match and possibly some false matches (that's just the nature of hashes). You'll take that result, order desc on the rank column. Then take the first rowid back to the book table and make sure that all of the values of #A #B #C are actually in that row. On the off chance it's not and you've be handed a false positive you'll need to check the next rowid.
Each of these operation in this "roll your own" are all very fast.
Hashing your 3 values into a small 7 row table variable = very fast.
joining them on an index in your Hash_index table = very fast index lookups
Loop over result set will result in 1 or maybe 2 or 3 table access by rowid = very fast
Of course, all of these together could be slower than an FTS... But an FTS will continue to get slower and slower. There will be a size which the FTS is slower than this. You'll have to play with it.
I don't have time to write out the query, but I think this idea would work.
For your predicate, use "author = #pAuthor OR title = #ptitle ...", so you get all candidate rows.
Use CASE expressions or whatever you like to create virtual columns in the result set, like:
SELECT CASE WHEN author = #pAuthor THEN 1 ELSE 0 END author_match,
...
Then add this order by and get the first row returned:
ORDER BY (author_match+title_match+date_match+page_match) DESC,
author_match DESC,
title_match DESC,
date_match DESC
page_match DESC
You still need to extend it for each new column, but only a little bit.
You don't explain what should happen if more than one result matches any given set of parameters that is reached, so you will need to change this to account for those business rules. Right now I've set it to return books that match on later parameters ahead of those that don't. For example, a match on author, title, and pages would come before one that just matches on author and title.
Your RDBMS may have a different way of handling "TOP", so you may need to adjust for that as well.
SELECT TOP 1
author,
title,
date,
pages
FROM
Books
WHERE
author = #author OR
title = #title OR
date = #date OR
pages = #pages OR
ORDER BY
CASE WHEN author = #author THEN 1 ELSE 0 END +
CASE WHEN title = #title THEN 1 ELSE 0 END +
CASE WHEN date = #date THEN 1 ELSE 0 END +
CASE WHEN pages = #pages THEN 1 ELSE 0 END DESC,
CASE WHEN author = #author THEN 8 ELSE 0 END +
CASE WHEN title = #title THEN 4 ELSE 0 END +
CASE WHEN date = #date THEN 2 ELSE 0 END +
CASE WHEN pages = #pages THEN 1 ELSE 0 END DESC
select id,
CASE WHEN #pPages = pages
THEN 1 ELSE 0
END
+ Case WHEN #pAuthor=author
THEN 1 ELSE 0
END AS
/* + Do this for each attribute. If each of your
attributes are just as important as the other
for example matching author is jsut as a good as matching title then
leave the values alone, if different matches are more
important then change the values */ as MatchRank
from books
where author = #pAuthor OR
title = #pTitle OR
date = #pDate
ORDER BY MatchRank DESC
Edited
When I run this query (modified only to fit one of my own tables) it works fine in SQL2005.
I'd recommend a where clause but you will want to play around with this to see performance impacts. You will need to use an OR clause otherwise you will loose potential matches
In regards to the Order By clause failing to compile:
As recursive said(in a comment), alias' may not be within expressions which are used in Order By clauses. to get around this I used a subquery which returned the rows, then ordered by in the outer query. In this way I am able to use the alias' in the order by clause. A little slower but a lot cleaner.
Okay, let me restate my understanding of your question: You want a stored procedure that can take a variable number of parameters and pass back the top row that matches the parameters in the weighted order of preference passed on SQL Server 2005.
Ideally, it will use WHERE clauses to prevent full tables scans plus take advantage of indices and will "short circuit" the search - you don't want to search all possible combinations if one can be found early. Perhaps we can also allow other comparators than = such as >= for dates, LIKE for strings, etc.
One possible way is to pass the parameters as XML like in this article and use .Net stored procedures but let's keep it plain vanilla T-SQL for now.
This looks to me like a binary search on the parameters: Search all parameters, then drop the last one, then drop the second last one but include the last one, etc.
Let's pass the parameters as a delimited string since stored procedures don't allow for arrays to be passed as parameters. This will allow us to get a variable number of parameters in to our stored procedure without requiring a stored procedure for each variation of parameters.
In order to allow any sort of comparison, we'll pass the entire WHERE clause list, like so: title like '%something%'
Passing multiple parameters means delimiting them in a string. We'll use the tilde ~ character to delimit the parameters, like this: author = 'Chris Latta'~title like '%something%'~pages >= 100
Then it is simply a matter of doing a binary weighted search for the first row that meets our ordered list of parameters (hopefully the stored procedure with comments is self-explanatory but if not, let me know). Note that you are always guaranteed a result (assuming your table has at least one row) as the last search is parameterless.
Here is the stored procedure code:
CREATE PROCEDURE FirstMatch
#SearchParams VARCHAR(2000)
AS
BEGIN
DECLARE #SQLstmt NVARCHAR(2000)
DECLARE #WhereClause NVARCHAR(2000)
DECLARE #OrderByClause NVARCHAR(500)
DECLARE #NumParams INT
DECLARE #Pos INT
DECLARE #BinarySearch INT
DECLARE #Rows INT
-- Create a temporary table to store our parameters
CREATE TABLE #params
(
BitMask int, -- Uniquely identifying bit mask
FieldName VARCHAR(100), -- The field name for use in the ORDER BY clause
WhereClause VARCHAR(100) -- The bit to use in the WHERE clause
)
-- Temporary table identical to our result set (the books table) so intermediate results arent output
CREATE TABLE #junk
(
id INT,
author VARCHAR(50),
title VARCHAR(50),
printed DATETIME,
pages INT
)
-- Ill use tilde ~ as the delimiter that separates parameters
SET #SearchParams = LTRIM(RTRIM(#SearchParams))+ '~'
SET #Pos = CHARINDEX('~', #SearchParams, 1)
SET #NumParams = 0
-- Populate the #params table with the delimited parameters passed
IF REPLACE(#SearchParams, '~', '') <> ''
BEGIN
WHILE #Pos > 0
BEGIN
SET #NumParams = #NumParams + 1
SET #WhereClause = LTRIM(RTRIM(LEFT(#SearchParams, #Pos - 1)))
IF #WhereClause <> ''
BEGIN
-- This assumes your field names dont have spaces and that you leave a space between the field name and the comparator
INSERT INTO #params (BitMask, FieldName, WhereClause) VALUES (POWER(2, #NumParams - 1), LTRIM(RTRIM(LEFT(#WhereClause, CHARINDEX(' ', #WhereClause, 1) - 1))), #WhereClause)
END
SET #SearchParams = RIGHT(#SearchParams, LEN(#SearchParams) - #Pos)
SET #Pos = CHARINDEX('~', #SearchParams, 1)
END
END
-- Set the binary search to search from all parameters down to one in order of preference
SET #BinarySearch = POWER(2, #NumParams)
SET #Rows = 0
WHILE (#BinarySearch > 0) AND (#Rows = 0)
BEGIN
SET #BinarySearch = #BinarySearch - 1
SET #WhereClause = ' WHERE '
SET #OrderByClause = ' ORDER BY '
SELECT #OrderByClause = #OrderByClause + FieldName + ', ' FROM #params WHERE (#BinarySearch & BitMask) = BitMask ORDER BY BitMask
SET #OrderByClause = LEFT(#OrderByClause, LEN(#OrderByClause) - 1) -- Remove the trailing comma
SELECT #WhereClause = #WhereClause + WhereClause + ' AND ' FROM #params WHERE (#BinarySearch & BitMask) = BitMask ORDER BY BitMask
SET #WhereClause = LEFT(#WhereClause, LEN(#WhereClause) - 4) -- Remove the trailing AND
IF #BinarySearch = 0
BEGIN
-- If nothing found so far, return the top row in the order of the parameters fields
SET #WhereClause = ''
-- Use the full order sequence of fields to return the results
SET #OrderByClause = ' ORDER BY '
SELECT #OrderByClause = #OrderByClause + FieldName + ', ' FROM #params ORDER BY BitMask
SET #OrderByClause = LEFT(#OrderByClause, LEN(#OrderByClause) - 1) -- Remove the trailing comma
END
-- Find out if there are any results for this search
SET #SQLstmt = 'SELECT TOP 1 id, author, title, printed, pages INTO #junk FROM books' + #WhereClause + #OrderByClause
Exec (#SQLstmt)
SET #Rows = ##RowCount
END
-- Stop the result set being eaten by the junk table
SET #SQLstmt = REPLACE(#SQLstmt, 'INTO #junk ', '')
-- Uncomment the next line to see the SQL you are producing
--PRINT #SQLstmt
-- This gives the result set
Exec (#SQLstmt)
END
This stored procedure is called like so:
FirstMatch 'author = ''Chris Latta''~pages > 100~title like ''%something%'''
There you have it - a fully expandable, optimised search for the top result in weighted order of preference. This was an interesting problem and shows just what you can pull off with native T-SQL.
A couple of small issues with this:
it relies on the caller to know that they must leave a space after the field name for the parameter to work properly
you can't have field names with spaces in them - fixable with some effort
it assumes that the relevant sort order is always ascending
the next programmer that has to look at this procedure will think you're insane :)
Try this:
ALTER PROCEDURE match
#pAuthor varchar(100)
,#pTitle varchar(100)
,#pDate varchar(100)
,#pPages varchar(100)
-- exec match 'a title', 'b author', '1/1/2007', 15
AS
SELECT id,
CASE WHEN author = #pAuthor THEN 1 ELSE 0 END
+ CASE WHEN title = #pTitle THEN 1 ELSE 0 END
+ CASE WHEN bookdate = #pDate THEN 1 ELSE 0 END
+ CASE WHEN pages = #pPages THEN 1 ELSE 0 END AS matches,
CASE WHEN author = #pAuthor THEN 4 ELSE 0 END
+ CASE WHEN title = #pTitle THEN 3 ELSE 0 END
+ CASE WHEN bookdate = #pDate THEN 2 ELSE 0 END
+ CASE WHEN pages = #pPages THEN 1 ELSE 0 END AS score
FROM books
WHERE author = #pAuthor
OR title = #pTitle
OR bookdate = #PDate
OR pages = #pPages
ORDER BY matches DESC, score DESC
However, this of course causes a table scan. You can avoid that by making it a union of a CTE and 4 WHERE clauses, one for each property - there will be duplicates, but you can just take the TOP 1 anyway.
EDIT: Added the WHERE ... OR clause. I'd feel more comfortable if it were
SELECT ... FROM books WHERE author = #pAuthor
UNION
SELECT ... FROM books WHERE title = #pTitle
UNION
...