我使用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中都包含相同的值(我想到以后添加其他值,但目前列内容的分布并未真正平衡)。 b列分布更好
有什么可以解释MySQL在执行查询上花了那么多时间(而不是花时间获取行)?
奖金问题。任何明显的快速胜利来优化这个用例?在b列上对表格进行分区?列a?删除列a,改为使用单独的表?
答案 0 :(得分:0)
实际上看起来像一个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来绕过索引。
答案 1 :(得分:0)
看完后,看起来这是MySQL的正常行为。从各种来源看,大多数选择工作看起来都是在MySQL的执行阶段完成的,并且在获取期间,只进行网络传输。我花了很多时间在Oracle上(在执行中执行通常几乎没有任何东西,并且处理的时间是在获取时),我没有意识到MySQL可能表现不同。
根据上下文,能够在没有延迟通过项目的情况下进行迭代的解决方法可以是实现分页系统。可以通过在Python生成器中封装较小的fetch来完成。另一方面,我们在调用之间失去了数据的一致性,但在我的情况下这是可以接受的。这是对这种方法感兴趣的人的基础。获取下一页所需的调整使得SQL查询以某种方式复杂混乱且不易维护,并且可以将代码绑定到主键结构而不是您想要的,因此您可能需要在去之前权衡利弊。一个好消息是,这种复杂性可能隐藏在发电机后面。
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)解析 - 非常好定义,即prepareStatement - 我们做了 软解析或硬解析,编译语句,弄清楚如何执行 它
2)执行 - 我们打开声明。对于更新,对于删除,为 插入 - 就是它,当你打开语句时,我们执行 它。所有工作都在这里进行。
对于选择它更复杂。大多数选择将在期间进行ZERO工作 执行。我们所做的只是打开光标 - 光标是一个 指向计划所在的共享池中空间的指针,即绑定 变量值,SCN代表"以及#34;你的时间 查询 - 简而言之,此时的光标就是你的背景,你的 虚拟机状态,将SQL计划视为字节码 (它是)作为程序(它)在虚拟机中执行(它)。 光标是你的指令指针(你在哪里执行 这个陈述),你的州(如寄存器)等。通常,a 选择在这里什么都不做 - 它只是"准备摇滚, 程序准备好了,但还没有真正开始"。
但是,所有事情都有例外 - 打开跟踪并执行操作 select * from scott.emp FOR UPDATE。这是一个选择,但它也是 更新。你会看到在执行期间完成的工作以及 获取阶段。执行期间完成的工作是外出 并触摸每一行并锁定它。在获取期间完成的工作 阶段是走出去并将数据检索回 客户端。
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中选择*;
不会将数据复制到任何地方,也不需要访问最后一个 返回第一行之前的行。我们会像你一样阅读数据 从它所在的块中获取它。
但是,查询表格:
通过unindexed_column;
从one_billion_row_table订单中选择*我们可能必须在返回之前读取最后一行 第一行(因为最后一行读取可能是第一行 返回!)我们需要复制那个地方(临时,排序区域) 空间)首先。
对于第一个查询,如果您:
解析它(小工作解析)打开它(没有现实世界,只是得到 准备好)获取1行并关闭它
你会在获取阶段看到很少的工作,我们只是 必须阅读一个块才可能返回第一条记录。
但是,针对第二个查询执行相同的步骤,您会看到 获取单行做了很多工作 - 因为我们必须找到 可以返回第一行之前的最后一行。