【问题标题】:Does SQLite optimize a query with multiple AND conditions in the WHERE clause?SQLite 是否优化 WHERE 子句中具有多个 AND 条件的查询?
【发布时间】:2018-04-16 15:54:38
【问题描述】:

在 SQL 数据库中(我使用 Python+Sqlite),如果我们有 100 万行,如何确保查询

SELECT * FROM mytable WHERE myfunction(description) < 500 AND column2 < 1000
                           [-----------------------------]   [--------------]
                               high-CPU cost condition         easy-to-test 
                              requiring 100 µs per test         condition

进行了优化,以便第一个条件(CPU 昂贵)仅在易于测试的第二个条件已经为真时才测试? (既然是合乎逻辑的AND,那是不是懒惰的AND?)

例子:

  • 如果始终测试第一个条件,则需要 100 万 x 100 µs = 100 秒!

  • 如果首先测试第二个条件,那么只会预过滤 5000 个项目(在我的用例中),然后应用第一个条件会非常快。

注意:

  • column2 不一定是 ID,也可以是别的东西

  • 在我的用例中,myfunction 涉及 Levenshtein 距离计算

【问题讨论】:

  • 我相信如果你改变“和”条件的顺序,你会达到你想要的优化。
  • 我需要看到 EXPLAIN PLAN 才能相信这一点。

标签: python sql sqlite lazy-evaluation


【解决方案1】:

强制执行顺序的一种方法是使用case 表达式。一般来说,SQL 优化器可以重新安排操作,一个例外是case

SELECT *
FROM mytable
WHERE (CASE WHEN column2 >= 1000  OR column2 IS NULL THEN 0
            WHEN myfunction(description) < 500 THEN 1
       END) = 1;

通常,case 表达式不鼓励在 WHERE 子句中使用。 . .一个主要原因是它们阻碍了优化。在这种情况下,这是一件好事。

【讨论】:

  • CASE WHEN column2 &lt; 1000 THEN 1 WHEN myfunction(description) &lt; 500 THEN 1 ENDcolumn2 &lt; 1000myfunction(description) &lt; 500 的情况下不会返回1?如果是这样,它是一个数学联合 (OR) 而不是 AND?还是我错了?
  • @Basj 是的;该函数属于第一个 THEN 子句。
  • @CL。 “属于第一个 THEN”是什么意思,为什么会这样?在我看来 myfunction 只出现在 second WHEN 情况下,如果我错了,请纠正我。提前谢谢你。
  • @Basj 。 . .你是对的。我通常使用OR 而不是AND 遇到这个问题。出于某种原因,我希望任何 SQL 优化器都能正确处理 AND 的情况,甚至 SQLite。
【解决方案2】:

(根据 cmets 和后续测试更新答案。)

你的问题的实际答案

如果我们有 100 万行,如何确保查询 ... 已优化,以便仅在易于测试的第二个条件已经为真时才测试第一个条件(CPU 昂贵)?

取决于

  • WHERE 子句中的实际条件,以及
  • SQLite 查询优化器在估算这些条件的成本方面有多聪明。

一个简单的测试应该告诉您您的查询是否会充分“优化”以满足您的需求。好消息是 SQLite 首先执行简单(便宜)的条件,至少在某些情况下是这样。

对于测试表“mytable”

CREATE TABLE mytable (
    description TEXT(50) NOT NULL,
    column2 INTEGER NOT NULL,
    CONSTRAINT mytable_PK PRIMARY KEY (column2)
);

包含一百万行

description  column2
-----------  -------
row000000          0
row000001          1
row000002          2
...
row999999     999999

Python 测试代码

import sqlite3
import time

log_file_spec = r'C:\Users\Gord\Desktop\log_file.txt'

def myfunc(thing):
    with open(log_file_spec, 'a') as log:
        log.write('HODOR\n')
    return(int(thing[-6:]))


with open(log_file_spec, 'w'):
    pass  # just empty the file
cnxn = sqlite3.connect(r'C:\__tmp\SQLite\test.sqlite')
cnxn.create_function("myfunction", 1, myfunc)
crsr = cnxn.cursor()
t0 = time.time()
sql = """\
SELECT COUNT(*) AS n FROM mytable
WHERE myfunction(description) < 500 AND column2 < 1000
"""
crsr.execute(sql)
num_rows = crsr.fetchone()[0]
print(f"{num_rows} rows found in {(time.time() - t0):.1f} seconds")

cnxn.close()

返回

500 rows found in 1.2 seconds

并计算我们看到的 log_file.txt 中的行数

C:\Users\Gord>find /C "HODOR" Desktop\log_file.txt

---------- DESKTOP\LOG_FILE.TXT: 1000

表示我们的函数只被调用了一千次,而不是一百万次。 SQLite 明确地首先应用了column2 &lt; 1000,然后对第一个条件的行子集应用了myfunction(description) &lt; 500 条件。


(原来的“即兴”答案。)

您的问题的实际答案取决于查询优化器的聪明程度。一个简单的测试应该会告诉您您的查询是否会充分“优化”以满足您的需求。

但是,如果您的测试发现您的原始方法太慢,您确实有几个选择:

选项 1:尝试“先”进行简单比较

