記事一覧へ戻る

掲載元
Oracle Magazine
2013年9/10月

テクノロジー:PL/SQL

  

PL/SQLの機能強化

Steven Feuerstein著Oracle ACE Director

 

Oracle Database 12cでは、PL/SQLファンクション結果キャッシュの機能強化、SQLでのPL/SQL実行の改善、ホワイトリストの追加、権限の微調整が実現されています。

Oracle Database 12cではPL/SQLプログラム・ユニットの定義と実行の方法について、さまざまな機能強化が行われています。この記事では、次のことを実現できるOracle Database 12cの各機能について説明します。

  • RESULT_CACHE句のInvoker権限のファンクションを最適化する

  • SQL文内でPL/SQLファンクションを定義し、実行する

  • ACCESSIBLE BY句で指定した"ホワイトリスト"を利用してプログラム・ユニットへのアクセスを制限する

  • プログラム・ユニットにロールを付与して、そのユニットの権限を微調整する 

Invoker権限とPL/SQLファンクション結果キャッシュ

Oracle Database 11gではPL/SQLファンクション結果キャッシュが導入されました。ファンクション結果キャッシュは、強力かつ効率的で使いやすいキャッシュ・メカニズムです。このキャッシュのおもな目的は、あるデータ行がデータベースからの前回のフェッチ時点から変更されていない場合に、その行を再度取得するためにSQL文を実行しないで済むようにすることです。

 前号のPL/SQL Challengeの正解

 
Oracle Magazine 3/4月号の"カーソルの操作"で出題されたPL/SQL Challengeの質問は、PL/SQLでのカーソルの操作に関連し、指定した主キーの行の情報を含むレコードを返すファンクションを選択させるものでした。4つの選択肢のすべてが正解ですが、SELECT-INTO文を使用する(a)のみが推奨されます。 

これは、データベース・インスタンス全体に当てはまります。つまり、USER_ONEスキーマに接続するユーザーが、結果キャッシュ・ファンクションを実行して、employees表の従業員ID 100の行を取得したとします。次に、USER_TWOスキーマに接続する別のユーザーが、同じ従業員IDに対して同じファンクション・コールを実行した場合、その行の情報はSELECT文の実行なしに、キャッシュから直接取得されます。

この機能をまだ使用していない場合(かつOracle Database 11gを使用している場合)は、この機能について調査し、適用を始めることを強くお勧めします。また、その際には、DBAと密に連携して、結果キャッシュ・プールのサイズを適切に設定してください。

ただし、Oracle Database 11g Release 2でも、Invoker権限(AUTHID CURRENT_USER句)とファンクション結果キャッシュ(RESULT_CACHEキーワード)を組み合わせて使用することはできません。たとえば、次のファンクションをコンパイルしてみます。 

CREATE OR REPLACE FUNCTION last_name (
  employee_id_in 
  IN employees.employee_id%TYPE)
  RETURN employees.last_name%TYPE
  AUTHID CURRENT_USER
  RESULT_CACHE
IS
  l_return   employees.last_name%TYPE;
BEGIN
  SELECT last_name
    INTO l_return   
    FROM employees
   WHERE employee_id = employee_id_in;

  RETURN l_return;
END;
/

 
この場合、次のコンパイル・エラーが発生します。 

PLS-00999: implementation restriction 
(may be temporary) RESULT_CACHE is 
disallowed on subprograms in 
Invoker-Rights modules

 
このような制限は、Invoker権限の本質によるものです。実行時に、PL/SQLエンジンは現在のユーザーの権限を利用して、表やビューなどのデータベース・オブジェクトへの参照を解決します。しかし、そのようなファンクションをRESULT_CACHEとともにコンパイルした場合、(上の例に基づくと)USER_ONEが100を渡してファンクションを実行した後に、USER_TWOが同じファンクションをコールすると、USER_TWOの権限に従うためにファンクションの本体が実行されず、EMPLOYEES表への参照が解決されません。これは重大なセキュリティ問題につながる可能性があります。

もっとも、嬉しいことに、この制約は一時的なものでした。Oracle Database 12cでは、上の例のlast_nameのようなファンクションをエラーなしにコンパイルできます。また、言うまでもなくOracle Database 12cでは正しい処理が実行されます。

