一、把SQLite临时表和子查询全量跑在内存里,到底快在哪?

很多人用SQLite的时候,会发现跑个临时数据的计算,要么慢得像蜗牛,要么占磁盘空间,其实很多时候是没把临时数据的“主场”选对——把临时表、子查询全放内存跑,速度提升不是一星半点,核心原因要从“数据读写的本质”说透。

先搞懂一个常识:磁盘的读写速度和内存比,差了好几个数量级。比如机械硬盘的随机读写速度可能只有每秒几十MB,内存的读写速度能到每秒几十GB,差了上千倍;就算是固态硬盘,随机读写速度也只是内存的几十分之一。而SQLite的临时表、子查询,天生就是“用完就扔”的临时数据,本来就不需要存到磁盘里,要是默认存到磁盘,就相当于把快递从家门口的快递柜(内存),非要搬到几公里外的仓库(磁盘),再搬回来,纯纯的浪费时间。

1.1 具体速度差异的实际测试

为了让大家有直观感受,我们做个实际测试,测试的场景是:生成10万条随机数据,先存到磁盘临时表,再存到内存临时表,分别统计时间。 测试前先明确测试用的技术栈:Python 3.10 + SQLite 3.39.0(Python自带的SQLite版本,不用额外安装)。

# 测试用的Python代码,技术栈:Python 3.10 + SQLite 3.39.0
import sqlite3
import time

# 先测试磁盘临时表的速度
start_time = time.time()
# 连接SQLite,默认临时表存在磁盘
conn_disk = sqlite3.connect('test_disk.db')
cursor_disk = conn_disk.cursor()
# 创建临时表,默认临时表是磁盘级的
cursor_disk.execute("""
CREATE TEMPORARY TABLE temp_disk (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    num INTEGER,
    text TEXT
)
""")
# 批量插入10万条随机数据
for i in range(100000):
    cursor_disk.execute("INSERT INTO temp_disk (num, text) VALUES (?, ?)", (i, f"text_{i}"))
conn_disk.commit()
end_time = time.time()
print(f"磁盘临时表耗时:{end_time - start_time:.2f}秒")
conn_disk.close()

# 再测试内存临时表的速度
start_time = time.time()
# 连接SQLite,指定临时表存在内存
conn_mem = sqlite3.connect(':memory:')
cursor_mem = conn_mem.cursor()
# 创建临时表,因为连接是内存级的,临时表也会在内存
cursor_mem.execute("""
CREATE TEMPORARY TABLE temp_mem (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    num INTEGER,
    text TEXT
)
""")
# 批量插入10万条随机数据
for i in range(100000):
    cursor_mem.execute("INSERT INTO temp_mem (num, text) VALUES (?, ?)", (i, f"text_{i}"))
conn_mem.commit()
end_time = time.time()
print(f"内存临时表耗时:{end_time - start_time:.2f}秒")
conn_mem.close()

实际运行这段代码,你会发现磁盘临时表可能耗时5-10秒,内存临时表可能只耗时0.5-1秒,差了10倍左右。这个差异的核心,除了读写速度,还有SQLite的事务机制:SQLite默认是事务型数据库,每次写操作如果存到磁盘,会触发磁盘的同步操作(比如刷脏页、更新索引),而内存里的操作不需要这些,事务提交就是改改内存里的指针,速度极快。

1.2 子查询跑在内存里的额外优势

子查询的场景和临时表有点像,但更特殊:子查询很多时候是“嵌套计算”,比如先从A表查一组数据,再用这组数据去查B表。如果子查询的结果存到磁盘,相当于两次磁盘读写,而如果子查询跑在内存里,相当于一次内存读、一次内存写,速度差更大。

举个例子,比如我们要查“每个部门里工资最高的员工”,用子查询的写法:

# 技术栈:Python 3.10 + SQLite 3.39.0
import sqlite3
import time

# 测试子查询跑在磁盘的速度
start_time = time.time()
conn_disk = sqlite3.connect('test_sub.db')
cursor_disk = conn_disk.cursor()
# 先创建主表,存到磁盘
cursor_disk.execute("CREATE TABLE employee (id INTEGER, dept_id INTEGER, salary INTEGER)")
# 插入10万条员工数据
for i in range(100000):
    cursor_disk.execute("INSERT INTO employee VALUES (?, ?, ?)", (i, i%100, i*10))
