#リレーショナルテーブル

0 フォロワー · 14 投稿

リレーショナルモデルは、各行を識別する一意のキーを使用して、列と行の1つ以上のテーブル(または「リレーション」)にデータを編成します。 行は、レコードまたはタプルと呼ぶこともあります。 さらに データプラットフォーム (IDP)は、マルチモデルストレージの概念の一部としてリレーショナルテーブルをサポートします。 IDPは、リレーショナルテーブルへのSQLおよびAPIアクセスを提供します。 ドキュメント

記事 Hiroshi Sato · 3月 10, 2025 1m read

これは InterSystems FAQ サイトの記事です。

大量のレコードが含まれるテーブルに対して、select count(*) from テーブル名の応答が遅い、または応答がタイムアウトする場合には、ビットマップ・エクステント・インデックスを追加することで問題を解消することができます。 

ビットマップ・エクステント・インデックスを追加する方法は、以下をご参照ください。 

ビットマップ・エクステント・インデックスの追加方法

クラス定義に以下のような定義を行うことで、ビットマップ・エクステント・インデックスを追加することもできます。

Index IndexName [ Extent, Type = bitmap ];
0
0 137
記事 Hiroshi Sato · 12月 18, 2024 1m read

Caché技術ガイドのいくつかをIRIS版に書き換えた文書を作成しましたので公開します。

IRISファーストステップガイド
IRISBasicTechnologyGuide

- ObjectScript操作ガイド

- オブジェクト操作ガイド

- 多次元データエンジンの概念およびアーキテクチャー
IRIS SQLガイド

0
2 123
記事 Tomohiro Iwamoto · 12月 19, 2023 11m read

Debeziumをご存じでしょうか?

グローバルサミット2023にて、Debeziumを題材としたセッション「Near Real Time Analytics with InterSystems IRIS & Debezium Change Data Capture」がありましたので、ご覧になられた方もおられるかと思います。

ご興味がありましたら、グローバルサミット2023の録画アーカイブをご覧ください。

FAQによると、"dee-BEE-zee-uhm"(ディビジウム..ですかね)と読むそうです。元素周期表のように複数のDB(s)を束ねる、というニュアンスみたいです。

CDC(Change data capture)という分野のソフトウェアです。

外部データベースでの変更を追跡して、IRISに反映したいという要望は、インターオペラビリティ機能導入の動機のひとつになっています。一般的には、定期的にSELECT文のポーリングをおこなって、変更対象となるレコード群(差分。対象が少なければ全件)を外部システムから取得する方法が、お手軽で汎用性も高いですが、タイムスタンプや更新の都度に増加するようなバージョンフィールドが元テーブルに存在しない場合、どうしても、各ポーリング間で重複や見落としがでないように、受信側で工夫する必要があります。また、この方法ではデータの削除を反映することはできませんので、代替案として削除フラグを採用するといったアプリケーションでの対応が必要になります。

CDCは、DBMSのトランザクションログをキャプチャすることで、この課題への解決策を提供しています。DebeziumはRedHatが中心となっているCDCのオープンソースプロジェクトです。

CDCの何が良いのか

CDCにはいくつかの利点があります。

  • ポーリングではないので、更新が瞬時に伝わる

  • DELETEも反映できる

  • SourceになるDBMSに対して非侵襲的

    テーブル定義を変更したり専用のテーブルを作成しなくて済む。パフォーマンスへの影響が軽微。

    先進医療っぽい表現ですね。対象に与える影響が軽微というニュアンスだと思います。

  • 受信側(アプリケーション側)の設計が楽

下記は受信側の仕組みに依存する話ですが、例えばIRISのRESTサービスで受信する場合

  • ひとつのハンドラ(Restのディスパッチクラス)で、複数のテーブルを処理できる

    このことはSQLインバウンドサービスがテーブル単位であることと対照的です。

一方、トランザクションログのメカニズムは各DBMS固有なので、DBMSやそのバージョン毎にセットアップ手順、振る舞い、特性が異なる可能性があるというマイナス面があります。

セットアップ作業は、SQLインバウンドアダプタほど簡単ではありません。

Kafkaのコネクタとしての用法

DebeziumはKafkaのSourceコネクタとして使用する用法が一般的です。

引用元: https://debezium.io/documentation/reference/stable/architecture.html

Kafkaのコネクタとしての用法は本稿では扱いません。

今回のメインテーマはKafkaではありませんが、関連するいくつかのKafka用語を確認をしておきたいと思います。

ProducerとConsumer

Kakfaにメッセージを送信するデータの発生元のことをProducer、メッセージを消費する送信先のことをConsumerと呼びます。

SourceとSink

外部システムとの連携用のフレームワークをKafkaコネクトと呼びます。Kafkaコネクトにおいて、外部システムと接続する部分をコネクタと呼び、Producer 側の コネクタ は Sourceコネクタ、Consumer 側の コネクタ は Sinkコネクタと、それぞれ呼びます。

DebeziumはKafkaのSourceコネクタです。

Debeziumのスタンドアロン環境

Kafkaが提供するエンタープライズ級の機能を使いたければ、Kafkaの構成・運用を含めて検討する価値があります。一方、そうでない場合、Debeziumを単体のサーバで動作させることが出来ます。

Debezium Serverと言います。その他の選択肢として、自作のJavaアプリケーションに組み込む方法もあります。

随分とシンプルな構成になります。

KafkaのSinkコネクタを経由しなくても、Debezium自身が様々な送信先に対応しています。Debeziumから見ると、Kafkaは送信先のひとつという位置づけです。

例えば、「POSTGRES上でのデータ更新をCDCして、その内容をhttp serverに送信」したい場合、 POSTGRES用のSourceコネクタと、http Clientを使うことになります。

Debeziumは、SourceとしてこれらのDBMSに対応しています。

残念ながらIRISはSourceになれません。IRISからIRISへのデータの同期であれば非同期ミラリングがお勧めです。

Debezium Serverの起動

今回使用するソースコード一式はこちらにあります。 IRIS環境はコミュニティエディションにネームスペースMYAPPの作成と、3個の空のテーブル作成(01_createtable.sqlを使用)を行ったものになります。

$ git clone https://github.com/IRISMeister/DebeziumServer-IRIS
$ cd DebeziumServer-IRIS
$ cd postgres   (POSTGRESを試す場合。以降POSTGRESを使用します)
あるいは
$ cd mysql      (MYSQLを試す場合)
$ ./up.sh

正常に起動した場合、3個のサービスが稼働中になります。

$ docker composeps ps
NAME                         IMAGE                       COMMAND                                                     SERVICE           CREATED          STATUS                    PORTS
iris                         postgres-iris               "/tini -- /iris-main --ISCAgent false --monitorCPF false"   iris              12 minutes ago   Up 12 minutes (healthy)   2188/tcp, 53773/tcp, 0.0.0.0:1972->1972/tcp, :::1972->1972/tcp, 54773/tcp, 0.0.0.0:52873->52773/tcp, :::52873->52773/tcp
postgres-debezium-server-1   debezium/server:2.4         "/debezium/run.sh"                                          debezium-server   12 minutes ago   Up 12 minutes             8080/tcp, 8443/tcp, 8778/tcp
postgres-postgres-1          debezium/example-postgres   "docker-entrypoint.sh postgres"                             postgres          12 minutes ago   Up 12 minutes             0.0.0.0:5432->5432/tcp, :::5432->5432/tcp

動作確認

初期状態を確認します。起動直後に、POSTGRES上の既存のレコード群がIRISに送信されますのでそれを確認します。端末を2個ひらいておくと便利です。以下(端末1)をPOSTGRESの, (端末2)をIRISのSQL実行に使用します。

(端末1 PG)

$ docker compose exec -u postgres postgres psql
psql (15.2 (Debian 15.2-1.pgdg110+1))
Type "help" for help.

postgres=# select * from inventory.orders;
  id   | order_date | purchaser | quantity | product_id
