PostgreSQL 9.4 中使用 jsonb

轉載翻譯自http://nandovieira.com/using-postgresql-and-jsonb-with-ruby-on-railsjavascript

PostgreSQL 9.4 引入了jsonb,一個新的列類型用於存儲文檔到你的關係數據庫中。jsonbjson在更高的層面上看起來幾乎是同樣的,但在存儲實現上是不一樣的。html

使用jsonb的優點在於你能夠輕易的整合關係型數據和非關係型數據,在性能方面,能夠比大多數相似於MongoDB這樣的非關係數據庫更好java

理解json和jsonb之間的不一樣

所以,兩種列類型之間的區別是什麼?當咱們比較寫入數據速度時,因爲數據存儲的方式的緣由,jsonb會比json稍微的慢一點。python

  • json存儲完整複製過來的文本輸入,必須一遍又一遍的解析在你調用任何函數的時候。它不支持索引,但你能夠爲查詢建立表達式索引。git

  • jsonb存儲的二進制格式,避免了從新解析數據結構。它支持索引,這意味着你能夠不使用指定的索引就能查詢任何路徑。github

其餘的不一樣包括,json列會每次都解析存儲的值,這意味着鍵的順序要和輸入的時候同樣。但jsonb不一樣,以二進制格式存儲且不保證鍵的順序。所以,若是你有軟件須要依賴鍵的順序,jsonb可能不是你的應用的最佳選擇。sql

讓咱們運行一個簡單的基準測試。在這個例子中,我使用下面這樣一個json數據結構:數據庫

json{
  "twitter": "johndoe1",
  "github": "johndoe1",
  "bio": "Lorem ipsum dolor sit amet, consectetur adipisicing elit. Labore impedit 
          aliquam sapiente dolore magni aliquid ipsa ad, enim, esse ut reprehenderit 
          quaerat deleniti fugit eaque. Vero eligendi voluptatibus atque, asperiores.",
  "blog": "http://johndoe1.example.com",
  "interests": [
    "music",
    "movies",
    "programming"
  ],
  "age": 42,
  "newsletter": true
}

插入30000條徹底同樣的記錄,我相信jsonb在插入複雜結構時會慢一些。express

Rehearsal ------------------------------------------------
insert jsonb   2.690000   0.590000   3.280000 ( >12.572343)
insert json    2.690000   0.590000   3.280000 ( 12.766534)
--------------------------------------- total: 6.560000sec

-----------------------------------------user     system      total        real
insert jsonb   2.680000   0.590000   3.270000 ( 13.206602)
insert json    2.650000   0.580000   3.230000 ( 12.577138)

真正的差距在查詢json/jsonb列的時候。首先讓咱們看看這張表和索引。json

sql
CREATE TABLE users ( id serial not null, settings jsonb not null default '{}', preferences json not null default '{}' ); CREATE INDEX settings_index ON users USING gin (settings); CREATE INDEX twitter_settings_index ON users ((settings->>'github')); CREATE INDEX preferences_index ON users ((preferences->>'github'));

注意咱們有一個GIN索引在settings列上,兩個給出的路徑(github)表達式索引。在30000條數據中搜索Github用戶名爲john30000的記錄(最後一個插入的記錄),會給出如下數字

Rehearsal -----------------------------------------------------------------
read jsonb (index column)       0.030000   0.030000   0.060000 (  3.673465)
read jsonb (expression index)   0.010000   0.010000   0.020000 (  0.087105)
read json (expression index)    0.010000   0.020000   0.030000 (  0.080121)
read json (no index)            0.060000   0.030000   0.090000 (113.206747)
-------------------------------------------------------- total: 0.200000sec

-----------------------------------------user     system      total        real
read jsonb (index column)       0.010000   0.020000   0.030000 (  0.092476)
read jsonb (expression index)   0.010000   0.010000   0.020000 (  0.078916)
read json (expression index)    0.010000   0.010000   0.020000 (  0.081908)
read json (no index)            0.050000   0.040000   0.090000 (110.761944)

和你看到的那樣,表達式索引在兩種數據類型中的性能幾乎徹底同樣,因此它們在這裏並無實際的意義。剩下的兩列不一樣的地方在於在查詢列時有沒有索引;jsonb能在整列創建GIN/GIST索引,而json不能創建這樣的索引。這也是爲何這json查詢速度這麼慢的緣由。

讓咱們檢查下在沒有索引的狀況下查詢分析器查詢數據。

