【发布时间】:2013-01-16 10:21:53
【问题描述】:
我编写了一个 Ruby 脚本来执行以下操作:
- 将一个非常大的(2GB/12,500,000 行)CSV 读入 SQLite3
- 查询数据库
- 将结果输出到新的 CSV
在我看来,这似乎是最简单、最合乎逻辑的方法。这个过程需要定期配置和重复,因此需要脚本。我使用 SQLite 是因为数据总是以 CSV 形式出现(无法访问原始数据库),而且将处理卸载到(易于更改的)SQL 语句更容易。
问题在于第 1 步和第 2 步需要很长时间。我已经搜索了improve the performance of SQLite 的方法。我已经实施了其中一些建议,但收效甚微。
- SQLite3 的内存实例
- 使用事务(围绕第 1 步)
- 使用准备好的语句
PRAGMA synchronous = OFF-
PRAGMA journal_mode = MEMORY(不确定这在使用内存数据库时是否有帮助)
在所有这些之后,我得到以下时间:
- 读取时间:17m 28s
- 查询时间:14m 26s
- 写入时间:0m 4s
- 经过时间:31m 58s
尽管我使用的是与上述帖子不同的语言,并且存在编译/解释等差异,但插入时间约为 79,000 条记录/秒对 12,000 条记录/秒 - 慢了 6 倍。
我也尝试过索引部分(或全部)字段。这实际上具有相反的效果。索引需要很长时间,以至于查询时间的任何改进都完全被索引时间所掩盖。此外,由于需要额外的空间,执行该内存 DB 最终会导致内存不足错误。
SQLite3 不适合这种数据量的数据库吗?我也尝试过使用 MySQL,但它的性能更差。
最后,这是代码的精简版本(删除了一些不相关的细节)。
require 'csv'
require 'sqlite3'
inputFile = ARGV[0]
outputFile = ARGV[1]
criteria1 = ARGV[2]
criteria2 = ARGV[3]
criteria3 = ARGV[4]
begin
memDb = SQLite3::Database.new ":memory:"
memDb.execute "PRAGMA synchronous = OFF"
memDb.execute "PRAGMA journal_mode = MEMORY"
memDb.execute "DROP TABLE IF EXISTS Area"
memDb.execute "CREATE TABLE IF NOT EXISTS Area (StreetName TEXT, StreetType TEXT, Locality TEXT, State TEXT, PostCode INTEGER, Criteria1 REAL, Criteria2 REAL, Criteria3 REAL)"
insertStmt = memDb.prepare "INSERT INTO Area VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8)"
# Read values from file
readCounter = 0
memDb.execute "BEGIN TRANSACTION"
blockReadTime = Time.now
CSV.foreach(inputFile) { |line|
readCounter += 1
break if readCounter > 100000
if readCounter % 10000 == 0
formattedReadCounter = readCounter.to_s.reverse.gsub(/...(?=.)/,'\&,').reverse
print "\rReading line #{formattedReadCounter} (#{Time.now - blockReadTime}s) "
STDOUT.flush
blockReadTime = Time.now
end
insertStmt.execute (line[6]||"").gsub("'", "''"), (line[7]||"").gsub("'", "''"), (line[9]||"").gsub("'", "''"), line[10], line[11], line[12], line[13], line[14]
}
memDb.execute "END TRANSACTION"
insertStmt.close
# Process values
sqlQuery = <<eos
SELECT DISTINCT
'*',
'*',
Locality,
State,
PostCode
FROM
Area
GROUP BY
Locality,
State,
PostCode
HAVING
MAX(Criteria1) <= #{criteria1}
AND
MAX(Criteria2) <= #{criteria2}
AND
MAX(Criteria3) <= #{criteria3}
UNION
SELECT DISTINCT
StreetName,
StreetType,
Locality,
State,
PostCode
FROM
Area
WHERE
Locality NOT IN (
SELECT
Locality
FROM
Area
GROUP BY
Locality
HAVING
MAX(Criteria1) <= #{criteria1}
AND
MAX(Criteria2) <= #{criteria2}
AND
MAX(Criteria3) <= #{criteria3}
)
GROUP BY
StreetName,
StreetType,
Locality,
State,
PostCode
HAVING
MAX(Criteria1) <= #{criteria1}
AND
MAX(Criteria2) <= #{criteria2}
AND
MAX(Criteria3) <= #{criteria3}
eos
statement = memDb.prepare sqlQuery
# Output to CSV
csvFile = CSV.open(outputFile, "wb")
resultSet = statement.execute
resultSet.each { |row| csvFile << row}
csvFile.close
rescue SQLite3::Exception => ex
puts "Excepion occurred: #{ex}"
ensure
statement.close if statement
memDb.close if memDb
end
请随意取笑我幼稚的 Ruby 编码 - 杀不死我的东西有望让我成为更强大的编码器。
【问题讨论】:
-
可能是我的Ruby技术欠缺,但你好像没有使用批量插入?
-
你试过这样的事情吗? MySQL CSV Storage Engine
-
@Mirko 你能澄清一下批量插入是什么意思吗?我可以看到将数据推送到 SQLite3 的唯一方法是使用插入语句。另外,根据我的阅读,即使使用“ INSERT INTO targetTable (fields) (values)[, (values)]* ”格式也不能提高效率
-
@Zach 我尝试了 MySQL 来比较性能,但不是 CSV 存储引擎。我需要一个可移植的解决方案——即我需要能够将脚本提供给任何人,而不需要他们安装其他软件。这就是选择 SQLite3 内存解决方案的原因。
-
索引创建有多慢?
标签: sql ruby performance csv sqlite