4

我编写了一个 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(不确定这在使用内存数据库时是否有帮助)

毕竟,我得到以下时间:

  • 阅读时间: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 编码 - 不会杀死我的东西有望让我成为更强大的编码器。

4

1 回答 1

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);
于 2013-02-01T08:14:32.823 回答