This discussion is archived
9 Replies Latest reply: Dec 5, 2012 9:04 AM by Aj09 RSS

PARALLEL_ENABLED with SQL subquery input.

Aj09 Newbie
Currently Being Moderated
Oracle Database 11g Enterprise Edition Release 11.2.0.3.0 - 64bit
PL/SQL Release 11.2.0.3.0
IBM/AIX RISC System

Hello Experts,
I tried to emulate William Robertson's "Parallel PL/SQL launcher Program" to accept a select query as input. The source of the code is from http://www.williamrobertson.net/ (on the Left hand side select 'Oracle' >> 'Oracle Code and Scripts'. On the Right Hand list select the 2nd link 'Parallel PL/SQL launcher update').

Mr. Robertson uses a funtion with "dbms_lock.sleep" and sends that as an input to a PARALLEL_ENABLED function. However, I tried create another Pipeline Funtion with a select query as input parameter. Can the experts kindly suggest the approach on how I can use a select query as input to a PARALLEL_ENABLED Function. My purpose is not to have a PARALLEL hint in a select query but to have a function parallelly execute multiple select queries at the same time and PIPE out the result.

DROP TABLE pq_driver PURGE;
DROP TABLE log_times PURGE;
DROP  TYPE varchar2_tt;

CREATE TYPE varchar2_tt AS TABLE OF VARCHAR2(32000);
/

GRANT EXECUTE ON varchar2_tt TO PUBLIC;

CREATE TABLE pq_driver
( thread_id NUMBER(1) NOT NULL PRIMARY KEY )
PARTITION BY LIST(thread_id)
( PARTITION p1 VALUES(1)
, PARTITION p2 VALUES(2)
, PARTITION p3 VALUES(3)
, PARTITION p4 VALUES(4) )
PARALLEL 4
/

INSERT ALL
INTO pq_driver VALUES (1)
INTO pq_driver VALUES (2)
INTO pq_driver VALUES (3)
INTO pq_driver VALUES (4)
SELECT * FROM dual;

COMMENT ON TABLE pq_driver IS 'Control table for generating parallel ref cursors with package parallel_launch';

EXEC dbms_stats.lock_table_stats('SYSADM','PQ_DRIVER');

-- PQ selection is sensitive to table stats. Analyzing table with
-- exec DBMS_STATS.GATHER_TABLE_STATS(user,'pq_driver')
-- causes serialisation.
-- Will automatic stats gathering have the same effect?

CREATE TABLE log_times
( thread_id   INTEGER NOT NULL CONSTRAINT log_times_pk PRIMARY KEY
, what        VARCHAR2(4000)
, sid         INTEGER
, serial#     INTEGER
, start_time  TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
, end_time    TIMESTAMP
, label       VARCHAR2(4000)
, errors      VARCHAR2(4000) );

-- Note: The trigger can be ignored if it fails.

drop trigger log_times_defaults_trg;

CREATE or replace TRIGGER log_times_defaults_trg
BEFORE INSERT ON log_times
FOR EACH ROW
BEGIN
    SELECT sid, serial#
    INTO   :new.sid, :new.serial#
    FROM  sys.v_$session
    WHERE  sid IN
           ( SELECT sid FROM sys.v_$mystat );
END;
/

show errors

CREATE OR REPLACE PACKAGE parallel_launch
AS
    TYPE rc_pq_driver IS REF CURSOR RETURN pq_driver%ROWTYPE;
    TYPE inrec_type is record (table_name VARCHAR2 (32000));

    TYPE inrec IS TABLE OF inrec_type;
    -- PQ launch vehicle:
    -- Must be strongly typed ref cursor to allow partition by range or hash
    -- Must be in package spec to be visible to SQL
    -- Must be an autonomous transaction if we are doing any DML in the submitted procedures

    FUNCTION pq_submit
        ( p_job_list  VARCHAR2_TT
        , p_pq_refcur rc_pq_driver )
        RETURN varchar2_tt
        PARALLEL_ENABLE(PARTITION p_pq_refcur BY ANY)
        PIPELINED;

    PROCEDURE submit
        ( p_job1 VARCHAR2
        , p_job2 VARCHAR2
        , p_job3 VARCHAR2
        , p_job4 VARCHAR2 );

    -- Convenient test procedure - calls 'parallel_launch.slow_proc()' 4 times to see whether they all run at once:
    PROCEDURE test;

    -- Dummy procedure for testing - calls dbms_lock.sleep(2)
    PROCEDURE slow_proc( p_id INTEGER );
    
  FUNCTION test_rs (p_sql IN SYS_REFCURSOR)
  RETURN inrec
  PIPELINED;