-------+------------+-----------+----------+------------
 10001 | 2016-01-16 |      1001 |        1 |        102
 10002 | 2016-01-17 |      1002 |        2 |        105
 10003 | 2016-02-19 |      1002 |        2 |        106
 10004 | 2016-02-21 |      1003 |        1 |        107
(4 rows)

postgres=# select * from inventory.products;
 id  |        name        |                       description                       | weight
-----+--------------------+---------------------------------------------------------+--------
 101 | scooter            | Small 2-wheel scooter                                   |   3.14
 102 | car battery        | 12V car battery                                         |    8.1
 103 | 12-pack drill bits | 12-pack of drill bits with sizes ranging from #40 to #3 |    0.8
 104 | hammer             | 12oz carpenter's hammer                                 |   0.75
 105 | hammer             | 14oz carpenter's hammer                                 |  0.875
 106 | hammer             | 16oz carpenter's hammer                                 |      1
 107 | rocks              | box of assorted rocks                                   |    5.3
 108 | jacket             | water resistent black wind breaker                      |    0.1
 109 | spare tire         | 24 inch spare tire                                      |   22.2

(9 rows)
postgres=# select * from inventory.customers;
  id  | first_name | last_name |         email
------+------------+-----------+-----------------------
 1001 | Sally      | Thomas    | sally.thomas@acme.com
 1002 | George     | Bailey    | gbailey@foobar.com
 1003 | Edward     | Walker    | ed@walker.com
 1004 | Anne       | Kretchmar | annek@noanswer.org
(4 rows)

postgres=# \q
$

IRIS上のレコードは下記のコマンドで確認できます。POSTGRES上のレコードと同じになっているはずです。

(端末2 IRIS)

$ docker compose exec iris iris sql iris -Umyapp
[SQL]MYAPP>>set selectmode=odbc
[SQL]MYAPP>>select * from inventory.orders
         出力は省略
[SQL]MYAPP>>select * from inventory.products
[SQL]MYAPP>>select * from inventory.customers

次に、POSTGRESで各種DMLを実行します。

(端末1 PG)

update inventory.orders set quantity=200 where id=10001;
UPDATE 1
postgres=# delete from inventory.orders where id=10002;
DELETE 1
insert into inventory.orders (order_date,purchaser,quantity,product_id) values ('2023-01-01',1003,10,105);
INSERT 0 1
update inventory.products set description='商品説明' where id=101;
UPDATE 1

その結果がIRISに伝わり反映されます。

(端末2 IRIS)

[SQL]MYAPP>>select * from inventory.orders
3.      select * from inventory.orders

id      order_date      purchaser   quantity    product_id
10001   2016-01-16      1001        300         102
10003   2016-02-19      1002        2           106
10004   2016-02-21      1003        1           107
10005   2023-01-01      1003        10          105

4 Rows(s) Affected

[SQL]MYAPP>>select * from inventory.products where id=101
4.      select * from inventory.products where id=101

id      name    description     weight
101     scooter 商品説明        3.14

1 Rows(s) Affected

IRIS側の仕組み

Debezium Serverのhttp clientは、指定したエンドポイントにREST+JSON形式で内容を送信してくれます。エンドポイントにIRISのRESTサービスを指定することで、IRISでその内容をパースし、必要な処理を実行(今回は単純にSQLの実行)しています。

INSERT時には、こちら、UPDATE時には、こちらのようなJSONがPOSTされます。

payload.opにPOSTGRESへの操作の値であるc:Create, u:Update, d:Delete, r:Readが伝わりますので、その内容に基づいて、IRISのRESTディスパッチャークラス(Dispatcher.cls)にて、SQL文を組み立てて実行しています。

r:Readは、初回接続時に実行されるスナップショット取得作業の際に既存のレコード群を読み込み(READ)、それらが送信される場合に使用されます。詳細はこちらをご覧ください。

Debezium Serverについて

Debezium Serverの詳細は公式ドキュメントをご覧ください。

ドキュメントを見ると大量のコーディング例(Java)と構成例が載っており、これ全部理解してプログラムを書かないと使えないのかと思ってしまいますが、幸いコンテナイメージとして公開されていますので、今回はそれを利用しています。ソースコードも公開されています。

明言はされていませんでしたが、グローバルサミット2023のデモは、JavaベースのカスタムアプリケーションサーバからJava APIを使用してDebeziumの機能を使用するスタイルかもしれません

その他

Debezium Serverの欠点といいますか特徴として、接続先が未達になると直ぐ落ちるというのがあります。例えばIRISが停止すると、Debezium Serverが停止(今回の構成では、コンテナが停止)してしまいます。ただ、どこまで処理したかをO/Sファイル(本例ではdata/offsets.dat)に保存していますので、IRIS起動後に、Debezium Serverのコンテナを再開すれば、停止中に発生した更新をキャプチャしてくれます。

停止したコンテナの再開は下記コマンドで行います。

docker compose start debezium-server

「あれ、落ちるんだ」と思いましたが、フェールセーフ思想なのだと思います。 対障害性はKafka Connectに管理してもらう前提になっているためだと思います。

MYSQLもほぼ同じ操作で動作確認が出来ます。./mysqlに必要なファイルがあります。mysql.txtを参照ください。

また、今回は、レコードを同期しているだけですが、GS2023のように組み込みBIのキューブを作成して分析用途にしたり、何某かのビジネスロジックを実行したり、インターオペラビリティ機能に連動させたりといった応用が考えられます。

0
0 784
お知らせ Toshihiko Minamoto · 6月 20, 2023

先週の InterSystems Global Summit にて、今年の初めにリリースしました2023.1のエクスペリメンタル機能として、新たな 外部テーブル を発表しました。現在、 外部テーブルの Early Access Program にご参加いただきご評価いただくことで、この機能がお客さまのニーズに合っているか、次に向けてどの機能を優先するべきか、お知らせいただきたいと考えています。

外部テーブルって何なの?
この素晴らしい概要ビデオを見る時間やポップコーンがない場合に備えて、外部テーブルは、ファイルやリモートデータベースなど、物理的に別の場所に保存されているデータをIRIS SQLとしてアクセスするのに役立つ機能です。外部テーブルは、通常のIRISテーブルとしてSQLに表示され、他の通常テーブルや外部テーブルとのJOINなど、あらゆるSQLステートメントで使用することができます。クエリを実行する際、外部テーブルから何を検索する必要があるのかを理解し、そのサーバーがリレーショナルデータベースの場合は、ネットワーク経由で取得するデータを最小限に抑えるようなクエリを出力しています。

0
0 146
記事 Toshihiko Minamoto · 3月 2, 2022 5m read

これは、IRIS でリレーショナルデータをクエリするアナリストとアプリケーションに、さらに優れた適応性とパフォーマンスによるエクスペリエンスを提供する IRIS SQL のイノベーションをトピックとした短い連載の 3 つ目の記事です。 2021.2 では連載の最後の記事になるかもしれませんが、この分野ではさらにいくつかの機能強化が行われています。 この記事では、このリリースで収集し始めたヒストグラムという追加のテーブル統計について、もう少し詳しく説明します。

ヒストグラムとは?

ヒストグラムは数値フィールド(またはより広範には、厳密な順序を持つデータ)のデータ分布の近似表現です。 このようなフィールドの最小値、最大値、および平均値がわかれば役立ちますが、データが 3 つのポイント間でどのように分布しているかはほとんどわかりません。 ここで役立つのがヒストグラムです。値の範囲をバケットに分割し、バケットごとに出現するフィールド値の数をカウントします。

これは非常に柔軟な定義であるため、バケットがフィールド値に関して同じ「幅」になるように、またはカバーされるサンプル値の数に関して同じ「大きさ」になるように、バケットのサイズを選択することができます。 後者の場合、各バケットには同じパーセンテージの値が含まれるため、バケットはパーセンタイルを表します。 以下のグラフは、日数で表現された同じバケット幅を使用して、Aviation Demo データセットの EventData フィールドのヒストグラムをプロットしています。

