Friday, October 25, 2013

BPC Member formula ... devil is in the details

I've found a "non working" item in BPC ... it's member formula was something like



IIF([HDCNT]=0,0,[TOT_COST]/[HDCNT])

very nice and workend fine, but ... while cost is currency converted headcount is not ... so it was "not working" ... 

A little revision, rewriting it in order to take into account dimensions brought the desired result



IIF(([HRCOST].[HDCNT],[RPTCURRENCY].[LC])=0,0,([HRCOST].[TOT_COST])/([HRCOST].[HDCNT],[RPTCURRENCY].[LC]))

 

Not that difficult, but I see that BPC people generally don't like to fully qualify things :(

After a while I had to change it again ... adding another bit of MDX to the game, IsLeaf and CurrentMember

IIF(
IsLeaf([TIME].CURRENTMEMBER)
,
IIF(([HRCOST].[FTE],[RPTCURRENCY].[LC])=0,0,([HRCOST].[TOT_COST])/([HRCOST].[FTE],[RPTCURRENCY].[LC]))
,
IIF(([HRCOST].[HEADCNT],[RPTCURRENCY].[LC])=0,0,([HRCOST].[TOT_COST])/([HRCOST].[HEADCNT],[RPTCURRENCY].[LC]))
)

Sunday, August 18, 2013

Tuesday, April 02, 2013

BPC work status setup ... APPROVALORG, a little parameter often overlooked

When setting up workstatus in BPC there is a little web parameter to setup, APPROVALORG, it's often overlooked because generally one doesn't manage more than one hierarchy for an entity ... in our case we have 3 hierarchies and hierarchy number 3 (H3) is the one used to manage read/write permissions and thus the natural candidate for controlling workstatus.
So the web parameter APPROVALORG should not be H1 in this case, or you'll get some "strange" errors, but H3, and of course H3 will also have the owner property correctly filled.

An example:


Some useful tutorials can be found on SDN.


Tuesday, January 29, 2013

BPC load 0CALMONTH from BW

As incredible as it sounds ... in BPC 7.5 NW you have to prepare a conversion to load time from 0CALMONTH to TIME dimension!
Thanks God it's not that difficult, once you know how to do it.
An example:

Time conversion file



Monday, January 28, 2013

BPC lite optimize zero elimination

Just a quick visual to add zero elimination to the light optimize package in BPC 7.5 NW, see SAP Note 1396761


Organize package list

Modify package Lite Optimize
Modify package Lite Optimize

Add task


Tuesday, July 03, 2012

BW dataflow examples for HR masterdata

It happens quite often, source data comes from a single infotype, but users want informations coming from different subtypes moved to different infoobjects on the same row (i.e. one row per employee per time period), this usually requires some ABAP but there is a trick.
You can use infosets to differentiate logically between rows of the infotype, see below:

This example is quite specific for time dependend masterdata attributes (think 0EMPLOYEE).
It's like having the ability to run different transformations from the same source to the same target according to different values of one column.

Something like inverted rule groups, which are great at turning data in a long row into data in different columns:

An example of rule group usage:

PERNR LGA01 BET01 LGA02 BET02 etc

turned into:

PERNR LGA01 BET01
PERNR LGA02 BET02

which can fit nicely into an infocube.

Friday, April 06, 2012

Texts in the output of an APD

There seems to be interest in outputting not only the query results in APD but also some texts, which is currently not supported by simply having the query output contain texts as anyone would expect.
But, there is an easy solution, see figure below






Here you are.
Sending it out to a file doesn't change the other steps.

Thursday, October 27, 2011

Calculations with key date

A bit of follow up to the previous post ... we'd like to have the key date at hand for performing some calculations, unfortunately we didn't find an easy way so with some micro abap we managed to get that variable into another variable (??) that can be used.
Coding for variables (CMOD on BW side, EXIT_SAPLRRS0_001, include ZXRSRU01 etc.) follows:
Wish there was a simpler way ...
Code:
case i_vnam.
 when 'YV_DAY_2_VAR'.
    if i_step = 2.
      loop at i_t_var_range into loc_var_range where vnam = 'YV_DAY'.
 clear: l_s_range. 
       l_s_range-low = loc_var_range-low.
       l_s_range-sign = 'I'.
       l_s_range-opt = 'EQ'.
 append l_s_range to e_t_range.
      endloop.
    endif.