END parallel_launch;
/

show errors

CREATE OR REPLACE PACKAGE BODY parallel_launch
AS
    TYPE stats_rec IS RECORD
    ( thread_id            log_times.thread_id%TYPE
    , what                 log_times.what%TYPE
    , start_timestr        VARCHAR2(8)
    , sid                  sys.v_$mystat.sid%TYPE
    , serial#              v$px_session.serial#%TYPE
    , pq_actual_degree     NUMBER
    , pq_requested_degree  NUMBER
    , rowcount             PLS_INTEGER := 0 );

    g_clear_stats_rec stats_rec;


    PROCEDURE log_start
        ( p_thread_id  log_times.thread_id%TYPE
        , p_what       log_times.what%TYPE )
    IS
        PRAGMA AUTONOMOUS_TRANSACTION;
    BEGIN
        DELETE log_times WHERE thread_id = p_thread_id;

        INSERT INTO log_times
        ( thread_id, what )
        VALUES
        ( p_thread_id, p_what );

        COMMIT;
    END log_start;

    PROCEDURE log_end
        ( p_thread_id  log_times.thread_id%TYPE
        , p_errors     log_times.errors%TYPE DEFAULT NULL )
    IS
        PRAGMA AUTONOMOUS_TRANSACTION;
    BEGIN
        UPDATE log_times
        SET    end_time = SYSTIMESTAMP
             , errors = p_errors
        WHERE  thread_id = p_thread_id;

        COMMIT;
    END log_end;

    PROCEDURE execute_command
        ( p_what log_times.what%TYPE )
    IS
        PRAGMA AUTONOMOUS_TRANSACTION;
    BEGIN
        EXECUTE IMMEDIATE p_what;
        COMMIT;
    END execute_command;

    FUNCTION pq_submit
        ( p_job_list  VARCHAR2_TT
        , p_pq_refcur rc_pq_driver )
        RETURN varchar2_tt
        PARALLEL_ENABLE(PARTITION p_pq_refcur BY ANY)
        PIPELINED
    IS
        v_error_text VARCHAR2(32000);
        r pq_driver%ROWTYPE;
        r_row_stats stats_rec;
    BEGIN
        LOOP
            FETCH p_pq_refcur INTO r;
            EXIT WHEN p_pq_refcur%NOTFOUND;

            SELECT TO_CHAR(SYSDATE,'HH24:MI:SS')
                 , s.sid
                 , pqs.serial#
                 , pqs.degree
                 , pqs.req_degree
            INTO   r_row_stats.start_timestr
                 , r_row_stats.sid
                 , r_row_stats.serial#
                 , r_row_stats.pq_actual_degree
                 , r_row_stats.pq_requested_degree
            FROM   ( SELECT sid FROM sys.v_$mystat WHERE rownum = 1 ) s
                   LEFT JOIN sys.v_$px_session pqs ON pqs.sid = s.sid;

            r_row_stats.thread_id := r.thread_id;
            r_row_stats.rowcount := p_pq_refcur%ROWCOUNT;
            r_row_stats.what := 'BEGIN ' || RTRIM(p_job_list(r.thread_id),';') || '; END;';
            DBMS_OUTPUT.PUT_LINE( r_row_stats.thread_id);
            BEGIN
                log_start(r.thread_id, r_row_stats.what);

                execute_command(r_row_stats.what);

                log_end(r.thread_id);

                PIPE ROW
                ( RPAD('sid=' || r_row_stats.sid || ' serial#=' || r_row_stats.serial# || ':',25) ||
                  'Degree of parallelism: requested '  || r_row_stats.pq_requested_degree ||
                  ', actual ' || r_row_stats.pq_actual_degree || ': ' ||
                  r_row_stats.start_timestr || ' - ' || TO_CHAR(SYSDATE,'HH24:MI:SS') );
            EXCEPTION
                WHEN OTHERS THEN
                    log_end(r_row_stats.thread_id, SQLERRM);
                    PIPE ROW('sid=' || r_row_stats.sid || ' serial#=' || r_row_stats.serial# || ': ' || SQLERRM);
            END;
        END LOOP;

--        IF r_row_stats.rowcount = 0 THEN
--            RAISE_APPLICATION_ERROR
--            ( -20000
--            , 'Cursor returned no rows' );
--        END IF;

        RETURN;
    END pq_submit;

     PROCEDURE submit
        ( p_job1 VARCHAR2
        , p_job2 VARCHAR2
        , p_job3 VARCHAR2
        , p_job4 VARCHAR2 )
    IS
        v_results VARCHAR2_TT;
    BEGIN
        SELECT /*+ PARALLEL(4) */ column_value 
        BULK COLLECT INTO v_results
        FROM TABLE(
             parallel_launch.pq_submit
             ( VARCHAR2_TT(p_job1,p_job2,p_job3,p_job4)
             , CURSOR(SELECT thread_id FROM pq_driver)
             )
        );

        IF v_results.COUNT = 0 THEN
            v_results.EXTEND;
            v_results(1) := 'parallel_launch.SUBMIT: No rows returned from table function PQ_SUBMIT';
        END IF;

        FOR i IN v_results.FIRST..v_results.LAST LOOP
            DBMS_OUTPUT.PUT_LINE(v_results(i));
        END LOOP;
    END submit;

 PROCEDURE test
    IS
    BEGIN
        submit
        ( 'parallel_launch.test_rs (CURSOR 
  (SELECT /*+ parallel (ALLT,4) */ table_name 
   from all_tables ALLT))'
        , 'parallel_launch.test_rs (CURSOR 
  (SELECT /*+ parallel (ALLT,4) */ table_name 
   from all_tab_columns ALLT))'
        ,'parallel_launch.test_rs (CURSOR 
  (SELECT /*+ parallel (ALLT,4) */ table_name 
   from all_tables ALLT))'
         , 'parallel_launch.test_rs (CURSOR 
  (SELECT /*+ parallel (ALLT,4) */ table_name 
   from all_tab_columns ALLT))' );
    END test;


    PROCEDURE slow_proc
        ( p_id INTEGER )
    IS
    BEGIN
        dbms_lock.sleep(2);
    END slow_proc;
    
    
 FUNCTION test_rs (p_sql IN SYS_REFCURSOR)
  RETURN inrec
  PIPELINED
 IS
 v_inrec inrec_type;
 
 BEGIN
  LOOP
   FETCH p_sql INTO v_inrec;

   EXIT WHEN p_sql%NOTFOUND;
  END LOOP;

  CLOSE p_sql;

  pipe row (v_inrec);
  RETURN;
 END;
     