conn_disk.commit()
# 子查询:先查每个部门的最高工资,再查对应的员工,子查询默认跑在磁盘
cursor_disk.execute("""
SELECT e.id, e.dept_id, e.salary
FROM employee e
WHERE e.salary = (
    SELECT MAX(salary) FROM employee WHERE dept_id = e.dept_id
)
""")
result = cursor_disk.fetchall()
end_time = time.time()
print(f"子查询跑在磁盘耗时:{end_time - start_time:.2f}秒")
conn_disk.close()

# 测试子查询跑在内存的速度
start_time = time.time()
conn_mem = sqlite3.connect(':memory:')
cursor_mem = conn_mem.cursor()
# 主表存到内存
cursor_mem.execute("CREATE TABLE employee (id INTEGER, dept_id INTEGER, salary INTEGER)")
for i in range(100000):
    cursor_mem.execute("INSERT INTO employee VALUES (?, ?, ?)", (i, i%100, i*10))
conn_mem.commit()
# 子查询跑在内存
cursor_mem.execute("""
SELECT e.id, e.dept_id, e.salary
FROM employee e
WHERE e.salary = (
    SELECT MAX(salary) FROM employee WHERE dept_id = e.dept_id
)
""")
result = cursor_mem.fetchall()
end_time = time.time()
print(f"子查询跑在内存耗时:{end_time - start_time:.2f}秒")
conn_mem.close()

实际运行的话,子查询跑在磁盘可能耗时2-3秒,跑在内存可能只耗时0.2-0.3秒,差了近10倍。这是因为子查询的结果是临时的,不需要持久化,存到内存里完全足够,避免了磁盘的随机读写开销。

二、temp_store参数与内存数据库模式的适用边界

很多人知道SQLite可以用:memory:来开启内存数据库,也知道有个temp_store参数可以控制临时表的存储位置,但很多人搞不清这两个的区别,也不知道什么时候该用哪个。

2.1 先搞懂两个核心配置的本质

首先说temp_store参数:这个参数是SQLite的一个全局配置,用来控制临时表、临时索引、临时排序的存储位置,它有三个可选值:

  • 0:默认值,临时表存在磁盘(和数据库文件同目录)
  • 1:临时表存在内存
  • 2:强制临时表存在磁盘

设置temp_store参数的方法很简单,在连接数据库之后,执行一条PRAGMA语句即可,比如:

# 技术栈:Python 3.10 + SQLite 3.39.0
conn = sqlite3.connect('test.db')
cursor = conn.cursor()
# 设置临时表全量存在内存
cursor.execute("PRAGMA temp_store = 1")

然后说内存数据库模式:就是连接数据库的时候,用:memory:作为数据库文件名,比如sqlite3.connect(':memory:'),这个模式下,整个数据库(包括主表、临时表、索引)全量存在内存里,连接关闭后,所有数据都会消失。

2.2 两者的适用边界,别用错了

很多人会混淆这两个配置,其实它们的适用场景完全不同,我们用表格的方式总结(为了方便大家理解,不用专业术语):

对比维度 temp_store=1(临时表放内存) 内存数据库模式(全表放内存)
主表存储位置 磁盘(原来的数据库文件) 内存(连接关闭就没了)
临时表存储位置 内存 内存
适合的场景 主表要持久化,临时数据要快 所有数据都不需要持久化,只是临时计算
数据丢失风险 低(主表还在磁盘) 高(连接关闭全没了)
内存占用情况 只占临时数据的内存 占所有数据的内存

举个实际的例子,帮大家分清楚:

  • 场景1:你有一个持久化的用户数据库,存在磁盘上,每天要跑一次“统计每个用户的月度消费”,这个统计需要用到临时表来存中间结果,这个时候就适合用temp_store=1,主表还在磁盘(不会丢),临时表在内存(速度快)。
  • 场景2:你要做一个在线的实时分析工具,用户上传一个CSV文件,你要对这个CSV做各种临时的统计、筛选、排序,不需要把CSV的内容存到磁盘,这个时候就适合用内存数据库模式,所有操作都在内存里,速度快,用完就扔。