Oracle Database 12cは水面下で、現在のユーザーの名前を隠しパラメータとして渡します。この値も、ファンクションに渡されるすべての引数の値とともにキャッシュされます。そのため、last_nameファンクションがコールされるたびに、Oracle Database 12cでは、そのファンクションが以前にコールされたときに、同じ従業員IDと同じ現在のユーザーが使用されたかどうかが検証されます。

つまり、Invoker権限のファンクションの結果キャッシュが、現在のユーザー名ごとに(論理的に)パーティション分割されます。結果的に、Invoker権限のファンクションの結果キャッシュでは、同じユーザーが同じ引数の値を使用してファンクションを繰り返しコールする場合に限り、パフォーマンスが向上します。別の角度から説明すると、Oracle Database 11g Release 2では、リスト1のようにlast_nameファンクションの実装を変更した場合に限り、同じ効果を得ることができました。

コード・リスト1:"パーティション分割された"Oracle Database 11g Release 2のInvoker権限のファンクション 

CREATE OR REPLACE PACKAGE employee_api
   AUTHID CURRENT_USER
IS
   FUNCTION last_name (
      employee_id_in IN employees.employee_id%TYPE)
      RETURN employees.last_name%TYPE;
END;
/

CREATE OR REPLACE PACKAGE BODY employee_api
IS
   FUNCTION i_last_name (
      employee_id_in   IN employees.employee_id%TYPE,
      user_in          IN VARCHAR2 DEFAULT USER)
      RETURN employees.last_name%TYPE
      RESULT_CACHE
   IS
      l_return   employees.last_name%TYPE;
   BEGIN
      SELECT last_name
        INTO l_return
        FROM employees
       WHERE employee_id = employee_id_in;

      RETURN l_return;
   END;

   FUNCTION last_name (
      employee_id_in IN employees.employee_id%TYPE)
      RETURN employees.last_name%TYPE
   IS
      l_return   employees.last_name%TYPE;
   BEGIN
      RETURN i_last_name (employee_id_in,
                          USER);
   END;
END;
/

 
last_nameファンクションがパッケージ仕様部に定義されており、結果がキャッシュされない点に注意してください。この(パッケージ仕様部に宣言された)パブリック・ファンクションは、単純にファンクションの内部的なプライベート・"バージョン"をコールしているに過ぎません。このプライベート・バージョンには、ユーザーを表す第2パラメータがあります。

そのため、employee_api.last_nameをコールするたびに、Oracle Database 11g Release 2ではデータベースによって使用される値のセットにユーザー名が追加され、結果キャッシュ内に一致するものがあるかが判定されます。

しかし、このような工夫は今や必要ありません。Oracle Database 12cでは、Invoker権限のプログラムにRESULT_CACHEを追加する価値があるかを判断するだけで良いのです。

SQL文内でのPL/SQLサブプログラムの定義

SQL文内から独自のPL/SQLファンクションをコールできるのは、ずいぶん以前からのことです。たとえば、指定した開始位置から終了位置までのサブストリングを返すBETWNSTRいうファンクションを作成したとします。 

FUNCTION betwnstr (
   string_in      IN   VARCHAR2
 , start_in       IN   PLS_INTEGER
 , end_in         IN   PLS_INTEGER
)
   RETURN VARCHAR2 
IS
BEGIN
   RETURN ( SUBSTR (
        string_in, start_in, 
        end_in - start_in + 1 ));
END;

 
このファンクションは、問合せ内で次のように使用できます。 

SELECT betwnstr (last_name, 3, 5) 
  FROM employees

 
このアプローチにより、SQL言語をアプリケーション固有の機能で"拡張"し、かつアルゴリズムを(コピーするのではなく)再利用できます。しかし、ユーザー定義ファンクションをSQL内で実行することには欠点があります。それは、SQL実行エンジンとPL/SQL実行エンジンの間でコンテキスト・スイッチが発生することです。

Oracle Database 12cでは、PL/SQLのファンクションとプロシージャを副問合せのWITH句内で定義し、他の組込みファンクションやユーザー定義ファンクションと同じように使用できるようになりました。この機能により、前述のBETWNSTRファンクションと問合せを次の1文にまとめることができます。 

WITH
 FUNCTION betwnstr (
     string_in   IN VARCHAR2,
     start_in    IN PLS_INTEGER,
     end_in      IN PLS_INTEGER)
 RETURN VARCHAR2
 IS
 BEGIN
   RETURN (SUBSTR (
       string_in, 
       start_in, 
       end_in - start_in + 1));
 END;

