Use Your Database!
我喜欢高级的、动态类型的语言,例如 Python、Ruby 和 JavaScript。它们易于使用,甚至很有趣。它们让我能够丰富地表达自己,并且它们的代码易于重用和维护。毫不奇怪,对这些语言的兴趣正在上升,尤其是在创建 Web 应用程序时。
现在,这些语言的缺点之一是,它们往往比静态语言(如 Java、C# 和 Go)执行得更慢。但是对于大量的 Web 应用程序来说,这种速度差异并不重要,或者工程师们所享受的生产力提升证明了这种差异是合理的,或者可以通过投入硬件来(在某种程度上)解决这个问题。
然而,动态语言比静态语言运行得慢这一事实并不意味着您想要完全忽略速度问题。一旦您了解了动态语言以及在其中构建的框架,您就会感觉到什么运行得快,什么运行得慢。
然而,就在过去几周,我遇到了一个模式——或者也许我应该说,一个“反模式”——在我的一些咨询客户编写的代码中。这个反模式对于经验丰富的开发人员来说是众所周知的,但它似乎不如我希望或预期的那么广为人知。简单来说,这个反模式就是您应该让数据库尽可能多地完成工作。
将尽可能多的工作交给数据库服务器有几个原因。首先,您的数据库几乎肯定是用 C 语言编写的,因此它可能比您的高级动态代码执行得更快。
其次,您的数据库多年来经过了高度优化,因此从中检索数据的过程经过调整,考虑了内存、磁盘和检索频率。
第三,虽然现在的网络带宽很便宜,但它不是无限快的。这意味着,尽管理论上您可以用 Ruby 编写一个数据库查询,该查询返回大量行,然后使用 Enumerate#map 过滤它们,但如果您让数据库为您做其中一部分工作,它可以大大减少您检索的数据量,从而加快应用程序响应速度并减少网络使用量。
因此,在本文中,我探讨了在应用程序中执行工作(可能应该在数据库中完成)的这种反模式。您将看到如何通过应用此规则获得相同的结果,但速度更快。显然,没有一种正确的方法可以做所有事情,但是让数据库尽可能多地完成工作可能会使您的应用程序更快且更易于维护。
不要加载所有内容高级语言和大多数高级 Web 框架不鼓励您直接编写 SQL。相反,您使用对象和方法来处理数据库;您调用的方法由 ORM(对象关系映射器)转换为 SQL。我认为,人们的数据库查询普遍效率低下的部分原因是他们没有看到他们正在编写的 SQL,因此他们不了解其方法调用的一些含义。
例如,假设我正在 Django 中处理一个项目。如果我有一个名为 Person 的模型,我可以(并且应该)调用“objects”方法,以便处理数据库中的相应表。然后,我可以获取结果对象并应用其他过滤器,例如获取与系统管理员对应的人员记录
>>> admins = Person.objects.filter(admin=True).all()
完成此操作后,“admins”将包含一组记录,在 Django 世界中称为“QuerySet”。但实际上,QuerySet 不包含记录本身。相反,它充当数据库之间的媒介。如果您迭代 QuerySet,您将逐个获取每个记录。
因此,即使您最终将从数据库中获取一百万条记录,上面的代码也不会检索它们。您可以通过迭代结果集来逐个获取记录。例如,以下代码将显示所有管理员的用户名
>>> for admin in admins:
print(admin.username)
这是在 Django 中使用对象的正确方法。尽管不将整个结果集放在内存中可能看起来很奇怪,但其意义是巨大的。如果结果记录太大,您无需担心耗尽服务器的所有内存。
如果您习惯使用迭代器,那么使用迭代器很容易且直接。如果您不习惯,那么不一次拥有整个结果集并对其进行迭代可能会显得很奇怪。此外,您只需要结果集和以下代码的正确组合
>>> admins = list(Person.objects.filter(admin=True).all())
请注意我在上面的赋值中更改了什么?我不再请求 QuerySet,我可以对其进行迭代。相反,我要求使用 QuerySet 的数据来创建列表,然后将其分配给“admins”。如果您的结果集中有一百万条记录,这将消耗相当大的内存。
诚然,有时这可能是必要的,但这些时候非常罕见。毕竟,您很可能正在检索记录以便将其显示给用户,这可以通过迭代轻松有效地完成。
筛选假设我对我系统上的所有管理员感兴趣。上面,我展示了您可以使用以下方法执行此操作
>>> admins = Person.objects.filter(admin=True).all()
>>> for admin in admins:
print(admin.username)
但是,我经常看到人们这样做的一种变体
>>> people = Person.objects.all()
>>> for person in people:
if person.admin:
print(person.username)
请注意我在这里做什么:我正在检索所有对象,然后迭代它们。然后,我使用 Python 中的“if”语句来确定是否要打印用户名。如果您习惯使用 Python 对象,这似乎是很自然的事情。
但是,让我们考虑一下这里实际发生了什么。您正在检索所有记录,并且仅使用其中的少量记录。这意味着数据库被迫读取其所有记录,将所有记录加载到内存中,并将这些记录发送到 Python 应用程序——即使很可能只有一小部分记录将被打印。
此外,虽然 Python 中的“if”语句绝对非常高效,但查找 person.admin
属性仍然存在一些开销,更不用说为您从数据库返回的每个记录创建一个新的“Person”对象。换句话说,您正在创建大量 Person 对象只是为了显示一些输出。
在数据库中进行筛选并仅为您最有可能想要显示的记录创建 Python 对象,效率要高得多。如果定义正确,数据库具有索引,如果您告诉它筛选记录以减少内存、CPU 和网络带宽的消耗,则可以使用这些索引来加速查询。
我看到过这种反模式的一种变体,即人们有时希望对他们从数据库中检索到的数据执行转换。例如,假设我想对一组记录中的所有价格应用 10% 的销售税。我当然可以说
>>> products = Product.objects.all()
>>> for product in products:
print(product.price * 1.10)
但如果我只是简单地说
>>> products = Product.objects.raw('select id, price * 1.10 as
>>> price_with_tax from store_product))
>>> for product in products:
print(product.price_with_tax)
请注意 raw
的使用如何允许您绕过 Django 的 ORM,使用您想要的任何 SQL。这是您一直想要做的事情吗?当然不是。但在特定情况下,或者当您想要使用函数时,它绝对可以派上用场。请注意,您从调用 raw()
返回的对象是 RawQuerySet
,它是一个迭代器,就像常规的 QuerySet
一样。但是,它缺少 all()
方法,这很好,因为 RawQuerySet
已经是一个迭代器,可以在请求时(而不是之前)提供适当的记录。
请注意,对于常用的 SQL 函数(例如 COUNT
),有内置的 Django 方法来处理这些事情。因此,如果您要计数、排序或分组,则无需降级到 SQL 级别。作为一般规则,您不想这样做。但是,有时它会派上用场——特别是当您试图减少必须在 Python 中处理的数据量时。
最后一个反模式是我在撰写本文前几天在客户办公室看到的。该公司有大量产品,并希望为每个产品执行查询。所以,他们做了这样的事情
>>> products = Product.objects.all()
>>> for product in products:
ProductInfo.objects.filter(product_id=product.id).all()
此查询运行了很长时间。为什么?因为对于数千种产品中的每一种,他们都在发出额外的 SQL 查询。有趣的是,每个单独的查询都执行得很快,因此它没有显示在我们的 PostgreSQL 慢查询日志监视器中。但是,执行此类查询的效果是巨大的,最终花费了数分钟。
解决方案是将我们的多个查询变成一个查询。在 SQL 中,我们将使用内连接。事实上,当我在原始 SQL 中使用内连接时,我们发现它执行时间为 1.5 秒,而不是几分钟——显然,节省了大量时间。
在 Django 中,对于此问题有两种可能的解决方案。第一个是使用原始 SQL 查询,正如我上面所展示的那样。这不是一个理想的解决方案,特别是考虑到 ORM 的全部理念是消除 SQL 的使用并保持在单一语言(在本例中为 Python)中。但是,有时您无法避免它。
但是,如果您想更聪明地处理它,您可以使用 Django 的 selected_related
方法。这允许您不仅检索一个模型,还检索一个相关模型——实际上,在数据库中创建连接并生成一个大型查询而不是许多小型查询。在这种情况下,您的应用程序性能的影响可能是巨大的,正如我在与客户合作时发现的那样。
对象关系映射器是很棒的东西。然而,归根结底,有时它们会欺骗您,让您忘记将数据从数据库带入您的语言是有成本的(时间和空间)。大多数现代框架都尝试通过使用延迟加载和迭代器来提供帮助,这样您就可以检索单个记录而不是整个数据集。然而,一次检索所有内容、使您的应用程序工作过于努力,甚至在数据库上调用过多查询都太容易了。
资源Django 文档位于 https://docs.django.ac.cn。查看 QuerySet 文档以了解有关此主题的更多信息。
如果您正在使用 Ruby on Rails,您应该查看 ActiveRecord 的文档,网址为 http://rubyonrails.com。特别是,请参阅 ActiveRecord 中现在标准的“延迟加载”功能。
最后,Pat Shaughnessy 就此主题撰写了一篇精彩的博客文章,探讨了 Ruby on Rails 和 PostgreSQL。即使您不使用这些特定技术来理解将数据从数据库中取出所产生的影响,也值得一读。他的文章位于 http://patshaughnessy.net/2015/6/18/dont-let-your-data-out-of-the-database。