August 2018 update: please do feel encourage to read this and my other posts on table functions, but you will learn much more about table functions by taking my Get Started with PL/SQL Table Functions class at the Oracle Dev Gym. Videos, tutorials and quizzes - then print a certificate when you are done!
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 CONSTANT qdb_competitions%ROWTYPE
:= qdb_competition_mgr.one_competition (
competition_id_in) ;
c_freq_column CONSTANT VARCHAR2 (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_list2 CONSTANT qdb_config.maxvarchar2
:= ',
l_query qdb_config.maxvarchar2;
l_return ir_other_ranking_nt;
PROCEDURE trc (context_in IN VARCHAR2,
text_in IN VARCHAR2)
IS
BEGIN
qdb_utilities.trace_activity_force (
'ir_other_ranking_tf ' || context_in,
text_in);
END;
FUNCTION period_qualifier (if_not_lifetime_in IN VARCHAR2)
RETURN VARCHAR2
IS
BEGIN
RETURN CASE period_type_in
WHEN qdb_config.c_lifetime THEN NULL
ELSE if_not_lifetime_in
END;
END;
FUNCTION period_qualifier_col (
add_comma_in IN BOOLEAN DEFAULT TRUE)
RETURN VARCHAR2
IS
BEGIN
RETURN period_qualifier (
'mv.'
|| c_freq_column
|| CASE
WHEN add_comma_in THEN ','
ELSE NULL
END);
END;
FUNCTION detail_tables
RETURN VARCHAR2
IS
BEGIN
RETURN CASE category_in
WHEN 'organization_name'
THEN
' qdb_users cat'
END;
END;
FUNCTION detail_where
RETURN VARCHAR2
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);
END IF;
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 THEN NULL
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.
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.
:-)
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 CONSTANT qdb_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 IN VARCHAR2)
IS
BEGIN
qdb_utilities.trace_activity_force (
'ir_other_ranking_tf ' || context_in,
text_in);
END;
RETURN VARCHAR2
IS
BEGIN
RETURN CASE period_type_in
WHEN qdb_config.c_lifetime THEN NULL
ELSE if_not_lifetime_in
END;
END;
add_comma_in IN BOOLEAN DEFAULT TRUE)
RETURN VARCHAR2
IS
BEGIN
RETURN period_qualifier (
'mv.'
|| c_freq_column
|| CASE
WHEN add_comma_in THEN ','
ELSE NULL
END);
END;
RETURN VARCHAR2
IS
BEGIN
RETURN CASE category_in
WHEN 'organization_name'
THEN
' qdb_memberships mb, qdb_organizations cat '
ELSE' qdb_users cat'
END;
END;
RETURN VARCHAR2
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);
END IF;
'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 THEN NULL
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);
END IF;
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.)
EXECUTE IMMEDIATE l_query
BULK COLLECT INTO l_returnUSING competition_id_in,
competition_id_in,
competition_id_in;
EXCEPTION
WHEN OTHERSTHEN
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.
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.
:-)
Links to Table Function Series
Steven, came across this post and noticed the seemingly duplicate declaration of c_select_list1. Whilst the declaration can be 'overloaded' as soon as you reference this it should throw a PLS-00371.
ReplyDeleteI am probably lacking caffeine and fail to spot the obvious as I am just reading the code rather than compiling it....
Thanks, Chris, that is definitely a typo. Not sure how it crept in. Will fix now!
ReplyDeleteHi Steven
ReplyDeleteI have to say, this is a terrible way to try to teach, explain and illustrate a concept (here, table functions as parameterized views). It's something that is done time-to-time by other folks on t'interweb but it's the first time I've seen it done by your good self.
There is so much code in your example that the reader, unless a genius (perhaps like your good self or someone like Jonathan Lewis) is left struggling to discern or distill what the key points are.
I should say, for fear of inciting hatred, that I love your PL/SQL tutorials; this is the only one that I can remember in all the years that I've come across them that I'm left thinking, 'WT....' I think this is reflected in the lack of comments on this article.
Best regards
J
Thanks for writing, Jon (and sorry about the very delayed response).
DeleteMy only excuse is that I was showing code from a "real world example" - and they can be quite complicated.
I will update this post to direct readers to my new(er) class at the Dev Gym on table functions, which includes a set of tutorials in LiveSQL. I would very much appreciate it if you would check it out and let me know if they provide an easier, smoother way in to this subject.