SELECT betwnstr (last_name) 
  FROM employees

 
では、なぜ開発者はPL/SQLファンクションのロジックをSQL文内にコピーしたいと思うのでしょうか。それは、パフォーマンスを改善するためです。独自のPL/SQLファンクションをSQL文内でコールすると、SQLエンジンは、パフォーマンスに影響を及ぼすPL/SQLエンジンへのコンテキスト・スイッチを実行する必要があります。しかし、このコードをSQL文内に移動することで、そのようなコンテキスト・スイッチは発生しなくなります。

パッケージ化された定数の参照

パッケージ化されたファンクションはSQL内でコールできますが、パッケージ内で宣言された定数を参照することはできません(そのSQL文がPL/SQLブロック内で実行される場合を除く)。このような定数の参照に関する制限について、次に例を示します。 

SQL> CREATE OR REPLACE PACKAGE pkg
  2  IS
  3     year_number   
        CONSTANT INTEGER := 2013;
  4  END;
  5  /

Package created.

SQL> SELECT pkg.year_number 
FROM employees
  2   WHERE employee_id = 138
  3  /
SELECT pkg.year_number FROM employees
ERROR at line 1:
ORA-06553:PLS-221:'YEAR_NUMBER' is not 
a procedure or is undefined

 
この制限に対する従来の回避策は、パッケージ内にファンクションを定義して、そのファンクションをコールすることでした。 

SQL> CREATE OR REPLACE PACKAGE pkg
  2  IS
  3     FUNCTION year_number
  4        RETURN INTEGER;
  5  END;
  6  /

Package created.

SQL> CREATE OR REPLACE PACKAGE BODY pkg
  2  IS
  3     c_year_number   
        CONSTANT INTEGER := 2013;
  4
  5     FUNCTION year_number
  6        RETURN INTEGER
  7     IS
  8     BEGIN
  9        RETURN c_year_number;
 10     END;
 11  END;
 12  /

Package body created.

SQL> SELECT pkg.year_number
  2    FROM employees
  3   WHERE employee_id = 138
  4  /

YEAR_NUMBER
———————————
       2013

 
単にSQL文内で定数の値を参照するだけのために、大量のコードや作業が必要になります。しかし、Oracle Database 12cではこのような回避策も不要になりました。代わりに、WITH句内にファンクションを作成します。 

WITH
 FUNCTION year_number
 RETURN INTEGER
 IS
 BEGIN
   RETURN pkg.year_number;
 END;
SELECT year_number
  FROM employees
 WHERE employee_id = 138

 
SQL内にあるPL/SQLファンクションは、読取り専用のスタンバイ・データベースでも便利に機能するでしょう。そのようなデータベース内に"ヘルパー"のPL/SQLファンクションを作成することはできませんが、問合せ内に直接定義できます。

このWITH FUNCTION機能は非常に便利なSQL言語拡張です。ただし、WITH FUNCTION機能の利用を検討する際にはかならず次のように自問してください。「この同じ機能がアプリケーションの複数の場所で必要になるだろうか」

必要になる場合は、WITH FUNCTIONの利用によるパフォーマンス改善が、このロジックを複数のSQL文にコピーアンドペーストすることによる潜在的な欠点よりも価値があるかを判断する必要があります。

ホワイトリストとACCESSIBLE BY句

ほとんどのPL/SQLベースのアプリケーションは、多数のパッケージで構成されます。その中には、プログラマーがユーザー要件を実装するために使用する"トップ・レベル"のAPIもあれば、他の特定のパッケージにのみ使用されることを想定した"ヘルパー"のパッケージもあります。

Oracle Database 12cより前のPL/SQLでは、セッションのスキーマにEXECUTE権限が付与されているパッケージ内の任意またはすべてのサブプログラムをセッションが使用することを防止する手段はありませんでした。これに対してOracle Database 12cでは、すべてのPL/SQLプログラム・ユニットでオプションのACCESSIBLE BY句を使用できるようになりました。ACCESSIBLE BY句により、作成または変更対象のPL/SQLユニットに対して、アクセス可能な他のPL/SQLユニットのホワイトリストを指定できます。

例を見てみましょう。まず、"パブリック"なパッケージ仕様部を作成します。このパッケージは、他の開発者がアプリケーション構築のために使用することを想定しています。 