2.3 别踩的坑:两个配置不能乱搭配

很多人会犯一个错误:既用内存数据库模式,又设置temp_store=1,其实完全没必要,因为内存数据库模式下,临时表本来就在内存里,设置temp_store=1不会有任何效果,反而会让代码变得复杂。

还有一个坑:如果你的临时数据量特别大,比如临时表要存10GB的数据,这个时候就不能用temp_store=1或者内存数据库模式,因为内存不够用,会导致程序崩溃,这个时候只能把临时表存到磁盘,或者分批次处理数据。

三、冷数据落盘以及内存失控后的风险规避与兜底设计

把数据放内存跑,速度快,但也有风险:内存不够用怎么办?临时数据里有需要持久化的冷数据怎么办?内存失控导致程序崩溃,数据丢了怎么办?这些都需要提前做兜底设计。

3.1 冷数据落盘:什么是冷数据?怎么落盘?

首先搞懂“冷数据”的定义:在临时数据中,不是“用完就扔”的,而是需要持久化保存的数据,比如你跑一个大的统计任务,中间生成的一个中间结果,这个结果可能会被后续的任务用到,或者需要保存下来供后续查询,这个中间结果就是冷数据。

冷数据落盘的核心逻辑是:实时监控临时数据的访问频率和大小,当临时数据的访问频率低于某个阈值,或者大小超过某个阈值,就把它存到磁盘里,释放内存空间。

举个实际的例子,我们用Python写一个简单的冷数据落盘的逻辑:

# 技术栈:Python 3.10 + SQLite 3.39.0
import sqlite3
import time

# 初始化内存数据库
conn_mem = sqlite3.connect(':memory:')
cursor_mem = conn_mem.cursor()
# 创建临时表,存热数据
cursor_mem.execute("CREATE TABLE temp_hot (id INTEGER, data TEXT, access_time REAL)")

# 模拟插入10万条热数据
for i in range(100000):
    cursor_mem.execute("INSERT INTO temp_hot VALUES (?, ?, ?)", (i, f"data_{i}", time.time()))
conn_mem.commit()

# 冷数据落盘逻辑:遍历临时表,把10分钟前访问的数据存到磁盘
def cold_data_flush():
    # 先创建磁盘表,用来存冷数据
    conn_disk = sqlite3.connect('cold_data.db')
    cursor_disk = conn_disk.cursor()
    cursor_disk.execute("CREATE TABLE IF NOT EXISTS cold_data (id INTEGER, data TEXT, access_time REAL)")
    # 从内存临时表查询10分钟前访问的数据
    ten_min_ago = time.time() - 600
    cursor_mem.execute("SELECT * FROM temp_hot WHERE access_time < ?", (ten_min_ago,))
    cold_data = cursor_mem.fetchall()
    if cold_data:
        # 把冷数据插入到磁盘表
        cursor_disk.executemany("INSERT INTO cold_data VALUES (?, ?, ?)", cold_data)
        conn_disk.commit()
        # 从内存临时表删除冷数据,释放内存
        cursor_mem.execute("DELETE FROM temp_hot WHERE access_time < ?", (ten_min_ago,))
        conn_mem.commit()
    conn_disk.close()

# 定时执行冷数据落盘,比如每5分钟执行一次
# 实际项目中可以用APScheduler或者Celery来定时执行
cold_data_flush()

这个逻辑很简单:每过一段时间,检查临时表中的数据,把很久没访问的冷数据存到磁盘,然后从内存里删掉,这样就能保证内存里只存热数据,不会占用太多内存。

3.2 内存失控的风险:怎么提前预判?

内存失控的核心原因是:内存里的数据量超过了系统给程序分配的内存上限,导致程序被系统杀掉(比如Linux下的OOM Killer),或者程序抛出内存不足的异常。

要提前预判内存失控,核心是实时监控内存的使用情况,比如用Python的psutil库来监控程序的内存占用,当内存占用超过某个阈值(比如系统给程序分配的内存的80%),就触发兜底逻辑。

举个例子,用psutil监控内存的代码:

# 技术栈:Python 3.10 + SQLite 3.39.0 + psutil 5.9.0
import psutil
import os

