Access paths

Ditto Server

Primary access paths are determined internally in the subscription server with DQL only supplying relevant predicates. The one current exception is the countScan (see below). The predicates and details of plan type selection can be seen in the scan operator in the EXPLAIN output (plan) for a statement.

Ditto SDK

With the event of indexing the Query planner now has multiple options when deciding how to execute a query. The details of the access path chosen can be seen in the EXPLAIN output (plan) for the statement. The access path selected is determined by the statement’s filters, ordering and directives.

Types of access paths

Index, intersect and union scans may be combined into a hierarchy.

Collection scan

A collection scan retrieves all the (complete) documents in a collection sequentially without any guarantee of order. A collection scan is the fall-back when a statement can’t be implemented with another access path. It can be forced with an empty or null index directive. Example:
    {
      "#operator": "scan",
      "collection": "test",
      "datasource": "default",
      "descriptor": {
        "path": {
          "_id": "query_details",
          "full_scan": {}
        }
      }
    }
If any portion of a query is to be served by a collection scan then no other scan will be used.

Index scan

An index scan is a scan of an index on the collection which returns document IDs matching the noted spans. (A “span” is a filter for an index key and indicates a range of values to return.) An index scan is picked based on filtering and possibly ordering present in the statement. A specific index can be forced if it still applies to the statement via the index directive. Example (filter: WHERE field1 = "test filter"):
    {
      "#operator": "indexScan",
      "collection": "test",
      "datasource": "default",
      "desc": {
        "index": "ix1",
        "spans": [
          {
            "index_key": {
              "direction": "asc",
              "include_missing": true,
              "key": [
                "field1"
              ]
            },
            "range": {
              "high": {
                "included": true,
                "value": "test filter"
              },
              "low": {
                "included": true,
                "value": "test filter"
              }
            }
          }
        ]
      }
    }

Intersect scan

An intersect scan combines index scans and union scans and returns only document IDs produced by all its children (logical AND). It is used support multiple filters on different indexed fields in a statement. It cannot be forced but the maximum number of children is controlled by the intersects directive. Example (filter: WHERE field1 = "test filter" AND field2 > 0:
    {
      "#operator": "intersectScan",
      "children": [
        {
          "#operator": "indexScan",
          "collection": "test",
          "datasource": "default",
          "desc": {
            "index": "ix1",
   ...
        },
        {
          "#operator": "indexScan",
          "collection": "test",
          "datasource": "default",
          "desc": {
            "index": "ix2",
    ...
        }
      ]
    }

Union scan

A union scan combines index scans and intersect scans and returns only unique document IDs across all its children (logical OR). It is used to support multiple ranges of a single index and for multiple optional condition sets (OR) in the statement. It cannot be forced. IN filters with a list length to the lower limit of the number of remaining scans or maximum in-list-element directive, may be effected with a union scan. Example (filter: WHERE field1 = "test filter" OR field2 <= 123:
    {
      "#operator": "unionScan",
      "children": [
        {
          "#operator": "indexScan",
          "collection": "test",
          "datasource": "default",
          "desc": {
          "index": "ix1",
    ...
        }
        },
        {
          "#operator": "indexScan",
          "collection": "test",
          "datasource": "default",
          "desc": {
            "index": "ix2",
    ...
        }
      ]
    }

Count scan

This is only produced for the Ditto Server only for the specialised query SELECT COUNT(*) FROM collection. It skips reading the collection and accesses the collection count statistic directly. It cannot be forced but can be avoided with the addition of a non-trivial predicate, e.g. WHERE _id IS NOT MISSING (to not influence the result). Example:
    {
      "#operator": "countScan",
      "alias": "test",
      "collection": "test",
      "count_aliases": [
        "count(true)"
      ],
      "datasource": "default"
    }

Fetching documents

When a collection scan is not the access path and additional document fields are needed, a fetch operator is used to retrieve full documents based on the document IDs produced earlier in the plan. A collection scan retrieves the documents itself so this step is not necessary. Example:
    {
      "#operator": "fetch",
      "collection": "test",
      "datasource": "default"
    }

Covering

When a query makes use of only fields present in indices that are scanned in its access paths the planner can recognise this and skip fetching full documents, instead indicating that index scans should return their key field values too. Covering may be carried through intersect and union scans, but all branches of a union scan must cover equally (i.e. all the same index key fields must be present in every branch). Scans that are covering are marked as such in the statement’s EXPLAIN output (plan). Example:
    {
      "#operator": "indexScan",
      "alias": "test",
      "collection": "test",
->    "covering": true,
      "datasource": "default",
      "desc": {
        "index": "ix1",
    ...
    }