CREATE OR REPLACE PACKAGE public_pkg
IS
   PROCEDURE do_only_this;
END;
/

 
次に、自分用の"プライベート"なパッケージの仕様部を作成します。このパッケージは、パブリック・パッケージ(public_pkg)内からのみ呼び出すことができるという意味でプライベートです。そのため、ACCESSIBLE_BY句を追加します。 

CREATE OR REPLACE PACKAGE private_pkg   
   ACCESSIBLE BY (public_pkg)
IS
   PROCEDURE do_this;

   PROCEDURE do_that;
END;
/
 

次にパッケージの本体を実装します。public_pkg.do_only_thisプロシージャは、private_pkgのサブプログラムをコールします。 

CREATE OR REPLACE PACKAGE BODY public_pkg
IS
   PROCEDURE do_only_this
   IS
   BEGIN
      private_pkg.do_this;
      private_pkg.do_that;
   END;
END;
/

CREATE OR REPLACE PACKAGE BODY 
private_pkg
IS
   PROCEDURE do_this
   IS
   BEGIN
      DBMS_OUTPUT.put_line ('THIS');
   END;

   PROCEDURE do_that
   IS
   BEGIN
      DBMS_OUTPUT.put_line ('THAT');
   END;
END;
/

 
これで、パブリック・パッケージのプロシージャを問題なく実行できます。 

BEGIN
   public_pkg.do_only_this;
END;
/
THIS
THAT

 
しかし、無名ブロック内でプライベート・パッケージのサブプログラムをコールしようとすると、次のエラーが発生します。 

BEGIN
   private_pkg.do_this;
END;
/

ERROR at line 2:
ORA-06550: line 2, column 1:
PLS-00904: insufficient privilege to 
access object PRIVATE_PKG
ORA-06550: line 2, column 1:
PL/SQL:Statement ignored

 
さらに、プライベート・パッケージのサブプログラムをコールするプログラム・ユニットをコンパイルしようとしても、同じエラーが発生します。 

SQL> CREATE OR REPLACE PROCEDURE 
use_private
  2  IS
  3  BEGIN
  4     private_pkg.do_this;
  5  END;
  6  /
Warning:Procedure created with 
compilation errors.

SQL> SHOW ERRORS 

Errors for PROCEDURE USE_PRIVATE:

LINE/COL ERROR
———————— ——————————————————————————
4/4      PL/SQL:Statement ignored
4/4      PLS-00904: insufficient 
         privilege to access object 
         PRIVATE_PKG

 
"PLS"エラーが示すとおり、この問題はコンパイル時にキャッチされます。この機能を使用することによる実行時のパフォーマンスへの影響はありません。

プログラム・ユニットへの権限付与

Oracle Database 12cより前のリリースでは、Definer権限のプログラム・ユニット(AUTHID DEFINER句またはAUTHID句により定義)は常に、そのユニットのDefiner権限を使用して実行されました。一方、Invoker権限のプログラム・ユニット(AUTHID CURRENT_USER句により定義)は常に、そのユニットのInvoker権限を使用して実行されました。

この2種類のAUTHID設定の結果、すべてのユーザーが実行する必要のあるプログラム・ユニットを、Definer権限のユニットとして作成する必要がありました。そのプログラム・ユニットはDefinerのすべての権限を使用して実行されます。これは、セキュリティの観点からは望ましくありません。

Oracle Database 12cでは、PL/SQLのパッケージおよびスキーマレベルのプロシージャとファンクションに対してロールを付与できます。プログラム・ユニットに対するロールベースの権限によって、プログラム・ユニットのInvokerが利用できる権限を微調整できます。

Invoker権限を持つプログラム・ユニットを定義し、そのInvoker権限を、ロール経由で付与された限定的な固有の権限により補完できるのです。

プログラム・ユニットへのロールの付与方法とその影響を示す例を見てみましょう。HRスキーマに、departments表とemployees表が含まれるとします。これらの表を次のように定義してデータを移入します。 

CREATE TABLE departments
(
   department_id     INTEGER,
   department_name   VARCHAR2 (100),
   staff_freeze      CHAR (1)
)
/

BEGIN
   INSERT INTO departments
        VALUES (10, 'IT', 'Y');

   INSERT INTO departments
        VALUES (20, 'HR', 'N');

   COMMIT;