# 获取当前进程的内存占用(单位:MB)
def get_memory_usage():
    process = psutil.Process(os.getpid())
    # rss是进程实际占用的物理内存
    return process.memory_info().rss / 1024 / 1024

# 监控内存,当超过500MB时触发兜底逻辑
memory_threshold = 500  # 单位:MB
current_memory = get_memory_usage()
if current_memory > memory_threshold:
    # 触发兜底逻辑:比如冷数据落盘,或者停止临时计算
    print(f"内存占用超过阈值:{current_memory:.2f}MB,触发兜底逻辑")
    cold_data_flush()  # 调用之前的冷数据落盘函数

3.3 兜底设计:内存失控后怎么保证数据不丢?

就算提前监控,也可能出现内存失控的情况,比如突然插入了大量的临时数据,导致内存瞬间占满,这个时候就需要兜底设计,核心逻辑是:

  1. 数据的持久化备份:在内存里操作数据的同时,实时把数据同步到磁盘的临时备份表,当内存失控时,备份表还在磁盘里,不会丢数据。
  2. 断点续算:如果是跑一个大的计算任务,中间因为内存失控中断了,下次启动的时候,可以从备份表恢复数据,继续计算,不用从头开始。

举个实际的例子,我们写一个带实时备份的内存操作逻辑:

# 技术栈:Python 3.10 + SQLite 3.39.0
import sqlite3
import time

# 初始化内存数据库和磁盘备份数据库
conn_mem = sqlite3.connect(':memory:')
cursor_mem = conn_mem.cursor()
conn_backup = sqlite3.connect('backup.db')
cursor_backup = conn_backup.cursor()

# 创建内存表和对应的备份表
cursor_mem.execute("CREATE TABLE temp_calc (id INTEGER, num INTEGER, result INTEGER)")
cursor_backup.execute("CREATE TABLE IF NOT EXISTS temp_calc_backup (id INTEGER, num INTEGER, result INTEGER)")

# 模拟插入数据,同时同步到备份表
def insert_data(id, num, result):
    # 先插入到内存表
    cursor_mem.execute("INSERT INTO temp_calc VALUES (?, ?, ?)", (id, num, result))
    conn_mem.commit()
    # 实时同步到备份表
    cursor_backup.execute("INSERT INTO temp_calc_backup VALUES (?, ?, ?)", (id, num, result))
    conn_backup.commit()

# 模拟计算任务,插入10万条数据
for i in range(100000):
    insert_data(i, i, i*2)

# 模拟内存失控后的恢复:从备份表恢复数据到内存表
def recover_from_backup():
    # 先清空内存表
    cursor_mem.execute("DELETE FROM temp_calc")
    conn_mem.commit()
    # 从备份表查询数据
    cursor_backup.execute("SELECT * FROM temp_calc_backup")
    backup_data = cursor_backup.fetchall()
    if backup_data:
        # 插入到内存表
        cursor_mem.executemany("INSERT INTO temp_calc VALUES (?, ?, ?)", backup_data)
        conn_mem.commit()
        print(f"从备份表恢复了{len(backup_data)}条数据")

# 测试恢复逻辑
recover_from_backup()

这个逻辑的核心是:内存里的每一次操作,都实时同步到磁盘的备份表,就算内存里的数据丢了,也能从备份表恢复,保证数据不丢。

四、总结:怎么用对内存跑SQLite的临时数据?

最后我们总结一下,用内存跑SQLite的临时数据,要注意以下几点:

  1. 速度快的核心是:避免磁盘的读写开销,利用内存的高速读写能力,同时避免磁盘同步的事务开销。
  2. 配置选择:如果主表要持久化,用temp_store=1;如果所有数据都是临时的,用内存数据库模式;如果临时数据量太大,就分批次处理,或者存到磁盘。
  3. 风险规避:实时监控内存占用,提前做冷数据落盘;实时同步备份数据,保证内存失控后能恢复;避免临时数据量超过内存上限。

最后给大家一个最佳实践的建议:如果你的临时数据量不超过内存的30%,就用内存跑,速度快;如果超过30%,就分批次处理,或者做冷数据落盘;如果是大的计算任务,一定要做实时备份,避免数据丢失。