sql
EXPLAIN SELECT * FROM users WHERE settings @> '{"twitter": "john30000"}' LIMIT 1; -- QUERY PLAN -- ------------------------------------------------------------------------------------- -- Limit (cost=28.23..31.96 rows=1 width=468) -- -> Bitmap Heap Scan on users (cost=28.23..140.07 rows=30 width=468) -- Recheck Cond: (settings @> '{"twitter": "john30000"}'::jsonb) -- -> Bitmap Index Scan on settings_index (cost=0.00..28.23 rows=30 width=0) -- Index Cond: (settings @> '{"twitter": "john30000"}'::jsonb) EXPLAIN SELECT * FROM users WHERE preferences->>'twitter' = 'john30000' LIMIT 1; -- QUERY PLAN -- ------------------------------------------------------------------------- -- Limit (cost=0.00..25.23 rows=1 width=468) -- -> Seq Scan on users (cost=0.00..3784.00 rows=150 width=468) -- Filter: ((preferences ->> 'twitter'::text) = 'john30000'::text)

最重要的是,json作的是順序掃描,這意味着PostgreSQL將根據順序一條一條往下找,直到找到符合條件的數據,同時記住查找這些數據時,每條記錄中的JSON內容都會被解析,這將致使在複雜結構中查詢速度變慢。

但這些不會發生jsonb列中,這種查找使用了索引,卻並無像使用表達式索引那樣將速度優化的很好。

jsonb有一個須要注意的點是,jsonb會一直順序檢索若是你使用->>操做符在一個沒有表達式索引的路徑上。

sql
EXPLAIN SELECT * FROM users WHERE settings->>'twitter' = 'johndoe30000' LIMIT 1; -- QUERY PLAN -- ------------------------------------------------------------------------- -- Limit (cost=0.00..25.23 rows=1 width=468) -- -> Seq Scan on users (cost=0.00..3784.00 rows=150 width=468) -- Filter: ((settings ->> 'twitter'::text) = 'johndoe30000'::text) -- (3 rows)

所以,在你不提早知道查詢哪一個json數據中的鍵或者查詢全部json路徑的狀況下,請確保你定義了GIN/GIST索引和使用@>(或者其餘有利於索引的操做符)

json轉化爲jsonb

若是你已經使用了json格式或者text格式的列存儲JSON數據,你能夠將他們轉化爲jsonb,於是你能夠依靠列索引。

sql
BEGIN; ALTER TABLE users ADD COLUMN preferences_jsonb jsonb DEFAULT '{}'; UPDATE users set preferences_jsonb = preferences::jsonb; ALTER TABLE users ALTER COLUMN preferences_jsonb SET NOT NULL; ALTER TABLE users RENAME COLUMN preferences TO preferences_json; ALTER TABLE users RENAME COLUMN preferences_jsonb TO preferences; -- Don't remove the column until you're sure everything is working. -- ALTER TABLE users DROP COLUMN preferences_json; COMMIT;

如今你已經知道了json是如何工做的,讓咱們看看在Ruby on Rails中是怎麼使用的。

在Ruby on Rails中使用jsonb

Rails從4.2版本開始支持jsonb,使用他跟使用stringtext類型的列同樣簡單,在下面的代碼中,你將看到如何添加jsonb類型的列到已經存在的表中。

ruby
# db/migrate/*_create_users.rb class CreateUsers < ActiveRecord::Migration def change enable_extension 'citext' create_table :users do |t| t.text :name, null: false t.citext :username, null: false t.jsonb :preferences, null: false, default: '{}' end add_index :users, :preferences, using: :gin end end # db/migrate/*_add_jsonb_column_to_users.rb class AddJsonbColumnToUsers < ActiveRecord::Migration def change add_column :users, :preferences, :jsonb, null: false, default: '{}' add_index :users, :preferences, using: :gin end end

注意,咱們已經定義了GIN類型的索引,若是你想對給出的路徑建立表達式索引,你必須使用execute。在這個例子中,Rails不知道怎麼使用ruby來轉化這個索引,因此你最好選擇將格式轉爲SQL。

ruby
# config/initializers/active_record.rb Rails.application.config.active_record.schema_format = :sql # db/migrate/*_add_index_to_preferences_path_on_users.rb class AddIndexToPreferencesPathOnUsers < ActiveRecord::Migration def change execute <<-SQL CREATE INDEX user_prefs_newsletter_index ON users ((preferences->>'newsletter')) SQL end end

