Thứ Tư, 18 tháng 3, 2015

listagg function in 11g release 2

This article describes the new LISTAGG function in Oracle 11g Release 2. LISTAGG is a built-in function that enables us to perform string aggregation natively. String aggregation is a popular technique, and there are several methods available on the web, so we will compare their performance to the new LISTAGG function later in this article.

sample data

For reference, we will use the following sample data for our examples.

    DEPTNO ENAME      HIREDATE
---------- ---------- ----------
        10 CLARK      09/06/1981
        10 KING       17/11/1981
        10 MILLER     23/01/1982
        20 ADAMS      12/01/1983
        20 FORD       03/12/1981
        20 JONES      02/04/1981
        20 SCOTT      09/12/1982
        20 SMITH      17/12/1980
        30 ALLEN      20/02/1981
        30 BLAKE      01/05/1981
        30 JAMES      03/12/1981
        30 MARTIN     28/09/1981
        30 TURNER     08/09/1981
        30 WARD       22/02/1981

what is string aggregation?

String aggregation is simply the grouping and concatenation of multiple rows of data into a single row per group. For example, consider the following resultset:

   DEPTNO ENAME
--------- ----------
       10 CLARK
       10 KING
       10 MILLER
       20 ADAMS
       20 FORD
       20 JONES
With string aggregation, this resultset would be grouped (by DEPTNO) as follows:

   DEPTNO AGGREGATED_ENAMES
--------- -------------------------
       10 CLARK,KING,MILLER
       20 ADAMS,FORD,JONES
We can see that the employee names have simply been grouped and concatenated into a single column (values are delimited by comma) per group. As stated, there are several techniques available to perform this aggregation (references are provided at the end of this article), but the new LISTAGG function makes it much easier, as we will see below.

listagg syntax overview

The LISTAGG function has the following syntax structure:

LISTAGG( [,]) WITHIN GROUP (ORDER BY ) [OVER (PARTITION BY )]
LISTAGG is an aggregate function that can optionally be used as an analytic (i.e. the optional OVER() clause). The following elements are mandatory:
  • the column or expression to be aggregated;
  • the WITHIN GROUP keywords;
  • the ORDER BY clause within the grouping.
We will now see some examples of the function below.

listagg as an aggregate function

We will begin with a simple example that aggregates the employee names for each department in the EMP table, using a comma as delimiter.
SQL> SELECT deptno
  2  ,      LISTAGG(ename, ',') WITHIN GROUP (ORDER BY ename) AS employees
  3  FROM   emp
  4  GROUP  BY
  5         deptno;

    DEPTNO EMPLOYEES
---------- ------------------------------------------------------------
        10 CLARK,KING,MILLER
        20 ADAMS,FORD,JONES,SCOTT,SMITH
        30 ALLEN,BLAKE,JAMES,MARTIN,TURNER,WARD

3 rows selected.
Note that we chose to order the employees within each aggregation by the employee name. It should be noted that ordering the elements of a string aggregation is not a trivial task in some of the alternative techniques to LISTAGG.
In the following example, we will aggregate the employee names but order them by their respective hire dates.
SQL> SELECT deptno
  2  ,      LISTAGG(ename, ',') WITHIN GROUP (ORDER BY hiredate) AS employees
  3  FROM   emp
  4  GROUP  BY
  5         deptno;

    DEPTNO EMPLOYEES
---------- ------------------------------------------------------------
        10 CLARK,KING,MILLER
        20 SMITH,JONES,FORD,SCOTT,ADAMS
        30 ALLEN,WARD,BLAKE,TURNER,MARTIN,JAMES

3 rows selected.
We can see that the order of employee names within each group differs from the first example.

listagg as an analytic function

As with many aggregate functions, LISTAGG can be converted to an analytic function by adding the OVER() clause. The following example demonstrates the analytic equivalent of our previous example (for each department, aggregate employee names in hire date order).
SQL> SELECT deptno
  2  ,      ename
  3  ,      hiredate
  4  ,      LISTAGG(ename, ',')
  5            WITHIN GROUP (ORDER BY hiredate)
  6            OVER (PARTITION BY deptno) AS employees
  7  FROM   emp;

    DEPTNO ENAME      HIREDATE    EMPLOYEES
---------- ---------- ----------- -------------------------------------
        10 CLARK      09/06/1981  CLARK,KING,MILLER
        10 KING       17/11/1981  CLARK,KING,MILLER
        10 MILLER     23/01/1982  CLARK,KING,MILLER
        20 SMITH      17/12/1980  SMITH,JONES,FORD,SCOTT,ADAMS
        20 JONES      02/04/1981  SMITH,JONES,FORD,SCOTT,ADAMS
        20 FORD       03/12/1981  SMITH,JONES,FORD,SCOTT,ADAMS
        20 SCOTT      19/04/1987  SMITH,JONES,FORD,SCOTT,ADAMS
        20 ADAMS      23/05/1987  SMITH,JONES,FORD,SCOTT,ADAMS
        30 ALLEN      20/02/1981  ALLEN,WARD,BLAKE,TURNER,MARTIN,JAMES
        30 WARD       22/02/1981  ALLEN,WARD,BLAKE,TURNER,MARTIN,JAMES
        30 BLAKE      01/05/1981  ALLEN,WARD,BLAKE,TURNER,MARTIN,JAMES
        30 TURNER     08/09/1981  ALLEN,WARD,BLAKE,TURNER,MARTIN,JAMES
        30 MARTIN     28/09/1981  ALLEN,WARD,BLAKE,TURNER,MARTIN,JAMES
        30 JAMES      03/12/1981  ALLEN,WARD,BLAKE,TURNER,MARTIN,JAMES

14 rows selected.
Remember that when use an analytic function, we do not lose any rows from our resultset: rather we see the results of the aggregation on every source row (depending on the analytic function and the ordering/windowing clauses). We can clearly see this above. The string aggregation for each department is available on every corresponding row.

more on ordering

As stated earlier, the ORDER BY clause is mandatory, as following example demonstrates.
SQL> SELECT deptno
  2  ,      LISTAGG(ename, ',') WITHIN GROUP () AS employees
  3  FROM   emp
  4  GROUP  BY
  5         deptno;
,      LISTAGG(ename, ',') WITHIN GROUP () AS employees
                                         *