Monday, August 22, 2011

Prompts in SAP BO OLAP universe (query key date)

Having recently struggled with a key date prompt in a BO universe built on a BEx query ... I'm posting here some samples and screenshots of the outcome as I didn't find/couldn't understand documentation on this ....

The universe wizard in my case didn't generate something usable out of the box, so I had to tweak it a bit

First one, how to show it as a datepicker (sounds strange ...)




The corresponding code for the prompt is:

<filter key="[YV_DAY]"><condition operatorcondition="Equal"><constant tech_name=" @Prompt('Data Cardine (formato YYYYMMDD)','D',,mono,free)"></condition></filter>

Then if you want it to be a simple, no frills, single value prompt



<filter key="[YV_DAY]"><condition operatorcondition="Equal"><constant tech_name="@Prompt('Data Cardine (formato YYYYMMDD)','A',,mono,free)"></condition></filter>

And if you want the prompt to “forget” the previous value when the query is refreshed:

<filter key="[YV_DAY]"><condition operatorcondition="Equal"><constant tech_name="@Prompt('Data Cardine (formato YYYYMMDD)','A','Data\LovData CardineBase',mono,free, not_persistent)"></condition></filter>
Also see note 1407631 and keep up with the updates on BW and BO side, IMHO it’s not a mature solution yet.

Note 1142664 - MDX: Composite SAP note about performance improvements

Note 838800 - Composite SAP Note for consulting notes about MDX/OLAP BAPIs

Note 1156101 - MDX: Composite SAP note for incorrect data

Another useful one

Note 1567394 - MDX: Consulting note for sort sequence

Wednesday, July 20, 2011

Filtering on hierarchy values in DTP

Having spent some time unsuccessfully googleing for a way to use a hierarchy as filter in a DTP I've come up with a (rather inelegant) solution myself.
Posting it here in hope it's helpful.
Code:

DATA: l_idx like sy-tabix.

DATA: lw_hier_id TYPE rshi_s_rshiedirkey.
DATA: lt_nodes_a_leaves TYPE rshi_t_hienode.
DATA: lw_nodes_a_leaves LIKE LINE OF lt_nodes_a_leaves.

DATA: l_hier_name TYPE rshienm.
DATA: lw_hier_type TYPE rshiedir.

read table l_t_range with key
fieldname = '/BIC/ZFIELD_NAME'.
l_idx = sy-tabix.
*....

l_hier_name = 'ZH_HIER_NAME'.

SELECT SINGLE * FROM rshiedir INTO lw_hier_type
WHERE hienm = l_hier_name
AND objvers = 'A'.

IF sy-subrc = 0.
lw_hier_id-hieid = lw_hier_type-hieid.
lw_hier_id-objvers = 'A'.


CALL FUNCTION 'RSSH_HIERARCHY_READ'
EXPORTING
i_rshiedirkey = lw_hier_id
IMPORTING
e_t_rsnodes = lt_nodes_a_leaves
EXCEPTIONS
invalid_hierarchy = 1
name_error = 2
iobj_not_found = 3
OTHERS = 4
.
IF sy-subrc <> 0.

DELETE lt_nodes_a_leaves WHERE iobjnm = '0HIER_NODE'.

Loop at lt_nodes_a_leaves INTO lw_nodes_a_leaves.

l_t_range-fieldname = '/BIC/ZFIELD_NAME'.
l_t_range-sign = 'I'.
l_t_range-option = 'EQ'.
* substring is due to compounding, generally unneeded
l_t_range-low = lw_nodes_a_leaves-nodename+2(4).

append l_t_range.

endloop.

ENDIF.

ENDIF.


* if l_idx <> 0.
* modify l_t_range index l_idx.
* else.
* append l_t_range.
* endif.
p_subrc = 0.
OK, this is all SAP BW jargon ;-)

Saturday, March 20, 2010

I love windows functions

... or whatever they call those functions that allow ranking and anyway working on "windows" of data (or sets inside sets or ...).

A couple of questions on Devshed forums I recently answered using those functions.

First one is about recordset paging (well, there are hundred of ways to do recordset paging, just google, but ayway) LIMIT being one of them, stored procedures another one (and ugly IMHO).