ヒストグラムが必要な理由

カリフォルニア州で 2004 年より前のすべてのイベントについて、このデータセットのクエリを実行しているとします。

SELECT * FROM Aviation.Event WHERE EventDate < '2004-05-01' AND LocationCountry = 'California'

ランタイムプランの選択」という前の記事では、テーブル統計で LocationCounty のようなフィールドの選択性と潜在的な外れ値をキャプチャする方法についてすでに説明しています。 しかし、そのような個別のフィールド値の統計は、EventDate での < 条件ではあまり実用的ではありません。 この条件の選択制を計算するには、2004 年 5 月 1 日までのすべての潜在的な EventDate 値の選択制を集計する必要があり、クエリのプランニング時に行えるような手っ取り早い見積もりではなく、それだけで非常に厳しいクエリとなる可能性があります。 ここで使用できるのがヒストグラムです。

EventDate 値の分布のヒストグラムデータを見てみましょう。今回は、データを同じサイズの 16 個のバケットに分割し、各バケットには 6.667% のデータが保持されています。 このようにすると、クエリコストの見積もりに使用できるパーセンタイルと選択制の数値に簡単に変換できます。 このテーブルを読み取るために、4 行目を見てみましょう。値の 20%(各 6.667% の 3 つのバケット)がこのバケットの下限である 2003 年 6 月 22 日より前にあり、さらに 6.667% の値が 2003 年 9 月 19 日まで保持されています。 

<colgroup><col style="width:48pt" width="64"><col style="width:61pt" width="81"><col style="width:64pt" width="85"></colgroup>
<td>
  Percentile
</td>

<td>
  Value
</td>
<td>
  0%
</td>

<td>
  21/12/2001
</td>
<td>
  7%
</td>

<td>
  02/07/2002
</td>
<td>
  13%
</td>

<td>
  19/01/2003
</td>
<td>
  20%
</td>

<td>
  22/06/2003
</td>
<td>
  27%
</td>

<td>
  19/09/2003
</td>
<td>
  33%
</td>

<td>
  30/12/2003
</td>
<td>
  40%
</td>

<td>
  01/10/2004
</td>
<td>
  47%
</td>

<td>
  01/10/2005
</td>
<td>
  53%
</td>

<td>
  20/08/2006
</td>
<td>
  60%
</td>

<td>
  14/01/2007
</td>
<td>
  67%
</td>

<td>
  02/04/2008
</td>
<td>
  73%
</td>

<td>
  14/05/2008
</td>
<td>
  80%
</td>

<td>
  29/11/2008
</td>
<td>
  87%
</td>

<td>
  01/06/2010
</td>
<td>
  93%
</td>

<td>
  30/10/2011
</td>
<td>
  100%
</td>

<td>
  26/09/2012
</td>
Bucket
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

上記のクエリ例で使用されているカットオフ日(2004 年 5 月 1 日)は、5 番目のバケットにあり、その日付より前には 33% から 40% の値があります。 バケットが小さくなるにつれ、その_中_の分布はほぼ均一であると見なすことができ、下限と上限の間を単に補完することができます。つまり、この場合、選択性は約 37% となり、これをクエリコストの見積もりに使用することができます。 

ヒストグラムの使用を可視化するには、もう一つ、累積分布グラフとしてプロットする方法があります。 X 軸で 2004 年 5 月 1 日の線(値)がどのように描かれるかを確認すれば、Y 軸で 約 37% と解釈できます。 

  

上記の例では、わかりやすくするために上限のみの範囲条件を使用していますが、このアプローチは、下限または間隔条件(BETWEEN 句を使用するなど)を使用しても当然動作します。 

2021.2 より、文字列を含むすべての照合フィールドのテーブル統計の一環としてヒストグラムを収集しており、それを使用して RTPC の一部として範囲選択性を推定することができるようになっています。 実世界での多くのクエリには日付(およびその他の)フィールドでの範囲条件が伴うため、この IRIS SQL の機能強化によって、多くのお客様のクエリプランに役立つと信じています。いつものように、皆さんの体験をお聞かせください。

0
0 110
記事 Toshihiko Minamoto · 3月 1, 2022 5m read

これは、適応性とパフォーマンスに優れた SQL エクスペリエンスを提供する 2021.2 SQL 強化機能に関する連載第 2 回目の記事です。 この記事では、前の記事で説明したランタイムプランの選択機能の主要な入力であるテーブル統計の収集におけるイノベーションに焦点を当てます。

皆さんには次のことを何度もお伝えしてきました:

Tune tableを実行しましょう!』

TUNE TABLE SQL コマンドまたは$SYSTEM.SQL.Stats.Table ObjectScript API を通じてテーブルをチューニングすることは、IRIS SQL が適切なクエリプランをはじき出すのに役立つテーブルデータの統計情報を収集することです。 これらの統計情報には、テーブル内のおおよその行数など、オプティマイザーが JOIN の順序(通常、最も小さいテーブルから始めるのが最も効率的です)などを決定する上で役立つ重要な情報が含まれています。 クエリのパフォーマンスについて InterSystems サポートに寄せられる多くの問い合わせは、TUNE TABLE を実行してもう一度試すだけで解消されます。このコマンドを実行することで、既存のプランが無効になり、次の呼び出しによって新しい統計が得られるためです。 サポートへの問い合わせから、こういったユーザーがテーブルの統計情報を収集していなかった理由が 2 つわかりました。テーブル統計について知らなかった、または本番システムでチューニングを実行した際のオーバーヘッドの余裕がなかったという理由です。 2021.2 では、この 2 つの理由に対処しました。

ブロックレベルのサンプリング

2 つ目の理由から始めましょう。統計を収集するコストです。 テーブル統計を収集するには多大な I/O が必要となるため、テーブル全体をスキャンしているのであれば、オーバーヘッドも高まります。 API ではすでに、行の一部のみサンプリングすることをサポートしていましたが、この操作にはかなりのコストがかかるというご意見をいただいていました。 2021.2 では、マスターマップグローバルをループすることでランダムな行を選択するのではなく、その下の物理ストレージにすぐにアクセスしてそのグローバルにカーネルが実際のデータベースブロックのランダムなサンプルを取得するように変更しました。 サンプリングされたこれらのブロックから、それらが保存する SQL テーブル行を推論し、通常のフィールド単位の統計情報構築ロジックに対応します。

これを大規模なビールフェスティバルへの参加に例えると、すべての通路を歩き、いくつかの醸造所のブースを選んでそれぞれのボトルをカートに入れるのではなく、単に主催者に依頼してランダムなボトルが入った木箱を渡してもらうので歩かなくても済む、というものです。 (実際のビール試飲会では、歩き回ったほうが適切ですがwink)。 酔いを醒ますために、以下に、ブロックベースのアプローチ(青い十字)に対する今までの行ベースのアプローチ(赤い十字)をプロットした単純なグラフを示しています。 これは、一部のお客様が TUNE TABLE を実行することを警戒している巨大なテーブルについては大きなメリットがあることを表しています。

 

ブロックサンプリングの制限はあまりありませんが、最も重要なのは、デフォルトのストレージマッピングでないテーブル(例: %Storage.SQL を使用してグローバル構造をカスタムマッピングしているテーブル)では使用できないことです。 このような場合には、過去に機能していた方法である、行ベースのサンプリングに戻ります。

自動チューニング

オーバーヘッドに関する認識の問題が片付いたところで、お客様が TUNE TABLE を実行していなかったもう 1 つの理由について考えましょう。その存在を知らなかったという理由です。 それについて文書化することもできました(また、ドキュメントを改善する余地が常にあることは認識しています)が、この非常に効率的なブロックサンプリングは、私たちが長年求めてきたことを実行する機会であると捉えました。すべてを自動化するということです。 2021.2 からは、統計がまったく提供されていないテーブルに対してクエリを準備する場合、最初に上記のブロックサンプリングメカニズムを使用してそれらの統計を収集し、クエリプランニングに統計を使用し、以降のクエリで使用できるように、テーブルメタデータに保存します。

