与原始 sql 相比,Django ORM 性能较差

Posted

技术标签:

【中文标题】与原始 sql 相比,Django ORM 性能较差【英文标题】:Django ORM poor performance compared to raw sql 【发布时间】:2017-09-26 00:22:15 【问题描述】:

我正在使用 Django ORM 进行数据查询,我在这个表中得到了近 200 万行。我试过了

app_count = App.objects.count()

from django.db import connection
cursor = connection.cursor()
cursor.execute('''SELECT count(*) FROM app''')

mysql slow_query 日志给我的

时间:2017-04-27T09:18:38.809498Z

用户@主机:www[www]@[172.19.0.3] ID:5

Query_time: 4.107433 Lock_time: 0.004405 Rows_sent: 1 Rows_examined: 0

使用 app_platform;设置时间戳=1493284718;选择计数(*)从 应用程序;

这个查询平均耗时超过 4 秒,但是当我使用 mysql 客户端和 mysql shell 进行这个查询时

mysql> select count(*) from app;

+----------+
| count(*) |
+----------+
|  1870019 |
+----------+

1 row in set (0.41 sec)

只需 0.4 秒,10 倍的差异,为什么以及如何改进它。

编辑

这是我的模型

class AppMain(models.Model):
    """
    """
    store = models.ForeignKey("AppStore", related_name="main_store")
    name = models.CharField(max_length=256)
    version = models.CharField(max_length=256, blank=True)
    developer = models.CharField(db_index=True, max_length=256, blank=True)
    md5 = models.CharField(max_length=256, blank=True)
    type = models.CharField(max_length=256, blank=True)
    size = models.IntegerField(blank=True)
    download = models.CharField(max_length=1024, blank=True)
    download_md5 = models.CharField(max_length=256, blank=True)
    download_times = models.BigIntegerField(blank=True)
    snapshot = models.CharField(max_length=2048, blank=True)
    description = models.CharField(max_length=5000, blank=True)
    app_update_time = models.DateTimeField(blank=True)
    create_time = models.DateTimeField(db_index=True, auto_now_add=True)
    update_time = models.DateTimeField(auto_now=True)

    class Meta:
        unique_together = ("store", "name", "version")

编辑 2

我正在为我的项目使用 Docker 和 docker-compose

version: '2'
services:
  mysqldb:
    restart: always
    image: mysql:latest
    ports:
      - "3306:3306"
    environment:
      MYSQL_ROOT_PASSWORD: just_for_test
      MYSQL_USER: www
      MYSQL_PASSWORD: www
      MYSQL_DATABASE: app_platform
    volumes:
      - mysqldata:/var/lib/mysql
      - ./config/:/etc/mysql/conf.d
      - ./log/mysql/:/var/log/mysql/
  web:
    restart: always
    build: ./app_platform/app_platform
    env_file: .env
    environment:
      PYTHONPATH: '/usr/src/app/app_platform'
    command: bash -c "gunicorn --chdir /usr/src/app/app_platform app_platform.wsgi:application  -k gevent  -w 6 -b :8000 --timeout 8000 --reload"
    volumes:
      - ./app_platform:/usr/src/app
      - ./sqldata:/usr/src/sqldata
      - /usr/src/app/static
    ports:
      - "8000"
    dns:
        - 114.114.114.114
        - 8.8.8.8
    links:
      - mysqldb
  nginx:
    restart: always
    build: ./nginx/
    ports:
      - "80:80"
    volumes:
      - ./app_platform:/usr/src/app
      - ./nginx/sites-enabled/:/etc/nginx/sites-enabled
    links:
      - web:web
volumes:
    mysqldata:

我的 django 设置如下所示:

import os
from django.utils.translation import ugettext_lazy as _

LANGUAGES = (
    ('en', _('English')),
    ('zh-CN', _('Chinese')),
)


LANGUAGE_CODE = 'zh-CN'

BASE_DIR = os.path.dirname(
    os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

LOCALE_PATHS = (
    os.path.join(BASE_DIR, "locale"),
)

# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'just_for_test'

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'rest_framework',
    'app_scrapy',
    'app_user',
    'app_api',
    'app_check',
    'common',
    'debug_toolbar',
]


MIDDLEWARE_CLASSES = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'debug_toolbar.middleware.DebugToolbarMiddleware',
    'django.middleware.locale.LocaleMiddleware',
    'django.middleware.common.CommonMiddleware',
    'django.middleware.csrf.CsrfViewMiddleware',
    'django.contrib.auth.middleware.AuthenticationMiddleware',
    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
    'django.contrib.messages.middleware.MessageMiddleware',
    'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

