表。主键和生成器
NOTICE: This document is the chapter from the book “The InterBase World” which was written by Alexey Kovyazin and Serg Vostrikov.
InterBaseはリレーショナルDBMSです。つまり、InterBase内のすべてのデータはテーブルとして格納されます。SQLの観点から実現されるテーブルは、紙に手書きしたり、Microsoft Excelのようなプログラムで作成できる通常のテーブルと非常に似ています。InterBaseのテーブルには、データが配置される列と行があります。テーブルには必ず名前が必要であり、その名前は1つのデータベース内で一意である必要があります。テーブルはデータベース内の情報の主要な格納場所であり、そのためテーブルを作成する際には非常に注意する必要があります。
リレーショナルデータベースでテーブルを作成する方法を説明するルールがあり、現実世界のデータを反映しつつ、データベース内の情報を効率的に格納することを可能にします。これらのルールを「正しい」データベースを設計するために適用するプロセスは、正規化と呼ばれます。私たちは「正しい」という言葉を意図的に引用符で囲みました。なぜなら、「正規化されたデータベース」と「最適化されたデータベース」は同義語ではないからです。正規化のルールに曖昧さなく従う必要はありません。常に特定の問題の仕様に合わせて修正を適用してください。
データベース内のテーブルの正規化については、書籍[14.で詳細に検討されています。そのため、私たちは手に負えないものを抱え込もうとはせず、議論のテーマであるInterBaseテーブルに戻りましょう。テーブルを作成できるDDL(DDL - Data Definition Language、詳細は用語集を参照)文の構文を考えてみましょう:
CREATE TABLE table [EXTERNAL [FILE] “”] ( [, | …]);
ここで、tableは作成されるテーブルの名前、 は作成されるテーブルの列(フィールドと呼ぶこともあります)の説明です。オプションtable [EXTERNAL [FILE] “”]は、共有データベースファイルではなく、 という名前の別のファイルに格納されるいわゆる外部テーブルが作成されることを意味します。ご覧のとおり、すべては簡単です。テーブル名とそのテーブルが含む列を定義します。次に、列を定義する方法を詳しく見ていきましょう。列を作成する構文は、次のDDL文で説明されます:
= col { datatype | COMPUTED [BY] (< expr>) | domain}
[DEFAULT { literal | NULL | USER}]
[NOT NULL] [ ]
[COLLATE collation]
これはかなり大きな定義ですが、列の定義では、指定された文のごく一部だけが必須です。テーブル内の各列には、テーブル内で一意の名前が必要であり、また、datatype文で定義されるデータ型、または列の値を計算するための式(計算列の場合)、またはドメイン(下記参照)で定義されるドメインが必要です。データ型は「データ型」の章で検討しました。そのため、テーブルを作成するためのSQL式がどのように形成されるかを簡単に理解できます。
先ほど「データベースの作成」の章で作成したFIRSTBASE.gdbデータベースに接続し、実際にテーブルを操作してみましょう。テーブルの作成、削除、更新に関しては、「InterBase管理者および開発者ツール」アプリケーションにリストされているInterBase管理ツールのいずれか、および任意のInterBaseクローンの供給セットに含まれる標準ユーティリティisql.exeが適しています。
以下は、TABLE_EXAMPLEという名前の単純なテーブルの例で、さまざまな型の3つのフィールドを含んでいます:
CREATE TABLE Table_example ( ID INTEGER, NAME VARCHAR(80), PRICE_1 DOUBLE PRECISION);
このテーブルは、データベース開発プロセスで最も頻繁に発生するケースを示しています。ただし、フィールドを定義する他の方法もあります。たとえば、ドメインを使用してフィールド型を設定できます。ドメインは、特定の型パラメータの組み合わせを適用する利便性のためにユーザーが定義する型です。たとえば、識別子のフィールドを指定するためのドメインD_IDを定義できます。ドメインを定義すると、それを使用してフィールドの型を設定できます:
CREATE DOMAIN D_ID AS INTEGER; CREATE TABLE Тable_example ( ID D_ID, NAME VARCHAR(80), PRICE_1 DOUBLE PRECISION);
フィールドIDは、ドメインD_IDで定義された型を持ちます。このように、フィールドの型をドメインで定義し、必要なチェックと制約を設定することで、同じ機能のフィールドを作成するためにこのドメインを何度も適用できます。たとえば、変数の型の定義をコピーする際の面倒で間違いの危険を伴う作業なしに、金額を扱うことができます。テーブル内の列を設定する3番目の方法は、計算列(COMPUTED BY)として定義し、その値が計算される条件を指定することです。たとえば、テーブルにフィールドPRICE_1の値の10%を計算する列を持たせたい場合があります。この場合、次のコマンドを記述する必要があります:
CREATE TABLE Тable_example ( ID INTEGER, NAME VARCHAR(80), PRICE_1 DOUBLE PRECISION, PRICE_10 COMPUTED BY (PRICE_1.0.1));
しかし、フィールドPRICE_1にデータを挿入するとすぐに、フィールドPRICE_10にこのフィールドの値の10分の1が表示されるとは思わないでください。いいえ、ここでのプロセスはより複雑です。実際には、必要な10分の1は、たとえばこのテーブルに対するSELECTクエリを実行するときなど、フィールドPRICE_10を参照した場合にのみ取得されます。つまり、計算フィールドにはデータは格納されず、フィールドに関連付けられた式の計算がフェッチされ、その結果がクエリの回答として生成されます。
これで、テーブル内のフィールドを指定する3つの主要な方法を検討しました。次に、列を作成するときに設定できるオプションを詳しく見ていきましょう。オプション[DEFAULT {literal | NULL | USER}] - 列のデフォルト値を設定できます。これは自動データ入力に非常に便利です。デフォルトを設定する方法は3つあります。1つ目はliteralとして指定され、テキスト定数、数値、または日付としてデフォルト値を設定できます。たとえば、テキストデフォルトを持つ列を作成するための次の式を生成できます:NAME VARCHAR(80) DEFAULT ‘Василий Станиславович’
これにより、テーブルに挿入されたすべてのフィールドはデフォルト値を取ります。つまり、フィールドNAMEに他の値が定義されていない場合、文字列「Vasily Stanislavovich」が表示されます。デフォルトを設定する2番目の方法は、列の定義でDEFAULT NULLを指定することです。そして、再作成されたレコードでは、他の値が明示的に設定されていない限り、この列の値はNULLになります。例:
PRICE_1 DOUBLE PRECISION DEFAULT NULL
デフォルトを設定する3番目の方法は、列の定義でDEFAULT USERを指定することです。これにより、再作成されたレコードでは、このフィールドに現在のユーザーの名前が含まれます。つまり、InterBaseに接続してこの挿入を実行したユーザーです(ユーザーの詳細については、「InterBaseのセキュリティ:ユーザー、その機能と権利」(パート4)の章を参照してください)。一部のフィールドでは、フィールドに空でない値があることが不可欠です。たとえば、問題の仕様に従って空にできないフィールドです。フィールドが定義された値を持つべきであるという制約をデータベースレベルで設定するには、列の説明に次の追加を行う必要があります:
NAME VARCHAR(80) NOT NULL
これにより、NULLを格納できないフィールドができます。通常、NOT NULL制約は、このフィールドに正しい値を確実に割り当てるDEFAULTオプションと組み合わされます。ただし、NOT NULL制約だけでは不十分な場合がよくあります。たとえば、データベースに価格を格納する場合、価格が負の値を取れないことは明らかです(商品を購入するときに追加料金が支払われるなら素晴らしいのですが)。サーバーがデータベースに挿入された価格の値を正の条件でチェックするようにするには、列を次のように定義する必要があります:
PRICE_1 DOUBLE PRECISION CHECK (PRICE_1>0)
列PRICE_1に挿入された値は、正の条件でチェックされます。異なる一貫したオプションを組み合わせることができ、たとえば、空でない値と正のチェックを設定できることに注意してください:
PRICE_1 DOUBLE PRECISION NOT NULL CHECK (PRICE_1>0)
テーブル作成時に、いくつかのオプションは組み合わせることができません。例えば、デフォルトでNULLを許可し、同時に非空値制約を設定することは不可能です。チェック制約がデータベースのデータ管理において有用な機能を実行できることに注意すべきです。それらの使用方法については、「データベース制約」の章で詳しく説明します。
さて、さまざまなオプションを備えたテーブルとフィールドの作成方法について説明しました。しかし、既存のテーブルを変更しなければならない場合もあります。確かに、テーブルを完全に再作成することもできます。まず、テーブルを削除するコマンドを実行し、その後で再作成します。例えば:
DROP TABLE Table_example; CREATE TABLE Table_example(ID NUMERIC(15,2);
しかし、このようなテーブル変更方法には重大な欠点があります。DROPコマンドを使用してテーブルを削除すると、テーブルに含まれるすべてのデータが削除され、データを失わないようにするには、一時テーブルにコピーする必要があります。これはかなり面倒です。そこで、テーブルの構造を簡単に変更するためのALTER TABLEコマンドがあります。これにより、新しいフィールドの追加、既存フィールドの削除、参照整合性制約の追加・削除が可能になります。
例えば、人物の父称を格納するための列をテーブルに追加したい場合:
ALTER TABLE Table_example ADD Patronimic VARCHAR(80);
このコマンドの実行後、テーブルTable_exampleには、Patronimicという名前のVARCHAR(80)型の新しい列が追加されます。テーブルからNAMEという名前の列を削除したい場合は、次のコマンドを実行します:
ALTER TABLE Table_example DROP Name;
ALTER TABLEステートメントの完全な構文は[1]に記載されています。これは非常に便利なコマンドであり、頻繁に使用します。
では、列を変更する必要がある場合はどうすればよいでしょうか?例えば、名前を格納するためにNAMEではなくHUMAN_NAMEというフィールドを使用する方が良いと判断したとします。この場合、ALTER TABLEを適用できます:
ALTER TABLE Table_example ALTER COLUMN NAME TO HUMAN_NAME;
フィールドの型を変更する場合、例えばフィールドに格納される文字数を増やす場合、ALTER DOMAINステートメントを使用してこのフィールドのドメインを変更する必要があります(前述の「データ型」の章を参照)。
さて、InterBaseでのテーブルの作成と変更について説明しました。次は、データベースの理論について少し深く掘り下げる時です。InterBaseは前述のとおりリレーショナルデータベースです。さらに、これはテーブル内の各レコードが、他のレコードと区別できる特性を持つべきであることを意味します。この目的のために、一意キーの特別なメカニズムが使用されます。
テーブルの主キー
もちろん、キーを一切含まないテーブルを作成することもできます。これは禁止されていません。しかし、前述のとおり、効率的なデータベースの作成は正規化のルールを守らなければ不可能です。キーの存在は正規化の最も重要な要素です。したがって、データベースの理論と正規化を詳しく検討することを目的とはしていませんが、キーの定義を紹介し、InterBaseでの機能を確認する必要があります。段階的に進めていき、最も一般的なキーの種類である主キーから始めましょう。
では、主キーとは何でしょうか?それは、テーブル内のレコードを一意に識別する、テーブル内の1つ以上のフィールドです。難しく聞こえますが、実際にはすべて非常に簡単です。普通のテーブル、例えば会計シートを想像してください。最初の列は何でしょうか?そうです、連番 - 1、2、3… この番号はテーブル内の一意の行を示し、この番号を知っていればテーブル内の文字列を見つけるのに十分です。この例では、これが主キーになります。リレーショナルデータベースのテーブルの圧倒的多数は、必ず主キー(PK - Primary keyの略)を持っています。テーブル作成時の一般的なガイドラインは、主キーを作成することです。主キーはテーブル作成時または後で作成できます。テーブル作成時にフィールドIDを主キーにすると決めたと仮定しましょう。その場合、主キーは次のように追加できます:
CREATE TABLE Table_example ( ID INTEGER NOT NULL, NAME VARCHAR(80), PRICE_1 DOUBLE PRECISION, CONSTRAINT pkTable PRIMARY KEY (ID));
テーブルtable_exampleに主キーを作成するために何が行われたでしょうか?テーブルの定義で何が変わったか確認しましょう。まず、列IDに追加の定義NOT NULLが付けられました。これは重要です。主キーは一意であり、未定義の値を禁止する必要があるからです。そして、ご存知のとおりNULLは未定義の値です。したがって、主キーに含まれるすべてのフィールドには、NOT NULL制約が必要です。主キーの作成を完了するには、テーブルの最後に次のように記述します:CONSTRAINT ()
制約の完全な構文は「データベース制約」の章þ.1にあります。主キーの例では、次のようになります:
CONSTRAINT pkTable PRIMARY KEY (ID)
ここで、pkTableは主キーの名前、IDはそれに含まれる列です。このような主キーの定義方法は、テーブルを一括作成する場合(例えば、さまざまなCASEツールから取得したスクリプトに基づいてデータベースのプロトタイプを構築する場合)に便利です。しかし、テーブルが既に存在しデータが入力されている場合に、主キーを追加・削除する必要がある場合はどうすればよいでしょうか?この目的のために、ALTER TABLEコマンドのもう1つの拡張を適用する必要があります。テーブルに主キーを追加する例:
ALTER TABLE TABLE_EXAMPLE ADD CONSTRAINT FF PRIMARY KEY (ID);
これにより、テーブルTable_exampleには、前の例でテーブルと一緒に作成された場合とまったく同じ主キーが設定されます。主キーを削除するには、次のコマンドを入力します:
ALTER TABLE Table_example DROP CONSTRAINT pkTable;
これにより、pkTableという名前のキーがデータベースから削除されます。
ジェネレータ - 主キーの親友
主キーの実装について少し説明する必要があります。主キーは一意性を維持するためのものであるため、1つのテーブル内の2つのレコードが同じキー値を持つことはできません。つまり、テーブルに新しいレコードを挿入する際、InterBaseはテーブル内のすべてのレコードをチェックし、そのような値がテーブルに存在するかどうかを確認する必要があります。高速検索のために、InterBaseにはインデックスというメカニズムがあります。これは、テーブル内のレコードを非常に迅速に見つけることを可能にする特別なInterBaseオブジェクトです。したがって、主キーを作成・削除する際、主キーに含まれるフィールドに対してインデックスが作成・削除されます。
前述のとおり、主キーには複数のフィールドを含めることができます。これにより、これらのフィールドの値の組み合わせの一意性を保証できます。例えば、フィールドIDとNAMEにキーを定義する場合、サーバーはテーブル内にこれらのフィールドの同一の組み合わせがないことを制御します。つまり、IDと1および「Ivanov」、2および「Ivanov」の組み合わせは、IDフィールドの値が異なるため正しいと見なされます。
このように、主キーには任意の型の複数のフィールドを含めることができます。しかし、実際には最も一般的なキーの型はカウンタです - 増加する値を含む整数フィールドです。なぜでしょうか?これは、自然キーと代替キーの間の古くからの論争を反映しています。自然キーの概念では、データベースが反映するデータ領域に実際に存在する値をキーとして使用することを試みるべきだとされています。例えば、パスポート事務所のための人物登録システムを開発する場合、この概念によれば、パスポートの番号と系列の組み合わせを主キーとして取るべきです。実際、すべての人はパスポートの番号と系列の一意の組み合わせを持っています。しかし、人が生涯の間にパスポートを変更する可能性があるという事実(特定の年齢に達したこと、結婚などに関連して)はどうでしょうか?この場合、特定の人に対応するパスポートの番号と系列を変更する必要があります。つまり、実際には主キーを変更することになります。これはデータベースアプリケーションの開発の観点から望ましくありません:テーブル間の分岐した通信システム(次の章で説明)を考慮すると、開発者はこの状況を制御するために多大な労力を費やす必要があります。
因此,在大多数情况下会使用替代键。替代意味着人工的,即不存在于我们数据库所描述的数据域中,而是为了数据库应用开发的便利而人工创建的。如前所述,通常计数器作为主键。一些数据库管理系统,如Paradox和MS SQL,有特殊的类型–计数器(自动递增)。当向表中添加新记录时,该类型的字段值会自动按增量增加–通常是按单位增加。在InterBase中没有计数器类型的字段,但可以实现这种行为。为了创建在向表中添加记录时自动填充的字段,使用了一系列资源:其中第一个是生成器。
什么是生成器?简单来说,生成器是一个命名的计数器。在数据库内,我们可以创建一个计数器,给它一个在该数据库内唯一的名称,并控制该计数器的值。这就是生成器。下面是一个DDL语句的示例,它将为您解释这一点:
CREATE GENERATOR g1; SET GENERATOR g1 TO 2445;
在这个示例的第一行中,创建了名为g1的生成器,第二行中为该生成器分配了值2445。现在有一个问题是如何使用所获得的生成器。InterBase中有一个内置函数GEN_ID用于获取和更改生成器的值。该函数将生成器的名称和要应用于该生成器的增量值作为参数,并返回一个整数值,该值对应于将增量添加到生成器后得到的生成器值。以下是在触发器或存储过程中调用GEN_ID函数的示例:
Current_value = GEN_ID (g1, 1)
如果我们想获取生成器的值,可以使用以下查询:
SELECT GEN_ID(g1, 1)FROM RDB$ DATABASE
由于表RDB $ Database始终只包含一条记录,因此作为给定查询的结果,我们将获得生成器g1的值。
这里current_value是一个变量(在后续章节中您将找到如何在InterBase中使用变量的信息),g1是一个生成器,1是增量。在此示例中,生成器g1的值在将增量1添加到它之后将进入变量current_value,即生成器的下一个值。请注意,增量可以不等于1!而且,它甚至可以是负数:Current_value = GEN_ID (g1, -23)
执行此函数的结果是,生成器g1的当前值将减去23。如您所见,生成器的可能应用范围相当广泛–它不仅可用于获取主键的值,还可用于监视数据库中的全局更改。
熟悉数据库的人可能会问:“如果同时有多个客户端尝试将数据放入同一张表,并同时‘拉取’生成器,会发生什么?他们会获得相同还是不同的生成器值?”他们将明确获得不同的生成器值。无论尝试获取生成器值的“同时性”如何,每个申请者都将获得唯一的值。这是由生成器的“构造”保证的:它们在服务器的最低级别工作,没有任何记录和插入过程影响它们–通常说生成器在“事务上下文之外”工作。如果您想了解事务,请阅读“事务。事务参数”一章(第1部分);关于生成器是如何安排的–“InterBase数据库结构”(第4部分)。好了,凭借生成器,我们有了一个可靠的机制来创建唯一的主键。然而,我们能使用这个机制吗?如何将生成器获得的值放入主键字段?
为此,有两种方式–代表客户端插入主键和代表服务器插入主键。要掌握第一种方式,我们应该参考“FIBPlus主要组件的使用”一章,要理解第二种方式,请参考“触发器”一章(第1部分)。这里我们将简要回顾这两种方式的主要要点。
在代表客户端创建主键的情况下,会发生以下情况。当生成将要插入数据库的记录时,执行GEN_ID函数调用(, 1),并将获得的值替换为该记录。然后进行向表的插入,我们保证获得唯一的主键。
第二种方式–代表服务器创建主键–通常完全消除了客户端对主键值是什么的担忧。在这种情况下,插入记录时触发器会工作–这是一个特殊的数据库对象,可以在插入/删除/更新表中的记录时执行任何操作。在这个触发器中执行以下操作:调用GEN_ID函数,获取所需的生成器值并将其插入表中。第二种方式的优点是,在开发客户端应用程序时,完全不需要担心主键的创建,您只需编写一次必要的触发器。但缺点是,我们无法在插入后立即在应用程序中接收生成的键值!如果我们使用第一种方式,我们可以接收主键的值,尽管每次插入时都需要关心其创建。很难确切地说哪种方式更好,这取决于具体问题。在本书的后续部分,我们将考虑处理主键问题的可能变体。
结论
因此,在本章中,我们回顾了如何在InterBase中创建和更新表,以及如何处理主键。这样,我们考虑了InterBase中可以被有条件地称为静态的主要对象,因为它们只存储信息而不执行其转换。接下来,我们将讨论数据库内信息控制和信息转换的方式。