【问题标题】:Improve performance of Ruby script processing CSV提高 Ruby 脚本处理 CSV 的性能
【发布时间】:2013-01-16 10:21:53
【问题描述】:

我编写了一个 Ruby 脚本来执行以下操作:

  1. 将一个非常大的(2GB/12,500,000 行)CSV 读入 SQLite3
  2. 查询数据库
  3. 将结果输出到新的 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


【解决方案1】:

一般来说,如果可能,您应该尝试UNION ALL 而不是UNION,这样就不必检查两个子查询的重复项。 但是,在这种情况下,SQLite 必须在单独的步骤中执行 DISTINCT。这是否更快取决于您的数据。

根据我的EXPLAIN QUERY PLAN 实验,以下两个索引应该对这个查询最有帮助:

CREATE INDEX i1 ON Area(Locality, State, PostCode);
CREATE INDEX i2 ON Area(StreetName, StreetType, Locality, State, PostCode);

【讨论】:

  • 首先,感谢您提到 DISTINCT - 我已将其删除,因为 GROUP BY 已将其删除。另外,我使用 UNION ALL 来删除隐含的 DISTINCT,因为这不是必需的。最后,我现在尝试使用多字段索引。以前我使用了许多单字段索引......这表明我在 SQL 优化方面缺乏经验。会让你知道情况如何。
  • 以下是 CL 建议索引的新数字:CREATE INDEX i1 (233.44s) CREATE INDEX i1 (238.836s) 阅读时间:17m 40s 索引时间:7m 52s 查询时间:0m 54s 写入时间:0m 3s 经过时间: 26m 31s 哇——这个指数有多大的不同!我不知道这是创建索引的正确方法。以前,出于绝望,我创建了 8 个单独的索引(每个字段 1 个),但这只是浪费时间创建它们,对查询没有任何好处。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-11-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-06-05
  • 2013-04-21
  • 2019-09-14
相关资源
最近更新 更多