Clojure SQLKorma库:内存不足错误。

3
我正在执行一个我认为非常简单的任务:使用sqlkorma库(http://sqlkorma.com)运行一个sql查询(约65K行数据),并对每一行进行某种转换,然后将其写入CSV文件。考虑到我的笔记本电脑有8GB内存,我并不认为65K行数据特别大,但我也假定sql结果集会被惰性获取,因此整个查询结果永远不会同时在内存中保留。所以当我最终遇到以下堆栈跟踪时,我真的非常吃惊:
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at clojure.lang.PersistentHashMap$BitmapIndexedNode.assoc(PersistentHashMap.java:777)
at clojure.lang.PersistentHashMap.createNode(PersistentHashMap.java:1101)
at clojure.lang.PersistentHashMap.access$600(PersistentHashMap.java:28)
at clojure.lang.PersistentHashMap$BitmapIndexedNode.assoc(PersistentHashMap.java:749)
at clojure.lang.PersistentHashMap$TransientHashMap.doAssoc(PersistentHashMap.java:269)
at clojure.lang.ATransientMap.assoc(ATransientMap.java:64)
at clojure.lang.PersistentHashMap.create(PersistentHashMap.java:56)
at clojure.lang.PersistentHashMap.create(PersistentHashMap.java:100)
at clojure.lang.PersistentArrayMap.createHT(PersistentArrayMap.java:61)
at clojure.lang.PersistentArrayMap.assoc(PersistentArrayMap.java:201)
at clojure.lang.PersistentArrayMap.assoc(PersistentArrayMap.java:29)
at clojure.lang.RT.assoc(RT.java:702)
at clojure.core$assoc.invoke(core.clj:187)
at clojure.core$zipmap.invoke(core.clj:2715)
at clojure.java.jdbc$resultset_seq$thisfn__204.invoke(jdbc.clj:243)
at clojure.java.jdbc$resultset_seq$thisfn__204$fn__205.invoke(jdbc.clj:243)
at clojure.lang.LazySeq.sval(LazySeq.java:42)
at clojure.lang.LazySeq.seq(LazySeq.java:60)
at clojure.lang.Cons.next(Cons.java:39)
at clojure.lang.PersistentVector.create(PersistentVector.java:51)
at clojure.lang.LazilyPersistentVector.create(LazilyPersistentVector.java:31)
at clojure.core$vec.invoke(core.clj:354)
at korma.db$exec_sql$fn__343.invoke(db.clj:203)
at clojure.java.jdbc$with_query_results_STAR_.invoke(jdbc.clj:669)
at korma.db$exec_sql.invoke(db.clj:202)
at korma.db$do_query$fn__351.invoke(db.clj:225)
at clojure.java.jdbc$with_connection_STAR_.invoke(jdbc.clj:309)
at korma.db$do_query.invoke(db.clj:224)
at korma.core$exec.invoke(core.clj:474)
at db$query_db.invoke(db.clj:23)
at main$_main.doInvoke(main.clj:32)
at clojure.lang.RestFn.applyTo(RestFn.java:137)

据我所知,从堆栈中看,它还没有离开查询代码(这意味着它根本没有到达我的转换/写入CSV代码)。如果重要的话,我的SQL语句非常简单,基本上是SELECT * FROM my_table WHERE SOME_ID IS NOT NULL AND ROWNUM < 65000 ORDER BY some_id ASC。这是Oracle(解释一下上面的rownum),但我认为这并不重要。
编辑:
代码示例:
(defmacro query-and-print [q] `(do (dry-run ~q) ~q))
(defn query-db []  
    (query-and-print 
        (select my_table 
            (where (and (not= :MY_ID "BAD DATA")
                        (not= :MY_ID nil)
                        (raw (str "rownum < " rows))))
            (order :MY_ID :asc))))

; args contains rows 65000, and configure-app sets up the jdbc
; connection string, and sets a var with rows value
(defn -main [& args]
    (when (configure-app args) 
        (let [results (query-db)
              dedup (dedup-with-merge results)]
            (println "Result size: " (count results))
            (println "Dedup size: " (count dedup))
            (to-csv "target/out.csv" (transform-data dedup)))))

你能编辑你的原始帖子并添加一些源代码吗?同时建议将错误块稍微缩小一些。 - octopusgrabbus
完成。不确定从错误块中删除什么:它显示我的代码在(query-db)调用之后没有进展,并且还显示了OOM发生在clojure.java.jdbc内部的位置。顺便说一句,我开始查看clojure.java.jdbc代码,它看起来不像是惰性的(这对我来说很疯狂)。 - Kevin
当我添加(println "Result type: " (type results))时,我得到了 Result type: clojure.lang.PersistentVector,这应该回答了我的问题。 - Kevin
2个回答

2

clojure.java.sql 创建延迟序列:

(defn resultset-seq
"Creates and returns a lazy sequence of maps corresponding to
 the rows in the java.sql.ResultSet rs. Based on clojure.core/resultset-seq
 but it respects the current naming strategy. Duplicate column names are
 made unique by appending _N before applying the naming strategy (where
 N is a unique integer)."
[^ResultSet rs]
(let [rsmeta (.getMetaData rs)
      idxs (range 1 (inc (.getColumnCount rsmeta)))
      keys (->> idxs
             (map (fn [^Integer i] (.getColumnLabel rsmeta i)))
             make-cols-unique
             (map (comp keyword *as-key*)))
      row-values (fn [] (map (fn [^Integer i] (.getObject rs i)) idxs))
      rows (fn thisfn []
             (when (.next rs)
               (cons (zipmap keys (row-values)) (lazy-seq (thisfn)))))]
  (rows)))

Korma通过将每一行变成一个向量来完全实现序列:

(defn- exec-sql [{:keys [results sql-str params]}]
(try
(case results
  :results (jdbc/with-query-results rs (apply vector sql-str params)
             (vec rs))
  :keys (jdbc/do-prepared-return-keys sql-str params)
  (jdbc/do-prepared sql-str params))
(catch Exception e
  (handle-exception e sql-str params))))

你如何绕过这个事实,逐行获取并处理数据? - octopusgrabbus
不偷懒的原因很明显,因为你永远不知道懒加载的数据何时被实际使用,而在那个时候底层的 SQL 连接将会被关闭。 - Ankur
2
这方面有几个拉取请求:https://github.com/korma/Korma/pull/66 和 https://github.com/korma/Korma/pull/151。 - Eelco
@Ankur,针对非常大的结果集,一次性将整个结果集拉入内存是不可行的。你没有理由总是限制自己只查询那些足够小以适合内存的结果;你只需要在连接打开时小心实现惰性序列即可。我写了一个宏,在上面提到的 66 拉取请求中使这变得容易。 - Paul Legato

1
除了 https://github.com/korma/Korma/pull/66 中的 with-lazy-results 路由外,解决问题的完全不同方式是通过设置适当的标志来增加 JVM 可用的堆大小。JVM 不允许使用计算机上的所有可用内存;它们严格限制为您告诉它们可以使用的数量。其中一种方法是在您的 project.clj 文件中设置 :jvm-opts ["-Xmx4g"]。(根据需要调整确切的堆大小。)另一种方法是执行以下操作:
export JAVA_OPTS=-Xmx:4g 
lein repl # or whatever lanuches your Clojure process

with-lazy-results路由更好,因为您可以操作任何大小的结果集,但它没有合并到主线Korma中,并且需要更新才能与最近的版本一起使用。无论如何,了解如何调整JVM允许的堆大小都是很好的。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接