END;
/

CREATE TABLE employees
(
   employee_id     INTEGER,
   department_id   INTEGER,
   last_name       VARCHAR2 (100)
)
/

BEGIN
   DELETE FROM employees;

   INSERT INTO employees
        VALUES (100, 10, 'Price');

   INSERT INTO employees
        VALUES (101, 20, 'Sam');

   INSERT INTO employees
        VALUES (102, 20, 'Joseph');
   INSERT INTO employees
        VALUES (103, 20, 'Smith');

   COMMIT;
END;
/

 
また、SCOTTスキーマにはemployees表のみが含まれるとします。この表を次のように定義してデータを移入します。 

CREATE TABLE employees
(
   employee_id     INTEGER,
   department_id   INTEGER,
   last_name       VARCHAR2 (100)
)
/

BEGIN
   DELETE FROM employees;

   INSERT INTO employees
        VALUES (100, 10, 'Price');

   INSERT INTO employees
        VALUES (104, 20, 'Lakshmi');

   INSERT INTO employees
        VALUES (105, 20, 'Silva');

   INSERT INTO employees
        VALUES (106, 20, 'Ling');
   COMMIT;
END;
/ 


また、HRには、部門のスタッフが"固定(freeze)"されていない限り、指定した部門からすべての従業員を削除するプロシージャも含まれます。まずは、リスト2に示すように、このプロシージャをDefiner権限のユニットとして作成します。

コード・リスト2:従業員レコードを削除するDefiner権限のプロシージャ 

CREATE OR REPLACE PROCEDURE remove_emps_in_dept (
   department_id_in IN employees.department_id%TYPE)
   AUTHID DEFINER
IS
   l_freeze   departments.staff_freeze%TYPE;
BEGIN
   SELECT staff_freeze
     INTO l_freeze
     FROM HR.departments
    WHERE department_id = department_id_in;

   IF l_freeze = ‘N’
   THEN
      DELETE FROM employees
            WHERE department_id = department_id_in;
   END IF;
END;
/

 さらに、SCOTTがこのプロシージャを実行できるようにします。 

GRANT EXECUTE
   ON remove_emps_in_dept
   TO SCOTT
/ 

次のステップ 


 ダウンロード Oracle Database 12c

 テスト PL/SQLの知識

 その他の記事 PL/SQLの基礎、パート1~12  

詳細情報
 Oracle Database 12c
 PL/SQL


SCOTTがこのプロシージャを次の例のように実行すると、3行が削除されますが、その削除はHRのemployees表に対して実行されます。このプロシージャがDefiner権限のユニットであるためです。 

BEGIN
   HR.remove_emps_in_dept (20);
END;
/

 
このプロシージャを、HRではなくSCOTTのemployees表から行を削除するように変更する必要があります。これこそ、Invoker権限がまさに実行すべきことです。しかし、このプロシージャのAUTHID句を次のように変更し、 

AUTHID CURRENT_USER

 
プロシージャを再実行すると、次のように表示されます。 

BEGIN
*
ERROR at line 1:
ORA-00942: table or view does not exist
ORA-06512: at "HR.REMOVE_EMPS_IN_DEPT", line 7
ORA-06512: at line 2

 
問題は、Oracle DatabaseでSCOTTの権限を使用して、HR.departmentsとSCOTT.employeesという2つの表への参照を解決するようになったことです。SCOTTにはHRのdepartments表に対する権限がないため、Oracle DatabaseのORA-00942エラーが発生します。

Oracle Database 12cより前のリリースでは、DBAはHR.departmentsへの必要な権限をSCOTTに付与する必要がありました。しかし、Oracle Database 12cでは、次の手順を利用できます。 

CREATE ROLE hr_departments
/

GRANT hr_departments TO hr
/

 
HRに接続し、必要な権限をこのロールに付与して、さらにこのロールをプロシージャに付与します。 

GRANT SELECT
   ON departments
   TO hr_departments
/

GRANT hr_departments TO PROCEDURE remove_emps_in_dept
/

 
これで、SCOTTから次の文を実行した場合に、SCOTT.employees表から行が削除されます。 

SELECT COUNT (*)
  FROM employees
 WHERE department_id = 20
/

  COUNT(*)
—————————————
         3

BEGIN
   hr.remove_emps_in_dept (20);
END;
/

