SQL Indexes to be used in WHERE statement - sql

I have a table with 3 fields that I create a composite index. I need it to check if the record exists.
Here is my table
tblGamePlayLog
UserId (PK)
LogId (PK)
...
...
ProviderId
ResellerId
GameId
...
...
-- ProviderId, ResellerId and GameId is indexed (composite index)
And I have stored procedure like this
CREATE PROCEDURE [AS.uspProviderResellerGame_IsDeletable]
(
#ProviderId INT = -1, --Use -1 to ignore this field
#ResellerId INT = -1, --Use -1 to ignore this field
#GameId INT = 1, --Use -1 to ignore this field
#IsDeletable BIT = 0 OUT
)
AS
BEGIN
SET #IsDeletable = 1;
...
...
...
ELSE IF (EXISTS(SELECT TOP 1 1 FROM [tblGamePlayLog] WHERE ((#ProviderId = -1) OR ([ProviderId] = #ProviderId)) AND ((#ResellerId = -1) OR ([ResellerId] = #ResellerId)) AND ((#GameId = -1) OR ([GameId] = #GameId)))) SET #IsDeletable = 0;
END;
This stored procedure allowed the calling function to pass -1 to ignore the check on a particular field. However, it caused a significant slowness on the query (1000x slower as the log consists of 1 million records).
If I remove -1 check, the speed improve significantly.
...
...
...
ELSE IF (EXISTS(SELECT TOP 1 1 FROM [tblGamePlayLog] WHERE ([ProviderId] = #ProviderId) AND ([ResellerId] = #ResellerId) AND ([GameId] = #GameId))) SET #IsDeletable = 0;
I suspect, but adding -1 check, the SQL doesn't use index check. My question is, how to allow -1 check in WHERE clause but preserve the index check.

I would recommend option(recompile), so the database evaluates the literal parameters for every runs and computes a proper execution plan accordingly. The overhead of the recompilation should be less than the price you are paying now for a suboptimal plan.
IF(
EXISTS(
SELECT 1
FROM [tblGamePlayLog]
WHERE
(#ProviderId = -1 OR [ProviderId] = #ProviderId)
AND (#ResellerId = -1 OR [ResellerId] = #ResellerId)
AND (#GameId = -1 OR [GameId] = #GameId)
)
OPTION (RECOMPILE)
)
Then, we need to look at the indexing part of the question. If you want the query to run efficiently in all situations, you need several indexes, basically all possible combinations of search parameters:
(providerid)
(resellerid)
(gameid)
(providerid, resellerid)
(providerid, gameid)
(gameid, resellerid)
(providerid, resellerid, gameid)
We can factorize a little:
(providerid, resellerid, gameid)
(providerid, gameid)
(gameid, resellerid)
(resellerid)

You might find that dynamic SQL -- with the right indexes -- is more consistent:
DECLARE #sql NVARCHAR(max) = '
SELECT #exists = MAX(flag)
FROM (SELECT TOP (1) 1 as flag
FROM [tblGamePlayLog]
WHERE 1=1 #WHERE
) gpl
';
DECLARE #WHERE NVARCHAR(MAX) = '';
SET #WHERE = #WHERE +
(CASE WHEN #ProviderId <> -1 THEN ' AND [ProviderId] = #ProviderId' ELSE '' END) +
(CASE WHEN #ResellerId <> -1 THEN ' AND [ResellerId] = #ResellerId' ELSE '' END) +
(CASE WHEN #ProviderId <> -1 THEN ' AND [GameId] = #GameId' ELSE '' END);
SET #sql = REPLACE(#sql, '#WHERE', #WHERE);
DECLARE #exists INT;
exec sp_executesql #sql,
N'#ProviderId int, #ResellerId int, #ProviderId int, #exists INT OUTPUT',
#exists=#exists OUTPUT;
IF (#exists = 1) BEGIN
. . .
END;
The construction of the SQL and the overhead for running it should be relatively small -- you need to run a query anyway. This will guarantee the recompile. You will also want to be sure that you have appropriate indexes on table, which requires a lot of combinations (GMB points this out in that answer).

Related

How to select multiple columns from a table in an IF ELSE stored procedure

I am trying to create a stored procedure which has 3 IF ELSE sections, in one of them I would like to select 2 columns to evaluate but am getting this error :
Only one expression can be specified in the select list when the subquery is not introduced with EXISTS
In this case the guid&number are unique, here is the section of the procedure in question:
IF ((SELECT [guid],[number] from [tws] where [guid] = #ReferenceNumber and #Number = [number] and ([instruction_submitted] = '' or [instruction_submitted] is null )) is not null)
SET #ReturnValue = 2
ELSE ```
I think you want exists:
IF (EXISTS (SELECT [guid],[number]
from [tws]
where [guid] = #ReferenceNumber and #Number = [number] and
([instruction_submitted] = '' or [instruction_submitted] is null )
)
)
BEGIN
SET #ReturnValue = 2;
END;
ELSE . . .
I also strongly encourage you to use BEGIN/END around in IF statements -- that makes it less error-prone to add additional statements in the future.

Using different set of WHERE clauses in stored procedure depending on Parameter value

I have 2 stored procedures which return the same columns that I am trying to merge into a single procedure. They both have a different set of parameters and both have different WHERE clauses, but they use the same tables and select the exact same rows.
WHERE clause 1: (uses #UIOID, and #Level)
WHERE ( #UIOID = CASE WHEN #Level = 'Single' THEN C.C_UIOID_PK
WHEN #Level = 'Children' THEN CLC.UIOL_P
WHEN #Level = 'Parent' THEN CLP.UIOL_C
END
OR ( #UIOID = '0'
AND #Level = 'All'
)
)
Where clause 2: (Uses #TeamCode, #Year, #IncludeQCodes)
WHERE C.C_IsChild = 0
AND C.C_MOA <> 'ADD'
AND #TeamCode = C.C_OffOrg
AND C.C_Active = 'Y'
AND ( #Year BETWEEN dbo.f_GetAcYearByDate(C.C_StartDate) AND dbo.f_GetAcYearByDate(C.C_EndDate)
OR #Year = 0 )
AND ( C.C_InstCode NOT LIKE 'Q%'
OR #IncludeQCodes = 1 )
Ideally I want to add a new parameter which basically tells it which of the two WHERE clauses to run, but I can't seem to recreate that with CASE statement because as far as I can tell, they only work for a single WHERE clause, not a whole set of different clauses
I want to do this without having to repeat the select statement again and putting the whole thing in IF statements, and i don't want to put the query into a string either. I just want one select statement ideally.
The problem with using temp tables is the query itself takes a while to run without any parameters and is used in a live website, so I don't want it to have to put all records in a temp table and then filter it.
The problem with using a CTE is you can't follow it with an IF statement, so that wouldn't work either.
Here is the sort of logic I am trying to achieve:
SELECT A
B
C
FROM X
IF #WhichOption = 1 THEN
WHERE ( #UIOID = CASE WHEN #Level = 'Single' THEN C.C_UIOID_PK
WHEN #Level = 'Children' THEN CLC.UIOL_P
WHEN #Level = 'Parent' THEN CLP.UIOL_C
END
OR ( #UIOID = '0'
AND #Level = 'All'
)
)
ELSE IF #WhichOption = 2 THEN
WHERE C.C_IsChild = 0
AND C.C_MOA <> 'ADD'
AND #TeamCode = C.C_OffOrg
AND C.C_Active = 'Y'
AND ( #Year BETWEEN dbo.f_GetAcYearByDate(C.C_StartDate) AND dbo.f_GetAcYearByDate(C.C_EndDate)
OR #Year = 0 )
AND ( C.C_InstCode NOT LIKE 'Q%'
OR #IncludeQCodes = 1 )
Save the following process in a procedure. You can also directly insert into a physical table.
declare #varTable Table (columns exactly as Procedures return)
if(condition is met)
begin
insert into #varTable
exec proc1
end
else
begin
insert into #varTable
exec proc2
end
Add the parameter that you said that it would indicate what filter apply :
select XXXXX
from XXXXX
where (#Mode = 1 and ( filter 1 ))
or
(#Mode = 2 and ( filter 2 ))
option(recompile)
If the #Mode parameter is 1 then it will evaluate the filter 1, otherwise it will evaluate the filter 2.
Add an option(recompile) at the end of the statement, so the SQL engine will replace the variables with their values, eliminate the filter that won't be evaluated, and generate an execution plant for just the filter that you want to apply.
PS: Please notice that although these catchall queries are very easy to code and maintain, and generate a perfectly functional and optimal execution, they are not advised for high-demand applications. The option(recompile) forces the engine to recompile and generate a new execution plan at every execution and that would have a noticeable effect on performance if your query needs to be executed hundreds of times per minute. But for the occasional use it's perfectly fine.
Try to use dynamic SQL:
DECLARE #sql NVARCHAR(max), #where NVARCHAR(max), #WhichOption INT = 1;
SET #sql = 'SELECT A
B
C
FROM X';
IF #WhichOption = 1
SET #where = 'WHERE ( #UIOID = CASE WHEN #Level = ''Single'' THEN C.C_UIOID_PK
WHEN #Level = ''Children'' THEN CLC.UIOL_P
WHEN #Level = ''Parent'' THEN CLP.UIOL_C
END
OR ( #UIOID = ''0''
AND #Level = ''All''
)
)';
ELSE IF #WhichOption = 2
SET #where = ' WHERE C.C_IsChild = 0
AND C.C_MOA <> ''ADD''
AND #TeamCode = C.C_OffOrg
AND C.C_Active = ''Y''
AND ( #Year BETWEEN dbo.f_GetAcYearByDate(C.C_StartDate)
AND dbo.f_GetAcYearByDate(C.C_EndDate)
OR #Year = 0 )
AND ( C.C_InstCode NOT LIKE ''Q%''
OR #IncludeQCodes = 1 ) ';
SET #sql = CONCAT(#sql,' ', #where)
PRINT #sql
EXECUTE sp_executesql #sql

how to get results of function with datatype nvarchar

I have a database table like this
Id Code Amount Formula
-------------------------------------
1 A01 20.00
2 A08 0.00 dbo.ufn_Test(40)
3 A03 0.00 dbo.ufn_Test(60)
My Formula column is a string with name as a function in my database, how can I return the result into the Amount column?
My table has about 100000 rows so when I used while() it takes a lot of time.
I'm using SQL Server 2012
I've used dynamic SQL like this:
DECLARE #_j INT = 1
WHILE (#_j<=(SELECT MAX(Id) FROM #Ct_Lv))
BEGIN
SET #_CtLv = (SELECT Formula FROM #Ct_Lv WHERE Id = #_j)
DECLARE #sql NVARCHAR(MAX)
DECLARE #result NUMERIC(18, 2) = 0
SET #sql = N'set #result = N''''SELECT''' + #_CtLv
EXEC sp_executesql #sql, N'#result float output', #result out
UPDATE #Ct_Lv
SET Amount = #result
WHERE Id = #_j
SET #_j = #_j + 1
END
but my max #_j = 100000, I've run my code for 3 hours and it's still running
one thing, i would like know here is, does id attribute is identity column ?
2nd most important part is, you are declaring variables #sql and #result for each row and you are taking max at every row iterate, which might decrease the performance. I am not sure, how much faster the solution i have been given here, but you can try it once.
Set Nocount On;
Declare #_count Int
,#_j Int
,#_cnt Int
,#_dynamicSql Varchar(Max)
,#_formula Varchar(Max)
,#_row25Cnt Int
Select #_count = Count(1)
,#_j = 0
,#_cnt = 0
,#_dynamicSql = ''
,#_formula = ''
,#_row25Cnt = 1
From #Ct_Lv As ct With (Nolock)
While (#_cnt < #_count)
Begin
Select Top 1
#_j = ct.Id
,#_formula = ct.Formula
From #Ct_Lv As ct With (Nolock)
Where ct.Id > #_j
Order By ct.Id Asc
Select #_dynamicSql = 'Update ct Set ct.Amount = f.result From #Ct_Lv As ct Join ( Select ' + Cast(#_j As Varchar(20)) + ' As Id, [fuctionResultAttribute] As result From ' + #_formula + ' ) As f On ct.Id = f.Id; '
If (#_row25Cnt = 25)
Begin
Exec (#_dynamicSql)
Select #_dynamicSql = ''
,#_row25Cnt = 0
End
Else If ((#_cnt + 1) = #_count)
Begin
Exec (#_dynamicSql)
Select #_dynamicSql = ''
,#_row25Cnt = 0
End
Select #_cnt = #_cnt + 1
,#_row25Cnt = #_row25Cnt + 1
End
Here, what i have done so far is, I am looping Id by Id and generating dynamic sql for each 25 rows, once count is reach to 25, that dynamic sql will be executed which will update your amount. and again start generating dynamic sql for next 25 rows, and when count is about to end and there would be no 25 rows as end then dynamic sql will be executed when loop about to end in 'else if' condition.
above my solution will work only in that case when there would be only one formula in formula column for each row.
I just suggest one thing if Formula field calling the same function each time then better to store only parameter that you want to pass to the function, then you can easily process over huge data.
Else looping over huge data is not preferable way to perform any operation. So it's advisable to use some other trick over there in table structure and storing data.

Performance issues generating a unique name

I have a table 'Objects' present in SQL Server DB. It contains the names (string) of objects.
I have a list of names of new objects that need to be inserted in the 'Objects' table, in a separate table 'NewObjects'. This operation will be referred as 'import' henceforward.
I need to generate a unique name for each record to be imported to 'Objects' from 'NewObjects', if the record name is already present in 'Objects'. This new name will be stored in 'NewObjects' table against the old name.
DECLARE #NewObjects TABLE
(
...
Name varchar(20),
newName nvarchar(20)
)
I have implemented a stored procedures which generates unique name for each record to be imported from 'NewObjects'. However, I am not happy with the performance for 1000 records (in 'NewObjects'.)
I want help to optimize my code. Below is the implementation:
PROCEDURE [dbo].[importWithNewNames] #args varchar(MAX)
-- Sample of #args is like 'A,B,C,D' (a CSV string)
...
DECLARE #NewObjects TABLE
(
_index int identity PRIMARY KEY,
Name varchar(20),
newName nvarchar(20)
)
-- 'SplitString' function: this is a working implementation which is right now not concern of performance
INSERT INTO #NewObjects (Name)
SELECT * from SplitString(#args, ',')
declare #beg int = 1
declare #end int
DECLARE #oldName varchar(10)
-- get the count of the rows
select #end = MAX(_index) from #NewObjects
while #beg <= #end
BEGIN
select #oldName = Name from #NewObjects where #beg = _index
Declare #nameExists int = 0
-- this is our constant. We cannot change
DECLARE #MAX_NAME_WIDTH int = 5
DECLARE #counter int = 1
DECLARE #newName varchar(10)
DECLARE #z varchar(10)
select #nameExists = count(name) from Objects where name = #oldName
...
IF #nameExists > 0
BEGIN
-- create name based on pattern 'Fxxxxx'. Example: 'F00001', 'F00002'.
select #newName = 'F' + REPLACE(STR(#counter, #MAX_NAME_WIDTH, 0), ' ', '0')
while EXISTS (select top 1 1 from Objects where name = #newName)
OR EXISTS (select top 1 1 from #NewObjects where newName = #newName)
BEGIN
select #counter = #counter + 1
select #newName = 'F' + REPLACE(STR(#counter, #MAX_NAME_WIDTH, 0), ' ', '0')
END
select top 1 #z = #newName from Objects
update #NewObjects
set newName = #z where #beg = _index
END
select #beg = #beg + 1
END
-- finally, show the new names generated
select * from #NewObjects
DISCLAIMER: I am in no position to test these recommendations therefore there may be syntax errors that you'll have to work out on your own as you implement them. They are here as a guide to both fix this procedure but also aid you in growing your skill set for future projects.
One optimization just skimming through, that would become more prevalent as you iterated over larger sets, is this code here:
select #nameExists = count(name) from Objects where name = #oldName
...
IF #nameExists > 0
consider changing it to this:
IF EXISTS (select name from Objects where name = #oldName)
Also, rather than doing this:
-- create name based on pattern 'Fxxxxx'. Example: 'F00001', 'F00002'.
select #newName = 'F' + REPLACE(STR(#counter, #MAX_NAME_WIDTH, 0), ' ', '0')
while EXISTS (select top 1 1 from Objects where name = #newName)
OR EXISTS (select top 1 1 from #NewObjects where newName = #newName)
BEGIN
select #counter = #counter + 1
select #newName = 'F' + REPLACE(STR(#counter, #MAX_NAME_WIDTH, 0), ' ', '0')
END
consider this:
DECLARE #maxName VARCHAR(20)
SET #newName = 'F' + REPLACE(STR(#counter, #MAX_NAME_WIDTH, 0), ' ', '0')
SELECT #maxName = MAX(name) FROM Objects WHERE name > #newName ORDER BY name
IF (#maxName IS NOT NULL)
BEGIN
#counter = CAST(SUBSTRING(#maxName, 2) AS INT)
SET #newName = 'F' + REPLACE(STR(#counter, #MAX_NAME_WIDTH, 0), ' ', '0')
END
that will ensure that you're not iterating and doing multiple queries just to find the maximum integer value of the generated name.
Further, based on what little context I have, you should also be able to make one more optimization that will ensure you only have to do the aforementioned one time, ever.
DECLARE #maxName VARCHAR(20)
SET #newName = 'F' + REPLACE(STR(#counter, #MAX_NAME_WIDTH, 0), ' ', '0')
IF (#beg = 1)
BEGIN
SELECT #maxName = MAX(name) FROM Objects WHERE name > #newName ORDER BY name
IF (#maxName IS NOT NULL)
BEGIN
#counter = CAST(SUBSTRING(#maxName, 2) AS INT)
SET #newName = 'F' + REPLACE(STR(#counter, #MAX_NAME_WIDTH, 0), ' ', '0')
END
END
The reason I say you can make that optimization is because unless you have to worry about other entities inserting records during this time that look like the ones you are (e.g. Fxxxxx), then you only have to find the MAX one time and can simply iterate #counter over the loop.
In fact, you could actually pull this entire piece out of the loop. You should be able to extrapolate that pretty easily. Just pull the DECLARE and SET of #counter out along with the code inside the IF (#beg = 1). But take it one step at a time.
Also, change this line:
select top 1 #z = #newName from Objects
to this:
SET #z = #newName
because you are literally running a query to SET two local variables. This is likely a huge cause for the performance issues. A good practice for you to get into is unless you're actually setting a variable from a SELECT statement, use the SET operation for local variables. There are some other places in your code where this applies, consider this line:
select #beg = #beg + 1
use this instead:
SET #beg = #beg + 1
Finally, as stated above regarding simply iterating #counter, at the end of the loop where you have this line:
select #beg = #beg + 1
just add a line:
SET #counter = #counter + 1
and you're golden!
So to recap, you can gather the maximum conflicting name just one time so you'll be getting rid of all those iterations. You're going to start using SET to get rid of performance ridden lines like select top 1 #z = #newName from Objects where you're actually querying a table to set two local variables. And you're going to leverage the EXISTS method instead of setting a variable that leveraged an AGGREGATE function COUNT to do that work.
Let me know how these optimizations work.
You should avoid queries inside loops.. Especially if this is in a table variable...
You should try to use a temp table and index this table on newname column. I bet it would improve a bit the performance..
But would be better you rewrite it all avoiding those loop with query inside..
Setting my ambient for test...
--this would be your object table... I feed it with some values for test
DECLARE #Objects TABLE
(
_index int identity PRIMARY KEY,
Name varchar(20)
)
insert into #Objects(name)
values('A'),('A1'),('B'),('F00001')
--the parameter of your procedure
declare #args varchar(MAX)
set #args = 'A,B,C,D,F00001'
--#NewObjects2 is your #NewObjects just named the n2 cause I did run your solution together when testing
DECLARE #NewObjects2 TABLE
(
_index int identity PRIMARY KEY,
Name varchar(20),
newName nvarchar(20)
)
INSERT INTO #NewObjects2 (Name)
SELECT * from SplitString(#args, ',')
declare #end int
select #end = MAX(_index) from #NewObjects2
DECLARE #MAX_NAME_WIDTH int = 5
At this point its very similar your solution
Now what I would do instead your looping
--generate newNames in format FXXXXX with free names sufficient to give newnames for all lines in #newObject
--you should alter this to get the greater FXXXXX name inside the Objects and start generate newNames from this point.. to avoid overhead creating newNames that will sure not to be used..
with N_free as
(
select
0 as [count],
'F' + REPLACE(STR(0, #MAX_NAME_WIDTH, 0), ' ', '0') as [newName],
0 as fl_free,
0 as count_free
union all
select
N.[count] + 1 as [count],
'F' + REPLACE(STR(N.[count]+1, #MAX_NAME_WIDTH, 0), ' ', '0') as [newName],
OA.fl_free,
count_free + OA.fl_free as count_free
from
N_free N
outer apply
(select
case
when not exists(select name from #Objects
where Name = 'F' + REPLACE(STR(N.[count]+1, #MAX_NAME_WIDTH, 0), ' ', '0'))
then 1
else 0
end as fl_free) OA
where
N.count_free < #end
)
--return only those newNames that are free to be used
,newNames as (select ROW_NUMBER() over (order by [count]) as _index_name
,[newName]
from N_free where fl_free = 1
)
--update the #NewObjects2 giving newname for the ones that got the name already been used on Objects
update N2
set newName = V2.[newName]
from #NewObjects2 N2
inner join (select V._index,V.Name,newNames.[newName]
from( select row_number() over (partition by case when O.Name is not null
then 1
else 0
end
order by N._index) as _index_name
,N._index
,N.Name
,case when O.Name is not null
then 1
else 0
end as [fl_need_newName]
from #NewObjects2 N
left outer join #Objects O
on O.Name = N.Name
)V
left outer join newNames
on newNames._index_name = V._index_name
and V.fl_need_newName = 1
)V2
on V2._index = N2._index
option(MAXRECURSION 0)
select * from #NewObjects2
The results that I achieved was the same then using your solution for this ambient...
You may check if this really generate same result...
The result for this query was
_index Name newName
1 A F00002
2 B F00003
3 C NULL
4 D NULL
5 F00001 F00004

Is it faster to check if length = 0 than to compare it to an empty string?

I've heard that in some programming languages it is faster to check if the length of a string is 0, than to check if the content is "". Is this also true for T-SQL?
Sample:
SELECT user_id FROM users WHERE LEN(user_email) = 0
vs.
SELECT user_id FROM users WHERE user_email = ''
Edit
You've updated your question since I first looked at it. In that example I would say that you should definitely always use
SELECT user_id FROM users WHERE user_email = ''
Not
SELECT user_id FROM users WHERE LEN(user_email) = 0
The first one will allow an index to be used. As a performance optimisation this will trump some string micro optimisation every time! To see this
SELECT * into #temp FROM [master].[dbo].[spt_values]
CREATE CLUSTERED INDEX ix ON #temp([name],[number])
SELECT [number] FROM #temp WHERE [name] = ''
SELECT [number] FROM #temp WHERE LEN([name]) = 0
Execution Plans
Original Answer
In the code below (SQL Server 2008 - I "borrowed" the timing framework from #8kb's answer here) I got a slight edge for testing the length rather than the contents below when #stringToTest contained a string. They were equal timings when NULL. I probably didn't test enough to draw any firm conclusions though.
In a typical execution plan I would imagine the difference would be negligible and if you're doing that much string comparison in TSQL that it will be likely to make any significant difference you should probably be using a different language for it.
DECLARE #date DATETIME2
DECLARE #testContents INT
DECLARE #testLength INT
SET #testContents = 0
SET #testLength = 0
DECLARE
#count INT,
#value INT,
#stringToTest varchar(100)
set #stringToTest = 'jasdsdjkfhjskdhdfkjshdfkjsdehdjfk'
SET #count = 1
WHILE #count < 10000000
BEGIN
SET #date = GETDATE()
SELECT #value = CASE WHEN #stringToTest = '' then 1 else 0 end
SET #testContents = #testContents + DATEDIFF(MICROSECOND, #date, GETDATE())
SET #date = GETDATE()
SELECT #value = CASE WHEN len(#stringToTest) = 0 then 1 else 0 end
SET #testLength = #testLength + DATEDIFF(MICROSECOND, #date, GETDATE())
SET #count = #count + 1
END
SELECT
#testContents / 1000000. AS Seconds_TestingContents,
#testLength / 1000000. AS Seconds_TestingLength
I would be careful about using LEN in a WHERE clause as it could lead to table or index scans.
Also note that if the field is NULLable that LEN(NULL) = NULL, so you would need to define the behaviour, e.g.:
-- Cost .33
select * from [table]
where itemid = ''
-- Cost .53
select * from [table]
where len(itemid) = 0
-- `NULL`able source field (and assuming we treat NULL and '' as the same)
select * from [table]
where len(itemid) = 0 or itemid is NULL
I just tested it in a very limited scenario and execution plan ever so slightly favours comparing it to an empty string. (49% to 51%). This is working with stuff in memory though so it would probably be different if comparing against data from a table.
DECLARE #testString nvarchar(max)
SET #testString = ''
SELECT
1
WHERE
#testString = ''
SELECT
1
WHERE
LEN(#testString) = 0
Edit: This is with SQL Server 2005.
I suspect the answer depends largely on the context. For example, I have been working with expressions in the SELECT list and in user-defined functions. Knowing what I do about the Microsoft .NET Base Class Library and the legacy that Transact-SQL owes to Visual Basic, I suspect that in such circumstances, LEN ( string ) gets the nod.