ERROR at line 2:
ORA-30491: missing ORDER BY clause
If the order of the aggregated elements is irrelevant to us, we can use a constant expression such as NULL, as follows.
SQL> SELECT deptno
  2  ,      LISTAGG(ename, ',') WITHIN GROUP (ORDER BY NULL) AS employees
  3  FROM   emp
  4  GROUP  BY
  5         deptno;

    DEPTNO EMPLOYEES
---------- ------------------------------------------------------------
        10 CLARK,KING,MILLER
        20 ADAMS,FORD,JONES,SCOTT,SMITH
        30 ALLEN,BLAKE,JAMES,MARTIN,TURNER,WARD

3 rows selected.
In this example, the elements have been aggregated alphabetically, despite the NULL ordering clause. This appears to be the default behaviour when using a constant ORDER BY expression such as the above.

delimiters

We can use a range of constants or expressions as a delimiter for our aggregated strings. In fact, the delimiter is optional and can be excluded altogether, as the following example demonstrates.
SQL> SELECT deptno
  2  ,      LISTAGG(ename) WITHIN GROUP (ORDER BY ename) AS employees
  3  FROM   emp
  4  GROUP  BY
  5         deptno;

    DEPTNO EMPLOYEES
---------- ------------------------------------------------------------
        10 CLARKKINGMILLER
        20 ADAMSFORDJONESSCOTTSMITH
        30 ALLENBLAKEJAMESMARTINTURNERWARD

3 rows selected.
One restriction is that delimiters have to be either constants (such as a literal) or based on a deterministic expression that includes a column or expression used in the grouping. For example, we cannot use ROWNUM as a delimiter, as we see below.
SQL> SELECT deptno
  2  ,      LISTAGG(ename, '(' || ROWNUM || ')')
  3            WITHIN GROUP (ORDER BY hiredate) AS employees
  4  FROM   emp
  5  GROUP  BY
  6         deptno;
,      LISTAGG(ename, '(' || ROWNUM || ')')
                             *
ERROR at line 2:
ORA-30497: Argument should be a constant or a function of expressions in GROUP BY.
The error message is clear: ROWNUM is neither a constant nor an expression that involves our grouping column (i.e. DEPTNO). If we try to use our grouping column, we are limited to the type of expression we can use, as we can see below.
SQL> SELECT deptno
  2  ,      LISTAGG(ename, '(' || MAX(deptno) || ')')
  3            WITHIN GROUP (ORDER BY hiredate) AS employees
  4  FROM   emp
  5  GROUP  BY
  6         deptno;
,      LISTAGG(ename, '(' || MAX(deptno) || ')')
                                 *
ERROR at line 2:
ORA-30496: Argument should be a constant.
This time, Oracle recognises that we tried to use our grouping column, but we did not use a valid expression (hence we receive another error message; albeit different to the previous example). The following is an example of a deterministic expression on our grouping column that Oracle will accept.
SQL> SELECT deptno
  2  ,      LISTAGG(ename, '(' || CHR(deptno+55) || '); ')
  3            WITHIN GROUP (ORDER BY hiredate) AS employees
  4  FROM   emp
  5  GROUP  BY
  6         deptno;

    DEPTNO EMPLOYEES
---------- ------------------------------------------------------------
        10 CLARK(A); KING(A); MILLER
        20 SMITH(K); JONES(K); FORD(K); SCOTT(K); ADAMS
        30 ALLEN(U); WARD(U); BLAKE(U); TURNER(U); MARTIN(U); JAMES

3 rows selected.
This is a contrived example, but we've simply converted the DEPTNO into an ASCII character to use as a delimiter. This is a deterministic expression on the value of the grouping column and Oracle allows it.

other restrictions

The results of LISTAGG are constrained to the maximum size of VARCHAR2 in SQL (i.e. 4000) as we can see below.
SQL> SELECT LISTAGG(object_name) WITHIN GROUP (ORDER BY NULL)
  2  FROM   all_objects;
FROM   all_objects
       *
ERROR at line 2:
ORA-01489: result of string concatenation is too long
There is no CLOB or larger VARCHAR2 equivalent, so for larger strings we would need to use an alternative means of gathering the elements (such as a collection or a user-defined PL/SQL function).

performance considerations

We will compare the performance of LISTAGG to some of the alternatives that pre-date it. The methods we will compare are as follows:
  • LISTAGG (11g Release 2);
  • COLLECT + PL/SQL function(10g);
  • Oracle Data Cartridge - user-defined aggregate function (9i)
  • MODEL SQL (10g).
The main difference with LISTAGG is that it is a built-in function, so we should expect its performance to be at the very least comparable to its alternatives.

setup

For the performance comparisons, we will use a larger source dataset of 1 million rows spread evenly between 2000 groups, as follows.
SQL> CREATE TABLE t
  2  AS
  3     SELECT ROWNUM                     AS id
  4     ,      MOD(ROWNUM,2000)           AS grp
  5     ,      DBMS_RANDOM.STRING('u',5)  AS val
  6     ,      DBMS_RANDOM.STRING('u',30) AS pad
  7     FROM   dual
  8     CONNECT BY ROWNUM <= 1000000;

Table created.

SQL> SELECT COUNT(*) FROM t;

  COUNT(*)
----------
   1000000

1 row selected.

SQL> exec DBMS_STATS.GATHER_TABLE_STATS(USER, 'T');

PL/SQL procedure successfully completed.
We will compare LISTAGG to the alternatives using both Autotrace and the wall-clock. Note that the sample data is cached prior to the comparisons. We will begin by preparing our environment as follows.
SQL> set autotrace traceonly statistics
SQL> set timing on
SQL> set arrays 500

listagg

Our first test will be with LISTAGG. We will aggregate and order the value string within each of our 2000 groups, as follows.
SQL> SELECT grp
  2  ,      LISTAGG(val, ',') WITHIN GROUP (ORDER BY val) AS vals
  3  FROM   t
  4  GROUP  BY
  5         grp;

2000 rows selected.

Elapsed: 00:00:05.85

Statistics
----------------------------------------------------------
          1  recursive calls
          0  db block gets
       7092  consistent gets
          0  physical reads
          0  redo size
    6039067  bytes sent via SQL*Net to client
        552  bytes received via SQL*Net from client
          5  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
       2000  rows processed
This executes in just under 6 seconds on the test database, with no physical I/O and all sorting in memory.

stragg/wm_concat