更改顺序可能会影响查询计划,例如

... WHERE <easy_condition> AND <expensive_condition>

可能会比

更快
... WHERE <expensive_condition> AND <easy_condition> 

选项 2:尝试使用子查询强制排序

同样,取决于查询优化器的聪明程度

SELECT easy.* 
FROM 
    (SELECT * FROM mytable WHERE column2 < 1000) easy
WHERE myfunction(easy.description) < 500

可能首先应用廉价条件,然后在结果行子集上应用昂贵条件。 (但是,有评论表明 SQLite 过于复杂,不会被这种策略所欺骗。)

【讨论】:

  • SQLite 执行 subquery flattening.
  • @CL。我是新手,你提到的事实是什么意思? (这是否会使 Option1 或 Option2 或两者都不起作用?或者它会改善什么?)
  • @GordThompson 有没有办法 100% 确定,在选项 1 中,改变条件的顺序真的会改变什么? (might turn out to be faster than ...)
  • @Basj - 您可以尝试将两个变体都传递给EXPLAIN,看看结果是否不同。您还可以尝试针对测试数据运行它们,以查看执行时间是否有显着差异。
  • @Basj 我链接到的页面解释了这一点。
【解决方案3】:

SQLite 会高兴地重新排序 AND 连接的表达式,只要它愿意。因此,虽然重写查询以检查 column2 首先似乎在当前版本中有效,但不能保证。

查询优化器假设速度主要由磁盘 I/O 决定,因此它估计两个条件的成本是相同的。 成本估算受索引和ANALYZE 统计数据(仅适用于索引数据)的影响。 因此,加快此查询(可能还有您将使用的大多数其他查询)的最简单方法是在 column2 上创建索引:

CREATE INDEX my_little_index ON mytable(column2);

如果您出于某种原因不想使用索引,则必须使用查询优化器无法优化的构造。 Gordon 的答案中显示的 CASE 表达式可以正常工作。在一般情况下,将第一个条件移动到子查询中并通过破坏列出的规则之一来防止subquery flattening;向两个查询添加虚拟 LIMIT 子句通常是最简单的:

SELECT *
FROM (SELECT *
      FROM mytable
      WHERE column2 < 1000
      LIMIT -1)
WHERE myfunction(description) < 500
LIMIT -1;

【讨论】:

  • 感谢您的回答。只是为了确保理解:1)如何在后续查询SELECT * FROM mytable WHERE myfunction(description) &lt; 500 AND column2 &lt; 1000中使用CREATE INDEX my_little_index ON mytable(column2);来确保索引确实使用来优化查询? 2) 是否需要column2 具有唯一值(不重复)才能创建索引?
  • @Basj - 不,column2 上的索引不一定需要唯一才能让查询优化器利用它。但是,根据索引统计信息,它可能决定非唯一索引与表扫描相比没有显着优势。但是,在绝大多数情况下,索引会有所帮助。
【解决方案4】:

受@GordThompson 的回答启发,这里有一个基准:

(1)  SELECT * FROM mytable WHERE col2 < 1000 AND myfunction(col1) < 500

对比

(2)  SELECT * FROM mytable WHERE myfunction(col1) < 500 AND col2 < 1000

测试(1)(易测条件优先):1.02秒

import sqlite3, time, random

def myfunc(x):
    time.sleep(0.001) # wait 1 millisecond for each call of this function
    return x

# Create database
db = sqlite3.connect(':memory:')
db.create_function("myfunction", 1, myfunc)
c = db.cursor()
c.execute('CREATE TABLE mytable (col1 INTEGER, col2 INTEGER)');
for i in range(10*1000):
    a = random.randint(0,1000)
    c.execute('INSERT INTO mytable VALUES (?, ?)', (a, i));

# Do the evil query
t0 = time.time()
c.execute('SELECT * FROM mytable WHERE col2 < 1000 AND myfunction(col1) < 500')
for e in c.fetchall():
    print e
print "Elapsed time: %.2f" % (time.time() - t0)

结果:1​​.02 秒,这意味着 myfunc 已被调用最多 1000 次,即并非针对所有 10k 行


测试(2)(首先计算缓慢条件):10.05 秒

同上:

c.execute('SELECT * FROM mytable WHERE myfunction(col1) < 500 AND col2 < 1000')

改为。

结果:1​​0.05 秒,这意味着 myfunc 已被调用了 ~ 10k 次,即对于所有 10k 行,即使是那些条件 col2 &lt; 1000 不为 True 的行。 p>


全局结论:Sqlite 对AND 进行惰性求值,即简单条件必须先这样写:

... WHERE <easy_condition> AND <expensive_condition>

【讨论】:

  • 请注意,您的测试表在 col2 上没有索引,因此两个 WHERE 条件都需要表扫描。这就解释了为什么即使在 WHERE 子句中首先列出了昂贵的条件,我的测试也很快。 (由于没有可用的索引,SQLite 查询优化器显然认为这两个条件同样昂贵,因此它不会费心摆弄条件评估的顺序。)
猜你喜欢
  • 1970-01-01
  • 2016-11-04
  • 2020-01-06
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-05-26
  • 1970-01-01
相关资源
最近更新 更多