Python MySQLdb执行缓慢

Posted

技术标签:

【中文标题】Python MySQLdb执行缓慢【英文标题】:Python MySQLdb execute slow 【发布时间】:2016-11-03 11:35:53 【问题描述】:

我使用 Python mysqldb 从大表中获取数据有很长的执行时间(而不是很长的获取时间),我想了解是否有明显错误。

我的表定义如下:

create table mytable(
  a varchar(3),
  b bigint,
  c int,
  d int,
  e datetime,
  f varchar(20),
  g varchar(10),
  primary key(a, b, c, d))
ENGINE=InnoDB;

它目前包含 1.5 亿行,表大小估计为 19GB。

Python代码如下:

import MySQLdb
database = MySQLdb.connect(passwd="x", host="dbserver", user="user", db="database", port=9999)
mysql_query = """select a, b, c, d, e, f, g from mytable use index (primary) where a = %s order by a, b, c, d"""
mysql_cursor = database.cursor()
mysql_cursor.execute(mysql_query, ["AA"])
for a, b, c, d, e, f, g in mysql_cursor:
    #Do something

我的惊喜来自花在execute 命令上的时间。它在这里花费了很长时间,尽管我预计 execute 几乎不会花费任何时间(因为它应该使用主键遍历表),并且在 for 循环中花费了相当长的时间。

解释计划如下:

explain select a, b, c, d, e, f, g from mytable use index (primary) where a = %s order by a, b, c, d
'1','SIMPLE','eventindex','ref','PRIMARY','PRIMARY','5','const','87402369','Using where'

目前,所有行在 a 列中都包含相同的值(我打算稍后添加其他值,但目前 a 列内容的分布并不真正平衡)。 b列分布更好

什么可以解释 MySQL 花费如此多的时间来执行查询(而不是花费时间获取行)?

奖金问题。优化这个用例有什么明显的快速胜利吗?对 b 列上的表进行分区? A栏?删除 a 列,并改用单独的表?

【问题讨论】:

【参考方案1】:

实际上看起来更像是一个 MySQL 问题——我认为这个问题与 Python 或 mysql-python 无关。

wrt/ SQL 的东西:一个选择性不够的索引(具有太多相似的值)可能是非常有害的,因为您最终会在索引树遍历之外进行顺序扫描 - 实际上很多比普通表扫描更多的磁盘访问 - 所以你在两边都松了(IOW:你只得到索引树遍历的开销,但没有任何好处)。你可以在这里找到更多信息:MySQL: low cardinality/selectivity columns = how to index? 和这里Role of selectivity in index scan/seek

在您的情况下,您可能想尝试不使用 use index 子句的查询,甚至可能强制优化器使用 ignore index clause 直接绕过索引。

【讨论】:

【参考方案2】:

经过查看,这似乎是 MySQL 的正常行为。从各种来源来看,似乎大多数选择工作都是在 MySQL 的执行阶段完成的,而在 fetch 期间,只发生网络传输。我在 Oracle 上花了很多时间(其中执行通常在实践中几乎什么都不做,处理的核心发生在 fetch 时间),我没有意识到 MySQL 可能会有不同的行为。

根据上下文,一种能够无延迟地迭代项目的解决方法可以是实现分页系统。这可以通过在 Python 生成器中封装较小的提取来完成。另一方面,我们在调用之间失去了数据的一致性,但这在我的情况下是可以接受的。这是对这种方法感兴趣的人的基础。获取下一页所需的调整使得 SQL 查询在某种程度上complex 混乱且不易维护,并且可以将您的代码绑定到您的主键结构而不是您想要的,因此您可能需要权衡这样做之前的利弊。一个好消息是,这种复杂性可以隐藏在生成器后面。

import MySQLdb
database = MySQLdb.connect(passwd="x", host="dbserver", user="user", db="database", port=9999)

