Как PostgreSQL выполняет запись намного быстрее, чем SQLite?

Я сделал простой тест производительности целочисленного обновления. SQLite выполнял только 15 обновлений в секунду, в то время как PostgreSQL выполнял 1500 обновлений в секунду.

Число с примером SQLite выглядит как normal.

FAQ на сайте SQLite объясняет, как фундаментальное ограничение вращательного диска.

На самом деле SQLite легко выполнит 50 000 или более инструкций INSERT за второй на среднем настольном компьютере. Но это будет всего лишь несколько десятков транзакций в секунду. Скорость транзакции ограничена скорость вращения вашего диска. Обычно транзакция требует два полных оборота дискового диска, который на диске 7200 об/мин диск ограничивает до 60 транзакций в секунду. Сделка скорость ограничена скоростью диска, потому что (по умолчанию) SQLite на самом деле ждет, пока данные действительно будут сохранены на диске до завершения транзакции. Таким образом, если вы вдруг потеряйте питание или если ваша ОС сработает, ваши данные по-прежнему безопасны. Для подробности, читайте об атомарной фиксации в SQLite..

По умолчанию каждый оператор INSERT является собственной транзакцией. Но если ты окружайте несколько инструкций INSERT с помощью BEGIN... COMMIT, затем все вставки сгруппированы в одну транзакцию. Время, необходимое для фиксация транзакции амортизируется по всей прилагаемой вставке и поэтому время для инструкции вставки значительно уменьшается.

Другой вариант - запустить PRAGMA synchronous = OFF. Эта команда будет заставить SQLite не ждать, пока данные достигнут поверхности диска, что сделать операции записи намного быстрее. Но если вы потеряете власть в середине транзакции ваш файл базы данных может испортиться.

Является ли это описание истинным? Затем, как PostgreSQL может работать намного быстрее, чем SQLite? (Я установил для fsync и synchronous_commit опцию on в PostgreSQL)

UPDATE:

Здесь полный тестовый код, записанный в Clojure:

(defproject foo "0.1.0-SNAPSHOT"
  :repositories {"sonatype-oss-public" "https://oss.sonatype.org/content/groups/public/"}
  :dependencies [[org.clojure/clojure "1.5.1"]
                 [org.clojure/java.jdbc "0.3.0-SNAPSHOT"]
                 [com.mchange/c3p0 "0.9.2.1"]
                 [org.xerial/sqlite-jdbc "3.7.2"]
                 [postgresql "9.1-901.jdbc4"]])
(ns foo.core
  (:require [clojure.java.jdbc :as jdbc]
            [clojure.java.jdbc.ddl :as ddl])
  (:import  [com.mchange.v2.c3p0 ComboPooledDataSource]))

(def sqlite
  (let [spec {:classname "org.sqlite.JDBC"
              :subprotocol "sqlite"
              :subname "test.db"}]
    {:datasource (doto (ComboPooledDataSource.)
                   (.setDriverClass (:classname spec))
                   (.setJdbcUrl (str "jdbc:" (:subprotocol spec) ":" (:subname spec)))
                   (.setMaxIdleTimeExcessConnections (* 30 60))
                   (.setMaxIdleTime (* 3 60 60)))}))

(def postgres
  (let [spec {:classname "org.postgresql.Driver"
              :subprotocol "postgresql"
              :subname "//localhost:5432/testdb"
              :user "postgres"
              :password "uiop"}]
    {:datasource (doto (ComboPooledDataSource.)
                   (.setDriverClass (:classname spec))
                   (.setJdbcUrl (str "jdbc:" (:subprotocol spec) ":" (:subname spec)))
                   (.setUser (:user spec))
                   (.setPassword (:password spec))
                   (.setMaxIdleTimeExcessConnections (* 30 60))
                   (.setMaxIdleTime (* 3 60 60)))}))

(doseq [x [sqlite postgres]]
  (jdbc/db-do-commands x
    (ddl/create-table :foo [:id :int "PRIMARY KEY"] [:bar :int])))

(doseq [x [sqlite postgres]]
  (jdbc/insert! x :foo {:id 1 :bar 1}))

