簡體   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