11

I have a couple of issues with SQLite query. Actually I start thinking that SQLite is not designed for tables with more then 10 rows, really, SQLite is a nightmare.

The following query

SELECT * FROM [Table] WHERE [Name] LIKE 'Text%'

It works fine. EXPLAIN shows that the index is used and result is returned after about 70ms.

Now I need to run this query from .NET SQLite driver, so I'm changing query

SELECT * FROM [Table] WHERE [Name] LIKE @Pattern || '%'

Index is not used. When I run the following query in any SQLite tool the index is not used as well

SELECT * FROM [Table] WHERE [Name] LIKE 'Text' || '%'

So I guess SQLite doesn't have any kind of preprocessing logic implemented.

OK. Let's try to solve it, I'm still binding variables and doing the following

SELECT * FROM [Table] WHERE [Name] LIKE @Pattern

But now I append % wildcard symbol to the end of my pattern string, like this

command.Parameters.Add(new SQLiteParameter("@Pattern", pattern + '%'));

It works very slow. I can't say why, because when I run this query from SQLite tool it works fine, however when I bind this variable from .NET code it works slow.

OK. I'm still trying to solve this. I'm getting rid of the pattern parameter binding and building this condition dynamically.

pattern = pattern.Replace("'", "''");
pattern = pattern.Replace("%", "\\%");
where = string.Format("LIKE '{0}%' ESCAPE '\\'", pattern);

Index is not used again. It's not used because of ESCAPE. I see that when I run

EXPLAIN QUERY PLAN SELECT * FROM [Table] WHERE [Name] LIKE 'Text%' ESCAPE '\'

As soon as I remove ESCAPE it starts using index again and the query finishes in 60-70ms.

UPDATE

Here are the results.

EXPLAIN QUERY PLAN
SELECT * FROM [RegistryValues]
WHERE
     [ValueName] LIKE 'windir%' ESCAPE '\' 

SCAN TABLE RegistryValues (~3441573 rows)

and the one without ESCAPE

EXPLAIN QUERY PLAN
SELECT * FROM [RegistryValues]
WHERE
     [ValueName] LIKE 'windir%'

SEARCH TABLE RegistryValues USING INDEX IdxRegistryValuesValueNameKeyIdKeyHiveFileId (ValueName>? AND ValueName<?) (~31250 rows)

UPDATE

Just found this

http://www.sqlite.org/optoverview.html

4.0 The LIKE optimization

The ESCAPE clause cannot appear on the LIKE operator

So what should I do then?

Do I understand it right? I can't search string containing wildcards using LIKE operator in SQLite. By saying wildcards I mean _ % ^ !

It's impossible simply because I can't escape them. Actually I can, but I can't use indexes in this case, so the query will not be efficient.

Am I right?

axe
  • 2,331
  • 4
  • 31
  • 53
  • I'm not familiar with SQLite details, but most DBMSes would normally prepare a query independently from the bound parameter _value_ - in fact, query preparation could happen even before the parameter value is known. Therefore, `LIKE @Pattern` cannot be optimized for the index, since `@Pattern` could be '%suffix'. The solution, as you already noted, would be to build the query dynamically - frankly I don't see why ESCAPE should mess that up. – Branko Dimitrijevic Oct 24 '12 at 19:32
  • Yes, I don't see it as well, but that's what is happening. I can somehow understand why it doesn't use index for concatenation, but what's the problem with `escape`? Unfortunately I need to escape `%` symbol in order to allow to search strings starting with something like `%windir%`. – axe Oct 24 '12 at 20:49
  • BTW, it doesn't use indexes for the following as well. `SELECT * FROM [Table] WHERE [Name] LIKE 'Text' || '%'` – axe Oct 24 '12 at 21:08
  • SQL Server for example will statically prepare the query even with a dynamic like expression (it will still plan for an index seek at a dynamic location). So it is definitely possible for an RDBMS to seek on a dynamic like predicate. – usr Oct 24 '12 at 21:13
  • I would be happy to drop this stupid SQLite and move the product to SQL Server, however that's not my decision :( – axe Oct 24 '12 at 21:15
  • 1
    @axe The behavior of ESCAPE seems to be an SQLite limitation. Quote from [The SQLite Query Planner](http://www.sqlite.org/optoverview.html): _"Terms that are composed of the LIKE or GLOB operator can sometimes be used to constrain indices. There are many conditions on this use: ... 3. The ESCAPE clause cannot appear on the LIKE operator."_ – Branko Dimitrijevic Oct 24 '12 at 22:44
  • 1
    @Branko Dimitrijevic: Yes, this is already quoted in my question. So what's the solution? Query working 40 seconds is obviously not acceptable. On the other side not allowing user to search string containing underscore is not acceptable as well. – axe Oct 25 '12 at 04:21

3 Answers3

6

An index can only be used with a LIKE clause when the % is at the end so that SQLite can rewrite it to two simple comparisons (as shown in the EXPLAIN output).

Therefore, to get the same effect, write the comparisons yourself. This requires that you construct some string that is guaranteed to compare 'larger' than any of the matched values (beware of non-ASCII characters). Instead of:

... WHERE Name LIKE 'Text%'

use:

... WHERE Name BETWEEN 'Text' AND 'Textzzzzzzzzzzzzz'

or, as parameter:

... WHERE Name BETWEEN @Pattern AND @Pattern || 'zzzzzzzzzz'

(This construct never needs escapes. :)

CL.
  • 173,858
  • 17
  • 217
  • 259
  • That's genius! :) Exactly what I need. Thank you. – axe Oct 25 '12 at 08:39
  • Collation aside, `LIKE` uses an index whenever the first character is not a wildcard. There is no need for the wildcard to be the last character – Mark Jun 28 '22 at 05:20
0

Since 3.21.0 (2017-10-24), LIKE optimization has been changed so that it works with an ESCAPE clause

Mark
  • 7,446
  • 5
  • 55
  • 75
-1

Make sure you wrap your queries in a transaction. I had been doing some queries via a library without it and adding it increased the speed 10 fold.

Probably the best resource on performance issues: Improve INSERT-per-second performance of SQLite?

Community
  • 1
  • 1
ElvisLives
  • 2,275
  • 2
  • 18
  • 24
  • 1
    `SQLite Expert` (tool which I'm using for testing queries) wraps each query in transaction by default. The problem is with index usage actually, I don't think transaction can help here (see update in original post). – axe Oct 24 '12 at 20:55
  • 1
    An explicit transaction will have no effect here - the issue is with a *single* DQL statement (which will implicitly run in its own transaction as needed). –  Oct 24 '12 at 21:18
  • @pst: Oh, sorry. I didn't notice that the comment is written by another person. I thought it's a kind of explanation to the answer. Now it's clear. – axe Oct 25 '12 at 04:25