Oracle+PHP 简明手册

使用 Oracle 和 PHP 创建在线报表


作者:Mikhail Seliverstov

了解使用 Oracle 和 PHP 创建在线报表的各种技术

本文相关下载:
 Oracle 数据库 10g 快捷版
 Zend Core for Oracle

2006 年 7 月发布

在大多数基于 Web 的应用程序中,能够生成数据库驱动的报表是一个“必需功能”。为了创建复杂报表,大多数开发人员都借助于使用组合数据集,这些数据集无法通过运行单个 SQL 查询来检索,而是需要在循环中执行一组较小的查询。

虽然这个方法通常很适用,但还有其他替代方法。在本文中,您将了解依赖变量绑定和 Oracle CURSOR 表达式的其他技术,并针对传统方法来评估这些技术。这里提供了所有代码,因此您应该能够复制测试条件,并进行自己的评估。

 

设置数据结构

对于本文,我将重用“使用 PHP 和 Oracle 集合通过数据库调用来插入多行”一文中的示例,该文演示了如何使用 Oracle 游标加快批量数据的插入速度。在上述文章中,您创建了一个简单的数据库结构,可用于支持虚拟的电子商务 Web 应用程序。这里,您将添加另一个表,可用于追踪每个定单的状态更改 (Order received -> Order processed -> Order packed -> Order shipped)。换言之,通过查看这个表,您将能够了解任何给定定单的当前状态以及状态更改历史。

以下是经过重新散列的数据结构:

 

图 1

 

以下是创建以上这些表的 SQL 代码:

                                             
CREATE TABLE CUSTOMERS ( 
  CUSTOMER_ID    NUMBER        NOT NULL, 
  CUSTOMER_NAME  VARCHAR2 (100)  NOT NULL, 
  CONSTRAINT PK_CUSTOMERS
  PRIMARY KEY ( CUSTOMER_ID ) ) ; 
/
CREATE TABLE ITEMS ( 
  ITEM_ID           NUMBER        NOT NULL, 
  ITEM_DESCRIPTION  VARCHAR2 (50)  NOT NULL, 
  ITEM_PRICE        NUMBER        NOT NULL, 
  CONSTRAINT PK_ITEMS
  PRIMARY KEY ( ITEM_ID ) ) ; 
/
CREATE TABLE ORDERS ( 
  ORDER_ID     NUMBER        NOT NULL, 
  ORDER_DATE   DATE          NOT NULL, 
  CUSTOMER_ID  NUMBER, 
  CONSTRAINT PK_ORDERS
  PRIMARY KEY ( ORDER_ID ) ) ; 

ALTER TABLE ORDERS ADD  CONSTRAINT FK_CUSTOMER
 FOREIGN KEY (CUSTOMER_ID) 
  REFERENCES CUSTOMERS (CUSTOMER_ID) ;
/
CREATE TABLE ORDER_ITEMS ( 
  ORDER_LINE_ID  NUMBER        NOT NULL, 
  ORDER_ID       NUMBER        NOT NULL, 
  ITEM_ID        NUMBER        NOT NULL, 
  CONSTRAINT PK_ORDER_ITEMS
  PRIMARY KEY ( ORDER_LINE_ID ) ) ; 

ALTER TABLE ORDER_ITEMS ADD  CONSTRAINT FK_ITEM
 FOREIGN KEY (ITEM_ID) 
  REFERENCES ITEMS (ITEM_ID) ;

ALTER TABLE ORDER_ITEMS ADD  CONSTRAINT FK_ORDER
 FOREIGN KEY (ORDER_ID) 
  REFERENCES ORDERS (ORDER_ID) ;

/
CREATE TABLE ORDER_TRACKING ( 
  RECORD_ID       NUMBER        NOT NULL, 
  CURRENT_STATUS  VARCHAR2 (100), 
  ORDER_ID        NUMBER, 
  RECORD_DATE     DATE, 
  CONSTRAINT PK_ORDER_TRACKING
  PRIMARY KEY ( RECORD_ID ) ) ; 

ALTER TABLE ORDER_TRACKING ADD  CONSTRAINT FK_ORDER_TRACKING
 FOREIGN KEY (ORDER_ID) 
  REFERENCES ORDERS (ORDER_ID) ;

CREATE SEQUENCE ORDER_LINE_SQ
INCREMENT BY 1;
/

