繁体   English   中英

提高Ruby脚本处理CSV的性能

[英]Improve performance of Ruby script processing CSV

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

  1. 将非常大的(2GB / 12,500,000行)CSV读入SQLite3
  2. 查询数据库
  3. 将结果输出到新CSV

在我看来,这似乎是最容易和最合理的方式。 这个过程需要可以配置并定期重复,因此脚本。 我正在使用SQLite,因为数据将始终以CSV格式(无法访问原始数据库),并且将处理卸载到(容易更改的)SQL语句更容易。

问题是步骤1和2需要很长时间。 我一直在寻找提高SQLite性能的方法 我已经实施了其中一些建议,但收效甚微。

  • SQLite3的内存中实例
  • 使用交易(第1步)
  • 使用准备好的声明
  • PRAGMA synchronous = OFF
  • PRAGMA journal_mode = MEMORY (在使用内存数据库时不确定这是否有帮助)

完成所有这些后,我得到以下时间:

  • 阅读时间:17分28秒
  • 查询时间:14分26秒
  • 写入时间:0分4秒
  • 经过的时间:31分58秒

假设我使用的语言不同于上面提到的帖子,并且存在编译/解释等差异,但插入时间约为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编码 - 什么不杀我,希望能让我成为一个更强大的编码器。

通常,如果可能,应该尝试使用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);

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM