Background
I have the following function, which ranks results by how closely they match the value that was searched for:
CREATE FUNCTION [dbo].[fnGetRelevance]
(
@fieldName nvarchar(50),
@searchTerm nvarchar(50)
)
RETURNS int
AS
BEGIN
if (@fieldName like @searchTerm + '%') -- starts with
begin
return 0
end
else if ((@fieldName l开发者_如何学JAVAike '%' + @searchTerm + '%') and (@fieldName not like @searchTerm + '%')) -- contains, but doesn't start with
begin
return 1
end
return 1
END
So in the context of the following query (an NHibernate query), which is searching for share classes based on the search string Allianz RCM BRIC Stars A
it finds 39 results and ranks them so that the one exactly matching that string is on top, and the rest are sorted alphabetically below it.
select top 50 *
from ShareManager.ShareClass sc
-- and a few other tables with an inner join and a left join
where (sc.ShareClass_Id in
(
/* filter by some business criteria which is a single
select statement that does 2 more inner joins */
)
and 1 = 1
and (sc.ShareClass_Name like '%Allianz%' /* @p11 */)
and (sc.ShareClass_Name like '%RCM%' /* @p12 */)
and (sc.ShareClass_Name like '%BRIC%' /* @p13 */)
and (sc.ShareClass_Name like '%Stars%' /* @p14 */)
and (sc.ShareClass_Name like '%A%' /* @p15 */)
order by dbo.fngetrelevance(sc.ShareClass_Name, 'Allianz RCM BRIC Stars A'), sc.ShareClass_Name asc
Question
The problem I'm having is that dbo.fngetrelevance
is causing my NHibernate queries to time out. I've tried extending the timeout but that's not working, and I don't think it's really the issue anyway. When I remove the function it works as expected.
Is there a way on SQL Server 2008 to make this faster, or to implement the ranking with NHibernate in such a way that it will not timeout?
Supplementary Information
I expect someone might suggest that I reduce the number of joins. We've already been through a lot of optimisation in order to speed up these queries as much as possible. It would be a huge effort for us to figure out how to optimise any further, on the scale of modifying the overall schema. Unfortunately we're not going to get the go-ahead for this at this stage of the game, (and for only 1 fund, as far as I can see at the moment)
For the record, this is how I'm using the function with NHibernate:
string querystring =
"select sc, sctr" +
" from ShareClass as sc" +
// joins to 2 other tables
" and (" + expressionTokenizer.ToResult("sc.Name") + ") "
+ this.AddShareClassOrder(order, "sc", "sctr", searchExpression);
var result = _session.CreateQuery(querystring)
.AddNameSearchCriteria(expressionTokenizer)
.AddDataUniverseParameters(dataUniverseHelper)
.SetFirstResult((pageSize * (pageNum - 1)))
.SetMaxResults(pageSize)
.List();
with AddShareClassOrder
effectively returning
fieldName = string.Format("dbo.fngetrelevance({1}.{2}, '{0}'), {1}.{2}", textToSearchFor, shareClassPrefix, "Name");
return String.Format(" order by {0} {1}", fieldName, direction);
or, the following as it's represented in the SQL:
dbo.fngetrelevance(sc.ShareClass_Name, 'Allianz RCM BRIC Stars A'), sc.ShareClass_Name asc
I must disagree with the other answers posted here; the %%
use here is a red herring, as you're not doing any filtering on this expression (in which case they'd most certainly be right). Your problem is your UDF; as it stands now, your UDF will not be inlined into the query. Instead, the query engine will take the entire result set, invoke the function for every single row, then capture those results to sort. You need to define and use your function in such a way that it will be inlined into the query.
For more information, see this article, but the short version is to change your function to this:
CREATE FUNCTION [dbo].[fnGetRelevance]
(
@fieldName nvarchar(50),
@searchTerm nvarchar(50)
)
RETURNS table
AS
select (case when @fieldName like @searchTerm + '%' then 0 else 1 end) as Value
(I eliminated your second condition as it seemed unnecessary, since it returned the same value as the fallback value. If that was a mistake, it should be fairly obvious how to modify the expression above to get what you want.)
Then use it like this:
select top 50 *
from ShareManager.ShareClass sc
-- and a few other tables with an inner join and a left join
where (sc.ShareClass_Id in
(
/* filter by some business criteria which is a single
select statement that does 2 more inner joins */
)
and 1 = 1
and (sc.ShareClass_Name like '%Allianz%' /* @p11 */)
and (sc.ShareClass_Name like '%RCM%' /* @p12 */)
and (sc.ShareClass_Name like '%BRIC%' /* @p13 */)
and (sc.ShareClass_Name like '%Stars%' /* @p14 */)
and (sc.ShareClass_Name like '%A%' /* @p15 */)
order by (select Value from dbo.fngetrelevance(sc.ShareClass_Name, 'Allianz RCM BRIC Stars A')), sc.ShareClass_Name asc
I can't speak to how to change your NHibernate query to do this, but that's not your issue.
I guess your function can be equally rewritten like this:
CREATE FUNCTION [dbo].[fnGetRelevance]
(
@fieldName nvarchar(50),
@searchTerm nvarchar(50)
)
RETURNS int
AS
BEGIN
if (@fieldName like @searchTerm + '%') -- starts with
begin
return 0
end
return 1
END
because you return 0 only when @fieldName starts with @searchTerm and 1 in all other cases.
And instead of calling function
order by dbo.fngetrelevance(sc.ShareClass_Name, 'Allianz RCM BRIC Stars A'), sc.ShareClass_Name asc
you might use the following:
order by
case when sc.ShareClass_Name like 'Allianz RCM BRIC Stars A%'
then 0 else 1 end,
sc.ShareClass_Name asc
I am not sure how well you will be able to optimise this function.
The major problem here is the like %somethings%
everywhere.
an ex-colleague described it best to me as you can find all the names beginning with E in the phone book easily but finding everyname with an E in it is a very long running process.
You are effectively negating your indexing.
精彩评论