Next we will use the best known technique for string aggregation: Tom Kyte's user-defined aggregate function known as STRAGG. In 10g, Oracle implemented a similar function in the WMSYS schema, so we will assume this to be a direct equivalent of STRAGG and use it in our test below. Note that one of the details of the STRAGG method is that it doesn't enable the strings to be ordered.
SQL> SELECT grp
  2  ,      WMSYS.WM_CONCAT(val) AS vals --<-- WM_CONCAT ~= STRAGG
  3  FROM   t
  4  GROUP  BY
  5         grp;

2000 rows selected.

Elapsed: 00:00:19.45

Statistics
----------------------------------------------------------
          1  recursive calls
          0  db block gets
       7206  consistent gets
          0  physical reads
          0  redo size
    6039067  bytes sent via SQL*Net to client
        552  bytes received via SQL*Net from client
          5  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
       2000  rows processed
We can see that this is over 3 times slower than LISTAGG (even without element ordering). The user-defined aggregate function carries with it the general overhead of PL/SQL functions (i.e. context-switching) and we can see the impact of this in the results above.

collect (without ordering)

When 10g was released, I devised a quicker alternative to STRAGG using the COLLECT function combined with a "collection-to-string" PL/SQL function (described in this oracle-developer.net article). The 10g version of COLLECT had no ordering facility so we'll start with this variant, as follows. Note that the TO_STRING source code is available in the referenced article.
SQL> SELECT grp
  2  ,      TO_STRING(
  3            CAST(COLLECT(val) AS varchar2_ntt)
  4            ) AS vals
  5  FROM   t
  6  GROUP  BY
  7         grp;

2000 rows selected.

Elapsed: 00:00:02.90

Statistics
----------------------------------------------------------
         10  recursive calls
          0  db block gets
       7197  consistent gets
          0  physical reads
          0  redo size
    6039067  bytes sent via SQL*Net to client
        552  bytes received via SQL*Net from client
          5  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
       2000  rows processed
Without sorting the elements, the original COLLECT/TO_STRING method is twice as fast as LISTAGG. However, this isn't a fair comparison because LISTAGG always incurs the cost of ordering the elements. If the order of the elements is irrelevant, however, the original COLLECT technique will be the quickest.

collect (with ordering)

For a fair comparison between COLLECT and LISTAGG, we must include the sorting of the elements in the COLLECT call (an 11g Release 1 new feature described in this oracle-developer.net article). The following is equivalent to our LISTAGG example.
SQL> SELECT grp
  2  ,      TO_STRING(
  3            CAST(COLLECT(val ORDER BY val) AS varchar2_ntt)
  4            ) AS vals
  5  FROM   t
  6  GROUP  BY
  7         grp;

2000 rows selected.

Elapsed: 00:00:07.08

Statistics
----------------------------------------------------------
         10  recursive calls
          0  db block gets
       7197  consistent gets
          0  physical reads
          0  redo size
    6039067  bytes sent via SQL*Net to client
        552  bytes received via SQL*Net from client
          5  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
       2000  rows processed
This time, when we include the ordering of the elements, the COLLECT method is actually slower than the new LISTAGG function.

model

Our final performance comparison is with an example of string aggregation that uses the MODEL clause. The following example was sourced from Rob van Wijk's About Oracle blog and has been modified to fit our sample data.
SQL> SELECT grp
  2  ,      vals
  3  FROM  (
  4         SELECT grp
  5         ,      RTRIM(vals, ',') AS vals
  6         ,      rn
  7         FROM   t
  8         MODEL
  9            PARTITION BY (grp)
 10            DIMENSION BY (ROW_NUMBER() OVER (PARTITION BY grp ORDER BY val) AS rn)
 11            MEASURES (CAST(val AS VARCHAR2(4000)) AS vals)
 12            RULES
 13            (  vals[ANY] ORDER BY rn DESC = vals[CV()] || ',' || vals[CV()+1]
 14            )
 15        )
 16  WHERE  rn = 1
 17  ORDER  BY
 18         grp;

2000 rows selected.

Elapsed: 00:03:28.15

Statistics
----------------------------------------------------------
       3991  recursive calls
          0  db block gets
       7092  consistent gets
     494791  physical reads
          0  redo size
    6039067  bytes sent via SQL*Net to client
        553  bytes received via SQL*Net from client
          5  SQL*Net roundtrips to/from client
        130  sorts (memory)
          0  sorts (disk)
       2000  rows processed
This ran for over 3 minutes! We can see from the statistics that it has incurred a significant number of physical reads, recursive calls and in-memory sorting. In fact, this poor timing is largely due to an enormous number of direct path reads/writes to/from the temporary tablespace during query execution (although the disk sorts are not reported by the Autotrace statistics).
The execution plan for the MODEL string aggregation is as follows.

--------------------------------------------------------------
| Id  | Operation             | Name | Rows  | Bytes |TempSpc|
--------------------------------------------------------------
|   0 | SELECT STATEMENT      |      |       |       |       |
|   1 |  SORT ORDER BY        |      |  1000K|  1934M|  1953M|
|*  2 |   VIEW                |      |  1000K|  1934M|       |
|   3 |    SQL MODEL ORDERED  |      |  1000K|  9765K|       |
|   4 |     WINDOW SORT       |      |  1000K|  9765K|    19M|
|   5 |      TABLE ACCESS FULL| T    |  1000K|  9765K|       |
--------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter("RN"=1)
According to a SQL monitor report (using DBMS_SQLTUNE.REPORT_SQL_MONITOR), the ordering of data within the SQL MODEL ORDERED operation at step 3 accounted for almost 4Gb of temporary space! An alternative example from Gary Myers' Sydney Oracle Lab blog displays similar performance characteristics.

performance summary

To summarise, therefore, the new LISTAGG function is the fastest technique for string aggregation and has the additional benefit of being a simple built-in function. If the ordering of elements is irrelevant, a technique that uses COLLECT without sorting is quicker still, but for ordered string aggregation, LISTAGG is unbeatable. Note that there is an issue with the MODEL examples used in the performance comparisons, but this might be isolated to either the version of Oracle used for the examples (11.2.0.1 Windows 64-bit) or the test database itself.

further reading

