github.com/cockroachdb/cockroach@v20.2.0-alpha.1+incompatible/docs/RFCS/20171005_jsonb_encoding.md (about)

     1  - Feature Name: JSONB Encoding
     2  - Status: accepted
     3  - Start Date: 2017-10-05
     4  - Author: Justin Jaffray
     5  - RFC PR: #19062
     6  - Cockroach Issue: #2969
     7  
     8  # Summary
     9  
    10  Provide a result value and kv encoding for JSONB values as described in the
    11  [scoping RFC](https://github.com/cockroachdb/cockroach/pull/18739). The
    12  motivation and use-cases for including JSONB were covered in the scoping
    13  RFC. As a result of this change, users will be able to manipulate JSONB values
    14  both in SQL result values and in columns within tables.  We opt to disallow key
    15  encoding of JSONB values at this time, due to complications regarding the
    16  key encoding of numeric values.
    17  
    18  The format will be optimized to reduce unnecessary decoding of values. This is
    19  because as opposed to array columns, we expect the common case of this format
    20  to be extracting a subset of the fields, rather than the entire thing at once.
    21  
    22  # Motivation
    23  
    24  Covered in the scoping RFC.
    25  
    26  # Reference-level explanation
    27  
    28  There are several axes upon which a binary encoding for JSONB in CockroachDB
    29  should be evaluated, among these are
    30  
    31  * speed of access/decoding,
    32  * speed of encoding,
    33  * size of encoding (and compressibility),
    34  * and flexibility.
    35  
    36  In addition, as CockroachDB already provides a way to handle structured data
    37  with a known set of fields (the relational model!), we should have an eye
    38  towards the opposite use-case - unstructured data with an unknown (possibly
    39  large) set of fields.
    40  
    41  The proposed value encoding is to use that of Postgres, modified as appropriate
    42  to fit into CockroachDB.  This RFC will first describe the format in detail,
    43  then go into the alternatives considered and why they were rejected.
    44  
    45  ## JSONB Encoding
    46  
    47  A JSON value can be one of seven types:
    48  
    49  * `true`,
    50  * `false`,
    51  * `null`,
    52  * a UTF-8 `string`,
    53  * an arbitrary-precision `number`,
    54  * an `array` of zero or more JSON values,
    55  * or an `object` consisting of key-value pairs, where the keys are unique
    56    strings and the values are JSON values.
    57  
    58  Every JSON value save for the root has a corresponding header called a *JEntry*,
    59  which is a 32-bit value encoding the following information:
    60  ```
    61  0 000 0000000000000000000000000000
    62  ^ ^   ^
    63  │ │   └─ 28 bits denoting either the length of this value, or its end+1 offset from
    64  │ │       the beginning of the innermost container this value is within.
    65  │ └─ 3 bits denoting the type of the value (six possible values - one of the 5
    66  │     scalar types or a container)
    67  └─ 1 bit denoting whether this entry stores a length or an offset.
    68  ```
    69  
    70  The reason for the length vs. offset flag is explained in [this
    71  comment](https://github.com/postgres/postgres/blob/90627cf98a8e7d0531789391fd798c9bfcc3bc1a/src/include/utils/jsonb.h#L113-L138).
    72  It's not immediately clear that this compressibility trade-off is relevant for
    73  CockroachDB, in particular given that we use different compression algorithms than
    74  Postgres.
    75  The [rationale section](#rationale) includes the results of some rough experiments with
    76  Snappy (the compression algorithm used in CockroachDB), but more detailed
    77  experiments will be needed.
    78  
    79  A *container header* is a 32-bit value with the following format:
    80  ```
    81  000 00000000000000000000000000000
    82  ^   ^
    83  │   │
    84  │   │
    85  │   └─ 29 bits denoting the number of elements in an array, or the number of
    86  │       key-value pairs in an object.
    87  └─ 3 bits denoting the type of this container (`array`, `object`, or `scalar`)
    88  ```
    89  
    90  The root JSON object is always a container, but it might be a `scalar`
    91  container (defined below).
    92  
    93  ### `true`, `false`, `null`
    94  
    95  The values `true`, `false`, and `null` are expressed entirely by their type.
    96  Their encoding is the empty sequence of bytes.
    97  
    98  ### `string`
    99  
   100  `string`s are encoded as normal UTF-8 strings.
   101  
   102  ### `number`
   103  
   104  In Postgres, `number` maps on to the Postgres `NUMERIC` type. Thus, this
   105  differs from JavaScript's `number` type as Postgres JSONB `number`s are
   106  arbitrary-precision.
   107  In CockroachDB, `number` will be encoded using CockroachDB's `DECIMAL` encoding.
   108  
   109  We can experiment, using the remaining free type tags, with possibly encoding
   110  numbers as plain int64s or float64s where possible, to see if this gives a
   111  significant improvement in either encoding size or decoding speed.
   112  
   113  ### `array`
   114  
   115  An `array` is stored as
   116  * a container header tagged as an array, containing the number of elements in
   117    the array,
   118  * the JEntry header of every element, with every `$STRIDE`th JEntry storing an
   119    offset, and every other one storing a length, and then finally
   120  * the encodings of the elements themselves.
   121  
   122  ### `object`
   123  
   124  An `object` is stored as
   125  * a container header tagged as an object, containing the number of
   126    key-value pairs in the object, then
   127  * the JEntry header of every key (which are sorted, unique strings) followed by
   128    the JEntry header of every value, with every `$STRIDE`th JEntry storing an
   129    offset, and every other one storing a length, then finally
   130  * the encoding of every key followed by the encoding of every value (with the
   131    `i`th key mapping to the `i`th value).
   132  
   133  ### `scalar`
   134  
   135  The `scalar` container type exists so that the root value has a place to put
   136  its JEntry (which is needed if it isn't an `object` or an `array`). It only
   137  occurs at the root and appears only if the root value is `true`, `false`,
   138  `null`, a `string`, or a `number`.  It contains exactly one value and its
   139  encoding consists of
   140  * a container header tagged as a scalar,
   141  * the JEntry for the singular value contained within,
   142  * the encoding of the contained value.
   143  
   144  ## Examples
   145  
   146  ### encode(`null`)
   147  
   148  ```
   149  0x10000000 <- scalar container header
   150  0x40000000 <- JEntry header signifying null type and 0 length
   151  (no encoding)
   152  ```
   153  
   154  ### encode(`[true, "hello", {"a": "b"}]`)
   155  
   156  (This glosses over numerics because their particular encoding already exists
   157  within CockroachDB and is not very enlightening for this discussion.)
   158  
   159  ```
   160  0x40000003   <- array container header (3 elements)
   161  0x30000000   <- JEntry header of true
   162  0x00000005   <- JEntry header of "hello"
   163  0x5000000e   <- JEntry header of {"a": "b"} (whose encoding is 0x0e bytes long)
   164  (encoding of true, which is empty)
   165  0x68656c6c6f <- encoding of "hello"
   166  0x20000001   <- object container header (1 key-value pair)
   167  0x30000001   <- JEntry header of "a"
   168  0x30000001   <- JEntry header of "b"
   169  0x61         <- encoding of "a"
   170  0x62         <- encoding of "b"
   171  ```
   172  
   173  ## Implementation
   174  
   175  Implementation of JSONB will follow three major steps:
   176  * begin with only JSONB result values which are ignorant to any encoding,
   177    (similar to how CockroachDB supported array values before it supported array
   178    column types),
   179  * implement the encoding to-and-from the existing naive JSONB result values,
   180  * modify the JSONB result values to take advantage of the encoding.
   181  
   182  As the implementation of the JSONB result values can easily be changed in the
   183  future (as opposed to the encoding, which cannot), it's less important to nail
   184  down the precise implementation of them ahead of time, and they can be tuned
   185  and optimized as time permits.
   186  
   187  ## Rationale
   188  
   189  ### Offset-vs-length
   190  
   191  Some cursory benchmarks with Snappy imply a modest but significant improvement
   192  in compressibility with values encoded with lengths vs. offsets (string
   193  encoding and BSON included for interest's sake):
   194  
   195  #### 1000 JSON objects with 200 keys, no keys in common across objects
   196  ```
   197  compression of jsonb-len: 6838008B -> 4959971B (72.54%)
   198  compression of jsonb-off: 6838008B -> 5779418B (84.52%)
   199  compression of json-text: 7025581B -> 6596903B (93.90%)
   200  compression of bson:      7566787B -> 5651655B (74.69%)
   201  ```
   202  
   203  #### 1000 JSON objects with 200 keys, all keys in common across objects
   204  ```
   205  compression of jsonb-len: 6944660B -> 3860875B (55.59%)
   206  compression of jsonb-off: 6944660B -> 4911796B (70.73%)
   207  compression of json-text: 7249122B -> 5841104B (80.58%)
   208  compression of bson:      7726628B -> 4713029B (61.00%)
   209  ```
   210  
   211  Comparable results were seen comparing the compression of RocksDB SSTables, so
   212  this seems like a reasonable concern.
   213  
   214  `$STRIDE` is a tunable value, which will have to be chosen after some
   215  benchmarking of the compressibility vs. performance trade-off, along with
   216  information from customers on the shape of their JSON data.
   217  
   218  Initially we will set it to be infinite, always using lengths. If this proves
   219  to be bad for lookup performance we can tweak it as need be.
   220  
   221  ## Alternative Encodings
   222  
   223  ### String-encoding
   224  
   225  The simplest way we could represent JSON is simply as text.  This is the way
   226  the SQL/JSON spec says it should be done and was how Postgres's initial
   227  implementation worked (their `JSON` as opposed to `JSONB`), however, this has
   228  two primary undesirable properties. First, it's quite bloated (and not very
   229  compressible under Snappy).  More problematic however is the access speed.
   230  Extracting a field from a JSON object stored this way requires parsing the
   231  object, something we would like to avoid in the common case.
   232  
   233  ### BSON
   234  
   235  BSON is the encoding used in MongoDB.
   236  Its design is documented at [bsonspec.org](http://bsonspec.org/spec.html).
   237  This format is proven and well-understood.
   238  It seems optimized for documents with small sets of keys, as such, it doesn't
   239  contain an index of entries at the beginning of a document, and it doesn't
   240  mandate that keys be sorted. This makes sense, as Mongo doesn't support the type of
   241  inverted indexing Postgres supports and thus expects users to have a more
   242  structured schema with a small set of keys.
   243  As we are designing for use-cases more similar to Postgres, these
   244  considerations don't seem as valuable for us.
   245  
   246  # Unresolved questions