AUTH_USER_MODEL = 'app_user.MyUser'

AUTHENTICATION_BACKENDS = (
    'app_user.models.CustomAuth', 'django.contrib.auth.backends.ModelBackend')


ROOT_URLCONF = 'app_platform.urls'


TEMPLATES = [
    
        'BACKEND': 'django.template.backends.django.DjangoTemplates',
        'DIRS': ["/usr/src/app/app_platform/templates"],
        'APP_DIRS': True,
        'OPTIONS': 
            'context_processors': [
                'django.template.context_processors.debug',
                'django.template.context_processors.request',
                'django.template.context_processors.i18n',
                'django.contrib.auth.context_processors.auth',
                'django.contrib.messages.context_processors.messages',
            ],
        ,
    ,
]

WSGI_APPLICATION = 'app_platform.wsgi.application'

LOGIN_REDIRECT_URL = '/'
LOGIN_URL = '/login/'
# Database
# https://docs.djangoproject.com/en/1.9/ref/settings/#databases
# Password validation
# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators

AUTH_PASSWORD_VALIDATORS = [
    
        'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
    ,
    
        'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
    ,
    
        'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
    ,
    
        'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
    ,
]

STATICFILES_FINDERS = (
    'django.contrib.staticfiles.finders.FileSystemFinder',
    'django.contrib.staticfiles.finders.AppDirectoriesFinder'
)

# Internationalization
# https://docs.djangoproject.com/en/1.9/topics/i18n/

TIME_ZONE = 'Asia/Shanghai'

USE_I18N = True

USE_L10N = True

USE_TZ = True


# Static files (CSS, javascript, Images)
# https://docs.djangoproject.com/en/1.9/howto/static-files/

STATIC_ROOT = "/static/"

STATIC_URL = '/static/'

STATICFILES_DIRS = (
    'public/static/',
)


DEBUG = True

ALLOWED_HOSTS = []

REST_FRAMEWORK = 
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.BasicAuthentication',
        'rest_framework.authentication.SessionAuthentication',
    ),
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.AllowAny',
    ),
    'DEFAULT_PAGINATION_CLASS':
        'rest_framework.pagination.LimitOffsetPagination',
        'PAGE_SIZE': 5,


DATABASES = 
    'default': 
        'ENGINE': 'django.db.backends.mysql',
        'NAME': 'app_platform',
        'USER': 'www',
        'PASSWORD': 'www',
        'HOST': 'mysqldb',   # Or an IP Address that your DB is hosted on
        'PORT': '3306',
    


DEBUG_TOOLBAR_CONFIG = 
    "SHOW_TOOLBAR_CALLBACK": lambda request: True,

我的应用表信息

CREATE TABLE `app` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(256) NOT NULL,
  `version` varchar(256) NOT NULL,
  `developer` varchar(256) NOT NULL,
  `md5` varchar(256) NOT NULL,
  `type` varchar(256) NOT NULL,
  `size` int(11) NOT NULL,
  `download` varchar(1024) NOT NULL,
  `download_md5` varchar(256) NOT NULL,
  `download_times` bigint(20) NOT NULL,
  `snapshot` varchar(2048) NOT NULL,
  `description` varchar(5000) NOT NULL,
  `app_update_time` datetime(6) NOT NULL,
  `create_time` datetime(6) NOT NULL,
  `update_time` datetime(6) NOT NULL,
  `store_id` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `app_store_id_6822fab1_uniq` (`store_id`,`name`,`version`),
  KEY `app_7473547c` (`store_id`),
  KEY `app_developer_b74bcd8e_uniq` (`developer`),
  KEY `app_create_time_a071d977_uniq` (`create_time`),
  CONSTRAINT `app_store_id_aef091c6_fk_app_scrapy_appstore_id` FOREIGN KEY (`store_id`) REFERENCES `app_scrapy_appstore` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1870020 DEFAULT CHARSET=utf8;

编辑 3

这里是 EXPLAIN SELECT COUNT(*) FROM app;

mysql> EXPLAIN SELECT COUNT(*) FROM `app`;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra                        |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------------------+
|  1 | SIMPLE      | NULL  | NULL       | NULL | NULL          | NULL | NULL    | NULL | NULL |     NULL | Select tables optimized away |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------------------+
1 row in set, 1 warning (0.00 sec)

编辑 4

这是我的mysql.cnf