For more information on LISTAGG, see the online SQL Language Reference. For a good summary of common string-aggregation techniques, see this article by Tim Hall and this article by William Robertson. As referenced earlier in this article, oracle-developer.net articles on the COLLECT function can be found here and here. For more information on using MODEL for string aggregation, see blog entries by Gary Myers and Rob van Wijk.

source code


The source code for the examples in this article can be downloaded from here.

Source: http://www.oracle-developer.net/display.php?id=515

Thứ Hai, 16 tháng 3, 2015

BULK COLLECT and FORALL

I have a task to tune a little bit of SQL. It's very ETL like, but the target is not (yet) a star schema, it's pretty much a table flattened out for reporting purposes.

As I reviewed it, I noticed it went row by row, with a COMMIT inside the LOOP. That's gotta go.

Can I do this in one SQL statement? No, there's other processing that needs to be done (UPDATE two other tables before and after). Hmmm...could I just return the appropriate records into a collection? I'll have to look at that to see if it's possible.

For now though, I am going to try and use BULK COLLECT with the LIMIT clause and FORALL for the processes that occur before and after.

Let's create some data:
CREATE TABLE t( x NUMBER, y NUMBER );

INSERT INTO t( x, y )
SELECT 
  TRUNC( dbms_random.value( 1, 99999999 ) ), 
  TRUNC( dbms_random.value( 1, 100000 ) )
FROM dual
  CONNECT BY level < 1001;
OK, now let's create an anonymous block, BULK COLLECTing the data from T into a PL/SQL table and then populated another table with that data:
DECLARE
  TYPE t_record IS TABLE OF T%ROWTYPE;
  l_table T_RECORD;
  CURSOR c
  IS
  SELECT x, y
  FROM t;
BEGIN
  OPEN c;

  LOOP
    FETCH c
    BULK COLLECT INTO l_table
    LIMIT 100;

    FORALL i IN 1..l_table.COUNT
      INSERT INTO s ( x, y )
      VALUES ( l_table(i).x, l_table(i).y );

    EXIT WHEN C%NOTFOUND;
  END LOOP;
  CLOSE c;
END;
/
And then I run it and I get the following:
ERROR at line 18:
ORA-06550: line 18, column 16:
PLS-00436: implementation restriction: cannot reference 
fields of BULK In-BIND table of records
ORA-06550: line 18, column 16:
PLS-00382: expression is of wrong type
ORA-06550: line 18, column 30:
PLS-00436: implementation restriction: cannot reference 
fields of BULK In-BIND table of records
ORA-06550: line 18, column 30:
PLS-00382: expression is of wrong type
ORA-06550: line 18, column 16:
PL/SQL: ORA-22806: not an object or REF
ORA-06550: line 17, column 7:
PL/SQL: SQL Statement ignored
A quick google search and I end up here .

So I can bulk bind, but I have to INSERT into the table as a whole. I can't be selective.

I updated my code to this:
DECLARE
  TYPE t_record IS TABLE OF T%ROWTYPE;
  l_table T_RECORD;
  CURSOR c
  IS
  SELECT x, y
  FROM t;
BEGIN
  OPEN c;

  LOOP
    FETCH c
    BULK COLLECT INTO l_table
    LIMIT 100;

    FORALL i IN 1..l_table.COUNT
      INSERT INTO s 
      VALUES l_table(i);

    EXIT WHEN C%NOTFOUND;
  END LOOP;
  CLOSE c;