仰々しく聞こえるかもしれませんが、上記のグラフは、GB サイズのテーブルでは、この統計収集の作業がわずか数秒で開始していることがわかります。 不適切なクエリプランを使用してそのようなテーブルをクエリしている場合(適切な統計がないため)は、事前に簡易サンプリングを実行するよりもはるかにコストがかかる可能性があります。 もちろん、これは、ブロックサンプリングを使用できるテーブルのみに行い、行ベースのサンプリングのみをサポートする特殊なテーブルの場合は、(残念ながら)統計なしで対処するしかありません。

他の新機能と同様に、皆さんの最初の体験に関する感想とフィードバックをお送りください。 この分野では、テーブルの使用状況に基づいて統計を最新の状態に維持するなど、自動化に関するアイデアは他にもありますが、そういった機能をラボ外部の使用体験に基づくものにしたいと考えています。

0
1 183
記事 Toshihiko Minamoto · 2月 24, 2022 5m read

InterSystems IRIS Data Platform の 2021.2 リリースには、ミッションクリティカルなアプリケーションを高速で柔軟性に優れ、セキュアに開発するための刺激的な新機能が多数含まれています。 Embedded Python は間違いなく脚光を浴びています(正当な理由で!)が、SQL の分野でも、テーブルデータに関する詳細な統計情報を収集し、それを最適なクエリプランに提供する、より適応性の高いエンジンに向けて大きな一歩を踏み出しました。 この短い連載記事では、2021.2 で新しく追加された 3 つの要素について詳しく説明し、ランタイムプランの選択(RTPC)を手始めに、この目標に向かって進みます。

これらについて適切な順序で話していくのは困難です(この記事を書く上で、私がどれだけ順序を入れ替えたか想像できないほどです!) というのも、これらが相互に非常にうまく機能するためです。 そのため、ご自由に順序を変えてお読みくださいsmiley

IRIS クエリ処理について

IRIS SQL エンジンにステートメントを送信すると、エンジンはリテラル(クエリパラメーター)を置き換えてそのステートメントを正規化された形式に解析し、テーブル構造、インデックス、およびフィールド値に関する統計を見て、正規化されたクエリにとって最も効率的な実行ストラテジーをはじき出します。 これにより、おそらく異なるクエリパラメーター値を使用してクエリをもう一度実行したい場合に、エンジンは同じプランと生成されたコードを再利用することができます。 たとえば、以下のクエリを例にします。

SELECT * FROM Customer WHERE CountryCode = 'US' AND ChannelCode = 'DIRECT'

このクエリは以下のような形式に正規化されます。

SELECT * FROM Customer WHERE CountryCode = ? AND ChannelCode = ?

そのため、国とチャンネルの様々な組み合わせに対する後続の呼び出しは、即座にキャッシュされた同じクエリクラスを取得するため、計算量の多いプランニング作業を省略することができます。 6 つのチャンネルを通じて世界中にランニングシューズを販売すると仮定しましょう。言い換えると、データは CountryCode とChannelCode の可能な値全体で均等に分散されます。 これらの両方のフィールドに通常のインデックスがある場合、この正規化されたクエリで最も効率的なプランは、対応するインデックスを使用してマスターマップから一致する行にアクセスすることで最も選択的な条件(CountryCode)から始め、その後にマスターマップのすべての行に対し、別の条件(ChannelCode)をチェックします。 

外れ値

では、スポーツ用品メーカーではなく、ベルギーチョコレートの型を販売する専門ベンダーだとしましょう(どこから出てきた例でしょうかね wink)。 この場合、顧客の大半(たとえば 60%)がベルギーにいるとします。つまり、「BE」が CountryCode フィールドの_外れ値_であり、テーブルの多数の行を表します。 すると突然、CountryCode でのインデックスに他の使用方法が現れました。ランタイムで取得する CountryCode のクエリパラメーターが「BE」で_あるとした場合_、そのインデックスを最初に使用すると、マスターマップの大半を読み取ることになるため、ChannelCode でのインデックスから始める方がよくなります。 しかし、CountryCode のクエリパラメーター値が別の値で_あるとした場合_、他のすべての国が残りの 40% のベルギー以外の顧客を分割するため、CountryCode でのインデックスの方がはるかに高い価値が出てきます。

これは、ランタイム時に異なるプランを選択する場合の例です。または、言い換えると、ランタイムプランの選択(RTPC)と言えます。 RTPC は、従来のリテラル置換とキャッシュ済みのクエリルックアップロジックに小さなフックを追加して、CounryCode 列の「BE」値などの外れ値を見つけるメカニズムです。 IRIS SQL クエリ処理のさらに詳細な概要に興味のある方は、こちらの VS2020 動画をご覧ください。

IRIS SQL は過去に、非常に大雑把なオプトインバージョンをサポートしていましたが、2021.2 で、大幅に軽量化され、より広範な述語条件に対応できる、まったく新しい RTPC インフラストラクチャを導入しました。 このランタイムチェックのオーバーヘッドは確かに最小限であるため、ユーザーが何も行わなくてもこのメリットを得られるように、これをデフォルトでオンにしました(いつものように、アップグレード後にクエリプランを解凍する必要はありません)。

これまで頻繁に、実世界のデータセットでは外れ値がどれほど一般的であるか(また、厳格に一様な分布がどれほど稀であるか)を見てきましし、パートナーのベンチマークでのテストによって、以下のグラフからわかるように、パフォーマンスと I/O で目を見開くほどの改善が得られることがわかりました。 また、2020.1 のベンチマーク結果も含めているため、リリースのたびにパフォーマンスを改善できるように継続的に取り組んでいること(と結果)を見ていただけるでしょう。

改善のマイレージは、データセットの外れ値の量とインデックスの可用性によって異なりますが、この変更の可能性には非常に興奮しており、皆さんの体験をお聞かせいただければ幸いです。

0
1 198
記事 Toshihiko Minamoto · 9月 14, 2021 10m read

より産業向けのグローバルストレージスキーム

この連載の第1回では、リレーショナルデータベースにおけるEAV(Entity-Attribute-Value)モデルを取り上げ、テーブルにエンティティ、属性、および値を保存することのメリットとデメリットについて確認しました。 このアプローチには柔軟性という点でメリットがあるにもかかわらず、特にデータの論理構造と物理ストレージの基本的な不一致などによりさまざまな問題が引き起こされるという深刻なデメリットがあります。

こういった問題を解決するために、階層情報の保存向けに最適化されたグローバル変数を、EAVアプローチが通常処理するタスクに使用できるかどうかを確認することにしました。

パート1では、オンラインストア向けのカタログをテーブルを使って作成し、その後で1つのグローバル変数のみで作成しました。 それでは、複数のグローバル変数で同じ構造を実装してみることにしましょう。

最初のグローバル変数^catalogには、ディレクトリ構造を保存します。 2つ目のグローバル変数^goodには、店の商品を保存します。 ^indexグローバルには、店のインデックスを保存します。 プロパティは階層的なカタログに関連付けられているため、プロパティ用の個別のグローバル変数は作成しません。

このアプローチでは、エンティティごとに(プロパティを除く)、個別のグローバル変数を使用しているため、論理の観点では優れています。 グローバルカタログ構造は次のようになります。

Set ^сatalog(root_id, "Properties", "capacity", "name") = "Capacity, GB"
Set ^сatalog(root_id, "Properties", "capacity", "sort") = 1

Set ^сatalog(root_id, sub1_id, "Properties", "endurance", "name") = "Endurance, TBW"
Set ^сatalog(root_id, sub1_id, "Properties", "endurance", "sort") = 2

Set ^сatalog(root_id, sub1_id, "goods", id_good1) = 1
Set ^сatalog(root_id, sub1_id, "goods", id_good2) = 1