innodb_read_io_threads=12
innodb_write_io_threads=12
innodb_io_capacity=300
innodb_read_io_threads=12
innodb_write_io_threads=12  #To stress the double write buffer
innodb_buffer_pool_size=3G
innodb_log_file_size = 32M #Small log files, more page flush
innodb_log_buffer_size=8M
innodb_flush_method=O_DIRECT

我的 docker 设置是 2 个 CPU 和 4GB 内存

编辑 5

当我在 django shell 中运行 ORM 查询时,只花了我 0.5-1 秒。所以问题出在docker设置上?或者可能是 gunicorn 设置?

【问题讨论】:

我不确定,但我无法重现您的问题:缓慢的查询时间是一样的。所以我认为差异可能不是由 Django 引起的,您能否确认您只有一个 MySQL 实例,并且您通过 shell 和控制台运行同一个数据库服务器? 我正在使用 Docker 和 docker-compose,我确定只有一个 MySQL 实例盲 3306 端口。 也许第二个查询更快,因为缓存? unique_together 是您的Meta 中的所有内容吗?您是否在没有调试工具栏的情况下对其进行了测试?因为实际上objects.count() 应该具有与COUNT(*) 相同的性能。 @DhiaTN 是的,unique_together 是我所拥有的,我没有使用 django 调试工具栏来测试它。我应该吗? 【参考方案1】:

10X——我喜欢。这完全符合我的经验法则:“如果数据没有被缓存,查询的时间是缓存的 10 倍。” (Rick's RoTs)

但是,让我们继续讨论真正的问题:“4.1s 太慢了,我该怎么办。”

更改您的应用程序,使您不需要行数。您是否注意到搜索引擎不再说“12345678 次点击”?

保持估计,而不是重新计算。

让我们看看EXPLAIN SELECT COUNT(*) FROM app;它可能会提供更多线索。 (一个地方你说app,另一个地方你说app_scrapy_appmain;它们是一样的吗??)

只要您从未DELETE 任何行,这将给您相同的答案:SELECT MAX(id) FROM app,并“立即”运行。 (一旦发生DELETEROLLBACK 等),id(s) 将丢失,因此COUNT 将小于MAX。)

更多

innodb_buffer_pool_size=3G 在仅 4GB 的 RAM 上可能太多了。如果 MySQL 交换,性能会变得非常糟糕。建议只用 2G,除非你可以看到它没有交换。

注意:在该硬件或任何硬件上扫描 180 万行注定要花费至少 0.4 秒。完成任务需要时间。此外,执行“长”查询会以两种方式干扰其他任务:执行查询时会消耗 CPU 和/或 I/O,另外它可能会将其他块从缓存中撞出,从而导致它们变慢。所以,我真的认为“正确”的做法是注意我避免COUNT(*) 的提示。这是另一个:

建立和维护一张这张(和其他)表每日小计的“汇总表”。在其中包括每日COUNT(*) 以及您可能想要的任何其他内容。这甚至会通过使用此表中的SUM(subtotal) 来缩短 0.4 秒的时间。 More on Summary Tables

【讨论】:

第一点,你的意思是把表分成两个表?我不确定我是否理解第二点,但我添加了解释( app_scrapy_appmain 是应用程序,抱歉这个错误)。所以你认为问题是因为缓存?我跟着https://***.com/questions/4561292/how-to-clear-query-cache-in-mysqlhttps://***.com/questions/5231678/clear-mysql-query-cache-without-restarting-server 清除了缓存。然后用mysql shell做查询,还是给我0.5s的答案。 不是Query cache,这需要 0.001 秒。 innodb_buffer_pool。它是逐块 cache 的,因此时间可能会在 0.4 秒到 4.1 秒之间变化,具体取决于该 其他表上的其他活动。你有多少内存?你有多少数据? innodb_buffer_pool_size 的值是多少? 我添加了上面的mysql.cnf,以及docker设置。我在这个表中有大约 200 万行,我必须显示其中的一些吗? 如果是缓存的原因,为什么我用mysql shell查询后还要在ORM中花费4秒以上。 连续运行两次ORM COUNT;两次都是 4 秒吗?

以上是关于与原始 sql 相比,Django ORM 性能较差的主要内容,如果未能解决你的问题,请参考以下文章

[Django] 查看orm自己主动运行的原始查询sql

与现代 ORM 相比,MS SQL 上的 SELECT * 的性能/代码可维护性问题在今天仍然相关吗?

Python ORM框架之SQLALchemy

Django - 原始 SQL 查询或 Django QuerySet ORM

原始 SQL 与 ORM 如果我已经知道 SQL

使用原始 SQL 查询生成表,并希望在 Django 中将这些表用作 ORM