bigywater.tech
Django · 99

数据库查询次数测试

怎么让「不许出现 N+1」变成一条自动化测试?assertNumQueries 失败时能看到什么?这篇写 6 个真实测试并运行,把通过与失败的输出都贴出来。

6 != 2
assertNumQueries 失败信息:6 queries executed, 2 expected,并列出每条 SQL
SAVEPOINT
TestCase 里 atomic 变成 SAVEPOINT,TransactionTestCase 里是 BEGIN / COMMIT
上界
CaptureQueriesContext + assertLessEqual 适合只想限制上限的场景

运行结果

实测 · 本机执行输出
   test_atomic_counts_savepoints (lab.tests.QueryCountTests.test_atomic_counts_savepoints) ... ok
   test_capture_upper_bound (lab.tests.QueryCountTests.test_capture_upper_bound) ... ok
   test_fail_n_plus_one (lab.tests.QueryCountTests.test_fail_n_plus_one) ... FAIL
   test_fixed_with_select_related (lab.tests.QueryCountTests.test_fixed_with_select_related) ... ok
   test_pass_exact (lab.tests.QueryCountTests.test_pass_exact) ... ok
   test_atomic_counts_begin_commit (lab.tests.TxTests.test_atomic_counts_begin_commit) ... ok
   Ran 6 tests in 0.004s
   FAILED (failures=1)

测试文件里五个通过、一个故意写错(assertNumQueries(2) 而实际是 6 次)。

失败信息

实测 · 本机执行输出
  FAIL: test_fail_n_plus_one (lab.tests.QueryCountTests.test_fail_n_plus_one)
  ----------------------------------------------------------------------
  Traceback (most recent call last):
    File "<实验目录>", line 12, in test_fail_n_pl
      with self.assertNumQueries(2): [p.author.name for p in Post.objects.all()]
           ~~~~~~~~~~~~~~~~~~~~~^^^
  AssertionError: 6 != 2 : 6 queries executed, 2 expected
  Captured queries were:
  1. SELECT "lab_post"."id", "lab_post"."title", "lab_post"."author_id" FROM "lab_post"
  2. SELECT "lab_author"."id", "lab_author"."name" FROM "lab_author" WHERE "lab_author"."id" = 1 LIMIT 21
  3. SELECT "lab_author"."id", "lab_author"."name" FROM "lab_author" WHERE "lab_author"."id" = 1 LIMIT 21
  4. SELECT "lab_author"."id", "lab_author"."name" FROM "lab_author" WHERE "lab_author"."id" = 1 LIMIT 21
  5. SELECT "lab_author"."id", "lab_author"."name" FROM "lab_author" WHERE "lab_author"."id" = 1 LIMIT 21
  6. SELECT "lab_author"."id", "lab_author"."name" FROM "lab_author" WHERE "lab_author"."id" = 1 LIMIT 21
  
  ----------------------------------------------------------------------

这是 assertNumQueries 最有价值的一点:失败信息就是 N+1 的证据。

TestCase 与 TransactionTestCase

实测 · 本机执行输出
  TestCase 包了一层事务,所以 atomic 变成 SAVEPOINT / RELEASE SAVEPOINT(3 条:SAVEPOINT + INSERT + RELEASE);TransactionTestCase 没有外层事务,atomic 是 BEGIN / COMMIT(同样 3 条)。数字相同,但里面的内容不同。
  TestCase        : ['SAVEPOINT "s8399224704_x2"', 'INSERT INTO "lab_author" ("name") ', 'RELEASE SAVEPOINT "s8399224704_x2"']
  TransactionTest : ['BEGIN', 'INSERT INTO "lab_author" ("name") ', 'COMMIT']

因为 TestCase 把每个测试包在事务里,涉及 on_commit(第 45 篇)和 select_for_update(第 46 篇)的测试要注意选对基类。

分步演示:测试里的查询数

查询次数测试
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 / Python 3.14.7 / Django 6.1.1(默认用 SQLite,其它数据库会在文中注明),用 django.test.Client 或真实的本机服务器进程实测;计时用 time.perf_counter,数字每次运行会有小幅抖动。SQLite;测试由 manage.py test 真实运行,输出来自它
    • 没有验证:pytest-django 的 django_assert_num_queries、并行测试(--parallel)下的查询计数、异步测试中的计数、测试数据库为 PostgreSQL 时的差异
    京ICP备2026067502号-1京公网安备11010502063487号