Set ^сatalog(root_id, sub2_id, "Properties", "avg_seek_time", "name") = "Rotate speed, ms"
Set ^сatalog(root_id, sub2_id, "Properties", "avg_seek_time", "sort") = 3

Set ^сatalog(root_id, sub2_id, "goods", id_good3) = 1
Set ^сatalog(root_id, sub2_id, "goods", id_good4) = 1

 

商品のグローバル変数は、次のようになります。

Set ^good(id_good, property1) = value1
Set ^good(id_good, property2) = value2
Set ^good(id_good, property3) = value3
Set ^good(id_good, "catalog") = catalog_id

 

もちろん、商品のあるすべてのカタログセクションで、必要なプロパティで並べ替えを行えるようにインデックスが必要となります。 インデックスグローバルは、次のような構造になります。

Set ^index(id_catalog, property1, id_good) = 1
; To quickly get the full path to concrete sub-catalog
Set ^index("path", id_catalog) = "^catalog(root_id, sub1_id)"

 

したがって、カタログのすべてのセクションで、リストを並べ替えることができます。 インデックスグローバルはオプションです。 カタログのこのセクションの商品数が多い場合にのみ役立ちます。

デモデータを操作するためのObjectScriptコード

では、データを操作するために、ObjectScriptを使用しましょう。 まず、特定の商品のプロパティを取得することから始めます。 特定の商品のIDがあり、そのプロパティを並べ替えの値で指定された順序で表示する必要があります。 そのためのコードは次のようになります。

