我需要多个游标对象同时循环记录集并更新吗?

14 投票
3 回答
9133 浏览
提问于 2025-04-15 14:31

我有一个很大的数据库,无法一次性全部加载到内存中。我需要逐个处理表中的每一项,然后把处理后的数据放到表的另一列中。

在我遍历数据时,如果我尝试运行一个更新语句,记录集就会被截断(我认为是因为光标对象被重新使用了)。

问题:

如果我创建一个第二个光标对象来运行更新语句,是否可以继续遍历原来的选择语句?

我需要一个第二个数据库连接来拥有第二个光标对象吗,这样我才能做到这一点?

如果有两个连接,一个从表中读取,另一个写入,sqlite会有什么反应?

我的代码(简化版):

import sqlite3

class DataManager():
    """ Manages database (used below). 
        I cut this class way down to avoid confusion in the question.
    """
    def __init__(self, db_path):
        self.connection = sqlite3.connect(db_path)
        self.connection.text_factory = str
        self.cursor = self.connection.cursor()

    def genRecordset(self, str_sql, subs=tuple()):
        """ Generate records as tuples, for str_sql.
        """
        self.cursor.execute(str_sql, subs)
        for row in self.cursor:
            yield row

select = """
            SELECT id, unprocessed_content 
            FROM data_table 
            WHERE processed_content IS NULL
         """

update = """
            UPDATE data_table
            SET processed_content = ?
            WHERE id = ?
         """
data_manager = DataManager(r'C:\myDatabase.db')
subs = []
for row in data_manager.genRecordset(str_sql):
    id, unprocessed_content = row
    processed_content = processContent(unprocessed_content)
    subs.append((processed_content, id))

    #every n records update the database (whenever I run out of memory)
    if len(subs) >= 1000:
        data_manager.cursor.executemany(update, subs)
        data_manager.connection.commit()
        subs = []
#update remaining records
if subs:
    data_manager.cursor.executemany(update, subs)
    data_manager.connection.commit()

我尝试的另一种方法是修改我的选择语句为:

select = """
            SELECT id, unprocessed_content 
            FROM data_table 
            WHERE processed_content IS NULL
            LIMIT 1000
         """

然后我会这样做:

recordset = data_manager.cursor.execute(select)
while recordset:
    #do update stuff...
    recordset = data_manager.cursor.execute(select)

我遇到的问题是,我的真实选择语句中有一个JOIN操作,并且执行起来比较慢,所以多次执行JOIN会非常耗时。我想通过只执行一次选择来加快这个过程,然后使用生成器,这样就不需要把所有数据都放在内存里。

解决方案:

好的,我前两个问题的答案是“否”。对于我的第三个问题,一旦连接到数据库,就会锁定整个数据库,因此另一个连接在第一个连接关闭之前无法执行任何操作。

我找不到相关的源代码,但根据我的经验,我相信一个连接一次只能使用一个光标对象,最后运行的查询会优先执行。这意味着,当我在遍历选定的记录集时,每次只返回一行数据,一旦我运行第一个更新语句,我的生成器就会停止返回数据。

我的解决方案是创建一个临时数据库,把处理后的内容和ID放进去,这样我就可以为每个数据库保持一个连接/光标对象,并继续遍历选定的记录集,同时定期插入到临时数据库中。一旦我遍历完选定的记录集,就把临时数据库中的数据转移回原来的数据库。

如果有人对连接/光标对象有确切的了解,请在评论中告诉我。

3 个回答

1

游标在很多方面都不好。

我建议你使用一个单独的UPDATE语句,而不是使用游标的方式,这样做会更好,很多人也会同意这个观点。

你的Processed_Content能不能作为参数传递给一个单一的查询,这样可以进行基于集合的操作,比如:

UPDATE data_table
SET processed_content = ?
WHERE processed_content IS NULL
LIMIT 1000

根据反馈进行了修改:

因为每一行的Processed_Content都有一个独特的值,所以你只能使用记录集和循环。我以前也多次这样做过。你提到的方法应该能有效地工作。

2

有没有办法创建一个数据库函数来处理你的内容?如果可以的话,你应该只需要写一条更新语句,就能让数据库自己完成所有的工作。比如:

Update data_table
set processed_col = Process_Column(col_to_be_processed)
3

我觉得你的架构大致是对的。不过,用“游标”来描述会让一些老手感到困惑,因为他们会想到很多和 DECLARE foo CURSORFETCH FROM CURSORWHERE CURRENT OF CURSOR 这些相关的问题,这些都是和SQL游标有关的复杂内容。而在Python的数据库接口中,“游标”其实只是一个方便的工具,用来打包和执行SQL语句,并不一定和SQL游标有关系——它不会遇到那些问题,虽然它可能会有自己独特的问题;-) 不过,考虑到你现在的结果批处理和适当的提交等操作,你已经在一定程度上避免了我之前提到的那些“原始问题”。

在其他一些数据库引擎上,我会建议先把数据选择到一个临时表中,然后在更新主表的同时从这个临时表中读取数据。但我不确定在sqlite中这样做会对性能产生什么影响,这取决于你有哪些索引(如果你的更新没有影响到任何索引,那么我怀疑在sqlite中使用临时表根本不会优化性能——但我无法在你的数据上进行基准测试,这是验证性能假设的唯一真正方法)。

所以,我觉得可以试试看!-)

撰写回答