内存高效(恒定)和速度优化迭代在Django的一个大表上

我有一个非常大的桌子。 它目前在MySQL数据库中。 我用django。

我需要遍历表的每个元素来预先计算一些特定的数据(也许如果我是更好的我可以做,但这不是重点)。

我想尽可能快地使用内存来保持迭代。

因为它已经清楚地限制了内存在大型Django QuerySet中的使用,并且为什么要通过一个耗费大量内存的大型Django QuerySet进行迭代? ,对django中所有对象的简单迭代将会终止该机器,因为它将从数据库中检索所有对象。

迈向一个解决scheme

首先,为了减less你的内存消耗,你应该确保DEBUG是False(或者猴子补丁游标: closuresSQL日志logging,同时保持settings.DEBUG? ),以确保django没有存储的东西connectionsdebugging。

但即使如此,

 for model in Model.objects.all() 

是不行的。

甚至没有稍微改进的forms:

 for model in Model.objects.all().iterator() 

使用iterator()将通过不在内部存储caching结果(尽pipe不一定在PostgreSQL上iterator()来节省一些内存。 但显然还是会从数据库中检索整个对象。

一个天真的解决scheme

第一个问题的解决scheme是通过chunk_size基于计数器来分割结果。 有几种写法,但基本上都是SQL中的OFFSET + LIMIT查询。

就像是:

 qs = Model.objects.all() counter = 0 count = qs.count() while counter < count: for model in qs[counter:counter+count].iterator() yield model counter += chunk_size 

虽然这是内存有效率(恒定的内存使用量与chunk_size成正比),但是在速度方面确实很差:随着OFFSET的增长,MySQL和PostgreSQL(也可能是大多数数据库)都将开始窒息并放慢速度。

更好的解决scheme

Thierry Schellenbach在这篇文章中提供了一个更好的解决scheme。 它过滤了PK,这比偏移快(可能取决于数据库的速度有多快)

 pk = 0 last_pk = qs.order_by('-pk')[0].pk queryset = qs.order_by('pk') while pk < last_pk: for row in qs.filter(pk__gt=pk)[:chunksize]: pk = row.pk yield row gc.collect() 

这开始令人满意。 现在内存= O(C),速度= = O(N)

“更好”解决scheme的问题

更好的解决scheme只有在QuerySet中有可用的PK时才有效。 不幸的是,情况并非总是如此,特别是当QuerySet包含distinct(group_by)和/或values(ValueQuerySet)的组合时。

对于这种情况,“更好的解决scheme”不能使用。

我们可以做得更好吗?

现在我想知道如果我们可以更快地回避没有PK的QuerySets的问题。 也许使用我在其他答案中发现的东西,但只使用纯SQL:使用游标

由于我对原始SQL非常不满,特别是在Django中,这里真正的问题是:

我们如何为大型表build立一个更好的Django QuerySet迭代器

从我读到的是,我们应该使用服务器端游标(显然(请参阅参考资料)使用标准的Django游标不会达到相同的结果,因为默认情况下,python-MySQL和psycopg连接器caching结果)。

这真的是一个更快(和/或更有效)的解决scheme吗?

这可以使用django中的原始SQL吗? 还是应该根据数据库连接器编写特定的Python代码?

PostgreSQL和MySQL中的服务器端游标

就我所能得到的那一刻

一个Django chunked_iterator()

现在,当然最好是将这个方法作为queryset.iterator() ,而不是iterate(queryset) ,并成为django核心的一部分,或者至less是一个可插入的应用程序。

更新感谢评论中的“T”,以查找带有一些额外信息的Django票证 。 连接器行为的差异使得它可能是最好的解决scheme是创build一个特定的chunked方法,而不是透明地扩展iterator (听起来像是一个很好的方法)。 存在一个实现存根(stub),但是一年中没有任何工作,而且看起来作者还没准备好跳转。

其他参考:

  1. 为什么MYSQL的LIMIT偏移量越来越慢?
  2. 我怎样才能加快在LIMIT子句中有大偏移量的MySQL查询?
  3. http://explainextended.com/2009/10/23/mysql-order-by-limit-performance-late-row-lookups/
  4. postgresql:offset + limit变得非常慢
  5. 提高PostgreSQL中的OFFSET性能
  6. http://www.depesz.com/2011/05/20/pagination-with-fixed-order/
  7. 如何在MySQL中的python服务器端游标中获取逐行 MySQL结果集

编辑:

Django 1.6正在添加持久的数据库连接

Django数据库持久连接

在某些情况下,这应该有助于使用游标。 仍然超出了我目前的技能(以及学习时间),如何实施这样的解决scheme。

此外,“更好的解决scheme”绝对不能在所有情况下工作,不能用作通用方法,只能根据具体情况进行调整。

基本的答案: 使用原始的SQL与服务器端游标

可悲的是,直到Django 1.5.2没有正式的方式来创build一个服务器端的MySQL游标(不知道其他数据库引擎)。 所以我写了一些魔法代码来解决这个问题。

对于Django 1.5.2和MySQLdb 1.2.4,下面的代码将工作。 此外,它很好评论。

警告:这不是基于公共的API,所以它可能会在未来的Django版本中打破。

 # This script should be tested under a Django shell, eg, ./manage.py shell from types import MethodType import MySQLdb.cursors import MySQLdb.connections from django.db import connection from django.db.backends.util import CursorDebugWrapper def close_sscursor(self): """An instance method which replace close() method of the old cursor. Closing the server-side cursor with the original close() method will be quite slow and memory-intensive if the large result set was not exhausted, because fetchall() will be called internally to get the remaining records. Notice that the close() method is also called when the cursor is garbage collected. This method is more efficient on closing the cursor, but if the result set is not fully iterated, the next cursor created from the same connection won't work properly. You can avoid this by either (1) close the connection before creating a new cursor, (2) iterate the result set before closing the server-side cursor. """ if isinstance(self, CursorDebugWrapper): self.cursor.cursor.connection = None else: # This is for CursorWrapper object self.cursor.connection = None def get_sscursor(connection, cursorclass=MySQLdb.cursors.SSCursor): """Get a server-side MySQL cursor.""" if connection.settings_dict['ENGINE'] != 'django.db.backends.mysql': raise NotImplementedError('Only MySQL engine is supported') cursor = connection.cursor() if isinstance(cursor, CursorDebugWrapper): # Get the real MySQLdb.connections.Connection object conn = cursor.cursor.cursor.connection # Replace the internal client-side cursor with a sever-side cursor cursor.cursor.cursor = conn.cursor(cursorclass=cursorclass) else: # This is for CursorWrapper object conn = cursor.cursor.connection cursor.cursor = conn.cursor(cursorclass=cursorclass) # Replace the old close() method cursor.close = MethodType(close_sscursor, cursor) return cursor # Get the server-side cursor cursor = get_sscursor(connection) # Run a query with a large result set. Notice that the memory consumption is low. cursor.execute('SELECT * FROM million_record_table') # Fetch a single row, fetchmany() rows or iterate it via "for row in cursor:" cursor.fetchone() # You can interrupt the iteration at any time. This calls the new close() method, # so no warning is shown. cursor.close() # Connection must be close to let new cursors work properly. see comments of # close_sscursor(). connection.close() 

如果你想要做的只是遍历表中的所有内容,那么下面的代码在资源方面非常高效,而且比基本迭代器快得多。 注意,由于偏移操作的线性时间,通过主键进行分页对于高效实现是必需的。

 def table_iterator(model, page_size=10000): try: max = model.objects.all().order_by("-pk")[0].pk except IndexError: return pages = int(max / page_size) + 1 for page_num in range(pages): lower = page_num * page_size page = model.objects.filter(pk__gte=lower, pk__lt=lower+page_size) for obj in page: yield obj 

使用看起来像:

 for obj in table_iterator(Model): # do stuff 

还有另一个选项可用。 它不会使迭代速度更快 ,(实际上它可能会减慢速度),但它会使它使用更less的内存。 根据您的需要,这可能是适当的。

 large_qs = MyModel.objects.all().values_list("id", flat=True) for model_id in large_qs: model_object = MyModel.objects.get(id=model_id) # do whatever you need to do with the model here 

只有ID被加载到内存中,并且根据需要检索和丢弃对象。 请注意增加的数据库负载和较慢的运行时间,这两者都是为了减less内存使用量。

我在工作者实例上运行asynchronous调度任务时使用了它,对于这些实例,如果它们速度慢,它们并不重要,但如果它们尝试使用太多的内存,它们可能会使实例崩溃,因此中止该过程。