3

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

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

在每种情况下 # 是我在硬盘中创建 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
4

1 回答 1

2

SQLite 查询优化器概述说:

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

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

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

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


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

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

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

于 2019-08-05T06:30:02.173 回答