你的模型不須要作任何配置。你只須要建立支持json序列化的記錄來提供對象。

ruby
user = User.create!({ name: 'John Doe', username: 'johndoe', preferences: { twitter: 'johndoe', github: 'johndoe', blog: 'http://example.com' } }) # Reload record from database to enforce serialization. user.reload # Show preferences. user.preferences #=> {"blog"=>"http://example.com", "github"=>"johndoe", "twitter"=>"johndoe"} # Get blog. user.preferences['blog'] #=> http://example.com

能夠看到全部的鍵都是以string形式返回。你也可使用通用的序列化方式,你就能夠經過符號來訪問JSON對象。

ruby
# app/models/user.rb class User < ActiveRecord::Base serialize :preferences, HashSerializer end # app/serializers/hash_serializer.rb class HashSerializer def self.dump(hash) hash.to_json end def self.load(hash) (hash || {}).with_indifferent_access end end

另外一個比較有意思的是ActiveRecord特性就是store_accessor。若是你更改一些屬性比較頻繁,你能夠建立accessor,這樣你能夠賦值給屬性來代替JSON傳值。這也使得數據驗證和建立表單更加簡單。所以,若是咱們建立一個表單來保存博客url、Github和Twitter帳戶,你能夠像下面這樣使用:

ruby
class User < ActiveRecord::Base serialize :preferences, HashSerializer store_accessor :preferences, :blog, :github, :twitter end

如今你能夠簡單的賦值給這些屬性了。

ruby
user = User.new(blog: 'http://example.org', github: 'johndoe') user.preferences #=> {"blog"=>"http://example.org", "github"=>"johndoe"} user.blog #=> http://example.org user.preferences[:github] #=> johndoe user.preferences['github'] #=> johndoe

定義了 store accessors 後,你能夠像正常其餘屬性同樣,定義數據驗證和建立表單

查詢jsonb列

如今是時候使用一些查詢操做。關於PostgreSQL的更多操做,請閱讀完整的文檔列表

同時,記得使用註釋你執行的查詢語句;這有助於你更好的去作索引優化。

訂閱新聞郵件的用戶

ruby
# preferences->newsletter = true User.where('preferences @> ?', {newsletter: true}.to_json)

對Ruby感興趣的用戶

ruby
# preferences->interests = ['ruby', 'javascript', 'python'] User.where("preferences -> 'interests' ? :language", language: 'ruby')

這個查詢不會用到列索引;若是你想查詢數組,請確保你建立了表達式索引。

ruby
CREATE INDEX preferences_interests_on_users ON users USING GIN ((preferences->'interests'))

設置了Twitter和Github帳號的用戶

ruby
# preferences->twitter AND preferences->github User.where('preferences ?& array[:keys]', keys: ['twitter', 'github'])

設置Twitter或Github帳號的用戶

ruby
# preferences->twitter OR preferences->github User.where('preferences ?| array[:keys]', keys: ['twitter', 'github'])

住在洛杉磯/加利福尼亞的用戶

ruby
# preferences->state = 'SP' AND preferences->city = 'São Paulo' User.where('preferences @> ?', {city: 'San Francisco', state: 'CA'}.to_json)

關於hstore

hstore列不容許嵌套的結構,它將全部的值以字符串形式存儲,因此必需要在數據庫層或者應用程序層將數據強制轉化爲字符串類型。而在json/jsonb類型的列上不會遇到這個問題,數值類型(integers/float),布爾類型,數組,字符串和空類型均可以接受,甚至你想的任何方式的數據嵌套。

所以推薦你儘早放棄hstore而去使用jsonb,但要記住的是你必須使用PostgreSQL 9.4以上版本才行。

我之前寫的hstore,想知道更多相關的內容就點擊查看。

總結

PostgreSQL是一個很是強大的數據庫,幸運的是ActiveRecord能跟上PostgreSQL的更新,爲jsonb和hstore特性引入了內置支持。

而像表達式索引這樣的支持也在不斷的改善。將ActiveRecord的序列化改成SQL沒什麼大不了的,但卻使的索引變得更加簡單。

ruby
# This doesn't exist, but it would be nice to have it! add_index :users, "(settings->>'github')", raw: true

在每個新版本中,使用Rails和PostgreSQL都比過去更加容易,變得更加出色。所以,嘗試使用最新的Rails版本,付出老是會很快獲得回報的。

相關文章
相關標籤/搜索