So the public-facing side of my function is done. Time to dive inside! Of course, as with almost any piece of code, most of the details are very application-specific and not all that critical to the lesson at hand. So I will take liberties will code to focus in on the most relevant aspects, but I will also use this opportunity to suggest some ideas about how best to build such functions to maximize readability and make it easy to maintain.
First, the declaration section; what you see here are the usual constants (mostly, to execute "setup" functionality once and not repeat it) and variables. But then you see a number of nested subprograms. In case you were not aware, you can define functions and procedures within other functions and procedures.
For a table function that constructs a complex dynamic query, it was crucial for me to hide various construction steps behind a function interface to make it easier to see how the pieces fit together.
FUNCTION ir_other_ranking_tf (
category_in IN VARCHAR2,
period_type_in IN VARCHAR2,
competition_id_in IN INTEGER,
period_in IN VARCHAR2)
RETURN ir_other_ranking_nt
IS
c_competition CONSTANTqdb_competitions%ROWTYPE
:= qdb_competition_mgr.one_competition (
competition_id_in);
c_freq_column CONSTANTVARCHAR2(100)
:=CASE period_type_in
WHEN qdb_config.c_daily THEN'YYYY_DDD'
WHEN qdb_config.c_weekly THEN'YYYY_IW'
WHEN qdb_config.c_monthly THEN'YYYY_MM'
WHEN qdb_config.c_quarterly THEN'YYYY_Q'
WHEN qdb_config.c_yearly THEN'YYYY'
END;
c_select_list1 CONSTANT qdb_config.maxvarchar2
:=',
c_select_list1 CONSTANT qdb_config.maxvarchar2
:=',
l_query qdb_config.maxvarchar2;
l_return ir_other_ranking_nt;
PROCEDURE trc (context_in INVARCHAR2,
text_in INVARCHAR2)
IS
BEGIN
qdb_utilities.trace_activity_force (
'ir_other_ranking_tf '|| context_in,
text_in);
END;
FUNCTION period_qualifier (if_not_lifetime_in INVARCHAR2)
RETURNVARCHAR2
IS
BEGIN
RETURNCASE period_type_in
WHEN qdb_config.c_lifetime THENNULL
ELSE if_not_lifetime_in
END;
END;
FUNCTION period_qualifier_col (
add_comma_in INBOOLEANDEFAULTTRUE)
RETURNVARCHAR2
IS
BEGIN
RETURN period_qualifier (
'mv.'
|| c_freq_column
||CASE
WHEN add_comma_in THEN','
ELSENULL
END);
END;
FUNCTION detail_tables
RETURNVARCHAR2
IS
BEGIN
RETURNCASE category_in
WHEN'organization_name'
THEN
' qdb_users cat'
END;
END;
FUNCTION detail_where
RETURNVARCHAR2
IS
BEGIN
RETURN CASE period_type_in
WHEN qdb_config.c_lifetime
THEN
NULL
ELSE
period_qualifier_col (
add_comma_in =>FALSE)
||' = '''
|| period_in
||''' AND '
END
||CASE category_in
WHEN'organization_name'
THEN
END;
And now it's time to construct the query. Note that I include tracing both before and after query construction. I imagine it will be pretty obvious why: executing a dynamically-constructed statement isn't hard (I show that next). Putting together something as complicated as the query needed for this report is, on the other hand, a real treat; what's the change I will get it correct the first thirty or so iterations? Being able to see the string I put together is a big help.
I don't think it makes a whole lot of sense to go through the query construction in detail. The bottom line is that I was dealing with different materialized views (so: different name of view), and to some extent different names for columns in each view. I used the constants and nested functions (shown above) to avoid repetition and improve readability (perhaps just a little bit, anyway).
THEN
trc (
||'-'
|| competition_id_in
||'-'
|| c_freq_column
||'-'
|| period_in);
ENDIF;
l_query :=
WHEN qdb_config.c_lifetime THEN'''Lifetime'''
ELSE'mv.'|| c_freq_column
END
||', mv.'
|| category_in
|| c_select_list1
WHEN qdb_config.c_lifetime THENNULL
ELSE'mv.'|| c_freq_column ||','
END
||'cat.'
|| category_in
|| c_select_list2
||' mv,'
|| detail_tables ()
WHEN'company_name'
THEN
NULL
END
|| detail_where ()
||'cat.'
|| category_in
|| category_in
||' mv,'
|| detail_tables ()
|| category_in
|| category_in
||' mv, '
|| detail_tables ()
|| category_in
|| period_qualifier (
' AND top100.'
|| c_freq_column
||'(+) = mv.'
|| c_freq_column
||' AND top20.'
|| c_freq_column
||'(+) = mv.'
|| c_freq_column);
IF qdb_utilities.trace_is_on
THEN
USING competition_id_in,
competition_id_in,
competition_id_in;
RETURN l_return;
THEN
qdb_utilities.syslog_error (
So now you believe me, right? The implementation of a table function is....application-dependent. You are unlikely to build something exactly like this, but hopefully the approach I took (declaring lots of constants and nested subprograms to isolate repeated logic and improve readability) will offer you ideas on optimizing your own table function implementation.
Next: I just remembered that I have another table function in the PL/SQL Challenge backend that demonstrates the use case of programmatically constructing a dataset (as opposed to hiding a complex dynamically-constructed query).
Bet you can't wait to see it!
But now I must finish up my four hour workshop for the GLOC conference next week.
And write another quiz for the PL/SQL Challenge.
And start writing the first draft of my Project Solver Call to Action.
:-)
First, the declaration section; what you see here are the usual constants (mostly, to execute "setup" functionality once and not repeat it) and variables. But then you see a number of nested subprograms. In case you were not aware, you can define functions and procedures within other functions and procedures.
For a table function that constructs a complex dynamic query, it was crucial for me to hide various construction steps behind a function interface to make it easier to see how the pieces fit together.
FUNCTION ir_other_ranking_tf (
category_in IN VARCHAR2,
period_type_in IN VARCHAR2,
competition_id_in IN INTEGER,
period_in IN VARCHAR2)
RETURN ir_other_ranking_nt
IS
c_competition CONSTANTqdb_competitions%ROWTYPE
:= qdb_competition_mgr.one_competition (
competition_id_in);
:=CASE period_type_in
WHEN qdb_config.c_daily THEN'YYYY_DDD'
WHEN qdb_config.c_weekly THEN'YYYY_IW'
WHEN qdb_config.c_monthly THEN'YYYY_MM'
WHEN qdb_config.c_quarterly THEN'YYYY_Q'
WHEN qdb_config.c_yearly THEN'YYYY'
END;
c_select_list1 CONSTANT qdb_config.maxvarchar2
:=',
"Player Count" ,
NVL (top20.top20, 0) ,
NVL (top100.top100, 0) ,
"Score" ,
"Weighted Score" ,
"Pct Correct" ,
qdb_utilities.seconds_to_duration (answer_time, 0),
"Max Score" ,
"Max Weighted Score" ,
"Quizzes Taken" ,
"% Taken" ,
"Score per Quiz" ,
RANK () OVER (
ORDER BY "Weighted Score" DESC, answer_time ASC) ,
ORDER BY "Weighted Score" DESC, answer_time ASC) ,
RANK () OVER (
ORDER BY "Score per Quiz" DESC, answer_time ASC))';
ORDER BY "Score per Quiz" DESC, answer_time ASC))';
:=',
COUNT (*) "Player Count" ,
SUM (user_score) "Score" ,
SUM (user_wscore) "Weighted Score" ,
ROUND (SUM (pct_correct_answers) /
COUNT (*), 1) "Pct Correct" ,
COUNT (*), 1) "Pct Correct" ,
SUM (user_seconds) answer_time ,
SUM (best_possible_score) "Max Score" ,
SUM (best_possible_wscore) "Max Weighted Score" ,
SUM (total_quizzes) "Quizzes Taken" ,
ROUND (SUM (total_quizzes) /
SUM (potential_quizzes) * 100) "% Taken" ,
SUM (potential_quizzes) * 100) "% Taken" ,
ROUND (SUM (user_wscore) /
SUM (total_quizzes)) "Score per Quiz"';
SUM (total_quizzes)) "Score per Quiz"';
l_return ir_other_ranking_nt;
text_in INVARCHAR2)
IS
BEGIN
qdb_utilities.trace_activity_force (
'ir_other_ranking_tf '|| context_in,
text_in);
END;
RETURNVARCHAR2
IS
BEGIN
RETURNCASE period_type_in
WHEN qdb_config.c_lifetime THENNULL
ELSE if_not_lifetime_in
END;
END;
add_comma_in INBOOLEANDEFAULTTRUE)
RETURNVARCHAR2
IS
BEGIN
RETURN period_qualifier (
'mv.'
|| c_freq_column
||CASE
WHEN add_comma_in THEN','
ELSENULL
END);
END;
RETURNVARCHAR2
IS
BEGIN
RETURNCASE category_in
WHEN'organization_name'
THEN
' qdb_memberships mb, qdb_organizations cat '
ELSE' qdb_users cat'
END;
END;
RETURNVARCHAR2
IS
BEGIN
RETURN CASE period_type_in
WHEN qdb_config.c_lifetime
THEN
NULL
ELSE
period_qualifier_col (
add_comma_in =>FALSE)
||' = '''
|| period_in
||''' AND '
END
||CASE category_in
WHEN'organization_name'
THEN
' mv.user_id = mb.user_id AND
mb.organization_id =
mb.organization_id =
cat.organization_id '
ELSE ' mv.user_id = cat.user_id '
END;END;
And now it's time to construct the query. Note that I include tracing both before and after query construction. I imagine it will be pretty obvious why: executing a dynamically-constructed statement isn't hard (I show that next). Putting together something as complicated as the query needed for this report is, on the other hand, a real treat; what's the change I will get it correct the first thirty or so iterations? Being able to see the string I put together is a big help.
I don't think it makes a whole lot of sense to go through the query construction in detail. The bottom line is that I was dealing with different materialized views (so: different name of view), and to some extent different names for columns in each view. I used the constants and nested functions (shown above) to avoid repetition and improve readability (perhaps just a little bit, anyway).
BEGIN
IF qdb_utilities.trace_is_onTHEN
trc (
' period_type, competition, c_freq_column, period',
period_type_in||'-'
|| competition_id_in
||'-'
|| c_freq_column
||'-'
|| period_in);
ENDIF;
'SELECT ir_other_ranking_ot('
||CASE period_type_inWHEN qdb_config.c_lifetime THEN'''Lifetime'''
ELSE'mv.'|| c_freq_column
END
||', mv.'
|| category_in
|| c_select_list1
||' FROM (SELECT '
||CASE period_type_inWHEN qdb_config.c_lifetime THENNULL
ELSE'mv.'|| c_freq_column ||','
END
||'cat.'
|| category_in
|| c_select_list2
||' FROM mv_qdb_rankings_'
|| period_type_in||' mv,'
|| detail_tables ()
||' WHERE competition_id = :competition_id_in AND '
||CASE category_inWHEN'company_name'
THEN
' cat.company_name IS NOT NULL AND '
ELSENULL
END
|| detail_where ()
||' GROUP BY '
|| period_qualifier_col ()||'cat.'
|| category_in
||') mv , (SELECT '
|| period_qualifier_col ()|| category_in
||',COUNT (*) top100 FROM mv_qdb_rankings_'
|| period_type_in||' mv,'
|| detail_tables ()
||' WHERE overall_rank <= 100 AND '
|| detail_where () ||' AND competition_id = :competition_id_in GROUP BY '
|| period_qualifier_col ()|| category_in
||') top100 , (SELECT '
|| period_qualifier_col ()|| category_in
||', COUNT (*) top20 FROM mv_qdb_rankings_'
|| period_type_in||' mv, '
|| detail_tables ()
||' WHERE overall_rank <= 20 AND '
|| detail_where () ||' AND competition_id = :competition_id_in GROUP BY '
|| period_qualifier_col ()|| category_in
||') top20 WHERE top100.'
|| category_in ||'(+) = mv.'
|| category_in ||' AND top20.'
|| category_in ||'(+) = mv.'
|| category_in|| period_qualifier (
' AND top100.'
|| c_freq_column
||'(+) = mv.'
|| c_freq_column
||' AND top20.'
|| c_freq_column
||'(+) = mv.'
|| c_freq_column);
THEN
trc (' Other Ranking query', l_query);
ENDIF;OK, and now it's time to execute the query, collect the result set with a BULK COLLECT clause, and then send it out to the invoking SELECT.
And, of course, if something goes wrong, I send the value of l_query to the error log, so I can eyeball it and - hopefully - quickly identify the dumb mistake I made (left out a comma, didn't leave a space between keywords in the query, etc.)
EXECUTEIMMEDIATE l_query
BULKCOLLECTINTO l_returnUSING competition_id_in,
competition_id_in,
competition_id_in;
EXCEPTION
WHENOTHERSTHEN
qdb_utilities.syslog_error (
'ir_other_ranking_tf',
'Failure executing dynamic query',
l_query); RAISE;
END;
So now you believe me, right? The implementation of a table function is....application-dependent. You are unlikely to build something exactly like this, but hopefully the approach I took (declaring lots of constants and nested subprograms to isolate repeated logic and improve readability) will offer you ideas on optimizing your own table function implementation.
Next: I just remembered that I have another table function in the PL/SQL Challenge backend that demonstrates the use case of programmatically constructing a dataset (as opposed to hiding a complex dynamically-constructed query).
Bet you can't wait to see it!
But now I must finish up my four hour workshop for the GLOC conference next week.
And write another quiz for the PL/SQL Challenge.
And start writing the first draft of my Project Solver Call to Action.
:-)