CREATE SEQUENCE ORDER_STATUS_SQ
INCREMENT BY 1;
/

                                          
创建一个序列,以便在 Order_Items 表中插入行时总有一个唯一的主键:
                                             
CREATE SEQUENCE ORDER_LINE_SQ
INCREMENT BY 1;
/

                                          
以下是填充测试数据的代码。假定 CUSTOMERS 表已经用客户 CUSTOMER_ID = 1 进行了预填充,ITEMS 表包含了 15 个项(ITEM_ID 从 1 到 15):
                                             
//connect to the database using your login name and password
$schema = "my_schema";
$ password = "my_password";

$db = oci_connect('$schema, $password) or die;

$arr_order_items = array(1,2,3,4,5,6,7,8,9,10,11,12,13,14,15); //all items on the list
$arr_status = array("Received","Processed","Packed","Shipped");

//create 200 orders for CUSTOMER_ID = 1

for ($order_id = 1;$order_id<=200;$order_id++) {
   $sql = "INSERT INTO ORDERS (order_id,customer_id,order_date) VALUES (".$order_id.",1,SYSDATE)";
   $stmt = oci_parse($db,$sql);
   oci_execute($stmt);
//add 15 items to each order
   foreach ($arr_order_items as $item) {
      $sql = "INSERT INTO ORDER_ITEMS (order_line_id,order_id,item_id) VALUES 
      (ORDER_LINE_SQ.nextval,".$order_id.",".$item.")";
      $stmt = oci_parse($db,$sql);
      oci_execute($stmt);
   }
//populate status for each order        
   foreach ($arr_status as $status) {
      $sql = "INSERT INTO ORDER_TRACKING (order_id,current_status,record_date) VALUES 
      (".$order_id.",'http://www.oracle.com/technology/pub/articles/oracle_php_cookbook/".$status."',SYSDATE)";
      $stmt = oci_parse($db,$sql);
      oci_execute($stmt);
  }
}

                                          
此时,ORDERS 表应该填充有客户 1 放置的 200 个定单。每个定单都包含 ORDER_ITEMS 表中的 15 个项和 ORDER_TRACKING 表中的 4 个状态。

 

报表需求

首先,为您唯一的客户 (CUSTOMER_ID = 1) 创建一个定单历史报表。该报表将通过两个子部分显示客户定单的完整列表:一个部分显示每个给定定单所包含的项,另一个部分列出该定单的状态更新历史。在以下部分中,您将使用三种不同的技术创建该报表:即:在循环中执行一系列独立的数据库调用,通过变量绑定执行相同的独立查询,以及在单个查询中使用 Oracle 的 CURSOR 表达式。

 

简单的子查询。毫无疑问,该方法是构建复杂报表的最传统方法。在本例中,您将执行一个简单查询来检索给定客户 id 的定单列表,然后循环遍历该定单列表并执行两个子查询 — 一个用于获取状态更改历史,另一个用于获取项列表。结果将存储在关联数组 $arr_orders 中,随后以 HTML 表的形式显示。
                                             
$stmt = oci_parse($db,"SELECT order_id,order_date,customer_name FROM CUSTOMERS,ORDERS WHERE 
ORDERS.customer_id = 1 AND ORDERS.CUSTOMER_ID = CUSTOMERS.CUSTOMER_ID");

oci_execute($stmt);

while ($orders = oci_fetch_assoc($stmt)) {
   $arr_order_status = array(); // to temporarily store status history for given order id
   $arr_order_items = array(); // to temporarily store item list for given order id
   $arr_order_line = array(); // to temporarily store complete order information for given order id
                
   $arr_order_line =    array("ORDER_ID"=>$orders["ORDER_ID"],"ORDER_DATE"=>$orders["ORDER_DATE"], 
   "CUSTOMER_NAME"=>$orders["CUSTOMER_NAME"]);

//first "sub-query"
   $stmt2 = oci_parse($db,"SELECT current_status,record_date FROM ORDER_TRACKING WHERE order_id = ".$orders["ORDER_ID"]);

   oci_execute($stmt2);

   while ($status = oci_fetch_assoc($stmt2)) { //note that this function is PHP5 only, use OCIFetchInto for PHP4
      $arr_order_status[] = array("CURRENT_STATUS"=>$status["CURRENT_STATUS"], 
      "RECORD_DATE"=>$status[ "RECORD_DATE" ]);
   }

   $arr_order_line["STATUS"] = $arr_order_status;

//second "sub-query"
   $stmt3 = Oci_parse($db,"SELECT item_description,item_price FROM ITEMS,ORDER_ITEMS WHERE 
   ORDER_ITEMS.ITEM_ID = ITEMS.ITEM_ID AND ORDER_ITEMS.ORDER_ID = ".$orders["ORDER_ID"]);
   oci_execute($stmt3);
   while ($items = oci_fetch_assoc($stmt3)) {
      $arr_order_items[] = array("ITEM_DESCRIPTION"=>$items["ITEM_DESCRIPTION"],   
      "ITEM_PRICE"=>$items["ITEM_PRICE"]);
   }

   $arr_order_line["ITEMS"] = $arr_order_items;

//add new line to the orders array
   $arr_orders[] = $arr_order_line;
}

                                          

带有变量绑定的简单子查询。该方法类似于前一个技术,也依赖于在循环中执行独立的数据库查询。但是,这次您将使用变量绑定。

 

从理论上讲,使用变量绑定应该能够加快执行查询的速度,因为 Oracle 不必分析每个新语句。此外,绑定通常比较安全,因为它减少了 SQL 注入攻击的风险。

                                             
$stmt = oci_parse($db,"SELECT order_id,order_date,customer_name FROM CUSTOMERS,
                  ORDERS WHERE ORDERS.customer_id = 1 AND ORDERS.CUSTOMER_ID = CUSTOMERS.CUSTOMER_ID");

//parse the statement to get status update history and bind it to the variable
$stmt2 = oci_parse($db,"SELECT current_status,record_date FROM ORDER_TRACKING WHERE order_id = :var");
oci_bind_by_name($stmt2, ":var", $order_id,32);

//parse the statement to get item list and bind it to the variable
$stmt3 = oci_parse($db,"SELECT item_description,item_price FROM ITEMS,

                   ORDER_ITEMS WHERE ORDER_ITEMS.ITEM_ID = ITEMS.ITEM_ID AND ORDER_ITEMS.ORDER_ID = :var");
oci_bind_by_name ($stmt3, ":var", $order_id,32);
 oci_execute($stmt); //execute order list statement

 while ($orders = oci_fetch_assoc ($stmt)) {
   $arr_order_status = array();
   $arr_order_items = array();
   $arr_order_line = array();
    $arr_order_line = array("ORDER_ID"=>$orders["ORDER_ID"],"ORDER_DATE"=>$orders["ORDER_DATE"],
    "CUSTOMER_NAME"=>$orders["CUSTOMER_NAME"]);
    $order_id = $orders["ORDER_ID"]; //current order id

//first "sub-query"
   oci_execute($stmt2);
   while ($status = oci_fetch_assoc($stmt2)) {
      $arr_order_status[] = array("CURRENT_STATUS"=>$status["CURRENT_STATUS"],
       "RECORD_DATE"=>$status[ "RECORD_DATE" ]);
   }

   $arr_order_line["STATUS"] = $arr_order_status;
//second "sub-query"
   oci_execute($stmt3);
   while ($items = oci_fetch_assoc($stmt3)) {
      $arr_order_items[] = array("ITEM_DESCRIPTION"=>$items["ITEM_DESCRIPTION"],
       "ITEM_PRICE"=>$items["ITEM_PRICE"]);
   }
                                               
   $arr_order_line["ITEMS"] = $arr_order_items;
 //add new line to the orders array
   $arr_orders[] = $arr_order_line;
}
                                          

带有 Oracle CURSOR 子查询的单个查询。第三个方法涉及到在单个数据库调用中使用 Oracle 的 CURSOR 表达式。即使仍然涉及到一些循环,但这次您将查询初始调用中包含的集合,而非查询原始数据库表。

                                             
$stmt = oci_parse($db,"SELECT order_id,order_date,customer_name, 
CURSOR(SELECT current_status,record_date FROM ORDER_TRACKING WHERE order_id = ORDERS.order_id) as status,
CURSOR(SELECT item_description,item_price FROM ITEMS,
       ORDER_ITEMS WHERE ORDER_ITEMS.ITEM_ID = ITEMS.ITEM_ID AND ORDER_ITEMS.ORDER_ID = ORDERS.order_id) AS items
FROM CUSTOMERS, ORDERS WHERE ORDERS.customer_id = 1
AND ORDERS.CUSTOMER_ID = CUSTOMERS.CUSTOMER_ID");
oci_execute($stmt);
while ($orders = oci_fetch_assoc ($stmt)) {
   $arr_order_status = array();
   $arr_order_items = array();
   $arr_order_line = array();
   $arr_order_line = array("ORDER_ID"=>$orders["ORDER_ID"],"ORDER_DATE"=>$orders["ORDER_DATE"],  
    "CUSTOMER_NAME"=>$orders["CUSTOMER_NAME"]);
 //process cursors
   oci_execute($orders["STATUS"]); //get data from the nested collection
   while ($status = oci_fetch_assoc($orders["STATUS"])) {
      $arr_order_status[] = array("CURRENT_STATUS"=>$status["CURRENT_STATUS"],   
       "RECORD_DATE"=>$status["RECORD_DATE"]);
   }
    $arr_order_line["STATUS"] = $arr_order_status;
    oci_execute($orders["ITEMS"]);
   while ($items = oci_fetch_assoc($orders["ITEMS"])) {
      $arr_order_items[] = array("ITEM_DESCRIPTION"=>$items["ITEM_DESCRIPTION"],
       "ITEM_PRICE"=>$items["ITEM_PRICE"]);
   }
    $arr_order_line["ITEMS"] = $arr_order_items;
 //add new line to the orders array
   $arr_orders[] = $arr_order_line;
}
                                          

 

查看报表

现在,报表数据包含在 $arr_orders 数组中。以下是一个小型代码片段,该片段将循环遍历数组,并以 HTML 表的形式呈现数据以便于查看。在我们的示例中,由于所有定单都是相同的,因此该片段将仅显示前两个定单。

                                             
for($i=0;$i<=1;$i++) { //show only the first two orders
   echo "<table cellpadding=\"2\" cellspacing=\"2\" width=\"500\" border=\"1\">";
   echo "<tr><td colspan=\"2\">Order # ".($i+1)." (".$arr_orders[$i]["ORDER_ID"]." "
   .$arr_orders[$i]["ORDER_DATE"].",".$arr_orders[$i]["CUSTOMER_NAME"].")</td></tr>";
   echo "<tr><td valign=\"top\">";
   foreach($arr_orders[$i]["STATUS"] as $status) {
      echo $status["RECORD_DATE"]." - ".$status["CURRENT_STATUS"]."<br />";
   }
   echo "</td><td valign=\"top\">";
   foreach($arr_orders[$i]["ITEMS"] as $item) {
      echo $item["ITEM_DESCRIPTION"]." - ".$item["ITEM_PRICE"]."<br />";
   }
   echo "</td></tr></table>";
}

                                          

 

评估

最后,查看结果并决定哪个方法是为系统生成报表的最有效方法。但是请记住,没有完全相同的两个系统,并且在针对最佳性能进行优化时需要考虑许多因素。有些因素可能与数据库相关(如微调索引),有些可能涉及 PHP 或网络优化,等等。对于这里讨论的简单系统,可以查看下面的表来得出您自己的结论:

 

200 个定单

测试编号 方法 1(秒) 方法 2(秒) 方法 3(秒)
1 4.54 4.31 5.48
2 4.09 4.33 5.40
3 4.46 4.40 5.36
4 4.39 4.41 5.38
5 4.12 4.50 5.70

 

1,000 个定单

测试编号 方法 1(秒) 方法 2(秒) 方法 3(秒)
1 33.51 37.99 34.15
2 29.16 37.76 33.73
3 29.11 38.19 35.60
4 28.84 37.79 44.18
5 29.13 38.67 37.29

 

结论

当然,还有很多方法可以提供相同的功能 — 例如,使用带有 REF CURSOR 的 Oracle 存储过程输出数据。正如您看到的那样,在这个特殊测试中,使用独立的子查询可提供更好的性能。但是,该测试并不全面,因此我建议您使用本文中提供的代码来创建自己的测试,并找出最适合您的方法。

 


Mikhail Seliverstov 是位于蒙特利尔的麦吉尔大学校友会的 Web 编程小组的领导人。在过去 3 年中,他作为首席应用程序架构师和开发人员,帮助领导了一个大型的 Oracle+PHP 开发项目。

将您的意见发送给我们