END parallel_launch;
/
show errors
Thank you all,
Aj
  • 1. Re: PARALLEL_ENABLED with SQL subquery input.
    rp0428 Guru
    Currently Being Moderated
    >
    My purpose is not to have a PARALLEL hint in a select query but to have a function parallelly execute multiple select queries at the same time and PIPE out the result.
    >
    Then your purpose is flawed. A function, in and of itself, cannot execute select queries in parallel. A function call is part of a single session and the code in it executes serially.

    A function could launch multiple DBMS_SCHEDULER jobs and those jobs could execute in parallel. But the function itself would then have to collect the results and pipe them out and a query could probably gather the results just as easily.

    A client, for example Java, could call the same function multiple times with different parameters and have multiple copies of the function executing in parallel and then collect the results.

    But the function itself cannot execute queries in parallel.

    I suggest you stop focusing on the solution you want to implement and explain the problem you are trying to solve. Then we can help provide information about the different solutions that might be available to solve your problem.
  • 2. Re: PARALLEL_ENABLED with SQL subquery input.
    Aj09 Newbie
    Currently Being Moderated
    Hello rp0428,
    I appreciate your input. You yourself have accurately analysed the problum that I am trying to solve.

    I want to simulate exactly what Java does in your response. However, I wanted to try options before reaching out to the experts.

    Now to simulate what Java does using your solution, I have to use DBMS_SCHEDULER, I was able to spawn many jobs that could do DMLs but when it comes to SELECT queries I need a way to capture the output back (which matches your explaination as well). So how do I combine the PIPELINE feature with DBMS_SCHEDULER functionality.

    Can you kindly suggest me a sample code or a link where I can see an example.

    Inserting the output into a temp table will not help because, my front-end report can't call one function to run SELECTs + INSERT and then call a SELECT to fetch the output. Even if I take that approach, how would my report know that all jobs are done and the data is ready to be selected from that temp table. Kindly suggest a possibility.


    Thank you,
    Aj
  • 3. Re: PARALLEL_ENABLED with SQL subquery input.
    rp0428 Guru
    Currently Being Moderated
    >
    Can you kindly suggest me a sample code or a link where I can see an example.
    >
    Not without knowing what it is you are really trying to do.

    So far you are focused on the solution you want to use instead of explaining the problem you are trying to solve.

    What is it that you are trying to actually do? Why do you think you need a complicated parallel process to do it? What's wrong with using standard query to collect the data or even a parallel query?
  • 4. Re: PARALLEL_ENABLED with SQL subquery input.
    Aj09 Newbie
    Currently Being Moderated
    Sure, I have a PIPELINE function that needs to be called 3 times from the front end report (I dont have a Java layer, BO to ORACLE). In each of these function the SQL is built dynamically. For example, I need to call the same function tree times with different inputs and consolidate the result:

    test_pkg.output_function(2012);
    test_pkg.output_function(2011);
    test_pkg.output_function(2010);

    We have one table for each year (2012,2011,2010). Our tables have 6 mill records each. They are parititioned based on Organization Code. So I cant consolidate three table into one and create a partition on Year.
    Each Function call is taking 40 sec so three squential calls are taking 120sec. I cant create a view with a union of three tables, beause based on the input year and other inputs(org_code, city, state ..ect) the Fun determines the table and queries it.
    I need a way to spawn three calls at the same time and consolidate the result.

    Please let me know if I can provide any further info.

    Thank you,
    Aj
  • 5. Re: PARALLEL_ENABLED with SQL subquery input.
    rp0428 Guru
    Currently Being Moderated
    >
    I cant create a view with a union of three tables
    >
    You don't need to create a view with a UNION the function can create three SELECT queries and union them together.
    >
    , beause based on the input year and other inputs(org_code, city, state ..ect) the Fun determines the table and queries it.
    >
    I don't understand that part. If your input is just for one year value why would you query all three tables when each table is for a different year?

    Why can't you just call the function and let it query all three tables/years?

    And you provide this as sample code
    test_pkg.output_function(2012);
    test_pkg.output_function(2011);
    test_pkg.output_function(2010);
    That only shows one parameter being passed. It doesn't show org_code, city, state, etc). How many parameters are there? Is the 'year' parameter the only one that is different? Or are org_code, city, state, etc different for each of the years you query?

    What you posted is helping understand the problem but you still need to fill in some of the details. How about trying to state the problem in English? Something like 'When the user selects an org_code, city, state I want to return data (what data?) for years 2010, 2011 and 2012.

    Provide some sample parameters. Even if you think you need to call the function 3 times then provide sample parameters for each call
    1. call #1: year = 2012, org_code = xxx, city = 'abc', etc
    2. call #2: year = 2011, org_code = xxx, city = 'abc', etc

    Provide a sample of what the result data looks like.

    You need to do a high-level walk-through that shows the user input and the result without worrying yet about how you use that input to get the result. That comes later.


    From your previous posts you gave the impression that some client or other code is calling the function and passing the parameters. Where is that code at? Are you saying that one function call will use one set of parameters and the 2nd and 3rd calls different sets but you want one result set? Then you could still pass all three sets of parameters to the function and let it do the combining.
  • 6. Re: PARALLEL_ENABLED with SQL subquery input.
    Aj09 Newbie
    Currently Being Moderated
    Hi, Due to proprietary reasons I couldn’t expose the code earlier. However, below is the example.
    As I already mentioned, the front end calls the following select query 3 times for three diff years (in sequence), each table has 6 mill rows and performing a count takes 40 sec for one select to execute.
    Now, the report shows 3 graphs on one page. So it calls 3 selects hence the total is 40+40+40 = 120 sec.

    As per our new requirement we need to show 2009 and 2008 years data as well. So two more new tables have been created. Calling 5 select queries sequentially is taking 200 sec. Hence the challenge, how do I call 5 Pipe function at the same time and get the total result back in 40 sec. (We don’t have a Java layer) Please let me know if I can provide any further information.

    Thank you,
    Aj
    SELECT * FROM TABLE (db_ATG_yr_pkg.get_ATG_records ('JCITY','HOMEATG', '201210'  ) );   
    SELECT * FROM TABLE (db_ATG_yr_pkg.get_ATG_records ('ACITY','HOMEATG', '201110'  ) );   
    SELECT * FROM TABLE (db_ATG_yr_pkg.get_ATG_records ('BCITY','HOMEATG', '201010'  ) );   
    
    --After new requirement we are adding 
    SELECT * FROM TABLE (db_ATG_yr_pkg.get_ATG_records ('ACITY','HOMEATG', '200910'  ) );   
    SELECT * FROM TABLE (db_ATG_yr_pkg.get_ATG_records ('BCITY','HOMEATG', '200810'  ) );   
    CREATE OR REPLACE PACKAGE db_ATG_yr_pkg
    IS
     TYPE temprec_typ IS RECORD (gender_group VARCHAR2 (3), et_group VARCHAR2 (3)
                                ,period_year VARCHAR2 (10), PERIOD_MONTH VARCHAR2 (10)
                                ,emp_count NUMBER, total_temps NUMBER);
    
     TYPE temprecset IS TABLE OF temprec_typ;
    
     ------------------------------------------------------------------------------------------------------------------------------------------------------------------
     FUNCTION get_ATG_records ( 
      p_city_str IN VARCHAR2 DEFAULT 'JCITY',
      p_tab_name IN VARCHAR2 DEFAULT 'HOMEATG',
       p_period IN VARCHAR2 DEFAULT '201210')
      RETURN totalrecset
      PIPELINED
    -------------------------------------------------------------------------------------------------------------------------------------------------------------------
    END db_ATG_yr_pkg;
    /
    
    CREATE OR REPLACE PACKAGE BODY db_ATG_yr_pkg
    IS
     ----------------------------------------------------------------------------------------------------------
     FUNCTION get_ATG_records ( 
      p_city_str IN VARCHAR2 DEFAULT 'JCITY',
      p_tab_name IN VARCHAR2 DEFAULT 'HOMEATG',
       p_period IN VARCHAR2 DEFAULT '201210')
      RETURN totalrecset
      PIPELINED
     IS
      PRAGMA AUTONOMOUS_TRANSACTION;
    
      v_select_qry    LONG;
      v_city_str      VARCHAR2 (1000);
      p_rpt_year      VARCHAR2 (10);
      cursor_rowcount NUMBER;
      CurSOR1         SYS_REFCURSOR;
     BEGIN
     v_city_str :=   p_city_str;
      -- selecting only the year
      p_rpt_year      :=
       SUBSTR (p_period ,1,4);
      
      v_hdcnt_str := ' count(tot_count) ';
      v_totaltemps_str := ' count(temp_count) ';
      -- builing select query
      v_select_qry := 'Select  A.atg_GENDER_GROUP ,
                         A.atg_et_group,
               SUBSTR(a.atg_period,1,4) AS PERIOD_YEAR,
               SUBSTR( a.atg_period,5) AS PERIOD_MONTH,';
    
      v_select_qry := v_select_qry || v_hdcnt_str || ' as emp_count, ';
      v_select_qry := v_select_qry || v_totaltemps_str || ' as total_temps ';
    --determining table based on year  
    v_select_qry      :=  
         v_select_qry || ' FROM YR_TABLE' || p_tab_name || 'db_YR' || p_rpt_year || '  A  where atg_city = ' || CHR (39) || v_city_str ||CHR(39);
     -- grouping data
      v_select_qry      :=
       v_select_qry || ' group by  A.atg_GENDER_GROUP ,
                         A.atg_et_group,
               SUBSTR(a.atg_period,1,4) AS PERIOD_YEAR,
               SUBSTR( a.atg_period,5) AS PERIOD_MONTH,';
               
       -- opening cursor        
      OPEN CurSOR1 FOR v_select_qry;
    
      LOOP
       FETCH CurSOR1 INTO temp_rec;
    
       cursor_rowcount := CurSOR1%ROWCOUNT;
       EXIT WHEN CurSOR1%NOTFOUND;
       bo_data.EXTEND;
       bo_data (bo_data.COUNT)      :=
        ATGdb_obj (temp_rec.gender_group, temp_rec.et_group, temp_rec.PERIOD_MONTH
                  ,temp_rec.period_year, temp_rec.emp_count, temp_rec.total_temps);
    
       -- piping data
       PIPE ROW (temp_rec);
      END LOOP;
      
     EXCEPTION
      WHEN OTHERS THEN
       v_error := 'ERROR ' || SQLERRM;
       temp_rec.gender_group := v_error;
       PIPE ROW (temp_rec);
     END get_ATG_records;
    END db_ATG_yr_pkg;
    ----------------------------------------------------------------------------------------------------------
    /
    SHO ERR
  • 7. Re: PARALLEL_ENABLED with SQL subquery input.
    rp0428 Guru
    Currently Being Moderated
    >
    As I already mentioned, the front end calls the following select query 3 times for three diff years (in sequence), each table has 6 mill rows and performing a count takes 40 sec for one select to execute.
    Now, the report shows 3 graphs on one page. So it calls 3 selects hence the total is 40+40+40 = 120 sec.

    As per our new requirement we need to show 2009 and 2008 years data as well. So two more new tables have been created. Calling 5 select queries sequentially is taking 200 sec. Hence the challenge, how do I call 5 Pipe function at the same time and get the total result back in 40 sec. (We don’t have a Java layer) Please let me know if I can provide any further information.
    >
    Thanks for providing more information. That helps understand what you are trying to do.

    Based on what you said I would try to determine if that OLD data even really changes, and, if so, how often it changes.

    What you present seems like a classic use case for a report-ready table that has the data already rolled up to the grouping levels that you use for reports. Then you NEVER need to requery the old, static data; you just query the report-ready table.

    The data in the report ready table can be refreshed as a batch job periodically if need be. But how often does your 2008 or 2009 data really change? Do you really need to roll it up from the raw data for every user that requests it? Or can you just roll it up using a batch process and store the rolled up data in a report ready table?

    This often becomes a query of the current years (months, weeks, etc) data UNIONed with a query of the old static rolled-up data. So you would still use a function, pass the parameters and the function would query the current year data (if ask for) and roll it up. It would union that data with a query of the older static data.

    That would be if the function returns a REF CURSOR that the client uses to iterate the result set. If you use a pipeline function the function can start returning data as soon as it has data from either of the queries, old or new.

    Can you use a report-ready table approach?
  • 8. Re: PARALLEL_ENABLED with SQL subquery input.
    Aj09 Newbie
    Currently Being Moderated
    Hi, A little more background. Today, on the report we have City, Period and Tab as the filters selections. Moving forward we will be adding County, Zone, Age, Business Groups and more. So new columns will be added to these report-ready tables (we call them Report Summary tables. They refreshed everymonth) and the input parameters to the fun will also increse. Currently users select period and City. So the query group-clause will have city & node .. Moving forwards based on the combination of the filters selected on the report we will have to group by those col respectively.
    Note: Our Report Summary tables are in a way report-ready tables. They are aggregated from various warehouse tables. So 6mill rows in each table(each period table) are ready to be grouped by city ..etc .. tomorrow this grouping will expand.

    Now, Will 2008. 2009 tables change, if so how frequenty? They will change as we add new col. Right now these tables are ready to be grouped with City .. tomorrow the grouping will expand and allow us to group by Country, zone etc, So the frequency might be every month. But this just doesnt just apply to 2008 or 09 .. they will apply to all tables.

    Even if I use the approach to UNION queries, are you suggesting that I use parallel at query level ? and make the function add more union queries based on the periods (period range selected) ?

    So is there no feature in PL/SQL to simulate Java thread concept ? Isn't there a way to combine PIPE functions with DBMS_SCHEDULER and combine the results of multiple jobs ?

    Can I use DBMS_JOB to spawn 5 function calls .. and insert into a table and query that table when all the Job status turns to DONE or FINISHED ? Is that possible ? My Challenge here is Performance.

    Thank you,
    Aj
  • 9. Re: PARALLEL_ENABLED with SQL subquery input.
    Aj09 Newbie
    Currently Being Moderated
    HI rp0428,
    Any suggestions on my questions ? Kindly let me know.
    Thanks,
    Aj

Legend

  • Correct Answers - 10 points
  • Helpful Answers - 5 points