提高 MySQLdb 加载数据文件性能
Improving MySQLdb load data infile performance
我有一个table,在InnoDB中大致定义如下:
create table `my_table` (
`time` int(10) unsigned not null,
`key1` int(10) unsigned not null,
`key3` char(3) unsigned not null,
`key2` char(2) unsigned not null,
`value1` float default null,
`value2` float default null,
primary key (`key1`, `key2`, `key3`, `time`),
key (`key3`, `key2`, `key1`, `time`)
) engine=InnoDB default character set ascii
partition by range(time) (
partition start values less than (0),
partition from20180101 values less than (unix_timestamp('2018-02-01')),
partition from20180201 values less than (unix_timestamp('2018-03-01')),
...,
partition future values less than MAX_VALUE
)
是的,列顺序与键顺序不匹配。
在 Python 中,我用 500,000 行填充了一个 DataFrame(这可能不是最有效的方法,但可以作为数据的示例):
import random
import pandas as pd
key2_values = ["aaa", "bbb", ..., "ttt"] # 20 distinct values
key3_values = ["aa", "ab", "ac", ..., "az", "bb", "bc", ..., "by"] # 50 distinct values
df = pd.DataFrame([], columns=["key1", "key2", "key3", "value2", "value1"])
idx = 0
for x in range(0, 500):
for y in range(0, 20):
for z in range(0, 50):
df.loc[idx] = [x, key2_values[y], key3_values[z], random.random(), random.random()]
idx += 1
df.set_index(["key1", "key2", "key3"], inplace=True)
(实际上,这个 DataFrame 是由几个 API 调用和大量数学运算填充的,但最终结果是相同的:一个巨大的 DataFrame,包含约 500,000 行和与 InnoDB table)
要将此 DataFrame 导入 table,我目前正在执行以下操作:
import time
import MySQLdb
conn = MySQLdb.connect(local_infile=1, **connection_params)
cur = conn.cursor()
# Disable data integrity checks -- I know the data is good
cur.execute("SET foreign_key_checks=0;")
cur.execute("SET unique_checks=0;")
# Append current time to the DataFrame
df["time"] = time.time()
df.set_index(["time"], append=True, inplace=True)
# Sort data in primary key order
df.sort_index(inplace=True)
# Dump the data to a CSV
with open("dump.csv", "w") as csv:
df.to_csv(csv)
# Load the data
cur.execute(
"""
load data local infile 'dump.csv'
into table `my_table`
fields terminated by ','
enclosed by '"'
lines terminated by '\n'
ignore 1 lines
(`key1`, `key2`, `key3`, `time`, `value`)
"""
)
# Clean up
cur.execute("SET foreign_key_checks=1;")
cur.execute("SET unique_checks=1;")
conn.commit()
总的来说这方面的表现还不错。我可以在大约 2 分钟内导入 500,000 行。如果可能的话,我想更快地得到这个。
是否有我遗漏的任何技巧或我可以进行的任何更改以将其缩短到 30-45 秒?
一些注意事项:
- 我不知道重新排序 DataFrame 中的 列 是否会影响性能。当前 DataFrame 中列的顺序与数据库不匹配
- 我不知道改变数据库中列的顺序以匹配主键的顺序是否会影响性能(目前"time"排在第一位,即使它是索引的第四个键)
- 更改数据库配置可能很困难,因为我无法直接访问数据库服务器。我坚持使用任何已经存在的硬件和配置选项。任何性能改进都必须来自我的 Python 代码
- 我可以更改table定义(包括更改分区)但是我想尽可能避免这种情况,因为已经有大量的历史数据并将其复制到另一个 table 会花费很长时间。丢失这些数据是一种选择,但我宁愿避免这种情况
- 我不能使用
set sql_log_bin=0;
因为我没有数据库的 SUPER
权限
我做了三处更改,我没有停下来衡量每次更改之间的性能,所以我无法 100% 确定每次更改的确切影响,但是我可以合理地确定我知道什么产生了更大的影响。
更改 1(很确定这影响最大)-- 修改主键
看看我的脚本是如何运行的,您可以看到我批量插入的所有 500k 行都具有与 time
:
完全相同的值
# Append current time to the DataFrame
df["time"] = time.time
通过使 time
为主键的 left-most 列意味着我插入的所有行都将聚集在一起,而不必将它们拆分到 table .
当然,这个问题是它使索引对我最常见的查询无用:为给定的 key1
、key2
和 [=17 返回所有 "times" =]组合(例如:SELECT * FROM my_table WHERE key1 = ... AND key2 = ... AND key3 = ...
)
要解决这个问题,我必须添加另一个密钥:
PRIMARY KEY (`time`, `key1`, `key2`, `key3`),
KEY (`key1`, `key2`, `key3`)
更改 2(可能有影响)-- 修改列顺序
我调整了 table 以便列的顺序与主键的顺序匹配(time
、key1
、key2
、key3
)
我不知道这是否有影响,但可能有
更改 3(可能有影响)-- 调整了 CSV 中列的顺序
我 运行 我的 DataFrame 上有以下内容:
df.reindex(columns=["value1", "value2"], inplace=True)
这对列进行排序以匹配它们在数据库中出现的顺序。在此和更改 2 之间,可以完全按原样导入行,而无需交换列的顺序。不知道对导入性能有没有影响
结果
通过这三项更改,我的导入时间从 2 分钟减少到 9 秒! 太不可思议了
我担心向 table 添加额外的键,因为额外的索引意味着更长的写入时间和更多的磁盘 space,但效果几乎可以忽略不计——尤其是与大量节省相比我从正确地聚类我的密钥中得到。
我有一个table,在InnoDB中大致定义如下:
create table `my_table` (
`time` int(10) unsigned not null,
`key1` int(10) unsigned not null,
`key3` char(3) unsigned not null,
`key2` char(2) unsigned not null,
`value1` float default null,
`value2` float default null,
primary key (`key1`, `key2`, `key3`, `time`),
key (`key3`, `key2`, `key1`, `time`)
) engine=InnoDB default character set ascii
partition by range(time) (
partition start values less than (0),
partition from20180101 values less than (unix_timestamp('2018-02-01')),
partition from20180201 values less than (unix_timestamp('2018-03-01')),
...,
partition future values less than MAX_VALUE
)
是的,列顺序与键顺序不匹配。
在 Python 中,我用 500,000 行填充了一个 DataFrame(这可能不是最有效的方法,但可以作为数据的示例):
import random
import pandas as pd
key2_values = ["aaa", "bbb", ..., "ttt"] # 20 distinct values
key3_values = ["aa", "ab", "ac", ..., "az", "bb", "bc", ..., "by"] # 50 distinct values
df = pd.DataFrame([], columns=["key1", "key2", "key3", "value2", "value1"])
idx = 0
for x in range(0, 500):
for y in range(0, 20):
for z in range(0, 50):
df.loc[idx] = [x, key2_values[y], key3_values[z], random.random(), random.random()]
idx += 1
df.set_index(["key1", "key2", "key3"], inplace=True)
(实际上,这个 DataFrame 是由几个 API 调用和大量数学运算填充的,但最终结果是相同的:一个巨大的 DataFrame,包含约 500,000 行和与 InnoDB table)
要将此 DataFrame 导入 table,我目前正在执行以下操作:
import time
import MySQLdb
conn = MySQLdb.connect(local_infile=1, **connection_params)
cur = conn.cursor()
# Disable data integrity checks -- I know the data is good
cur.execute("SET foreign_key_checks=0;")
cur.execute("SET unique_checks=0;")
# Append current time to the DataFrame
df["time"] = time.time()
df.set_index(["time"], append=True, inplace=True)
# Sort data in primary key order
df.sort_index(inplace=True)
# Dump the data to a CSV
with open("dump.csv", "w") as csv:
df.to_csv(csv)
# Load the data
cur.execute(
"""
load data local infile 'dump.csv'
into table `my_table`
fields terminated by ','
enclosed by '"'
lines terminated by '\n'
ignore 1 lines
(`key1`, `key2`, `key3`, `time`, `value`)
"""
)
# Clean up
cur.execute("SET foreign_key_checks=1;")
cur.execute("SET unique_checks=1;")
conn.commit()
总的来说这方面的表现还不错。我可以在大约 2 分钟内导入 500,000 行。如果可能的话,我想更快地得到这个。
是否有我遗漏的任何技巧或我可以进行的任何更改以将其缩短到 30-45 秒?
一些注意事项:
- 我不知道重新排序 DataFrame 中的 列 是否会影响性能。当前 DataFrame 中列的顺序与数据库不匹配
- 我不知道改变数据库中列的顺序以匹配主键的顺序是否会影响性能(目前"time"排在第一位,即使它是索引的第四个键)
- 更改数据库配置可能很困难,因为我无法直接访问数据库服务器。我坚持使用任何已经存在的硬件和配置选项。任何性能改进都必须来自我的 Python 代码
- 我可以更改table定义(包括更改分区)但是我想尽可能避免这种情况,因为已经有大量的历史数据并将其复制到另一个 table 会花费很长时间。丢失这些数据是一种选择,但我宁愿避免这种情况
- 我不能使用
set sql_log_bin=0;
因为我没有数据库的SUPER
权限
我做了三处更改,我没有停下来衡量每次更改之间的性能,所以我无法 100% 确定每次更改的确切影响,但是我可以合理地确定我知道什么产生了更大的影响。
更改 1(很确定这影响最大)-- 修改主键
看看我的脚本是如何运行的,您可以看到我批量插入的所有 500k 行都具有与 time
:
# Append current time to the DataFrame
df["time"] = time.time
通过使 time
为主键的 left-most 列意味着我插入的所有行都将聚集在一起,而不必将它们拆分到 table .
当然,这个问题是它使索引对我最常见的查询无用:为给定的 key1
、key2
和 [=17 返回所有 "times" =]组合(例如:SELECT * FROM my_table WHERE key1 = ... AND key2 = ... AND key3 = ...
)
要解决这个问题,我必须添加另一个密钥:
PRIMARY KEY (`time`, `key1`, `key2`, `key3`),
KEY (`key1`, `key2`, `key3`)
更改 2(可能有影响)-- 修改列顺序
我调整了 table 以便列的顺序与主键的顺序匹配(time
、key1
、key2
、key3
)
我不知道这是否有影响,但可能有
更改 3(可能有影响)-- 调整了 CSV 中列的顺序
我 运行 我的 DataFrame 上有以下内容:
df.reindex(columns=["value1", "value2"], inplace=True)
这对列进行排序以匹配它们在数据库中出现的顺序。在此和更改 2 之间,可以完全按原样导入行,而无需交换列的顺序。不知道对导入性能有没有影响
结果
通过这三项更改,我的导入时间从 2 分钟减少到 9 秒! 太不可思议了
我担心向 table 添加额外的键,因为额外的索引意味着更长的写入时间和更多的磁盘 space,但效果几乎可以忽略不计——尤其是与大量节省相比我从正确地聚类我的密钥中得到。