我编写了一个 Ruby 脚本来执行以下操作:
- 将一个非常大(2GB/12,500,000 行)的 CSV 读入 SQLite3
- 查询数据库
- 将结果输出到新的 CSV
在我看来,这似乎是最简单、最合乎逻辑的方法。这个过程需要定期配置和重复,因此需要脚本。我使用 SQLite 是因为数据总是以 CSV 形式出现(无法访问原始数据库),而且将处理卸载到(易于更改的)SQL 语句更容易。
问题是步骤 1 和 2 需要很长时间。我一直在寻找提高 SQLite 性能的方法。我已经实施了其中一些建议,但收效甚微。
- SQLite3 的内存中实例
- 使用事务(大约第 1 步)
- 使用准备好的语句
PRAGMA synchronous = OFF
PRAGMA journal_mode = MEMORY
(不确定这在使用内存数据库时是否有帮助)
毕竟,我得到以下时间:
- 阅读时间:17m 28s
- 查询时间:14m 26s
- 写入时间:0m 4s
- 经过时间:31m 58s
当然,我使用与上述帖子不同的语言,并且存在编译/解释等差异,但是插入时间约为 79,000 条记录/秒对 12,000 条记录/秒 - 慢了 6 倍。
我也尝试过索引部分(或全部)字段。这实际上具有相反的效果。索引需要很长时间,以至于查询时间的任何改进都完全被索引时间所掩盖。此外,由于需要额外的空间,执行该内存数据库最终会导致内存不足错误。
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 编码 - 不会杀死我的东西有望让我成为更强大的编码器。