def get_next_item(database): #Definition of this generator encapsulating the paging system
    first_call = True
    mysql_cursor = database.cursor()
    nothing_more_found = False
    while not nothing_more_found:
        mysql_query = """select a, b, c, d, e, f, g from mytable use index (primary)
            where a = %s order by a, b, c, d
            limit 10000""" if first_call else """select a, b, c, d, e, f, g from mytable use index (primary)
            where a = %s and ((b > %s) or (b = %s and c > %s) or (b = %s and c = %s and d > %s))  
            order by a, b, c, d
            limit 10000"""

        if first_call:
            mysql_cursor.execute(mysql_query, ["AA", last_b, last_b, last_c, last_b, last_c, last_d])
            first_call = False
        else:
             mysql_cursor.execute(mysql_query, ["AA"])
        if mysql_cursor.rowcount == 0:
            nothing_more_found = True
        for a, b, c, d, e, f, g in mysql_cursor:
            yield (a, b, c, d, e, f, g)
            last_b, last_c, last_d = b, c, d

for a, b, c, d, e, f, g in get_next_item(database): #Usage of the generator
    #Do something

来自 Mike Lischke 的 post 中对 MySQL 执行与获取的解释。

获取时间纯粹衡量传输结果所花费的时间, 这与执行查询完全无关。取回 每次运行查询时,时间甚至会有所不同。为什么你的 网络连接决定查询的好坏?好的,一次使用 实际存在:如果查询返回的数据过多,则传输需要 更久,更长。但即使这样也不完全正确,因为有时 结果被缓存,因此可以更快地发送出去。

另一方面,对于 Oracle,在选择期间,大部分操作发生在提取期间。这是Tom Kyte自己解释的here

这样想

1) parse - 非常明确的定义,即 prepareStatement - 我们做一个 软或硬解析,编译语句,弄清楚如何执行 它。

2) 执行 - 我们打开语句。对于更新,对于删除,对于 插入 - 就是这样,当您打开语句时,我们执行 它。所有的工作都在这里进行。

对于选择它更复杂。大多数选择将在期间做零工作 执行。我们所做的只是打开光标——光标是一个 指向计划所在的共享池中的空间的指针,您的绑定 变量值,代表您的“截至”时间的 SCN 查询 - 简而言之,此时的光标是您的上下文,您的 虚拟机状态,把 SQL 计划想象成字节码 (它是)在虚拟机(它是)中作为程序(它是)执行。 光标是你的指令指针(你在哪里执行 此语句的),您的状态(如寄存器)等。通常,一个 select 在这里什么都不做——它只是“准备好摇滚, 程序已准备就绪,但尚未真正开始”。

但是,一切都有例外 - 打开跟踪并执行 从 scott.emp 中选择 * 进行更新。这是一个选择,但它也是 更新。您会看到在执行期间完成的工作以及 获取阶段。执行期间所做的工作是外出 并触摸每一行并将其锁定。抓取期间完成的工作 阶段是出去并将数据检索回 客户。

3) fetch - 这是我们几乎可以看到 SELECTS 的所有工作的地方 (而且对于其他 DMLS 来说真的没有什么,因为您没有从 更新)。

可以通过两种方式处理 SELECT。我称之为“快速 返回查询”和“慢返回查询”

http://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:275215756923#39255764276301

摘自 Effective Oracle by Design 中的描述 深度,但足以说明形式的查询:

从 one_billion_row_table 中选择 *;

不会将数据复制到任何地方,也不需要访问最后一个 返回第一行之前的行。我们会像你一样读取数据 从它所在的块中获取它。

但是,查询的形式:

select * from one_billion_row_table order by unindexed_column;

我们可能必须在返回之前读取最后一行 第一行(因为读取的最后一行很可能是第一行 返回!),我们需要将其复制到某个地方(临时,排序区域 空格)首先。

在第一次查询的情况下,如果您:

解析它(解析工作很少)打开它(没有现实世界,只是得到 准备好)获取 1 行并关闭它

您会看到在获取阶段执行的工作非常少,我们只是 可能必须读取一个块才能返回第一条记录。

但是,对第二个查询执行相同的步骤,您会看到 单行的提取做了大量的工作——因为我们必须找到 可以返回第一行之前的最后一行。

【讨论】:

以上是关于Python MySQLdb执行缓慢的主要内容,如果未能解决你的问题,请参考以下文章

python3的 pymysql把mysqldb库取代了,让python 3支持mysqldb的解决方法

python使用MySQLdb遇到的事务问题

Python的MySQLdb模块安装

Python MySQLdb 面向对象

MAMP中Python安装MySQLdb

在 Python 3.6 上安装 MySqlDB