Code:
select
bb.code
from
(
select
aa.code,
rank() over( partition by 1 order by aa.code) row_num,
trunc(((rank() over( partition by 1 order by aa.code))-1)/10)+1 pages
from
paging_test aa
) bb
where
bb.pages = 2
where 10 is the number of rows per page and 2 is the page you want to retrieve.

In this case the rank() is over the entire set (note the partition clause) basically turning it into a row number.

The other one about finding two (top, random, whatever) elements inside sets (i.e. two employees per department).
In this case the OP has already got the fact that LIMIT is not enough for solving the problem cleanly, so I gave him a little example:

Code:
select
emp_rk.emp_name,
emp_rk.emp_dept
from
(
select
rank() over (partition by emp_dept order by emp_name) rk,
emp_name,
emp_dept
from
employee
) emp_rk
where
emp_rk.rk <>
Of course the trick is to partition by the set definer and order by whatever makes sense to you, even random ordering can be the solution.

Unfortunately only PostgreSQL currently supports those functions, Firebird should when v3.0 will be out, MySQL doesn't seem too interested, they already gave us the LIMIT clause.


EDIT: another one here.
This time is about accessing the ever elusive "previous row", the solution is using the lag() function, note that a lead() function exists and that both can be defined over the entire ordered table or over sets defined by user through the partition by clause (as the functions described above).

Code:
select
tts.temp_date_time,
tts.temp_value,
lag(tts.temp_value) over (partition by 1 order by tts.temp_date_time),
tts.temp_value - coalesce(
lag(tts.temp_value) over (partition by 1 order by tts.temp_date_time), 0
) delta_previous
from
test_temp_seq tts;
Partition by 1 actually means no partition at all, I'm partitioning by a constant ;)

Saturday, January 16, 2010

The missing link ...

... between me and Firebird.
The Tracker, that's it, claims are it's up again but it doesn't work for me.
I'd love to open a feature request, or add my vote to an existing one, "role assignment to stored procedures".
Actually I'm after assignment of the rdb$admin role to a stored procedure, in order to give unprivileged users the ability to perform some administrative tasks without the need for sysdba.
I think that this solution is much better than adding the

... AS USER <...> PASSWORD <...> ROLE <...>

detailed in CORE-2452 : Add Role Name in input parameters for EXECUTE STATEMENT in the stored procedure code, because I don't want to hardcode passwords or such informations into stored procedures.
I don't think that changing a password should be able to break a stored procedure.

Saturday, May 23, 2009

Consuming webservices in JSP with Apache AXIS

This is a rather old tutorial moved here from my old site on Geocities, hope it's still useful.