(defmacro bench
  [expr n]
  `(dotimes [_# 3]
     (let [start# (. System (nanoTime))]
       (dotimes [_# ~n]
         ~expr)
       (let [end#               (. System (nanoTime))
             elapsed#           (/ (double (- end# start#)) 1000000.0)
             operation-per-sec# (long (/ (double ~n) (/ (double (- end# start#)) 1000000000)))]
       (prn (str "Elapsed time: " elapsed# " ms (" (format "%,d" operation-per-sec#) " ops)"))))))

(bench (jdbc/query sqlite ["select * from foo"]) 20000)
(bench (jdbc/execute! sqlite ["update foo set bar=bar+1 where id=?" 1]) 100)

(bench (jdbc/query postgres ["select * from foo"]) 20000)
(bench (jdbc/execute! postgres ["update foo set bar=bar+1 where id=?" 1]) 5000)

И результат:

; Running "select * from foo" 20000 times in SQLite

"Elapsed time: 1802.426963 ms (11,096 ops)"
"Elapsed time: 1731.118831 ms (11,553 ops)"
"Elapsed time: 1749.842658 ms (11,429 ops)"

; Running "update foo set bar=bar+1 where id=1" 100 times in SQLite

"Elapsed time: 6362.829057 ms (15 ops)"
"Elapsed time: 6405.25075 ms (15 ops)"
"Elapsed time: 6352.943553 ms (15 ops)"

; Running "select * from foo" 20000 times in PostgreSQL

"Elapsed time: 2898.636079 ms (6,899 ops)"
"Elapsed time: 2824.77372 ms (7,080 ops)"
"Elapsed time: 2837.622659 ms (7,048 ops)"

; Running "update foo set bar=bar+1 where id=1" 5000 times in PostgreSQL

"Elapsed time: 3213.120219 ms (1,556 ops)"
"Elapsed time: 3564.249492 ms (1,402 ops)"
"Elapsed time: 3280.128708 ms (1,524 ops)"

Результат pg_fsync_test:

C:\temp>"C:\Program Files\PostgreSQL\9.3\bin\pg_test_fsync"
5 seconds per test
O_DIRECT supported on this platform for open_datasync and open_sync.

Compare file sync methods using one 8kB write:
(in wal_sync_method preference order, except fdatasync
is Linux default)
        open_datasync                   81199.920 ops/sec      12 usecs/op
        fdatasync                                     n/a
        fsync                              45.337 ops/sec   22057 usecs/op
        fsync_writethrough                 46.470 ops/sec   21519 usecs/op
        open_sync                                     n/a

Compare file sync methods using two 8kB writes:
(in wal_sync_method preference order, except fdatasync
is Linux default)
        open_datasync                   41093.981 ops/sec      24 usecs/op
        fdatasync                                     n/a
        fsync                              38.569 ops/sec   25927 usecs/op
        fsync_writethrough                 36.970 ops/sec   27049 usecs/op
        open_sync                                     n/a

Compare open_sync with different write sizes:
(This is designed to compare the cost of writing 16kB
in different write open_sync sizes.)
         1 * 16kB open_sync write                     n/a
         2 *  8kB open_sync writes                    n/a
         4 *  4kB open_sync writes                    n/a
         8 *  2kB open_sync writes                    n/a
        16 *  1kB open_sync writes                    n/a

Test if fsync on non-write file descriptor is honored:
(If the times are similar, fsync() can sync data written
on a different descriptor.)
        write, fsync, close                45.564 ops/sec   21947 usecs/op
        write, close, fsync                33.373 ops/sec   29964 usecs/op

Non-Sync'ed 8kB writes:
        write                             889.800 ops/sec    1124 usecs/op

Ответ 1

Вы правы, чтобы быть подозрительными. PostgreSQL с указанными вами настройками не должен выполнять ничего около 1500 обновлений в отдельных последовательных транзакциях в секунду на вращающиеся носители.

Вероятно, что-то в вашем стеке ввода-вывода ложится или работает неправильно, как он реализует синхронизацию. Это означает, что ваши данные подвержены серьезной коррупции после неожиданного отключения питания или сбоя ОС.

Посмотрев на результат pg_test_fsync, это действительно так. open_datasync, который по умолчанию используется под Windows, представляется нереалистично быстрым и поэтому должен быть небезопасным. Я вижу то же самое, когда запускаю pg_test_fsync на своей машине Windows7.

Ответ 2

Он разбивается на то, как они реализуют изоляцию моментальных снимков.

SQLite использует блокировку файлов как средство для изоляции транзакций, что позволяет записывать записи только после завершения всех операций чтения.

Postgres, напротив, использует более сложный подход, называемый multiconcurrency контролем версий (mvcc), который позволяет выполнять несколько операций параллельно с несколькими чтениями.

http://www.sqliteconcepts.org/SI_index.html

http://www.postgresql.org/docs/current/static/mvcc-intro.html

http://wiki.postgresql.org/wiki/MVCC

Ответ 3

Ответ Дениса содержит все необходимые вам ссылки. Я пойду за менее подробный, но более понятный ответ.

Sqlite не использует сложный менеджер транзакций, в нем нет усовершенствованной логики многозадачности. Он выполняет то, что вы говорите, чтобы выполнить его в точно таком порядке. Другими словами: он делает именно то, что вы говорите ему. Если вы попытались использовать одну и ту же базу данных из двух процессов - вы столкнулись бы с проблемами.

PostgreSQL, с другой стороны, представляет собой очень сложную базу данных: она эффективно поддерживает множественные одновременные чтения и записи. Подумайте об этом как о асинхронной системе - вы планируете только выполнение работы, вы фактически не контролируете ее в своих подробностях - Postgres делает это для вас.

Что делать с эффективностью? Присоединяйтесь к нескольким десяткам - сотням обновлений/вставок в одну транзакцию. Для простой таблицы вы получите очень хорошую производительность.

Ответ 4

Собственно, любая запись на вращающемся диске составляет порядка 10 мс (типичное число составляет 8 мс).

Это означает немного более 100 записей в секунду, если вы пишете одну и ту же позицию на диске, что является очень странным случаем для базы данных. См. "Вы не знаете джек о дисках" из ACM, обычно диск может планировать 10 операций чтения или записи за один оборот.

http://queue.acm.org/detail.cfm?id=864058

Таким образом, база данных может выполнять 1000 операций записи в секунду и даже больше. Я видел приложения, выполняющие 1500 транзакций в секунду 10 лет назад на настольных компьютерах.

Ответ 5

Предполагая, что вы используете обычный жесткий диск (т.е. нет ssd), вы можете ожидать максимум 50-100 записей в секунду. Кажется, что 15 записей в секунду немного низки, но не невозможны.

Итак, если Postgres делает 1500 обновлений в секунду, они либо записываются в некоторый буфер/кеш, либо сворачиваются в одно обновление. Не зная больше о фактическом тесте, трудно сказать, какова фактическая причина, но если вы должны были открыть транзакцию, обновите одну строку 1500 раз и зафиксируйте после этого, чем Postgres должны быть достаточно умны, чтобы выполнять только один "реальный", пишите на диск.