get_sorted_properties(path, boolTable)
{
  ; remember all the properties in the temporary global
  While $QLENGTH(@path) > 0 {
    if ($DATA(@path("Properties"))) {
      set ln=""
      for {
        Set ln = $order(@path("Properties", ln))
        Quit: ln = ""

        IF boolTable & @path("Properties", ln, "table_view") = 1 {
          Set ^tmp(@path("Properties", ln, "sort"), ln) = @path("Properties", ln, "name")
        }
      ELSE {
        Set ^tmp(@path("Properties", ln, "sort"), ln) = @path("Properties", ln, "name")
      }
    }
  }
}

print_sorted_properties_of_good(id_good)
{
  Set id_catalog = ^good(id_good, "catalog")
  Set path = ^index("path", id_catalog)

  Do get_sorted_properties(path, 0)

  set ln =""
  for {
   Set ln = $order(^tmp(ln))
   Quit: ln = ""
   Set fn = ""
   for {
    Set fn = $order(^tmp(ln, fn))
    Quit: fn = ""
    Write ^tmp(ln, fn), " ", ^good(id_good, fn),!
   }
  }
}

 

次に、id_catalogに基づいて、カタログセクションの商品を表形式で取得します。

print_goods_table_of_catalog(id_catalog)
{ 
  Set path = ^index("path", id_catalog)
  Do get_sorted_properties(path, 1)

  set id=""
  for {
    Set id = $order(@path("goods"), id)
    Quit: id = ""

    Write id," ", ^good(id, "price"), " "

    set ln =""
    for {
      Set ln = $order(^tmp(ln))
      Quit: ln = ""
      Set fn = ""
      for {
        Set fn = $order(^tmp(ln, fn))
        Quit: fn = ""
        Write ^tmp(ln, fn), " ", ^good(id, fn)
      }
      Write !
    }
  }
}

 

可読性: EAV SQLとグローバル変数

では、EAVとSQLの使用をグローバル変数の使用と比較してみましょう。 コードの明確さについては、これが主観的なパラメーターであることは明らかです。 しかし、例として新しい商品の作成方法を見てみましょう。

SQLを使用したEAVアプローチから確認します。 まず、オブジェクトのプロパティリストを取得する必要があります。 これは別のタスクであり、非常に時間がかかります。 capacityweight、およびenduranceという3つのプロパティのIDがすでに分かっているとします。

START TRANSACTION
INSERT INTO good (name, price, item_count, catalog_id) VALUES ('F320 3.2TB AIC SSD', 700, 10, 15);

SET @last_id = LAST_INSERT_ID ();

INSERT INTO NumberValues ​​Values​​(@last_id, @id_capacity, 3200);
INSERT INTO NumberValues ​​Values​​(@last_id, @id_weight, 0.4);
INSERT INTO NumberValues ​​Values​​(@last_id, @id_endurance, 29000);
COMMIT

 

この例ではプロパティが3つしかないため、例にはそれほど圧倒されません。 一般的なケースでは、トランザクション内のテキストテーブルにいくつかの挿入があります。

INSERT INTO TextValues ​​Values​​(@last_id, @ id_text_prop1, 'Text value of property 1');
INSERT INTO TextValues ​​Values​​(@last_id, @ id_text_prop2, 'Text value of property 2');
...
INSERT INTO TextValues Values (@last_id, @id_text_propN, 'Text value of property N');

 

もちろん、数値の代わりに「capacity」を使うというように、IDプロパティの代わりにテキスト表記を使用すれば、SQLバージョンをもう少し簡略することも可能ですが、 SQLの世界では、これは受け入れられません。 エンティティのインスタンスを列挙するには、数値IDを使用するのが慣例です。 このため、インデックス処理が高速化し(インデックス処理のバイトが少なくなるため)、一意性を追跡しやすくなり、新しいIDを自動的に作成しやすくなります。 この場合、挿入フラグメントは次のようになります。

INSERT INTO NumberValues ​​Values​​(@last_id, 'capacity', 3200);
INSERT INTO NumberValues ​​Values​​(@last_id, 'weight', 0.4);
INSERT INTO NumberValues ​​Values​​(@last_id, 'endurance', 29000);

 

次は、同じ例をグローバル変数を使用した場合のコードです。

TSTART
Set ^good(id, "name") = "F320 3.2TB AIC SSD"
Set ^("price") = 700, ^("item_count") = 10, ^("reserved_count") = 0, ^("catalog") = id_catalog
Set ^("capacity") = 3200, ^("weight") = 0.4, ^("endurance") = 29000
TCOMMIT

 

では、EAVアプローチで商品を削除してみましょう。

START TRANSACTION
DELETE FROM good WHERE id = @ good_id;
DELETE FROM NumberValues ​​WHERE good_id = @ good_id;
DELETE FROM TextValues ​​WHERE good_id = @ good_id;
COMMIT

 

そして、グローバル変数でも同じことを行います。

Kill ^good(id_good)

2つのアプローチをコードの長さの観点から比較することもできます。 上記の例からわかるように、グローバル変数を使用した方が、コードは短くなります。 これはメリットです。 コードが短くなるほど、エラーの数も減り、コードを理解して管理するのも容易になります。

一般に、コードが短いほど処理が高速化します。 そして、この場合には、グローバル変数はリレーショナルテーブルよりも低位データ構造であるため、確かにそのとおりです。

EAVとグローバル変数におけるデータのスケーリング

次に、水平方向のスケーリングを見てみましょう。 EAVアプローチでは、少なくとも3つの最も大きなテーブル(Good、NumberValues、TextValues)を複数のサーバーに分散する必要があります。 エンティティと属性のあるテーブルにはほとんど情報がないため、これらのテーブルは単純にすべてのサーバーに丸ごとコピーすることができます。

各サーバーでは、水平方向のスケーリングにより、さまざまな商品がGood、NumberValues、およびTextValuesテーブルに保存されます。 異なる商品でIDが重複しないように、各サーバーの商品に対して特定のIDブロックを割り当てる必要があります。

グローバルを使って水平方向のスケーリングを行う場合、グローバルでID範囲を構成し、グローバル範囲を各サーバーに割り当てる必要があります。

複雑さは、EAVとグローバルであまり変わりませんが、EAVアプローチの場合は、3つのテーブルにID範囲を構成しなければなりません。 グローバルの場合は、1つのグローバル変数のみにIDを構成するだけで済みます。 つまり、水平方向のスケーリングを調整するには、グローバル変数の方が簡単と言えます。

EAVとグローバル変数におけるデータ損失

最後に、データベースファイルの破損によるデータ損失のリスクを検討してみましょう。 5つのテーブルか3つのグローバル(インデックスグローバルを含む)のどちらにすべてのデータを保存する方が簡単でしょうか。

3つのグローバルの方が簡単だと思います。 EAVアプローチでは、さまざまな商品のデータがテーブルに混在しているのに対し、グローバルでは情報がより全体的に保存されています。 基盤のブランチは、保存されて順に並べ替えられています。 そのため、データがパスタが絡み合うように保存されるEAVアプローチに比べれば、グローバルの一部の破損によってダメージにつながる可能性は低くなります。

データ回復におけるもう1つの悩みの種は、情報の表示方法です。 EAVアプローチでは、情報は複数のテーブルに分割されているため、1つにまとめるには特別なスクリプトが必要です。 グローバルの場合は、ZWRITEコマンドを使用するだけで、ノードのすべての値と基盤のブランチを表示することができます。

InterSystems IRISのグローバル: より優れたアプローチ?

EAVアプローチは、階層データを保存するためのトリックとして出現しました。 テーブルは元々、ネストされたデータを保存するようには設計されてはいなかったため、 テーブルでグローバルをエミュレーションするのがEAVの事実上のアプローチです。 テーブルがグローバルよりも高位で低速のデータストレージ構造であることを考えると、EAVアプローチは、グローバルと比較した場合に失敗となります。

個人的な意見を言えば、階層データ構造の場合、グローバルの方がプログラミングの点でより利便性が高く理解しやすいと思います。また、より高速でもあります。

プロジェクトでEAVアプローチを計画している場合は、InterSystems IRISのグローバルを使用して階層データを保存することを検討するようお勧めします。

0
0 430
記事 Toshihiko Minamoto · 9月 9, 2021 10m read

はじめに

この連載の最初の記事では、リレーショナルデータベースのEAV(Entity–Attribute–Value)モデルを見て、それがどのように使用されて、何に役立つのかを確認しましょう。 その上で、EAVモデルの概念とグローバル変数と比較します。

原則として検索する必要のある、フィールド数、または階層的にネストされたフィールドの数が不明なオブジェクトがある場合があります。

たとえば、多様な商品群を扱うオンラインストアを考えてみましょう。 商品群ごとに固有の一意のプロパティセットがあり、共通のプロパティもあります。 たとえば、SSDとHDDドライブには共通の「capacity」プロパティがありますが、SSDには「Endurance, TBW」、HDDには「average head positioning time」という一意のプロパティもあります。

場合によっては、同じ商品でも別のメーカーが製造した場合には、それぞれに一意のプロパティが存在します。

では、50種の商品群を販売するオンラインストアがあるとしましょう。 各商品群には、数値またはテキストの固有のプロパティが5つあります。

実際に使用するのは5個だけであっても、各商品に250個のプロパティがあるテーブルを作成するのであれば、ディスク容量の要件が大幅に増える(50倍!)だけでなく、有用性のない空のプロパティによってキャッシュが詰まってしまうため、データベースの速度特性が大幅に減少してしまいます。

さらに、それだけではありません。 固有のプロパティを持つ新しい商品群を追加するたびに、ALTER TABLEコマンドを使用してテーブルの構造を変更する必要があります。 大規模なテーブルであれば、この操作には数時間、さらには数日間かかる可能性もあり、ビジネスでは許容しかねます。

これを注意深く読んでいる方は「商品群ごとに異なるテーブルを用意しては?」と言うでしょう。 もちろんその通りではありますが、このアプローチを使用すると、大型ストアの場合には、数万個ものテーブルでデータベースを作成することになり、管理が困難になります。 さらに、サポートする必要のあるコードがますます複雑化してしまいます。

一方、新しい商品群を追加するときに、データベースの構造を変更する必要はありません。 新しい商品群向けの新しいテーブルを追加すればよいだけだからです。

いずれにせよユーザーは、ストア内の商品を簡単に検索できること、現在のプロパティを示す便利な表形式で商品を表示できること、そして商品を比較できることが必要です。

ご想像のとおり、商品群の5個のプロパティのみが必要であるにもかかわらず、商品テーブルにはさまざまなプロパティを示す250個のフィールドがあれば不便であるのと同様に、250個のフィールドを使った検索フォームは、ユーザーにとって非常に不便です。 これは商品の比較にも当てはまります。

マーケティングデータベースも別の有用な例と言えるでしょう。 それに格納されている人ごとに、絶えず追加、変更、または削除される可能性のある多数のプロパティが必要です(多くの場合はネストされています)。 過去にある商品を特定の数量で購入した、特定の商品群を購入した、何かに参加した、どこかで勤務した、親戚がいる、この都市に住む、特定の社会階級に属する、などのプロパティがあります。 フィールド数は数千個にもなり、変化も絶えないでしょう。 マーケターは常に、さまざまな顧客グループを区別して魅力的な特別オファーを提供する方法を考えています。

これらの問題を解決すると同時に、明確で確定的なデータベース構造を得るために、Entity-Attribute-Valueアプローチが編み出されました。

EAVアプローチ

EAVアプローチの本質は、エンティティ、属性、および属性値を個別に保存することにあります。 一般的に、EAVアプローチを説明するために、Entity、Attribute、およびValueという3つのテーブルのみが使用されます。

保存するデモデータの構造。

テーブルを使用したEAVアプローチの実装

5つ(最後の2つのテーブルを1つに統合することにした場合は4つ)のテーブルを使用したより複雑な例を考察してみましょう。

最初のテーブルはСatalogです。

CREATE TABLE Catalog (
id INT,
name VARCHAR (128),
parent INT
);

このテーブルは実際、EAVアプローチのエンティティに対応しています。 階層的な商品カタログのセクションを保存します。

2つ目のテーブルは ****Fieldです。

CREATE TABLE Field (
id INT,
name VARCHAR (128),
typeOf INT,
searchable INT,
catalog_id INT,
table_view INT,
sort INT
);

このテーブルでは、属性の名前、型、および属性が検索可能であるかどうかを指定します。 また、プロパティが属する商品を保持しているカタログのセクションも指定します。 catalog_id以下のカタログセクションにあるすべての商品には、このテーブルに保存されているさまざまなプロパティがある場合があります。

3つ目のテーブルはGoodです。 商品を、商品の価格、商品の合計数量、商品の予約数量、および商品名とともに保存するように設計されています。 厳密にはこのテーブルは必要ではありませんが、個人的には、商品用に別のテーブルを用意しておくと便利だと思います。

CREATE TABLE Good (
id INT,
name VARCHAR (128),
price FLOAT,
item_count INT,
reserved_count,
catalog_id INT
);

4つ目のテーブル(TextValues)と5つ目のテーブル(NumberValues)は、商品のテキストの値と数値属性を保存するように設計されており、構造も似ています。

CREATE TABLE TextValues ​​(
good_id INT,
field_id INT,
fValue TEXT
);

CREATE TABLE NumberValues ​​(
good_id INT,
field_id INT,
fValue INT
);

テキスト値と数値に個別のテーブルを使用する代わりに、次の構造で単一のCustomeValuesテーブルを使用することもできます。

CREATE TABLE CustomValues ​​(
good_id INT,
field_id INT,
text_value TEXT,
number_value INT
);

データ型ごとに個別に保存しておけば、速度が向上し、容量を節約できるため、私は別々に保存する方を好んでいます。

EAVアプローチを使用したデータへのアクセス

SQLを使用して、カタログ構造マッピングを表示してみましょう。

SELECT * FROM Catalog ORDER BY id;

これらの値からツリーを作成するには、個別のコードが必要となります。 PHPでは、次のようになります。

$stmt = $ pdo-> query ('SELECT * FROM Catalog ORDER BY id');
$aTree = [];
$idRoot = NULL;

while ($row = $ stmt->fetch())
{
    $aTree [$row ['id']] = ['name' => $ row ['name']];

    if (! $row['parent'])
      $idRoot = $row ['id'];
    else
      $aTree [$row['parent']] ['sub'] [] = $row['id'];
}

将来的には、ルートノードの $aTree[$ idRoot] から始めると、ツリーを簡単に描画できるようになります。

では、特定の商品のプロパティを取得しましょう。 

まず、この商品に固有のプロパティのリストを取得し、その後で、それらのプロパティとデータベースにあるプロパティを接続します。 実際には、示されるすべてのプロパティが入力されているわけではないため、LEFT JOINを使用する必要があります。

SELECT * FROM
(
SELECT g. *, F.name, f.type_of, val.fValue, f.sort FROM Good as g
INNER JOIN Field as f ON f.catalog_id = g.catalog_id
LEFT JOIN TextValues ​​as val ON tv.good = g.id AND f.id = val.field_id
WHERE g.id = $ nGood AND f.type_of = 'text'
UNION
SELECT g. *, F.name, f.type_of, val.fValue, f.sort FROM Good as g
INNER JOIN Field as f ON f.catalog_id = g.catalog_id
LEFT JOIN NumberValues ​​as val ON val.good = g.id AND f.id = val.field_id
WHERE g.id = $nGood AND f.type_of = 'number'
) t
ORDER BY t.sort;

数値とテキスト値の両方を保存するために1つのテーブルのみを使用すると、クエリを大幅に簡略化できます。

SELECT g. *, F.name, f.type_of, val.text_value, val.number_value, f.sort FROM Good as g
INNER JOIN Field as f ON f.catalog = g.catalog
LEFT JOIN CustomValues ​​as val ON tv.good = g.id AND f.id = val.field_id
WHERE g.id = $nGood
ORDER BY f.sort;

では、$nCatalogカタログセクションに含まれる商品を表形式で取得します。 まず、カタログのこのセクションのテーブルビューに反映する必要があるプロパティのリストを取得します。

SELECT f.id, f.name, f.type_of FROM Catalog as c
INNER JOIN Field as f ON f.catalog_id = c.id
WHERE c.id = $nCatalog AND f.table_view = 1
ORDER BY f.sort;

次に、テーブルを作成するクエリを構築します。 表形式ビューには、3つの追加プロパティ(Goodテーブルのプロパティのほかに)が必要だとします。 クエリを単純化するために、次を前提としています。

SELECT g.if, g.name, g.price,
            f1.fValue as f1_val,
            f2.fValue as f2_val,
            f3.fValue as f3_val,
FROM Good
LEFT JOIN TextValue as f1 ON f1.good_id = g.id
LEFT JOIN NumberValue as f2 ON f2.good_id = g.id
LEFT JOIN NumberValue as f3 ON f3.good_id = g.id
WHERE g.catalog_id = $nCatalog;

EAVアプローチの長所と短所

EAVアプローチは明らかに柔軟性のメリットがあります。 テーブルなどの固定されたデータ構造を使用すると、オブジェクトの広範なプロパティセットを保存することが可能になります。 また、データベースのスキーマを変更せずに、別のデータ構造を保存することができます。 

また、非常に多くの開発者に馴染みのあるSQLも使用することができます。 

最も明白なデメリットは、データの論理構造と物理ストレージの不一致であり、これによって様々な問題が引き起こされます。 

さらに、プログラミングには、非常に複雑なSQLクエリが伴うこともよくあります。 EAVデータの表示には標準的に使用されていないツールの作成が必要となるため、デバッグが困難になることがあります。 また、LEFT JOINクエリを使用する必要がある場合があるため、データベースの速度が低下してしまいます。

グローバル変数: EAVの代替

私はSQLの世界とグローバル変数の世界の両方に精通しているため、EAVアプローチが解決するタスクにグローバルを使用する方がはるかに魅力的になるのではないかと考えました。

グローバル変数はまばらで階層的な情報を保存できるデータ構造です。 グローバル変数は階層情報を保存するために慎重に最適化されているというのが非常に重要なポイントです。 グローバル変数自体はテーブルよりも低レベルの構造であるため、テーブルよりもはるかに素早く動作します。

同時に、グローバル構造自体をデータ構造に従って選択できるため、コードを非常に単純で明確にすることができます。

デモデータを保存するためのグローバル構造

グローバル変数はデータを保存する上で非常に柔軟でエレガントな構造であるため、1つのグローバル変数を管理するだけでカタログセクション、プロパティ、および商品などのデータを保存することができます。

グローバル構造がデータ構造にどれほど似ているのかに注目してください。 このコンプライアンスによって、コーディングとデバッグが大幅に簡略化されます。

実際には、全ての情報を1つのグローバルに保存したい気持ちが非常に強くても、複数のグローバルを使用することをお勧めします。 インデックス用に別のグローバルを作成することが合理的です。 また、ディレクトリのパーティション構造のストレージを商品から分離することもできます。

この続きは?

この連載の2つ目の記事では、EAVモデルに従う代わりに、InterSystems Irisのグローバルにデータを保存する方法の詳細とメリットについて説明します。

0
0 2052
記事 Mihoko Iijima · 3月 5, 2021 1m read

これは InterSystems FAQ サイトの記事です。

永続クラス(=テーブル)定義に提供される %BuildIndices() メソッドの引数に、インデックスを再構築したい ID の開始値と終了値を指定することにより、その範囲内のインデックスのみが再構築できます。
 

例えば、Sample.Person クラスにある NameIDX インデックスと ZipCode インデックスを ID=10~20 のみ再構築する場合は、以下のように実行します(ID の範囲は、第5引数、第6引数に指定してます)。

 set status = ##class(Sample.Person).%BuildIndices($LB("NameIDX","ZipCode"),1,,1,10,20) 

 

$LB() は $ListBuild() 関数で、%BuildIndices() メソッドでは、インデックス名を指定するために使用しています。

インデックスの再構築方法については、ドキュメントもご参照ください。

2018.1 以下はこちらのドキュメントをご参照ください。

0
0 454
記事 Toshihiko Minamoto · 11月 18, 2020 5m read

クラス、テーブル、グローバルとその仕組み

InterSystems IRIS を技術的知識を持つ人々に説明する際、私はいつもコアとしてマルチモデル DBMSであることから始めます。

個人的には、それが(DBMSとして)メインの長所であると考えています。 また、データが格納されるのは一度だけです。 ユーザーは単に使用するアクセス API を選択するだけです。

  • データのサマリをソートしたいですか?SQL を使用してください!
  • 1 つのレコードを手広く操作したいですか?オブジェクトを使用してください!
  • あなたが知っているキーに対して、1 つの値にアクセスしたりセットしたいですか? グローバルを使用してください!

これは短く簡潔なメッセージで、一見すると素晴らしく聞こえます。しかし、実際には intersystems IRIS を使い始めるたユーザーには クラス、テーブル、グローバルはそれぞれどのように関連しているのだろうか? 互いにどのような存在なのだろうか? データは実際にどのように格納されているのだろうか?といった疑問が生じます。

この記事では、これらの疑問に答えながら実際の動きを説明するつもりです。

パート 1. モデルに対する偏見。

データを処理するユーザーは多くの場合、処理対象のモデルに偏見を持っています。

開発者はオブジェクトで考えます。 このようなユーザーにとって、データベースとテーブルは CRUD(Create-Read-Update-Delete、ORM の使用が望ましい)を介して操作する箱のようなものですが、その基礎となる概念モデルはオブジェクトです(これは主に私たちのような多くのオブジェクト指向言語の開発者に当てはまります)。

一方、リレーショナル DBMS に多くの時間を費やしているデータベース管理者は往々にしてデータをテーブルと見なしています。 この場合、オブジェクトはレコードの単なるラッパー扱いです。

また、InterSystems IRIS では永続クラスはデータをグローバルに格納するテーブルでもあるため、いくつかの説明が必要になります。

パート 2. 具体例

次のような Point クラスを作成したとします。

Class try.Point Extends %Persistent [DDLAllowed]
{
    Property X;
    Property Y;
}

次のように DDL/SQL を使用して同じクラスを作成することもできます。

CREATE Table try.Point (
    X VARCHAR(50),
    Y VARCHAR(50))

コンパイル後、新しいクラスがグローバルにネイティブに格納されているデータをカラム(またはオブジェクト指向のユーザーの場合はプロパティ)にマッピングするストレージ構造を自動生成します。

Storage Default
{
<Data name="PointDefaultData">
    <Value name="1">
        <Value>%%CLASSNAME</Value>
    </Value>
    <Value name="2">
        <Value>X</Value>
    </Value>
    <Value name="3">
        <Value>Y</Value>
    </Value>
</Data>
<DataLocation>^try.PointD</DataLocation>
<DefaultData>PointDefaultData</DefaultData>
<IdLocation>^try.PointD</IdLocation>
<IndexLocation>^try.PointI</IndexLocation>
<StreamLocation>^try.PointS</StreamLocation>
<Type>%Library.CacheStorage</Type>
}

ここでは何が起きているのでしょうか?

下から順番に説明します(太字の単語が重要です。残りは無視してください)。

  • Type - 生成されたストレージタイプ。この場合は永続オブジェクトのデフォルトストレージです。
  • StreamLocation - ストリームを格納するグローバルです。
  • IndexLocation - インデックス用のグローバルです。
  • IdLocation - ID の自動インクリメントカウンターを格納するグローバルです。
  • DefaultData - グローバルの値をカラム/プロパティにマッピングするストレージの XML 要素です。
  • DataLocation - データを格納するグローバルです。

ここでは「DefaultData」が PointDefaultData となっていますので、その構造をもう少し詳しく見てみましょう。 基本的に、グローバルノードは次の構造を持っていると言われています。

  • 1 - %%CLASSNAME
  • 2 - X
  • 3 - Y

したがって、グローバルは次のようになると予想されます。

^try.PointD(id) = %%CLASSNAME, X, Y

しかし、グローバルを出力すると空になります。ここではデータを追加していなかったためです。

zw ^try.PointD

オブジェクトを 1 つ追加しましょう。

set p = ##class(try.Point).%New()
set p.X = 1
set p.Y = 2
write p.%Save()

すると、グローバルはこのようになります。

zw ^try.PointD
^try.PointD=1
^try.PointD(1)=$lb("",1,2)

ご覧のように、期待する構造 %%CLASSNAME, X, Y はオブジェクトの X プロパティと Y プロパティに対応する $lb("",1,2) とセットになっています(%%CLASSNAME はシステムプロパティですので無視してください)。

次のように SQL を使用してレコードを追加することもできます。

INSERT INTO try.Point (X, Y) VALUES (3,4)

すると、グローバルの内容は次のようになります。

zw ^try.PointD
^try.PointD=2
^try.PointD(1)=$lb("",1,2)
^try.PointD(2)=$lb("",3,4)

つまり、オブジェクトまたは SQL を介して追加するデータは、ストレージ定義に従ってグローバルに格納されます(補足:PointDefaultData の X と Y を置き換えることでストレージ定義を手動で変更できます。その場合に新しいデータがどうなるかを確認してください!)。

では、SQL クエリを実行したい場合はどうなるのでしょうか?

SELECT * FROM try.Point

これは ^try.PointD グローバルを反復処理し、ストレージ定義(正確にはその PointDefaultData 部分)に基づいてカラムにデータを入力する ObjectScript コードに変換されます。

今度は変更を行います。 テーブルからすべてのデータを削除しましょう。

DELETE FROM try.Point

すると、この時点でグローバルの内容は次のようになります。

zw ^try.PointD
^try.PointD=2

ここでは ID カウンターのみが残っているため、新しいオブジェクト/レコードの ID は 3 になることに注意してください。 また、クラスとテーブルは引き続き存在します。

しかし、次を実行するとどうなるでしょうか。

DROP TABLE try.Point

これはテーブルとクラスを破棄し、グローバルを削除します。

zw ^try.PointD

皆さんがこの具体例に従い、グローバル、クラス、テーブルがどのように統合され、相互に補完しているかをより深く理解できたことを願っています。 手元の仕事に適切な API を使用すれば、開発がより高速かつアジャイルになり、バグが少なくなります。

0
0 557
記事 Minoru Horita · 6月 3, 2020 10m read

前のパート(12)では、ツリーとしてのグローバルを話題に取り上げました。 この記事では、それらを疎な配列と見なします。 

疎な配列は、ほとんどの値が同一であると想定される配列の種類です。 

疎な配列は実際には非常に大きいため、同一の要素でメモリを占有することには意味がありません。 したがって、疎な配列を整理し、重複した値の格納にメモリが浪費されないようにすることには意味があります。 

疎な配列は、JMATLABなど一部のプログラミング言語では言語の一部になっています。 他の言語では、疎な配列を使用できるようにする特別なライブラリが存在します。 C++の場合は、Eigenなどがあります。 

次の理由により、グローバルは疎な配列を実装するのに適した候補であると言えます。 

  1. 特定のノード値のみを保存し、未定義のノード値を保存しないこと。 
  1. ノード値のアクセスインターフェースが、多くのプログラミング言語が多次元配列の要素にアクセスするために提供しているものとよく似ていること。 
Set ^a(1, 2, 3)=5 
Write ^a(1, 2, 3) 
  1. グローバルはデータを格納するためにかなり低レベルの構造を採用しているため、優れたパフォーマンス特性を備えていること(ハードウェアによっては毎秒数十万から数千万のトランザクションを処理可能、1をご覧ください)。 
0
0 785
記事 Minoru Horita · 6月 2, 2020 14m read

最初の記事については、パート1を参照してください。 

3. グローバルを使用する場合のさまざまな構造 

順序付きツリーなどの構造には、さまざまな特殊ケースがあります。 グローバルを使用する上で実用的な価値があるものを見てみましょう。 

3.1 特殊ケース1  - 枝のない1つのノード 

グローバルは配列のようにも、通常の変数のようにも使用できます。 例えば、カウンターを作成する場合を考えてみましょう。 

Set ^counter = 0  ; カウンターの設定 

Set id=$Increment(^counter) ;  アトミックなインクリメント操作 

  

また、グローバルには値に加えて枝を持たせることができます。 一方が他方を除外することはありません。 

3.2 特殊ケース2  - 1つのノードと複数の枝 

実際、これは典型的なキー・バリューベースのデータ構造です。 また、値の代わりに値のタプルを保存すると、主キーを持つ通常のテーブルが得られます。 

グローバルに基づくテーブルを実装するには、カラムの値から文字列を作成し、主キー別にそれをグローバルに保存する必要があります。 読み取りの時に文字列をカラムに分割できるようにするため、以下のいずれかを使用することができます。 

  1. 区切り文字
0
0 752
記事 Minoru Horita · 4月 30, 2020 9m read

データを格納するための魔法の剣であるグローバルは、かなり前から存在しています。しかしながら、これを効率的に使いこなせる人や、この素晴らしい道具の全貌を知る人はそう多くありません。 グローバルを本当に効果を発揮できるタスクに使用すると、パフォーマンスの向上やソリューション全体の劇的な単純化といった素晴らしい結果を得ることができます(12)。 

グローバルは、SQLテーブルとはまったく異なる特別なデータの格納・処理方法を提供します。 グローバルは1966年にM(UMPS)プログラミング言語で初めて導入され、医療データベースで使用されていました。 また、現在も同じように使用されていますが、金融取引など信頼性と高いパフォーマンスが最優先事項である他のいくつかの業界でも採用されています。 

M(UMPS)は後にCaché ObjectScript(COS)に進化しました。 COSはInterSystemsによってMの上位互換として開発されました。 元の言語は現在も開発者コミュニティに受け入れられており、いくつかの実装で生き残っています。 ウェブ上では、MUMPS GoogleグループMumpsユーザーグループISO規格といった複数の活動が見られます。 

0
0 809