提高Ruby脚本处理CSV的性能

时间:2013-02-01 05:59:53

标签: sql ruby performance csv sqlite

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

  1. 将非常大的(2GB / 12,500,000行)CSV读入SQLite3
  2. 查询db
  3. 将结果输出为新CSV
  4. 在我看来,这似乎是最简单,最合理的方式。这个过程需要可以配置并定期重复,因此脚本。我使用的是SQLite,因为数据总是以CSV格式存在(无法访问原始数据库),并且可以更轻松地将处理卸载到(容易更改的)SQL语句中。

    问题在于步骤1和2需要很长时间。我已经找到了improve the performance of 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编码 - 不要杀我的东西会让我成为一个更强大的编码器。

1 个答案:

答案 0 :(得分: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);