Login or Sign Up to become a member!
LessThanDot Sit Logo

LessThanDot

Data Management

Less Than Dot is a community of passionate IT professionals and enthusiasts dedicated to sharing technical knowledge, experience, and assistance. Inside you will find reference materials, interesting technical discussions, and expert tips and commentary. Once you register for an account you will have immediate access to the forums and all past articles and commentaries.

LTD Social Sitings

Lessthandot twitter Lessthandot Linkedin Lessthandot friendfeed Lessthandot facebook Lessthandot rss

Note: Watch for social icons on posts by your favorite authors to follow their postings on these and other social sites.

Your profile

    Search

    XML Feeds

    Google Ads

    « SQL Advent 2012 Day 4: Triggers, what to do, what not to doMy SQL Server Days slides and demos »
    comments

    This is day three of the SQL Advent 2012 series of blog posts. Today we are going to look at sargable queries. You might ask yourself, what is this weird term sargable. Sargable comes from searchable argument, sometimes also referred as Search ARGument ABLE. What that means is that the query will be able to use an index, a seek will be performed instead of a scan. In general any time you have a function wrapped around a column, an index won't be used

    Some examples that are not sargable

    1. WHERE LEFT(Name,1) = 'S'
    2. WHERE Year(SomeDate) = 2012
    3. WHERE OrderID * 3 = 33000

    Those three should be rewritten like this in order to become sargable

    1. WHERE Name LIKE 'S%'
    2. WHERE SomeDate >= '20120101' AND SomeDate < '20130101'
    3. WHERE OrderID = 33000/3

    Let's create a table, insert some data so that we can look at the execution plan
    Create this simple table

    1. CREATE TABLE Test(SomeID varchar(100))

    Let's insert some data that will start with a letter followed by some digits

    1. INSERT Test
    2. SELECT LEFT(v2.type,1) +RIGHT('0000' + CONVERT(varchar(4),v1.number),4)
    3. FROM master..spt_values v1
    4. CROSS JOIN (SELECT DISTINCT LEFT(type,1) AS type
    5. FROM master..spt_values) v2
    6. WHERE v1.type = 'p'

    That insert should have generated 32768 rows

    Now create this index on that table

    1. CREATE CLUSTERED INDEX cx_test ON Test(SomeID)

    Let's take a look at the execution plan, hit CTRL + M, this will add the execution plan once the query is done running

    1. SELECT * FROM Test
    2. WHERE SomeID LIKE 's%'
    3.  
    4. SELECT * FROM Test
    5. WHERE LEFT(SomeID,1) = 's'

    Here is what the plans looks like

    As you can see it is 9% versus 91% between the two queries, that is a big difference
    Hit CTRL + M again to disable the inclusion of the plan

    Run this codeblock, it will give you the plans in a text format

    1. SET SHOWPLAN_TEXT ON
    2. GO
    3.  
    4. SELECT * FROM Test
    5. WHERE SomeID LIKE 's%'
    6.  
    7. SELECT * FROM Test
    8. WHERE LEFT(SomeID,1) = 's'
    9. GO
    10.  
    11. SET SHOWPLAN_TEXT OFF
    12. GO

    Here are the two plans

    |--Clustered Index Seek(OBJECT:([master].[dbo].[Test].[cx_test]),
    SEEK:([master].[dbo].[Test].[SomeID] >= 'Rþ' AND [master].[dbo].[Test].[SomeID] < 'T'),
    WHERE:([master].[dbo].[Test].[SomeID] like 's%') ORDERED FORWARD)

    |--Clustered Index Scan(OBJECT:([master].[dbo].[Test].[cx_test]),
    WHERE:(substring([master].[dbo].[Test].[SomeID],(1),(1))='s'))

    As you can see the top one while looking more complicated is actually giving you a seek

    Making a case sensitive search sargable

    Now let's take a look at how we can make a case sensitive search sargable as well
    In order to do a search and make it case sensitive, you have to have a case sensitive collation, if your table is not created with a case sensitive collation then you can supply it as part of the query

    Here is an example to demonstrate what I mean

    This is a simple table created without a collation

    1. CREATE TABLE TempCase1 (Val CHAR(1))
    2. INSERT TempCase1 VALUES('A')
    3. INSERT TempCase1 VALUES('a')

    Running this select statement will return both rows

    1. SELECT * FROM TempCase1
    2. WHERE Val = 'A'

    Val
    -----
    A
    a

    Now create the same kind of table but with a case sensitive collation

    1. CREATE TABLE TempCase2 (Val CHAR(1) COLLATE SQL_Latin1_General_CP1_CS_AS)
    2. INSERT TempCase2 VALUES('A')
    3. INSERT TempCase2 VALUES('a')

    Run the same query

    1. SELECT * FROM TempCase2
    2. WHERE Val = 'A'

    Val
    -----
    A

    As you can see you only get the one row now that matches the case

    1. SELECT * FROM TempCase1
    2. WHERE Val = 'A' COLLATE SQL_Latin1_General_CP1_CS_AS

    Val
    -----
    A
    a

    Now let's take a look at how we can make the case sensitive search sargable

    First create this table and insert some data

    1. CREATE TABLE TempCase (Val CHAR(1))
    2.  
    3. INSERT TempCase VALUES('A')
    4. INSERT TempCase VALUES('B')
    5. INSERT TempCase VALUES('C')
    6. INSERT TempCase VALUES('D')
    7. INSERT TempCase VALUES('E')
    8. INSERT TempCase VALUES('F')
    9. INSERT TempCase VALUES('G')
    10. INSERT TempCase VALUES('H')

    Now we will insert some lowercase characters

    1. INSERT TempCase
    2. SELECT LOWER(Val) FROM TempCase

    Now we will create our real table which will have 65536 rows

    1. CREATE TABLE CaseSensitiveSearch (Val VARCHAR(50))

    We will do a couple of cross joins to generate the data for our queries

    1. INSERT CaseSensitiveSearch
    2. SELECT t1.val + t2.val + t3.val + t4.val
    3. FROM TempCase t1
    4. CROSS JOIN TempCase t2
    5. CROSS JOIN TempCase t3
    6. CROSS JOIN TempCase t4

    Create an index on the table

    1. CREATE INDEX IX_SearchVal ON CaseSensitiveSearch(Val)

    Just like before, if we run this we will get back the exact value we passed in and also all the upper case and lower case variations

    1. SELECT * FROM CaseSensitiveSearch
    2. WHERE Val = 'ABCD'

    Here are the results of that query
    Val
    -----
    AbCd
    ABcd
    Abcd
    ABCd
    aBCd
    abCd
    aBcd
    abcd
    abCD
    aBcD
    abcD
    aBCD
    ABCD
    AbCD
    ABcD
    AbcD

    If you add the collation to the query, you will get only what matches your value

    1. SELECT * FROM CaseSensitiveSearch
    2. WHERE Val = 'ABCD' COLLATE SQL_Latin1_General_CP1_CS_AS

    Here is the result, it maches what was passed in
    Val
    ---
    ABCD


    The problem with the query above is that it will cause a scan. So what can we do, how can we make it perform better? It is simple combine the two queries
    First grab all case sensitive and case insensitive values and then after that filter out the case insensitive values

    Here is what that query will look like

    1. SELECT * FROM CaseSensitiveSearch
    2. WHERE Val = 'ABCD' COLLATE SQL_Latin1_General_CP1_CS_AS
    3. AND Val LIKE 'ABCD'

    AND Val LIKE 'ABCD' will result in a seek, now when it also does the Val = 'ABCD' COLLATE SQL_Latin1_General_CP1_CS_AS part, it only returns the row that matches your value

    If you run both queries, you can look at the plan difference (hit CTRL + M so that the plan is included)

    1. SELECT * FROM CaseSensitiveSearch
    2. WHERE Val = 'ABCD' COLLATE SQL_Latin1_General_CP1_CS_AS
    3.  
    4.  
    5.  
    6. SELECT * FROM CaseSensitiveSearch
    7. WHERE Val = 'ABCD' COLLATE SQL_Latin1_General_CP1_CS_AS
    8. AND Val LIKE 'ABCD'

    Here is the plan

    As you can see, there is a big difference between the two

    Here is the plan in text as well

    1. SET SHOWPLAN_TEXT ON
    2. GO
    3.  
    4. SELECT * FROM CaseSensitiveSearch
    5. WHERE Val = 'ABCD' COLLATE SQL_Latin1_General_CP1_CS_AS
    6.  
    7.  
    8.  
    9. SELECT * FROM CaseSensitiveSearch
    10. WHERE Val = 'ABCD' COLLATE SQL_Latin1_General_CP1_CS_AS
    11. AND Val LIKE 'ABCD'
    12.  
    13. GO
    14.  
    15. SET SHOWPLAN_TEXT OFF
    16. GO

    |--Table Scan(OBJECT:([tempdb].[dbo].[CaseSensitiveSearch]),
    WHERE:(CONVERT_IMPLICIT(varchar(50),[tempdb].[dbo].[CaseSensitiveSearch].[Val],0)=CONVERT(varchar(8000),[@1],0)))

    |--Index Seek(OBJECT:([tempdb].[dbo].[CaseSensitiveSearch].[IX_SearchVal]), SEEK:([tempdb].[dbo].[CaseSensitiveSearch].[Val] >= 'ABCD'
    AND [tempdb].[dbo].[CaseSensitiveSearch].[Val] <= 'ABCD'),
    WHERE:(CONVERT_IMPLICIT(varchar(50),[tempdb].[dbo].[CaseSensitiveSearch].[Val],0)='ABCD' AND [tempdb].[dbo].[CaseSensitiveSearch].[Val] like 'ABCD') ORDERED FORWARD)

    Also take a look at Only In A Database Can You Get 1000% + Improvement By Changing A Few Lines Of Code to see how this works with dates




    That is all for day three of the SQL Advent 2012 series, come back tomorrow for the next one, you can also check out all the posts from last year here: SQL Advent 2011 Recap

    About the Author

    User bio imageDenis has been working with SQL Server since version 6.5. Although he worked as an ASP/JSP/ColdFusion developer before the dot com bust, he has been working exclusively as a database developer/architect since 2002. In addition to English, Denis is also fluent in Croatian and Dutch, but he can curse in many other languages and dialects (just ask the SQL optimizer) He lives in Princeton, NJ with his wife and three kids.
    Social SitingsTwitterFacebookLinkedInHomePageFlickrLTD RSS Feed
    3843 views
    InstapaperVote on HN

    2 comments

    Comment from: liviu [Visitor]
    liviu What about the situation where the server and database all have case sensitive collations.

    The trick with:
    SELECT * FROM CaseSensitiveSearch
    WHERE Val = 'ABCD' COLLATE SQL_Latin1_General_CP1_CS_AS
    AND Val LIKE 'ABCD'

    will not work anymore because

    'abcd' LIKE 'ABCD' is false...
    12/04/12 @ 08:46
    Comment from: SQLDenis [Member] Email
    SQLDenis Liviu, I don't understand your point, if they are all case sensitive then this should still return the same 1 row
    12/04/12 @ 08:55

    Leave a comment


    Your email address will not be revealed on this site.

    To mislead the spambots.

    Your URL will be displayed.
    (Line breaks become <br />)
    (Name, email & website)
    (Allow users to contact you through a message form (your email will not be revealed.)