Recently I started fumbling around with webservices, a nice integration and communication tool for my customers, what follows is a work in progress, which is going to be enhanced while I proceed in my “trial and error for the illiterate” learning path.
First of all I installed AXIS (http://ws.apache.org/axis/) from the Apache Software foundation and, after validating the installation with the “happyAxis” page, had a working environment for my tests.
The first thing that came to my mind was consuming an external webservice, and I chose to experiment with Tiscali’s public webservices (http://webservices.tiscali.com/).
Axis is able to generate a client for you and I decided to go for it!
It worked automagically, just typed (on my customer’s PC AXIS is located in c:\axis_1-1\):

And in folder ./tiscalisamples (specified with the –o option) some folders and java
files were generated:
./tiscalisample/com/tiscali/webservices/some java files :
  1. ArrayOfServiceTest.java
  2. deploy.wsdd
  3. ServiceTest.java
  4. SmtpResult.java
  5. Tiscali_x0020_Email_x0020_Services.java
  6. Tiscali_x0020_Email_x0020_ServicesLocator.java
  7. Tiscali_x0020_Email_x0020_ServicesSoap.java
  8. Tiscali_x0020_Email_x0020_ServicesSoapImpl.java
  9. Tiscali_x0020_Email_x0020_ServicesSoapStub.java
  10. undeploy.wsdd
Now I compiled the whole lot :

C:\axis-1_1\dist>javac -classpath C:\axis-1_1\lib\axis.jar;C:\C:\axis-1_1\lib\jaxrpc.jar;C:\axis-1_1\lib\commons-logging.jar;
C:\axis-1_1\lib\commons-discovery.jar;C:\axis-1_1\lib\saaj.jar;C:\axis-1_1\lib\xercesImpl.jar;C:\axis-1_1\lib\xml-apis.jar
C:/axis-1_1/dist/tiscalisample/com/tiscali/webservices/*.java


And so it all ended up in a bunch of .class files in the same folder.

Then I copied the whole ./com/tiscali/webservices/*.class to the ./WEBINF/classes folder of the standard “examples” webapp shipped with Tomcat.
Now the “real” client, those were only helper classes, I chose to write it in jsp for simplicity, it all boiled down to a few lines:

<%@ page contentType="text/html; charset=iso-8859-1" language="java" errorPage="" %>





<% com.tiscali.webservices.Tiscali_x0020_Email_x0020_ServicesLocator aLocator = new com.tiscali.webservices.Tiscali_x0020_Email_x0020_ServicesLocator();
com.tiscali.webservices.Tiscali_x0020_Email_x0020_ServicesSoap aServicesSoap = aLocator.getTiscali_x0020_Email_x0020_ServicesSoap();
String validatorResult = aServicesSoap.isValidEmail("mail@xyz.com");
out.write(validatorResult); %>



Which returned “true” as this is a valid (even if probably not existent) email address.

This made me happy and prompted for further investigation of those “webservices”.

The nice thing of this code is that WSDL2Java (the tool I called from the command line) has done almost all the work for me, I just had to compile the code and call it’s methods, then I did some reading on Axis’s site and decided to experiment with direct usage of Axis.

First I set up a simple SOAP service, using Axis’s ability to build a service from a java file, a very simple one:
Here is the code (helloEverybody.jws):

  1. public class helloEverybody {
  2. public String hello()
  3. {
  4. String results = "Hello everybody";
  5. return results;
  6. }
  7. }

I placed the file in c:\axis-1_1\webapps\axis and started looking at it’s WSDL description with the url http://localhost:8080/axis/helloEverybody.jws?wsdl
Looking at it you can quickly get some infos:

The service section:








Shows you the service address (to be called by your client) and the soap port, named “helloEverybody”.










And for details on the response:




The rest of the WSDL shows clearly that there is only one method to be called “hello” which returns a response of type “string” (Axis’s site details mapping between xsd types and java types).
This is the jsp client I put up:


<%@ page contentType="text/html; charset=iso-8859-1" language="java" import="org.apache.axis.client.Service, org.apache.axis.encoding.XMLType, javax.xml.rpc.*;" errorPage="" %>





<% String host = "http://localhost:8080"; String servicepath = "/axis/helloEverybody.jws";
String endpoint = host + servicepath;
String method = "hello" ;
String ret = null;
Service service = new Service();
Call call = (Call) service.createCall();
call.setTargetEndpointAddress(endpoint);
call.setOperationName(new javax.xml.namespace.QName (method));
ret = (String)
call.invoke(( Object [] )null);
out.write("Got result : " + ret);
%>


The “endpoint” and “method” corresponds to what I wrote above.
According to Axis guides I created a service and a call from that service, then I set an endpoint and an operation name for that call. After this I invoked the call (without input parameters and displayed the response converting it to a string for compatibility.

Next steps will be securing the service with a Realm and https and consuming it from jsp with direct usage of Axis.

Sunday, November 02, 2008

Old challenges, new synthax ...

I've blogged before about the new SQL synthax which is becoming available in databases and how it helps solving questions which are increasingly common.

Now it's time for another example, something which doesn't come up often in a reporting environment as most tools have this feature, but can be a problem if you're building your output with a scipting language.
Adding a "Total" row at the end of your tabular output, that's it! Here you can find the Devshed forum post that prompted this summary.

Aside from the classical solution using a UNION and an ORDER BY query, I suggested using the new WITH ROLLUP clause, which is implemented by many commercial databases and MySQL (available from 4.1).
Unfortunately MySQL's implementations poses some limitations to the ORDER BY clause, actually it's impossible to directly order by the aggregate column, which is a quite common requirement, on with the main problem using the Sakila sample database.

An example requirement could be showing all sales by category and a row holding the grand total.
In Sakila the basic query would be:

  1. SELECT
  2. c.name AS category,
  3. SUM(p.amount) AS total_sales
  4. FROM
  5. sakila.payment p
  6. JOIN sakila.rental r
  7. ON p.rental_id = r.rental_id
  8. JOIN sakila.inventory i
  9. ON r.inventory_id = i.inventory_id
  10. JOIN sakila.film f
  11. ON i.film_id = f.film_id
  12. JOIN sakila.film_category fc
  13. ON f.film_id = fc.film_id
  14. JOIN sakila.category c
  15. ON fc.category_id = c.category_id
  16. GROUP BY
  17. c.name;


As I said a requirement based on ordering by total_sales in MySQL can't be satisfied without using a trick, like nesting selects, and so I'll skip it (BTW: this made me notice a bad practice left around in Sakila, usage of order by in views, see "sales_by_film_category") .

The traditional solution for adding a total row would be:

  1. SELECT
  2. c.name AS category,
  3. SUM(p.amount) AS total_sales
  4. FROM
  5. sakila.payment p
  6. JOIN sakila.rental r
  7. ON p.rental_id = r.rental_id
  8. JOIN sakila.inventory i
  9. ON r.inventory_id = i.inventory_id
  10. JOIN sakila.film f
  11. ON i.film_id = f.film_id
  12. JOIN sakila.film_category fc
  13. ON f.film_id = fc.film_id
  14. JOIN sakila.category c
  15. ON fc.category_id = c.category_id
  16. GROUP BY
  17. c.name
  18. UNION ALL
  19. SELECT
  20. NULL,
  21. SUM(p.amount) AS total_sales
  22. FROM
  23. sakila.payment p
  24. JOIN sakila.rental r
  25. ON p.rental_id = r.rental_id
  26. JOIN sakila.inventory i
  27. ON r.inventory_id = i.inventory_id
  28. JOIN sakila.film f
  29. ON i.film_id = f.film_id
  30. JOIN sakila.film_category fc
  31. ON f.film_id = fc.film_id
  32. JOIN sakila.category c
  33. ON fc.category_id = c.category_id
  34. ORDER
  35. BY CASE WHEN category IS NULL
  36. THEN 'last'
  37. ELSE 'first' END
  38. , category

As you can see it can be quite hard to read ... the order by trick is last.

The explain plan can become quite nasty too ... (MySQLPerformanceBlog wrote about this earlier), now have a look at the more modern solution:

  1. SELECT
  2. c.name AS category,
  3. SUM(p.amount) AS total_sales
  4. FROM
  5. sakila.payment p
  6. JOIN sakila.rental r
  7. ON p.rental_id = r.rental_id
  8. JOIN sakila.inventory i
  9. ON r.inventory_id = i.inventory_id
  10. JOIN sakila.film f
  11. ON i.film_id = f.film_id
  12. JOIN sakila.film_category fc
  13. ON f.film_id = fc.film_id
  14. JOIN sakila.category c
  15. ON fc.category_id = c.category_id
  16. GROUP BY
  17. c.name
  18. WITH rollup;

Much simpler, isn't it?

But what about efficiency?
Let's look at explain plans for those queries:

First one, the classic way:

mysql> EXPLAIN EXTENDED select
-> c.name AS category,
-> sum(p.amount) AS total_sales
-> from
-> sakila.payment p
-> join sakila.rental r
-> on p.rental_id = r.rental_id
-> join sakila.inventory i
-> on r.inventory_id = i.inventory_id
-> join sakila.film f
-> on i.film_id = f.film_id
-> join sakila.film_category fc
-> on f.film_id = fc.film_id
-> join sakila.category c
-> on fc.category_id = c.category_id
-> group by
-> c.name
-> union all
-> select
-> null,
-> sum(p.amount) AS total_sales
-> from
-> sakila.payment p
-> join sakila.rental r
-> on p.rental_id = r.rental_id
-> join sakila.inventory i
-> on r.inventory_id = i.inventory_id
-> join sakila.film f
-> on i.film_id = f.film_id
-> join sakila.film_category fc
-> on f.film_id = fc.film_id
-> join sakila.category c
-> on fc.category_id = c.category_id
-> ORDER
-> BY CASE WHEN category IS NULL
-> THEN 'last'
-> ELSE 'first' END
-> , category;
+----+--------------+------------+--------+-----------------------------------+---------------------------+---------+-----------------------+------+----------+---------------------------------+
| id | select_type | table | type | possible_keys |key | key_len | ref | rows | filtered |Extra |
+----+--------------+------------+--------+-----------------------------------+---------------------------+---------+-----------------------+------+----------+---------------------------------+
| 1 | PRIMARY | c | ALL | PRIMARY |NULL | NULL | NULL | 16 | 100.00 |Using temporary; Using filesort |
| 1 | PRIMARY | fc | ref | PRIMARY,fk_film_category_category |fk_film_category_category | 1 | sakila.c.category_id | 9 | 100.00 |Using index |
| 1 | PRIMARY | f | eq_ref | PRIMARY |PRIMARY | 2 | sakila.fc.film_id | 1 | 100.00 |Using index |
| 1 | PRIMARY | i | ref | PRIMARY,idx_fk_film_id |idx_fk_film_id | 2 | sakila.fc.film_id | 2 | 100.00 |Using index |
| 1 | PRIMARY | r | ref | PRIMARY,idx_fk_inventory_id |idx_fk_inventory_id | 3 | sakila.i.inventory_id | 1 | 100.00 |Using index |
| 1 | PRIMARY | p | ref | fk_payment_rental |fk_payment_rental | 5 | sakila.r.rental_id | 1 | 100.00 |Using where |
| 2 | UNION | c | index | PRIMARY |PRIMARY | 1 | NULL | 16 | 100.00 |Using index |
| 2 | UNION | fc | ref | PRIMARY,fk_film_category_category |fk_film_category_category | 1 | sakila.c.category_id | 9 | 100.00 |Using index |
| 2 | UNION | f | eq_ref | PRIMARY |PRIMARY | 2 | sakila.fc.film_id | 1 | 100.00 |Using index |
| 2 | UNION | i | ref | PRIMARY,idx_fk_film_id |idx_fk_film_id | 2 | sakila.fc.film_id | 2 | 100.00 |Using index |
| 2 | UNION | r | ref | PRIMARY,idx_fk_inventory_id |idx_fk_inventory_id | 3 | sakila.i.inventory_id | 1 | 100.00 |Using index |
| 2 | UNION | p | ref | fk_payment_rental |fk_payment_rental | 5 | sakila.r.rental_id | 1 | 100.00 |Using where |
| NULL | UNION RESULT | | ALL | NULL| NULL | NULL | NULL | NULL | NULL| Using filesort |
+----+--------------+------------+--------+-----------------------------------+---------------------------+---------+-----------------------+------+----------+---------------------------------+
13 rows in set, 1 warning (0.00 sec)

mysql>

Pretty bad isn't it? Two queries are executed, then glued together.

Now the second one WITH ROLLUP:

mysql> EXPLAIN EXTENDED select
-> c.name AS category,
-> sum(p.amount) AS total_sales
-> from
-> sakila.payment p
-> join sakila.rental r
-> on p.rental_id = r.rental_id
-> join sakila.inventory i
-> on r.inventory_id = i.inventory_id
-> join sakila.film f
-> on i.film_id = f.film_id
-> join sakila.film_category fc
-> on f.film_id = fc.film_id
-> join sakila.category c
-> on fc.category_id = c.category_id
-> group by
-> c.name
-> with rollup;
+----+-------------+-------+--------+-----------------------------------+---------------------------+---------+-----------------------+------+----------+----------------+
| id | select_type | table | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+--------+-----------------------------------+---------------------------+---------+-----------------------+------+----------+----------------+
| 1 | SIMPLE | c | ALL | PRIMARY | NULL | NULL | NULL | 16 | 100.00 | Using filesort |
| 1 | SIMPLE | fc | ref | PRIMARY,fk_film_category_category | fk_film_category_category | 1 | sakila.c.category_id | 9 | 100.00 | Using index |
| 1 | SIMPLE | f | eq_ref | PRIMARY | PRIMARY | 2 | sakila.fc.film_id | 1 | 100.00 | Using index |
| 1 | SIMPLE | i | ref | PRIMARY,idx_fk_film_id | idx_fk_film_id | 2 | sakila.fc.film_id | 2 | 100.00 | Using index |
| 1 | SIMPLE | r | ref | PRIMARY,idx_fk_inventory_id | idx_fk_inventory_id | 3 | sakila.i.inventory_id | 1 | 100.00 | Using index |
| 1 | SIMPLE | p | ref | fk_payment_rental | fk_payment_rental | 5 | sakila.r.rental_id | 1 | 100.00 | Using where |
+----+-------------+-------+--------+-----------------------------------+---------------------------+---------+-----------------------+------+----------+----------------+
6 rows in set, 1 warning (0.00 sec)

mysql>

A lot better, isn't it? And faster ;)

BTW: More about EPLAIN EXTENDED here.