SELECT COUNT (*)
  FROM employees
 WHERE department_id = 20
/


  COUNT(*)
—————————————
         0

 
プログラム・ユニットに付与したロールはコンパイルに影響を及ぼしません。代わりに、実行時にユニットが発行するSQL文の権限チェックに影響を及ぼします。そのため、プロシージャまたはファンクションは、それ自体のロールと、現在有効化されているその他すべてのロールの両方の権限に基づいて実行されます。

この機能は、Invoker権限のプログラム・ユニットの場合にもっとも有用です。また、Definer権限のユニットが動的SQLを実行する場合にも、そのユニットにロールを付与するかを検討することになるでしょう。その動的な文の権限が実行時にチェックされるためです。

次のステップ:SQLを実行するためのPL/SQLの機能強化

Oracle Database 12cでは、プログラム・ユニットの定義と実行における柔軟性と機能性が大幅に向上しています。Oracle Database 12cの機能により、PL/SQL開発者はInvoker権限をファンクション結果キャッシュとともに使用すること、SQL文内でPL/SQLサブプログラムを定義して実行すること、ホワイトリストによってプログラム・ユニットへのアクセスを制限すること、プログラム・ユニットにロールを付与することが可能です。

Oracle Database 12cでは、PL/SQLプログラム・ユニット内でのSQL実行も、さまざまな方法で強化されています。この点について、次号のOracle Magazineで取り上げます。

 

クイズにチャレンジ

 

PL/SQLに関するそれぞれの記事では、記事の中で説明した情報の知識をテストするクイズを毎回出題しています。このクイズは以下の他、PL/SQL Challenge(plsqlchallenge.com)にも掲載されます。PL/SQL Challengeは、PL/SQL言語やSQL、Oracle Application Expressに関するオンライン・クイズを提供するWebサイトです。 

この記事のクイズ:

次のように表を作成してデータを移入しました。

CREATE TABLE plch_accounts
(
   account_name     VARCHAR2 (100),
   account_status   VARCHAR2 (6)
)
/

BEGIN
   INSERT INTO plch_accounts
        VALUES (‘ACME WIDGETS’, ‘ACTIVE’);

   INSERT INTO plch_accounts
        VALUES (‘BEST SHOES’, ‘CLOSED’);

   COMMIT;
END;
/

 
次のうち、実行後に"ACME WIDGETS"と表示されるものはどれですか。

a.  

CREATE OR REPLACE PACKAGE plch_constants
IS
   active   CONSTANT VARCHAR2 (6) := ‘ACTIVE’ ;
   closed   CONSTANT VARCHAR2 (6) := ‘CLOSED’ ;
END;
/

SELECT account_name
  FROM plch_accounts
 WHERE account_status = plch_constants.active
/

 
b. 

CREATE OR REPLACE PACKAGE plch_constants
IS
   FUNCTION active
      RETURN VARCHAR2;

   FUNCTION closed
      RETURN VARCHAR2;
END;
/

CREATE OR REPLACE PACKAGE BODY plch_constants
IS
   FUNCTION active
      RETURN VARCHAR2
   IS
   BEGIN
      RETURN ‘ACTIVE’;
   END;

   FUNCTION closed
      RETURN VARCHAR2
   IS
   BEGIN
      RETURN ‘CLOSED’;
   END;
END;
/

SELECT account_name
  FROM plch_accounts
 WHERE account_status = plch_constants.active
/
 

c. 

CREATE OR REPLACE PACKAGE plch_constants
IS
   active   CONSTANT VARCHAR2 (6) := ‘ACTIVE’ ;
   closed   CONSTANT VARCHAR2 (6) := ‘CLOSED’ ;
END;
/

WITH 
   FUNCTION active
      RETURN VARCHAR2
   IS
   BEGIN
      RETURN plch_constants.active;
   END;

SELECT account_name
  FROM plch_accounts
 WHERE account_status = active
/ 


Steven Feuersteinの顔写真


Steven Feuerstein
steven.feuerstein@quest.com)は、Quest SoftwareのPL/SQLエヴァンジェリストです。これまで、Oracle PL/SQLに関する著書(O’Reilly Media)を10冊発行しており、Oracle ACE Directorでもあります。詳細は、stevenfeuerstein.comをご覧ください。

▲ ページTOPに戻る

記事一覧へ戻る

 

ご意見ご感想をお寄せください。