【问题标题】:Why does multi-columns indexing in SQLite slow down the query's performance, unless indexing all columns?为什么 SQLite 中的多列索引会降低查询的性能,除非索引所有列?
【发布时间】:2024-01-24 05:51:01
【问题描述】:

我正在尝试通过使用索引来优化对 SQLite 数据库的简单查询的性能。例如,表有 5M 行,5 列; SELECT 语句用于获取所有列,WHERE 语句仅检查 2 列。但是,除非我在多列索引中拥有所有列,否则查询的性能会比没有任何索引时差。

我是否错误地对列进行了索引,或者在选择所有列时,我是否应该将所有列都包含在索引中以提高性能?

每个case下面#是我在硬盘中创建SQLite数据库时得到的结果。但是,由于某种原因,使用':memory:' 模式使得所有索引案例都比没有索引更快。

import sqlite3
import datetime
import pandas as pd
import numpy as np
import os
import time

# Simulate the data
size = 5000000
apps = [f'{i:010}' for i in range(size)]
dates = np.random.choice(pd.date_range('2016-01-01', '2019-01-01').to_pydatetime().tolist(), size)
prod_cd = np.random.choice([f'PROD_{i}' for i in range(30)], size)
models = np.random.choice([f'MODEL{i}' for i in range(15)], size)
categories = np.random.choice([f'GROUP{i}' for i in range(10)], size)

# create a db in memory
conn = sqlite3.connect(':memory:', detect_types=sqlite3.PARSE_DECLTYPES)
c = conn.cursor()
# Create table and insert data
c.execute("DROP TABLE IF EXISTS experiment")
c.execute("CREATE TABLE experiment (appId TEXT, dtenter TIMESTAMP, prod_cd TEXT, model TEXT, category TEXT)")
c.executemany("INSERT INTO experiment VALUES (?, ?, ?, ?, ?)", zip(apps, dates, prod_cd, models, categories))

# helper functions
def time_it(func):
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        print("time for {} function is {}".format(func.__name__, time.time() - start))
        return result
    return wrapper

@time_it
def read_db(query):
    df = pd.read_sql_query(query, conn)
    return df

@time_it
def run_query(query):
    output = c.execute(query).fetchall()
    print(output)

# The main query
query = "SELECT * FROM experiment WHERE prod_cd IN ('PROD_1', 'PROD_5', 'PROD_10') AND dtenter >= '2018-01-01'"

# CASE #1: WITHOUT ANY INDEX
run_query("EXPLAIN QUERY PLAN " + query)
df = read_db(query)
>>> time for read_db function is 2.4783718585968018

# CASE #2: WITH INDEX FOR COLUMNS IN WHERE STATEMENT
run_query("DROP INDEX IF EXISTs idx")
run_query("CREATE INDEX idx ON experiment(prod_cd, dtenter)")
run_query("EXPLAIN QUERY PLAN " + query)
df = read_db(query)
>>> time for read_db function is 3.221407890319824
# CASE #3: WITH INDEX FOR MORE THEN WHAT IN WHERE STATEMENT, BUT NOT ALL COLUMNS 
run_query("DROP INDEX IF EXISTs idx")
run_query("CREATE INDEX idx ON experiment(prod_cd, dtenter, appId, category)")
run_query("EXPLAIN QUERY PLAN " + query)
df = read_db(query)
>>>time for read_db function is 3.176532745361328

# CASE #4: WITH INDEX FOR ALL COLUMNS 
run_query("DROP INDEX IF EXISTs idx")
run_query("CREATE INDEX idx ON experiment(prod_cd, dtenter, appId, category, model)")
run_query("EXPLAIN QUERY PLAN " + query)
df = read_db(query)
>>> time for read_db function is 0.8257918357849121

【问题讨论】:

    标签: python sql sqlite indexing covering-index


    【解决方案1】:

    The SQLite Query Optimizer Overview 说:

    在对行进行索引查找时,通常的过程是对索引进行二进制搜索以找到索引条目,然后从索引中提取 rowid 并使用该 rowid 对原始表进行二进制搜索.因此,典型的索引查找涉及两次二进制搜索。

    索引条目的顺序与表条目的顺序不同,因此如果查询从表的大部分页面返回数据,所有这些随机访问查找都比仅扫描所有表行要慢。

    仅当您的 WHERE 条件过滤掉的行数多于返回的行数时,索引查找才比表扫描更有效。

    SQLite 假定对索引列的查找具有高选择性。填写表格后运行ANALYZE 可以获得更好的估算值。
    但是,如果您的所有查询都采用索引无济于事的形式,那么完全不使用索引会更好。


    当您为查询中使用的所有列创建索引时,不再需要额外的表访问:

    但是,如果要从表中获取的所有列都已在索引本身中可用,则 SQLite 将使用索引中包含的值,并且永远不会查找原始表行。这为每一行节省了一次二分搜索,并且可以使许多查询的运行速度提高一倍。

    当索引包含查询所需的所有数据并且从不需要查询原始表时,我们称该索引为“覆盖索引”。

    【讨论】:

    • 感谢您的解释!现在变得更清楚了。但是: 1. 当您提到索引查找在用于过滤掉比返回的行多得多的行时更有效时,是否有任何近似百分比?我上面的示例已经过滤掉了 >95% 的行。 2. 全表有 5M 行,所以我想使用 Log(n) 的 2 次二进制搜索仍然比全扫描 5M 行快得多。我的理解正确吗?
    • 确切的阈值取决于数据、软件和硬件。进行表扫描时,各个页面可能会连续存储在磁盘上。