END;
/
I run it and it completes successfully. This is all on XE, so I wonder (hope) that 11g will allow me to do what I want (I'll be working on an 11g RAC system). I scroll down the list of google results and I find this onewhich then takes me to AskTom. The first post demonstrates that my first attempt will work on 11g.

BULK COLLECT and FORALL are great tools if you can't do it in a single SQL statement and if you want to avoid the row by row processing.

Source: http://www.oraclenerd.com/2008/06/bulk-collect-and-forall.html

On BULK COLLECT

Best practices for knowing your LIMIT and kicking %NOTFOUND
I have started using BULK COLLECT whenever I need to fetch large volumes of data. This has caused me some trouble with my DBA, however. He is complaining that although my programs might be running much faster, they are also consuming way too much memory. He refuses to approve them for a production rollout. What's a programmer to do?
The most important thing to remember when you learn about and start to take advantage of features such as BULK COLLECT is that there is no free lunch. There is almost always a trade-off to be made somewhere. The tradeoff with BULK COLLECT, like so many other performance-enhancing features, is "run faster but consume more memory."
Specifically, memory for collections is stored in the program global area (PGA), not the system global area (SGA). SGA memory is shared by all sessions connected to Oracle Database, but PGA memory is allocated for each session. Thus, if a program requires 5MB of memory to populate a collection and there are 100 simultaneous connections, that program causes the consumption of 500MB of PGA memory, in addition to the memory allocated to the SGA.
Fortunately, PL/SQL makes it easy for developers to control the amount of memory used in a BULK COLLECT operation by using the LIMIT clause.
Suppose I need to retrieve all the rows from the employees table and then perform some compensation analysis on each row. I can use BULK COLLECT as follows:

PROCEDURE process_all_rows
IS
   TYPE employees_aat 
   IS TABLE OF employees%ROWTYPE
      INDEX BY PLS_INTEGER;
   l_employees employees_aat;
BEGIN
   SELECT *
   BULK COLLECT INTO l_employees
      FROM employees;
     
   FOR indx IN 1 .. l_employees.COUNT 
   LOOP
       analyze_compensation 
      (l_employees(indx));
   END LOOP;
END process_all_rows;


Very concise, elegant, and efficient code. If, however, my employees table contains tens of thousands of rows, each of which contains hundreds of columns, this program can cause excessive PGA memory consumption.
Consequently, you should avoid this sort of "unlimited" use of BULK COLLECT. Instead, move the SELECT statement into an explicit cursor declaration and then use a simple loop to fetch many, but not all, rows from the table with each execution of the loop body, as shown in Listing 1.
Code Listing 1: Using BULK COLLECT with LIMIT clause

PROCEDURE process_all_rows (limit_in IN PLS_INTEGER DEFAULT 100)
IS
    CURSOR employees_cur 
    IS 
        SELECT * FROM employees;

    TYPE employees_aat IS TABLE OF employees_cur%ROWTYPE
        INDEX BY PLS_INTEGER;

    l_employees employees_aat;
BEGIN   
    OPEN employees_cur;
    LOOP
        FETCH employees_cur 
            BULK COLLECT INTO l_employees LIMIT limit_in;

        FOR indx IN 1 .. l_employees.COUNT 
        LOOP
            analyze_compensation (l_employees(indx));
        END LOOP;

        EXIT WHEN l_employees.COUNT < limit_in;

   END LOOP;

   CLOSE employees_cur;
END process_all_rows;


The process_all_rows procedure in Listing 1 requests that up to the value of limit_in rows be fetched at a time. PL/SQL will reuse the same limit_in elements in the collection each time the data is fetched and thus also reuse the same memory. Even if my table grows in size, the PGA consumption will remain stable.
How do you decide what number to use in the LIMIT clause? Theoretically, you will want to figure out how much memory you can afford to consume in the PGA and then adjust the limit to be as close to that amount as possible.
From tests I (and others) have performed, however, it appears that you will see roughly the same performance no matter what value you choose for the limit, as long as it is at least 25. The test_diff_limits.sql script, included with the sample code for this column, demonstrates this behavior, using the ALL_SOURCE data dictionary view on an Oracle Database 11g instance. Here are the results I saw (in hundredths of seconds) when fetching all the rows (a total of 470,000):

Elapsed CPU time for limit of 1 = 1839
Elapsed CPU time for limit of 5 = 716
Elapsed CPU time for limit of 25 = 539
Elapsed CPU time for limit of 50 = 545
Elapsed CPU time for limit of 75 = 489
Elapsed CPU time for limit of 100 = 490
Elapsed CPU time for limit of 1000 = 501
Elapsed CPU time for limit of 10000 = 478
Elapsed CPU time for limit of 100000 = 527 


Kicking the %NOTFOUND Habit

I was very happy to learn that Oracle Database 10g will automatically optimize my cursor FOR loops to perform at speeds comparable to BULK COLLECT. Unfortunately, my company is still running on Oracle9i Database, so I have started converting my cursor FOR loops to BULK COLLECTs. I have run into a problem: I am using a LIMIT of 100, and my query retrieves a total of 227 rows, but my program processes only 200 of them. [The query is shown in Listing 2.] What am I doing wrong?
Code Listing 2: BULK COLLECT, %NOTFOUND, and missing rows

PROCEDURE process_all_rows
IS
   CURSOR table_with_227_rows_cur 
   IS 
      SELECT * FROM table_with_227_rows;

   TYPE table_with_227_rows_aat IS 
      TABLE OF table_with_227_rows_cur%ROWTYPE
      INDEX BY PLS_INTEGER;

   l_table_with_227_rows table_with_227_rows_aat;
BEGIN   
   OPEN table_with_227_rows_cur;
   LOOP
      FETCH table_with_227_rows_cur 
         BULK COLLECT INTO l_table_with_227_rows LIMIT 100;

         EXIT WHEN table_with_227_rows_cur%NOTFOUND;     /* cause of missing rows */

      FOR indx IN 1 .. l_table_with_227_rows.COUNT 
      LOOP
         analyze_compensation (l_table_with_227_rows(indx));
      END LOOP;
   END LOOP;

   CLOSE table_with_227_rows_cur;
END process_all_rows;


You came so close to a completely correct conversion from your cursor FOR loop to BULK COLLECT! Your only mistake was that you didn't give up the habit of using the %NOTFOUND cursor attribute in your EXIT WHEN clause.
The statement

EXIT WHEN 
table_with_227_rows_cur%NOTFOUND;


makes perfect sense when you are fetching your data one row at a time. With BULK COLLECT, however, that line of code can result in incomplete data processing, precisely as you described.
Let's examine what is happening when you run your program and why those last 27 rows are left out. After opening the cursor and entering the loop, here is what occurs:
1. The fetch statement retrieves rows 1 through 100. 
2. table_with_227_rows_cur%NOTFOUND evaluates to FALSE, and the rows are processed. 
3. The fetch statement retrieves rows 101 through 200. 
4. table_with_227_rows_cur%NOTFOUND evaluates to FALSE, and the rows are processed. 
5. The fetch statement retrieves rows 201 through 227. 
6. table_with_227_rows_cur%NOTFOUND evaluates to TRUE , and the loop is terminated—with 27 rows left to process!


So, to make sure that your query processes all 227 rows, replace this statement:
When you are using BULK COLLECT and collections to fetch data from your cursor, you should
 never rely on the cursor attributes to decide whether to terminate your loop and data processing.


EXIT WHEN 
table_with_227_rows_cur%NOTFOUND; 

with

EXIT WHEN 
l_table_with_227_rows.COUNT = 0; 


Generally, you should keep all of the following in mind when working with BULK COLLECT:

  • The collection is always filled sequentially, starting from index value 1.
  • It is always safe (that is, you will never raise a NO_DATA_FOUND exception) to iterate through a collection from 1 to collection .COUNT when it has been filled with BULK COLLECT.
  • The collection is empty when no rows are fetched.
  • Always check the contents of the collection (with the COUNT method) to see if there are more rows to process.
  • Ignore the values returned by the cursor attributes, especially %NOTFOUND.

Source: http://www.oracle.com/technetwork/issue-archive/2008/08-mar/o28plsql-095155.html

Thứ Tư, 4 tháng 3, 2015

Reclaiming Unused Space in Datafiles

There are a number of scenarios that can lead to unused space in datafiles. The two most common I see are:
  • A lack of housekeeping/maintenance means that one or more tables have grown excessively. After the data is pruned the datafiles contain unused space that needs to be reclaimed.
  • One or more segments (tables, partitions or indexes) have been moved to another tablespace leaving empty areas in the datafiles that previously held them.
In this article I will discuss a few of the ways of reclaiming this unused space.
I do not advise reorganizing tablespaces every time a bit of free space appears. In many cases the space will be used as segments in the tablespace continue to grow. I consider this type of action a one-off task when something significant has happened in the tablespace.
Remember, reorganising a tablespace is a big structural change. You should *always* take backup before doing any structural changes to databases you care about.

Setup Test Environment

Before we can look at the solutions we need to create a test environment so we can clearly see the problem. Each of the solutions presented require that this setup is done first.
CONN / AS SYSDBA

-- Create a tablespace and user for the test.
DROP USER reclaim_user CASCADE;
DROP TABLESPACE reclaim_ts INCLUDING CONTENTS AND DATAFILES;

CREATE TABLESPACE reclaim_ts DATAFILE '/u01/app/oracle/oradata/DB11G/reclaim01.dbf' SIZE 1M AUTOEXTEND ON NEXT 1M;

CREATE USER reclaim_user IDENTIFIED BY reclaim_user DEFAULT TABLESPACE reclaim_ts QUOTA UNLIMITED ON reclaim_ts;
GRANT CREATE SESSION, CREATE TABLE TO reclaim_user;


-- Create and populate two tables in the test schema.
CONN reclaim_user/reclaim_user

CREATE TABLE t1 (
  id NUMBER,
  description VARCHAR2(1000),
  CONSTRAINT t1_pk PRIMARY KEY (id)
);

CREATE TABLE t2 (
  id NUMBER,
  description VARCHAR2(1000),
  CONSTRAINT t2_pk PRIMARY KEY (id)
);

INSERT /*+append*/ INTO t1If we switch off autoextend for the relevant datafile, the last four columns will look more representati
SELECT rownum, RPAD('x', 1000, 'x')
FROM dual
CONNECT BY level <= 10000;
COMMIT;

INSERT /*+append*/ INTO t2
SELECT rownum, RPAD('x', 1000, 'x')
FROM dual
CONNECT BY level <= 10000;
COMMIT;

EXEC DBMS_STATS.gather_table_stats(USER, 't1');
EXEC DBMS_STATS.gather_table_stats(USER, 't2');
We can see both table segments are made up of multiple extents, each extent being made up of multiple blocks.
CONN / AS SYSDBA

COLUMN segment_name FORMAT A30

SELECT segment_type, segment_name, COUNT(*)
FROM   dba_extents
WHERE  owner = 'RECLAIM_USER'
GROUP BY segment_type, segment_name
ORDER BY segment_type, segment_name;

SEGMENT_TYPE       SEGMENT_NAME                     COUNT(*)
------------------ ------------------------------ ----------
INDEX              T1_PK                                   3
INDEX              T2_PK                                   3
TABLE              T1                                     27
TABLE              T2                                     27

SQL>

SELECT table_name, num_rows, blocks FROM dba_tables WHERE owner = 'RECLAIM_USER';

TABLE_NAME                       NUM_ROWS     BLOCKS
------------------------------ ---------- ----------
T1                                  10000       1461
T2                                  10000       1461

SQL>
Enterprise Manager gives us a nice image of the contents of the tablespace by doing the following:
  • Click on the "Server" tab.
  • Click the "Tablespaces" link.
  • Select the "RECLAIM_TS" tablespace by clicking the radio button.
  • Select the "Show Tablespace Contents" action and click the "Go" button.
  • On the resulting page, expand the "Extent Map" section.
The image below shows the extent map for the "RECLAIM_TS" tablespace. Clicking on a specific segment in the list, or an extent in the extent map, causes all extents for that segment to be highlighted yellow. From this point on I will ignore the rest of the page and just focus on the extent maps.
TS Map - Start
Now truncate the "T1" table to simulate a maintenance operation.
CONN reclaim_user/reclaim_user

TRUNCATE TABLE t1;
What we can see now is the "T1" table has a single extent (yellow) and there is lots of free space (green) in the centre of the tablespace.
TS Map - Post Truncate
The fact that the majority of the free space is before some of the "T2" extents means we can not shrink the size of the datafile to release the space.
CONN / AS SYSDBA

COLUMN name FORMAT A50

SELECT name, bytes/1024/1024 AS size_mb
FROM   v$datafile
WHERE  name LIKE '%reclaim%';

NAME                                                  SIZE_MB
-------------------------------------------------- ----------
/u01/app/oracle/oradata/DB11G/reclaim01.dbf                26

SQL> 

ALTER DATABASE DATAFILE '/u01/app/oracle/oradata/DB11G/reclaim01.dbf' RESIZE 24M;

ALTER DATABASE DATAFILE '/u01/app/oracle/oradata/DB11G/reclaim01.dbf' RESIZE 24M
*
ERROR at line 1:
ORA-03297: file contains used data beyond requested RESIZE value


SQL>
So this represents the starting point of our problem. We have free space we need to release from the datafile associated with our tablespace.
TS Map - Post Truncate
Before working through any of the methods described below, recreate this situation.

Identify Tablespaces with Free Space

You can easily identify tablespaces with lots of free space using the ts_free_space.sql script.
SQL> @ts_free_space.sql

TABLESPACE_NAME                   SIZE_MB    FREE_MB MAX_SIZE_MB MAX_FREE_MB   FREE_PCT USED_PCT
------------------------------ ---------- ---------- ----------- ----------- ---------- -----------
EXAMPLE                               345         35         345          35         10  XXXXXXXXX-
RECLAIM_TS                             25         12       32767       32754         99  ----------
SYSAUX                                580         33         580          33          5  XXXXXXXXX-
SYSTEM                                720          7         720           7          0  XXXXXXXXXX
UNDOTBS1                               70         41          70          41         58  XXXX------
USERS                                  20         15          20          15         75  XXX-------

6 rows selected.

SQL>
For tablespaces with autoextend enabled, the script calculates the maximum sizes and percentages based on maximum size the datafiles can grow to, but the "FREE_MB" column is based on the current file size, so use that figure for tablespaces with datafiles set to autoextend.
If we switch off autoextend for the relevant datafile, the last four columns will look more representative.
SQL> ALTER DATABASE DATAFILE '/u01/app/oracle/oradata/DB11G/reclaim01.dbf' AUTOEXTEND OFF;

Database altered.

SQL> @ts_free_space.sql

TABLESPACE_NAME                   SIZE_MB    FREE_MB MAX_SIZE_MB MAX_FREE_MB   FREE_PCT USED_PCT
------------------------------ ---------- ---------- ----------- ----------- ---------- -----------
EXAMPLE                               345         35         345          35         10  XXXXXXXXX-
RECLAIM_TS                             25         12          25          12         48  XXXXX-----
SYSAUX                                580         33         580          33          5  XXXXXXXXX-
SYSTEM                                720          7         720           7          0  XXXXXXXXXX
UNDOTBS1                               70         40          70          40         57  XXXX------
USERS                                  20         15          20          15         75  XXX-------

6 rows selected.

SQL>

Export/Import

The export/import process looks like this:
  • Export the schema objects that are stored in the specific tablespace/datafile you want to resize.
  • Drop the exported objects.
  • Resize the datafiles.
  • Import the objects.
  • Perform any required maintenance, like grants etc.
Although the process is simple, it may involve substantial downtime if the objects being dropped are large. The following shows how each of these stages is achieved.
First, create a directory object for the export and import to work with.
CONN / AS SYSDBA
CREATE DIRECTORY temp_dir AS '/tmp';
GRANT READ, WRITE ON DIRECTORY temp_dir TO reclaim_user;
Export the objects in question. In this case we are doing the whole schema, but you may be able to get away with a subset of the objects if your schema is spread across several tablespaces.
expdp reclaim_user/reclaim_user schemas=RECLAIM_USER directory=TEMP_DIR dumpfile=RECLAIM_USER.dmp logfile=expdpRECLAIM_USER.log
Drop the original objects and reduce the size of the datafile.
CONN / AS SYSDBA
DROP TABLE reclaim_user.t1;
DROP TABLE reclaim_user.t2;
PURGE DBA_RECYCLEBIN;

ALTER TABLESPACE reclaim_ts COALESCE;
ALTER DATABASE DATAFILE '/u01/app/oracle/oradata/DB11G/reclaim01.dbf' RESIZE 5M;
Import the objects back into the schema.
impdp reclaim_user/reclaim_user schemas=RECLAIM_USER directory=TEMP_DIR dumpfile=RECLAIM_USER.dmp logfile=impdpRECLAIM_USER.log
The tablespace map shows we have removed the large section of free space in the middle of the datafile associated with our tablespace.
TS Map - Post Export/Import
We can also see the size of the datafile has been reduced from 26M to 14M.
CONN / AS SYSDBA

COLUMN name FORMAT A50

SELECT name, bytes/1024/1024 AS size_mb
FROM   v$datafile
WHERE  name LIKE '%reclaim%';

NAME                                                  SIZE_MB
-------------------------------------------------- ----------
/u01/app/oracle/oradata/DB11G/reclaim01.dbf                14

SQL>
In this example I truncated the first table, so my table segments did not contain much free space internally. If you have segments with lots of internal free space to clean up in addition to the unused space in the datafile, you may want to include the TRANFORM parameter in your import command. Using "TRANSFORM=SEGMENT_ATTRIBUTES:N" setting tells the import command to forget the physical attributes (including the storage clause) of the table when recreating it.
impdp reclaim_user/reclaim_user schemas=RECLAIM_USER directory=TEMP_DIR dumpfile=RECLAIM_USER.dmp logfile=impdpRECLAIM_USER.log \
      transform=segment_attributes:n

Manual Tablespace Reorganization

This method can take one of two forms. It you are happy to change the datafile name do the following:
  • Create a new tablespace.
  • Move the segments to the new tablespace.
  • Drop the original tablespace.
  • Rename the new tablespace to match the original name. *
* As pointed out by Manfred Milhofer in the comments, some versions of the database are susceptible to an error if you rename a tablespace to a name that was used previously, as described in MOS Doc ID 604648.1. If the tablespace must keep the same name, it might be better to drop and recreate the original and move everything back.
If the datafile name must remain the same do the following:
  • Create a new tablespace.
  • Move the segments to the new tablespace.
  • Resize the original datafile.
  • Move the segments back to the original tablespace.
  • Drop the new tablespace.
Obviously the second method requires much more work as all segments are being moved twice.
The way to move segments depends on the type of segment being moved. Here are a few examples.
-- Move a table segment.
ALTER TABLE tab1 MOVE TABLESPACE new_ts;

-- Move an index segment.
ALTER INDEX ind1 REBUILD TABLESPACE new_ts;
ALTER INDEX ind1 REBUILD TABLESPACE new_ts ONLINE;

-- Move a table partition segment. (Remember to check for unusable indexes)
ALTER TABLE tab1 MOVE PARTITION part_1 TABLESPACE new_ts NOLOGGING;

-- Move an index partition segment.
ALTER INDEX ind1 REBUILD PARTITION ind1_part1 TABLESPACE new_ts;

-- Move LOB segments if we had them.
-- ALTER TABLE tab1 MOVE LOB(lob_column_name) STORE AS (TABLESPACE new_ts);
Of course, the tables and their respective indexes could be moved using the Online Table Redefinition functionality.
The following example performs a manual reorganization where the datafile name is not retained. Remember to recreate the test environment before starting this example.
First, create a new tablespace to hold the objects.
CONN / AS SYSDBA

CREATE TABLESPACE reclaim_ts_temp DATAFILE '/u01/app/oracle/oradata/DB11G/reclaim02.dbf' SIZE 1M AUTOEXTEND ON NEXT 1M;
ALTER USER reclaim_user QUOTA UNLIMITED ON reclaim_ts_temp;
Move the objects to the new tablespace.
ALTER TABLE reclaim_user.t1 MOVE TABLESPACE reclaim_ts_temp;
ALTER INDEX reclaim_user.t1_pk REBUILD TABLESPACE reclaim_ts_temp;
ALTER TABLE reclaim_user.t2 MOVE TABLESPACE reclaim_ts_temp;
ALTER INDEX reclaim_user.t2_pk REBUILD TABLESPACE reclaim_ts_temp;
Drop the original tablespace and rename the new one back to the original name.
DROP TABLESPACE reclaim_ts INCLUDING CONTENTS AND DATAFILES;
ALTER TABLESPACE reclaim_ts_temp RENAME TO reclaim_ts;
Once again, the tablespace map shows we have removed the large section of free space in the middle of the datafile associated with our tablespace.
TS Map - Post Manual Tablespace Reorganization
We can also see the size of the datafile has been reduced from 26M to 13M.
CONN / AS SYSDBA

COLUMN name FORMAT A50

SELECT name, bytes/1024/1024 AS size_mb
FROM   v$datafile
WHERE  name LIKE '%reclaim%';

NAME                                                  SIZE_MB
-------------------------------------------------- ----------
/u01/app/oracle/oradata/DB11G/reclaim01.dbf                13

SQL>

Enterprise Manager Tablespace Reorganization

The manual tablespace reorganization method works well, but when you start dealing with lots of segments it can become a bit painful to script, especially if you start using the online table redefinition functionality. This is where Enterprise Manager comes to the rescue because it can perform all the hard work for you, allowing you to easily define a job to perform a tablespace reorganization.
Starting at the "tablespaces" screen, select the "RECLAIM_TS" tablespace by clicking the radio group button next to it, select the "Reorganize" action and click the "Go" button.
Enterprise Manager - Tablespaces
Accept the default object selection by clicking the "Next" button.
Enterprise Manager - Reorganize Objects - Objects
The options screen allows you to decide how the reorganization should take place. The "Method" section has two options:
  • Speed: This is an offline operation using MOVE for tables and REBUILD for indexes. Essentially this is the same as the Manual Tablespace Reorganization shown previously.
  • Availability: This is an online operation, making use of the online table redefinition functionality. The online table redefinition can be done based on the primary key of the table or the ROWID of the rows.
The "Scratch Tablespace" section has two options which determine if the datafile name is preserved or not.
  • Use tablespace rename feature: As the name implies, this uses the first method described in the manual reorganization section.
  • Use scratch tablespace: This uses the second method described in the manual reorganization section. You must provide a existing scratch tablespace name to hold all the objects during the reorganaization.
When you have picked the options you want, click the "Next" button.
Enterprise Manager - Reorganize Objects - Options
The resulting screen provides an impact report. If it includes any anticipated problems, you may need to move back and alter your options. When you are happy with the impact report, click the "Next" button.
Enterprise Manager - Reorganize Objects - Impact Report
Enter the desired schedule information and click the "Next" button.
Enterprise Manager - Reorganize Objects - Schedule
The review page includes the script that will be run by the job. If you are happy with the review information, click the "Submit Job" button.
Enterprise Manager - Reorganize Objects - Review
After the job completes we can see the segments are now at the start of the tablespace map, allowing us to reduce the associated datafile size if we want. The reorganization process doesn't actually do the datafile resize for us, so the tablespace size is unchanged.
TS Map - Post Enterprise Manager Tablespace Reorganization

Shrink?

If there is only a single object in the datafile, it's possible a shrink operation will actually be enough to compact the data and free up the empty blocks, allowing the datafile to be resized to a smaller size.
Remember, the shrink only compacts the data. It doesn't guarantee the blocks will be placed at the front of the datafile, so it's possible you will have blocks at the end of the datafile, which means the datafile can't be resized smaller. If this is the case you will need to use one of the methods mentioned previously.

Tablespaces with Multiple Datafiles

So far I've conveniently side-stepped the issue of tablespaces with multiple datafiles. Why? Because it makes the tablespace extent map a little more confusing to look at. To see what I mean, perform the setup again, but this time before building the tables add an additional datafile to the tablespace.
ALTER TABLESPACE reclaim_ts ADD DATAFILE '/u01/app/oracle/oradata/DB11G/reclaim02.dbf' SIZE 1M AUTOEXTEND ON NEXT 1M;
With the tables built, populated and the "T1" table truncated, the extent map will look something like this.
TS Map - Multiple Datafiles
The problem here is we can't tell what files the gaps are in without hovering over the gaps and reading the tool tip. That's not very simple when we have lots of files, segments and gaps to contend with. At this point I stop looking at the extent map and just use a script to identify all the gaps in the tablespace, or individual datafiles.
The ts_extent_map.sql script produces a list of all the free space in the tablespace or individual datafile. The combined output and separate output for each datafile is shown below.
SQL> @ts_extent_map reclaim_ts all
Tablespace Block Size (bytes): 8192
*** GAP *** (23 -> 896) FileID=8 Blocks=872 Size(MB)=6.81
*** GAP *** (959 -> 1024) FileID=8 Blocks=64 Size(MB)=.5
*** GAP *** (127 -> 768) FileID=9 Blocks=640 Size(MB)=5
Total Gap Blocks: 1576
Total Gap Space (MB): 12.31

SQL> @ts_extent_map reclaim_ts 8
Tablespace Block Size (bytes): 8192
*** GAP *** (23 -> 896) FileID=8 Blocks=872 Size(MB)=6.81
*** GAP *** (959 -> 1024) FileID=8 Blocks=64 Size(MB)=.5
Total Gap Blocks: 936
Total Gap Space (MB): 7.31

SQL> @ts_extent_map reclaim_ts 9
Tablespace Block Size (bytes): 8192
*** GAP *** (127 -> 768) FileID=9 Blocks=640 Size(MB)=5
Total Gap Blocks: 640
Total Gap Space (MB): 5

SQL>
This can help you make a judgment as to whether a tablespace reorganization is necessary.

Undo Tablespace

The simplest way to reclaim space from the undo tablespace is to create a new undo tablespace, make it the database undo tablespace and drop the old tablespace. In the following example I've used autoextend, but you may wish to remove this if you want manual control over the datafile size.
CREATE UNDO TABLESPACE undotbs2 DATAFILE '/u01/app/oracle/oradata/DB11G/undotbs02.dbf' SIZE 2G AUTOEXTEND ON NEXT 1M;

ALTER SYSTEM SET UNDO_TABLESPACE=undotbs2;

DROP TABLESPACE undotbs1 INCLUDING CONTENTS AND DATAFILES;
Remember, flashback operations requiring undo will not be possible because you have deleted the retained undo. You may want to consider your UNDO_RETENTION parameter setting.

Temp Tablespace

If you are using oracle 11g, you can shrink a temporary tablespace using the ALTER TABLESPACE command, as shown here.
If you are using a database version prior to 11g, reducing the size of the temporary tablespace is similar to reclaiming space from the undo tablespace. Create a new temp tablespace, move the users on to it, then drop the old temp tablespace.

CREATE TEMPORARY TABLESPACE temp2 TEMPFILE '/u01/app/oracle/oradata/DB11G/temp02.dbf' SIZE 2G AUTOEXTEND ON NEXT 1M;

ALTER DATABASE DEFAULT TEMPORARY TABLESPACE temp2;

-- Switch all existing users to new temp tablespace.
BEGIN
  FOR cur_user IN (SELECT username FROM dba_users WHERE temporary_tablespace = 'TEMP') LOOP
    EXECUTE IMMEDIATE 'ALTER USER ' || cur_user.username || ' TEMPORARY TABLESPACE temp2';
  END LOOP;
END;
/

DROP TABLESPACE temp INCLUDING CONTENTS AND DATAFILES;

Source: http://oracle-base.com/articles/misc/reclaiming-unused-space.php