CSS SQL Server Engineers

This is the official team Web Log for Microsoft Customer Service and Support (CSS) SQL Support. Posts are provided by the CSS SQL Escalation Services

How can I get that user out of my table quickly

How can I get that user out of my table quickly

Rate This
  • Comments 4

Recently, I worked on an customer issue to help tune their slow query.   The query was fairly complex involving multiple table joins.  The key issue is the inability to do seeks on a particular table.

The table has a column that stores User Name like below.  The values stored contain domain name\user name.  Here is a set of sampled fake data:

UserName
mydomain0\user0
mydomain1\user1
mydomain2\user2
mydomain3\user3
mydomain4\user4
mydomain5\user5
mydomain6\user6
mydomain7\user7
mydomain8\user8
mydomain9\user9

Problem

The problem is that their application only passes user name without the domain name.   So the parameter will be user0, user1 etc.   Now this makes it very challenging to seek on the user.  Application couldn’t be changed to take domain name.

So this customer basically used like as the procedure below.  This resulted in scanning of the table and caused performance slowdown.

create procedure p_test1 @user varchar(20)
as
select * from t where UserName like '%' + @user

This user did put an index on UserName and saw the query plan had ‘seek’.  But the performance still didn’t meet the requirement. 

When you use like statement and have an index on the column that uses like, SQL Server tries its best to leverage that index.  It actually tries to calculate a range of values based on the value you pass in and then does index seek on the column.

Below is what the plan looks like:

 

image

 

The problem is that the range can be very large.  If you notice, the index seek is based on two expressions calculated (with > and <) followed by a filter (where …).  So this can be fairly expensive.

In fact, for 2 million dummy rows, it resulted in a logical reads of 9309 pages (as indicated below).

Table 't'. Scan count 1, logical reads 9309, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

Though it’s better than table scan, it is still not the best to suite their needs.

 

Solution

Computed column comes to rescue.   We have talked about computed column in our blog.  But when it comes to solve practical problems, it still scares some users.

For this particular problem, all we need is to use charindex and substring.   The expression of the computed column seems scary.  But it is used to cover special cases such as NULL, blank values etc.

After you create the computed column and then create an index on it.

alter table t
add UserAlias as (substring (UserName, case when charindex ('\', UserName) is null or charindex ('\', UserName)  = 0 then 1 else  charindex ('\', UserName) + 1 end, len (UserName) - case when charindex ('\', UserName) is null or charindex ('\', UserName)  = 0 then 1 else  charindex ('\', UserName) + 1 end +1) ) persisted

create index ix2 on t(UserAlias)

 

now change your query like this:

create procedure p_test2 @user varchar(20)
as
select * from t where UserAlias = @user

Here is the query plan:

 

image

It had dramatic reduction of logical reads (only 3) from 9309 noted before with the range seek with like

Table 't'. Scan count 1, logical reads 3, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

 

The CPU went from over 700 ms to 1 ms after the change.

 

 

Complete demo script

 

use tempdb
go
--settting up table and rows
drop table t
go
create table t (UserName varchar(50))
go
set nocount on
declare  @i int = 0, @domainname varchar(20), @username varchar(30)
begin tran
while @i < 2000000
begin
      
       set @domainname =    'mydomain' + cast ( @i %20  as varchar(20))
       set @username = 'user' + cast(@i % 100000 as varchar(20))
       insert into t values (@domainname + '\' + @username)
      
       set @i = @i + 1
end
commit tran
go

if object_id ('p_test1') is not null drop procedure p_test1
go


create procedure p_test1 @user varchar(20)
as
select * from t where UserName like '%' + @user
go

create index ix on t (UserName)

go
set statistics profile on
set statistics time on
set statistics io on
go
--this query still ends up with many logical reads and fairly high CPU consumption
exec p_test1 'user'
go
set statistics profile off
set statistics time off
set statistics io off
go

--adding a computed column
alter table t
add UserAlias as (substring (UserName, case when charindex ('\', UserName) is null or charindex ('\', UserName)  = 0 then 1 else  charindex ('\', UserName) + 1 end, len (UserName) - case when charindex ('\', UserName) is null or charindex ('\', UserName)  = 0 then 1 else  charindex ('\', UserName) + 1 end +1) ) persisted
go

go
--creating an index on the computed column
create index ix2 on t(UserAlias)
go


if object_id ('p_test2') is not null drop procedure p_test2
go
--new procedure to take advantage of the computed column index
create procedure p_test2 @user varchar(20)
as
select * from t where UserAlias = @user

go

set statistics profile on
set statistics time on
set statistics io on
go
exec p_test2 'user'
go
set statistics profile off
set statistics time off
set statistics io off
go

Leave a Comment
  • Please add 1 and 1 and type the answer here:
  • Post
  • Interesting post. Just one comment.

    Adding a cluster index in UserName column will eliminate RID Lookup, right? Do you think it really Worth the cost of creating the index in order to eliminate the RID Lookup and read the same amount of pages e get the same time of execution, and compilation?

    Thanks!

  • Or just use REVERSE().  That'll give you the same index utilization.  Actually for your particular (unrealistic) sample set, that's the ideal index to use :).

    You don't get the utility of having the user name in a separate column, but in other examples where you're wanting to search just the end of the string that's going to be easier to write and maintain.  Really it's probably less code....

    REVERSE(SUBSTRING(REVERSE(@userid), 1, CHARINDEX('\', REVERSE(@UserID))-1))

    to do your name splitting that way anyway.  You'd need to add in a \ to the start of the string if not present though, so it'd get messier.

  • Nice post with nice solution. Thanks for the share. <a href="www.venturerepublic.net/">Venture Web Development</a>

  • Or define the computed column as REVERSE(UserName) and define the search predicate as UserAlias = REVERSE(@user)+'%'.  Would that work?

Page 1 of 1 (4 items)