注意事項

ベンチマークは非常に難しく、ここでは応答時間のパターンに着目し、特にPostgresElasticsearchの両方に対して実行できるORMクエリに着目して、特定の応答のパターンと条件を特定することを目的としています。

常に、ご自身のデータに対して独自のベンチマークを実行する必要があります。ランダムに生成されたデータでは、(データ量、カーディナリティ、リレーションシップ、データの歪みなど)多くの要素が考慮されないためです。

絶対的な数値に注目しないでください。
…Elasticsearchでは、デフォルトで事実上すべてのプロパティにインデックスが作成されます。通常、OLTPデータベースではそれを行うことはできません(テーブルのすべての列にインデックスを作成することはできません)。

設定

顧客、製品、注文、注文明細を移入します(サンプルアプリケーションの修正版を使用: example-kotlin-web with

  • 注文:500,000件
  • 顧客:100,000件
  • 製品:30,000件
  • 注文明細:5,263,023件

注文は、2015年の12ヶ月にわたってランダムに分散されたorderDateと、ランダムに分散されたorderStatusで生成されます。

これはPostgresデータベースにロードされ、さらにElasticsearchにインデックスされます。これは比較的小さなデータベースですが、クエリの様々な側面(注文日の選択性と非正規化)を試すには十分です。

Postgresでは、注文日とステータスにインデックスを追加しましたが、それ以外はPostgresとElasticsearchの両方でデフォルトのままにしました。これは最良の数値を得ることが目的ではなく、述語の選択性の変化と非正規化で見られる変化を見ることです。

Elasticsearchの場合、これは非スコアリングクエリ(フィルタクエリのみ)であることに注目することも興味深い点です。この単純なクエリに対して、Postgresとどの程度比較できるかを見てみるのは良いでしょう。

クエリ

PostgresとElasticsearchの両方に対して単純なORMクエリを実行します…

特定のorderDate以降のNEW注文の上位100件を検索します。ブール値を渡して、PostgresデータベースまたはElasticsearchに対してクエリを実行するかを切り替えることができます。

// top 100 new orders after (given date)
// ... run against Postgres or ElasticSearch
// ... based on boolean - asDocStore

return Order.find.where()
  .status.in(Order.Status.NEW)
  .orderDate.after(useDate)
  .order()
    .orderDate.desc()

  .setMaxRows(100)
  .setUseDocStore(asDocStore)
  .findList()

SQLクエリ

SELECT
  t0.id                  c0,
  t0.status              c1,
  t0.order_date          c2,
  t0.ship_date           c3,
  t0.version             c4,
  t0.when_created        c5,
  t0.when_modified       c6,
  t0.customer_id         c7,
  t0.shipping_address_id c8
FROM orders t0
WHERE t0.status IN (?) AND t0.order_date > ?
ORDER BY t0.order_date DESC, t0.id
LIMIT 100; --bind(NEW,2015-12-08)

Elasticsearchクエリ

{
  "size": 100,
  "sort": [ { "orderDate": { "order": "desc" } } ],
  "query": {
    "filtered": {
      "filter": {
        "bool": {
          "must": [
            { "terms": { "status": [ "NEW" ] } },
            { "range": { "orderDate": { "gt": 1450263600000 } } }
          ]
        }
      }
    }
  }
}

実行モード1

この最初の実行モードでは、注文日を12月の日付に制限することで、クエリを合理的に選択的にします。つまり、上位100件に必要なソートとフィルタのコストはそれほど高くありません。

DB mean: 10.2    std dev: 2.34    min:6    max:18
ES mean: 8.77    std dev: 1.08    min:7    max:14

ElasticsearchとPostgresはここではほぼ同じレベルにあり、ほぼ同じで、素晴らしいことです。

アプリケーションでElasticsearchにこのクエリを実行させることができますか?はい。

実行モード2 - 非正規化の効果

非正規化のメリットが有効(データベースのマテリアライズドビューの代替としてのElasticsearch)

Elasticsearchは非常に優れたパフォーマンスを示しましたが、実際には、返されたオブジェクトには、Postgresクエリにはなかった追加の非正規化された情報が含まれていました。

具体的には、Elasticsearchの注文インデックスには、顧客(id、名前)注文明細(*、製品(id、SKU、名前))に関する追加の詳細が含まれています。インデックスが返しているデータに一致するようにORMクエリを変更するには、顧客名と注文明細(いくつかの追加の結合を含む)を取得する必要があります。

// change the ORM query to additionally fetch
// customer and order details to match the index
// which has been denormalised
query
  .fetch("customer", "id,name")
  .fetch("details", "*")
  .fetch("details.product","id,sku,name")

そのため、顧客、注文明細、製品に結合するため、データベースに対するORMクエリの費用が高くなります。

Elasticsearchインデックスでは、これらの「結合」はインデックスに非正規化されているため、以前のように実行されます。データベースでは、同様の効果を得るためにマテリアライズドビューを作成できます。

DB mean: 56.4    std dev: 5.95    min:48   max:95
ES mean: 8.75    std dev: 1.01    min:7    max:13

したがって、必要な場合のElasticsearchインデックスの非正規化は有効です(これはかなり自明ですが、常に確認することが重要です)。

データベースのマテリアライズドビューは同様の効果をもたらしますが、Elasticsearchを使用することでデータベースの負荷を軽減できるという利点もあります。

実行モード3 - 選択性が低い

この実行モードでは、顧客や注文明細などの取得を省略しますが(実行モード1に戻りますが)、使用する注文日を変更して、年の最初の100日にランダムに選択されるようにします。つまり、クエリは選択性がはるかに低くなり、上位100件を取得するために、結果から多くの行をソートしてフィルタリングする必要があります。

DB mean: 51.3    std dev: 3.71    min:44   max:60
ES mean: 14.0    std dev: 2.00    min:10   max:22

したがって、予想どおり、両方のクエリの速度は実行モード1よりも低下します(上位100件を取得するために、多くの結果をソートしてフィルタリングする必要があるため)。Elasticsearchはこの上位100件の比較的選択性の低いクエリでかなり優れたパフォーマンスを示しており、それほど遅くはありませんでした。

Elasticsearchはページングクエリ結果を対象としていますが、これは非スコアリングフィルタのみのクエリであることを考えると、非常に印象的な結果です。

付録 - Beanマッピング

注文エンティティBeanのマッピングは次のとおりです。

// this is Kotlin
// .. the @DocEmbedded is what you should look at

@DocStore
@Entity
@Table(name = "orders")
class Order : BaseModel() {

  ...

  var status: Status = Status.NEW;

  @DocEmbedded(doc = "id,name")
  @ManyToOne @NotNull
  var customer: Customer? = null;

  @DocEmbedded(doc = "*,product(id,sku,name)")
  @OneToMany(mappedBy = "order", cascade = arrayOf(CascadeType.PERSIST))
  @OrderBy("id asc")
  var details: MutableList<OrderDetail> = ArrayList();

付録:インデックスマッピング

Elasticsearch注文インデックスのマッピング。列挙型は自動的にコードとして扱われます(したがって、statusはnot_analyzedです)。それぞれの@DocEmbeddedアノテーションからの顧客と詳細のマッピング。

{
  "mappings" : {
    "order" : {
      "properties" : {
        "status": { "type": "string", "index": "not_analyzed" },
        "orderDate": { "type": "date" },
        "shipDate": { "type": "date" },
        "customer" : {
          "properties" : {
            "id": { "type": "long" },
            "name": { "type": "string" }
          }
        },
        "details" : {
          "type" : "nested",
          "properties" : {
            "id": { "type": "long" },
            "orderQty": { "type": "integer" },
            "shipQty": { "type": "integer" },
            "unitPrice": { "type": "double" },
            "product" : {
              "properties" : {
                "id": { "type": "long" },
                "sku": { "type": "string" },
                "name": { "type": "string" }
              }
            },
            "version": { "type": "long" },
            "whenCreated": { "type": "date" },
            "whenModified": { "type": "date" }
          }
        },
        "version": { "type": "long" },
        "whenCreated": { "type": "date" },
        "whenModified": { "type": "date" }
      }
    }
  }
}

付録:注文例

Elasticsearchインデックスの例。

{
  "_index": "order_v1",
  "_type": "order",
  "_id": "72033",
  "_version": 1,
  "found": true,
  "_source": {
    "status": "NEW",
    "orderDate": 1446462000000,
    "shipDate": 1446721200000,
    "customer": {
      "id": 31772,
      "name": "big 31775"
    },
    "details": [
      {
        "id": 759300,
        "orderQty": 7,
        "unitPrice": 78,
        "product": {
          "id": 9697,
          "sku": "A1672",
          "name": "A1672"
        },
        "version": 1,
        "whenCreated": 1460670857630,
        "whenModified": 1460670857630
      },
      {
        "id": 759301,
        "orderQty": 8,
        "unitPrice": 94,
        "product": {
          "id": 18351,
          "sku": "E2322",
          "name": "E2322"
        },
        "version": 1,
        "whenCreated": 1460670857630,
        "whenModified": 1460670857630
      },
      {
        "id": 759302,
        "orderQty": 11,
        "unitPrice": 73,
        "product": {
          "id": 12358,
          "sku": "B2332",
          "name": "B2332"
        },
        "version": 1,
        "whenCreated": 1460670857630,
        "whenModified": 1460670857630
      },
      {
        "id": 759303,
        "orderQty": 14,
        "unitPrice": 33,
        "product": {
          "id": 11847,
          "sku": "B1821",
          "name": "B1821"
        },
        "version": 1,
        "whenCreated": 1460670857630,
        "whenModified": 1460670857630
      },
      {
        "id": 759304,
        "orderQty": 11,
        "unitPrice": 34,
        "product": {
          "id": 14230,
          "sku": "C2203",
          "name": "C2203"
        },
        "version": 1,
        "whenCreated": 1460670857630,
        "whenModified": 1460670857630
      },
      {
        "id": 759305,
        "orderQty": 17,
        "unitPrice": 95,
        "product": {
          "id": 20625,
          "sku": "F2595",
          "name": "F2595"
        },
        "version": 1,
        "whenCreated": 1460670857630,
        "whenModified": 1460670857630
      },
      {
        "id": 759306,
        "orderQty": 4,
        "unitPrice": 89,
        "product": {
          "id": 11653,
          "sku": "B1627",
          "name": "B1627"
        },
        "version": 1,
        "whenCreated": 1460670857630,
        "whenModified": 1460670857630
      },
      {
        "id": 759307,
        "orderQty": 2,
        "unitPrice": 42,
        "product": {
          "id": 2237,
          "sku": "C450",
          "name": "C450"
        },
        "version": 1,
        "whenCreated": 1460670857630,
        "whenModified": 1460670857630
      },
      {
        "id": 759308,
        "orderQty": 1,
        "unitPrice": 60,
        "product": {
          "id": 3404,
          "sku": "D726",
          "name": "D726"
        },
        "version": 1,
        "whenCreated": 1460670857630,
        "whenModified": 1460670857630
      },
      {
        "id": 759309,
        "orderQty": 18,
        "unitPrice": 80,
        "product": {
          "id": 19433,
          "sku": "F1403",
          "name": "F1403"
        },
        "version": 1,
        "whenCreated": 1460670857630,
        "whenModified": 1460670857630
      },
      {
        "id": 759310,
        "orderQty": 9,
        "unitPrice": 53,
        "product": {
          "id": 22608,
          "sku": "G2577",
          "name": "G2577"
        },
        "version": 1,
        "whenCreated": 1460670857630,
        "whenModified": 1460670857630
      }
    ],
    "version": 1,
    "whenCreated": 1460670857630,
    "whenModified": 1460670857630
  }
}