<?xml version='1.0' encoding='UTF-8'?><?xml-stylesheet href="http://www.blogger.com/styles/atom.css" type="text/css"?><feed xmlns='http://www.w3.org/2005/Atom' xmlns:openSearch='http://a9.com/-/spec/opensearchrss/1.0/' xmlns:georss='http://www.georss.org/georss' xmlns:gd='http://schemas.google.com/g/2005' xmlns:thr='http://purl.org/syndication/thread/1.0'><id>tag:blogger.com,1999:blog-8760686654156942303</id><updated>2011-11-16T14:53:00.222-08:00</updated><title type='text'>Banner Reporting</title><subtitle type='html'>This blog is devoted to the nuances and tips/tricks that I've learned over the past 5 years of reporting against SunGard HE's Banner ERP system. I am hopeful it will give people struggling to extract data from Banner an edge and while it is written from the perspective of a technical individual, I hope that people from all administrative and academic areas will benefit from the information included here. Happy hunting!</subtitle><link rel='http://schemas.google.com/g/2005#feed' type='application/atom+xml' href='http://bannerreporting.blogspot.com/feeds/posts/default'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8760686654156942303/posts/default?max-results=100'/><link rel='alternate' type='text/html' href='http://bannerreporting.blogspot.com/'/><link rel='hub' href='http://pubsubhubbub.appspot.com/'/><author><name>Zach Heath</name><uri>http://www.blogger.com/profile/09253162165797370040</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><generator version='7.00' uri='http://www.blogger.com'>Blogger</generator><openSearch:totalResults>11</openSearch:totalResults><openSearch:startIndex>1</openSearch:startIndex><openSearch:itemsPerPage>100</openSearch:itemsPerPage><entry><id>tag:blogger.com,1999:blog-8760686654156942303.post-7099472195010138894</id><published>2011-01-13T22:03:00.000-08:00</published><updated>2011-01-13T22:03:50.853-08:00</updated><title type='text'>Oracle's Data Dictionary: The Holy Grail</title><content type='html'>Well, I just read through my blog, and it appears that I have gone woefully astray.&amp;nbsp; I promised (in September of 2009...ouch!) to provide a monthly series called Banner tips of the month.&amp;nbsp; Alas, my goal has been completely forgotten, never fulfilled.&amp;nbsp; So, here I am again to see what I can do to remedy the situation!&amp;nbsp; Starting off, with January's "tip of the month" is a post on using Oracle's Data Dictionary.&lt;br /&gt;&lt;h3&gt;Terms and Definitions&lt;/h3&gt;It surprises me when I go visit a client and their analysts haven't heard or rarely use Oracle's Data Dictionary (ODD).&amp;nbsp; Wikipedia defines a data dictionary as a "centralized repository of information about data such as meaning, relationships to other data, origin, usage, and format." (http://en.wikipedia.org/wiki/Data_dictionary)&amp;nbsp; Another important term commonly used in conjunction with data dictionary is "meta data".&amp;nbsp; Meta data is simply data or information about data.&amp;nbsp; One of the most commonly used forms of meta data in the world today is the a library's card catalog, where information about the entire library system inventory can be &lt;b&gt;found&lt;/b&gt;, &lt;b&gt;categorized&lt;/b&gt;, and &lt;b&gt;searched&lt;/b&gt;.&amp;nbsp; Those three words (especially the last) are critical to a good data dictionary.&amp;nbsp; In any case, back to the main point: using the ODD, we can quickly discover many interesting things about our Oracle database.&amp;nbsp; But more important than learning about the database itself, is the fact that we can glean valuable information about our Banner system from the ODD!&lt;br /&gt;&lt;br /&gt;Here is a short, real-world, example.&amp;nbsp; I received an email today (the inspiration for this post in fact), requesting any information I may have regarding a function that may help him utilize their CAPP setup more effectively.&amp;nbsp; After thinking about it for a few minutes, I started typing an email to him indicating that I really didn't have much to aid to give in regards to his specific question.&amp;nbsp; However, after thinking about it a second more, I thought I'd do a quick search for some possible solutions using.....you guessed it, the ODD on my Banner 8 installation!&amp;nbsp; After 5 minutes spent querying the dictionary, I found several possible leads to the answer for which he was searching!&amp;nbsp; So, you're probably wondering, "what was it he was looking for!?".&amp;nbsp; Alas, I'm not going to spoil the surprise.&amp;nbsp; I will, however, give you several useful queries and tidbits about the ODD that may come in handy in your own searches along the way.&lt;br /&gt;&lt;br /&gt;First, more specifics &lt;span style="font-size: small;"&gt;on the O&lt;/span&gt;DD.&amp;nbsp; The ODD is a series or Oracle &lt;a href="http://en.wikipedia.org/wiki/View_%28database%29"&gt;views&lt;/a&gt; that contain data about the oracle system.&amp;nbsp; This includes data on all of the views, functions, indexes, packages, tables, procedures, users, and any other database related objects you may care to know about in your system.&amp;nbsp; So, if we adapt this concept to banner, when we install banner on oracle, the ODD then collects information about all of banner's tables, functions, views, and so on.&amp;nbsp; So, the next logical step would be to query those ODD views to find the information we need about those stored objects.&amp;nbsp; So, here we go.&lt;br /&gt;&lt;h3&gt;Querying the ODD&lt;/h3&gt;The first thing we need to know is the name(s) of the ODD view(s) we are going to query.&amp;nbsp; For a complete list, you can visit http://www.oracle.com/pls/tahiti/tahiti.catalog_views.&amp;nbsp; The ones I use most often include the following:&lt;br /&gt;&lt;ul&gt;&lt;li&gt;all_tables&lt;/li&gt;&lt;li&gt;all_views&lt;/li&gt;&lt;li&gt;all_objects&lt;/li&gt;&lt;li&gt;all_source&lt;/li&gt;&lt;li&gt;all_columns&lt;/li&gt;&lt;li&gt;all_col_comments&lt;/li&gt;and&lt;li&gt;all_tab_comments&lt;/li&gt;&lt;/ul&gt;Interesting that they all start with "all".  There is, in fact, a reason for that, but it's not too important for the purpose of this discussion.  If you're interested, you can read more about the ODD at the site I linked at the start of this paragraph.  In any case, let's look at a reason you may want to query some of these views.&lt;br /&gt;&lt;br /&gt;Let's consider a situation in which you don't know where a piece of data resides within the database (or perhaps IF it even exists at all).  The first logical place to look would be all_tables OR all_tab_comments.  The reason you would pick all_tab_comments is because you may not know what the name of the column or table is so you can't check all_tables.  So, let's say we're looking for a column that contains data about transcripts.  Here is a simple query that would return some interesting facts about tables that pertain to transcripts:&lt;br /&gt;&lt;code&gt;&lt;br /&gt;select owner&lt;br /&gt;,table_name&lt;br /&gt;,comments&lt;br /&gt;from all_tab_comments&lt;br /&gt;where lower(comments) like '%transcript%';&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;Now, I'm not kidding when I say that query (on my system) returned no fewer than &lt;b&gt;37&lt;/b&gt; tables that had comments related to transcripts.  However, notice that we also have an owner column as well as table_name and comments.  So we could refine the query a little more if we knew or could guess something about what we're looking for.  For example, with transcripts, there's a good bet that we only want STUDENT tables, so we could filter on the owner (SATURN) or on the table name (like 'S%' as follows:&lt;br /&gt;&lt;code&gt;&lt;br /&gt;select owner&lt;br /&gt;,table_name&lt;br /&gt;,comments&lt;br /&gt;from all_tab_comments&lt;br /&gt;where lower(comments) like '%transcript%'&lt;br /&gt;and table_name like 'S%'&lt;br /&gt;and owner = 'SATURN';&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;Now, this was a little redundant, I know, but hopefully it got the point across!&lt;br /&gt;So, as you can see, using the ODD to find data location is very handy!  Another common practice that is VERY useful and VERY RARELY used is using the data dictionary to find functions and procedures to aid in your reporting and application development.  SunGard has spent a great deal of money (well, SCT did really) developing the Banner product and it's myriad of PL/SQL APIs, we may as well use them!  Here is a query that could help you find a function to calculate age:&lt;br /&gt;&lt;code&gt;&lt;br /&gt;select owner, object_name, object_type&lt;br /&gt;from all_objects&lt;br /&gt;where object_type = 'FUNCTION'&lt;br /&gt;and lower(object_name) like '%age%';&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;On my system, this query resulted in 8 different functions with the letters 'age' somewhere in the name of the function.  However, only 1 function was called: F_CALCULATE_AGE.  Now, that only tells us the name of the function.  If we wanted to see it's guts to determine if it's something we can use, we could write the following query:&lt;br /&gt;&lt;code&gt;&lt;br /&gt;select line, text&lt;br /&gt;from all_source&lt;br /&gt;where name = 'F_CALCULATE_AGE'&lt;br /&gt;and type = 'FUNCTION'&lt;br /&gt;order by line;&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;This query results in two columns, one that contains the line number for every line in the function code and the other column contains the actual text for the function itself.  This feature is invaluable when trying to determine if the function will be useful for whatever problem you're trying to solve.&lt;br /&gt;&lt;br /&gt;So, the last thing I want to leave you with is an question.&amp;nbsp; What if we were to build a DataBlock or two in Argos that "parameterized" a few of these queries and made them available to some of our power users?&amp;nbsp; Happy Hunting!!!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8760686654156942303-7099472195010138894?l=bannerreporting.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://bannerreporting.blogspot.com/feeds/7099472195010138894/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://bannerreporting.blogspot.com/2011/01/oracles-data-dictionary-holy-grail.html#comment-form' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8760686654156942303/posts/default/7099472195010138894'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8760686654156942303/posts/default/7099472195010138894'/><link rel='alternate' type='text/html' href='http://bannerreporting.blogspot.com/2011/01/oracles-data-dictionary-holy-grail.html' title='Oracle&apos;s Data Dictionary: The Holy Grail'/><author><name>Zach Heath</name><uri>http://www.blogger.com/profile/09253162165797370040</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8760686654156942303.post-111269203992930152</id><published>2010-06-01T16:53:00.000-07:00</published><updated>2010-06-01T16:53:44.733-07:00</updated><title type='text'>Report Conversion: X to Argos</title><content type='html'>Well, it's only been a month since my last posting.&amp;nbsp; That's not great (I'm hoping to get it down to a couple weeks or so), but it's better than it was before.&amp;nbsp; And honestly, I have an excuse; I got married!&amp;nbsp; Crazy, I know, but I can't help it.&amp;nbsp; My wife (weird) is the coolest person in the world.&amp;nbsp; I'm a lucky guy.&amp;nbsp; Anyway, onto business.&lt;br /&gt;&lt;br /&gt;Presumably, you've purchased Argos and are preparing to migrate legacy reports from your legacy system for use in your new reporting tool.&amp;nbsp; This is excellent news!&amp;nbsp; The first opportunity presented to us as a result of this process is a chance to take an inventory of your legacy reports and to DEPRECATE (yes, I said it) reports that are no longer used or will become obsolete with your new system.&amp;nbsp; As you can imagine, if not handled well, this can be a very difficult process for some people. So, before beating down the doors of your office mates and telling them their reports are going away in 3 months, take a deep breath and simply make a list of everything that exists in your legacy system, when it was last run, how often it's run, and whether it needs current data or can be run against a data warehouse (if available).&amp;nbsp; Organize them by area of ownership/responsibility (such as Records, Admissions, Financial Aid, Finance, etc), then organize a meeting with the decision makers in each of those areas to prioritize their list. That meeting will be your first opportunity to broach the subject of deprecation.&amp;nbsp; By the way, I highly recommend NOT using the word deprecate (or any form of it).&amp;nbsp; Gently encourage each of your clients to recognize the fact that they have not executed or requested report x for 6 months and that they say they're supposed to be running it once a week.&amp;nbsp; Ask them if they still need it and if the answer is "Yes", ask them why (again, gently...try not to sound like you're accusing them of anything).&amp;nbsp; Finally, make sure that they understand that "that's the way we've always done it" is not an acceptable answer.&lt;br /&gt;&lt;br /&gt;After you've eliminated as many unnecessary reports as possible, it's time to prioritize.&amp;nbsp; Luckily, this doesn't really fall on you (generally speaking).&amp;nbsp; Work with your clients again to determine which reports are most important and, where possible, set reasonable deadlines.&amp;nbsp; Now comes the fun part, and the second most important aspect of your conversion: consolidation.&amp;nbsp; One of Argos' key strengths is it's ability to combine similar requests into a single object, called a DataBlock.&amp;nbsp; Imagine a situation in which your first four reports relate to student information.&amp;nbsp; If you were to encapsulate the data required for said reports into a single DataBlock, then set your report writers onto that DataBlock, you've just reduced your overall workload by 75%!&amp;nbsp; You're probably grumbling to yourself "well...how often does that actually happen."&lt;br /&gt;&lt;br /&gt;I was recently working with a client whose anticipate conversion load was around 450 legacy reports.&amp;nbsp; After elimination, prioritization, and consolidation, I reduced the expected number of DataBlocks to 78.&amp;nbsp; And this is generally how things go!&amp;nbsp; Seeing a reduction in expected output of 50% or more is pretty typical!&amp;nbsp; So, you might be asking yourself "how" one can consolidate to such a degree.&amp;nbsp; Firstly, it has to do with technology.&amp;nbsp; Generally speaking, older reporting tools were designed to facilitate one report per "designable object" (what we refer to as a DataBlock).&amp;nbsp; As such, it meant expansive menus and large amounts of development resources.&amp;nbsp; BI tools continue to evolve and are now smarter than ever.&amp;nbsp; That said, when I work to consolidate like reports, I stick with a concept commonly referred to as "level of granularity".&amp;nbsp; By this, I simply mean the level at which you do not want duplicate records to appear.&amp;nbsp; Some common examples include: student, course, financial transaction, financial account, financial aid award, and prospect.&amp;nbsp; Each of these items has certain attributes that can be reported and often are reported in situations in which you only want one record per item in your report.&amp;nbsp; That is your "level of granularity".&amp;nbsp; Here's an example.&amp;nbsp; Let's say one of your reports has to do with students majoring in Psychology.&amp;nbsp; The report needs the students name, id, and email address.&amp;nbsp; Obviously, you don't want more than one row per student, so this is a case where the "student" is your level of granularity.&amp;nbsp; Now, all we have to do is figure out how to achieve that level and then "bolt on" the appropriate attributes (in this case, ID, name, and email).&amp;nbsp; It's that simple!&lt;br /&gt;&lt;br /&gt;Alright, I think that's enough for now.&amp;nbsp; This post should have given you some ideas on what I consider the most important aspects of report conversion.&amp;nbsp; This, in conjunction with my post about development best practices, should put you well on your way to solving all the worlds problems. Happy Hunting!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8760686654156942303-111269203992930152?l=bannerreporting.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://bannerreporting.blogspot.com/feeds/111269203992930152/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://bannerreporting.blogspot.com/2010/06/report-conversion-x-to-argos.html#comment-form' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8760686654156942303/posts/default/111269203992930152'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8760686654156942303/posts/default/111269203992930152'/><link rel='alternate' type='text/html' href='http://bannerreporting.blogspot.com/2010/06/report-conversion-x-to-argos.html' title='Report Conversion: X to Argos'/><author><name>Zach Heath</name><uri>http://www.blogger.com/profile/09253162165797370040</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8760686654156942303.post-2035966362098265178</id><published>2010-04-28T15:22:00.000-07:00</published><updated>2010-04-28T15:22:34.983-07:00</updated><title type='text'>Create a Function Library - Decrease Intellectual Angst and Increase Efficiency</title><content type='html'>Just like your typical code monkey, I'm constantly looking for ways to reduce my required intellectual output while increasing my overall efficiency. For example, right now, I'm walking on a treadmill, listening to my wedding rehearsal dinner play list to make sure it's all the same feel/volume (aka sans cuss words:)), and updating a blog that I haven't written on in 4 months (I'm very sorry).&lt;br /&gt;&lt;br /&gt;As far as coding goes, I have been doing a lot of work with schools who are moving with increasing speed and fervor toward data-based decision making. This requires a much more analytical approach to reporting than your typical "students by major" list, which, in turn requires more database resources and involves a great deal more "logic" with regard to your institution's business rules (often the hardest part). To help aid me in this transition, I have started to compile a library of functions. For the most part, the functions are relatively simple, I just add one whenever I need to answer a new question. For example, I was recently asked by a client what percentage of their new freshmen this fall enrolled full time. My old "hack" self would have sat down at a computer for a couple of hours and written the necessary query to gather that information. BUT WAIT! This time, I wrote a database function that took as parameters term, student type, and time status.  Now, to get my pct, this is the query:&lt;br /&gt;&lt;br /&gt;select f_enrl_count('201010','N','FT') FT_NewFreshmen,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; f_enrl_count('201010','N','%') Total_NewFreshmen,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; f_enrl_count('201010','N','FT')/&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; f_enrl_count('201010','N','%')*100 Pct_New_FT&lt;br /&gt;&amp;nbsp; from dual;&lt;br /&gt;&lt;br /&gt;It's now that simple!&amp;nbsp; Not only is the query simplified, the overall results come back quicker because I am using pre-compiled code and I can use it from any query tool that connects to this database!&amp;nbsp; Functions can be combined too.&amp;nbsp; For example, if you're asked the percentage question a lot and you're tired of writing the query above, you could encapsulate that code in a function called f_enrl_pct that would look like this:&lt;br /&gt;&lt;br /&gt;create or replace function&lt;br /&gt;f_enrl_pct(in_term varchar2, in_styp varchar2, in_time_status varchar2)&lt;br /&gt;return number as&lt;br /&gt;&lt;br /&gt;&amp;nbsp;v_output number;&lt;br /&gt;&lt;br /&gt;begin&lt;br /&gt;&lt;br /&gt;select round(f_enrl_count(in_term,in_styp,in_time_status)/&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; f_enrl_count(in_term,in_styp,'%')*100,2) into v_output&lt;br /&gt;&amp;nbsp; from dual;&lt;br /&gt;&lt;br /&gt;return v_output;&lt;br /&gt;end;&lt;br /&gt;&lt;br /&gt;Now when you want that percentage, all you have to do is write the following:&lt;br /&gt;&lt;br /&gt;select f_enrl_pct('201010','N','FT') from dual&lt;br /&gt;&lt;br /&gt;and WALAH, you're done!&amp;nbsp; How sweet is that.&amp;nbsp; Now, you're probably chomping at the bit for the code behind the enrollment function.&amp;nbsp; Unfortunately, because of the variety of architectures (Baseline, ODS, EDW, home grown data marts), I would rather not post the code here.&amp;nbsp; However, if you're really interested post a comment and I'll think about sharing:).&amp;nbsp; Also, if you have developed some neat functions and would like to share, I welcome your comments there as well!&amp;nbsp; Anyway, the moral of today's story is two fold:&lt;br /&gt;&lt;br /&gt;1. Writing a blog entry while on the treadmill takes way longer than it's worth&lt;br /&gt;2. Creating a function library may be the smartest thing you ever do as a sql programmer&lt;br /&gt;&lt;br /&gt;As a side note, functions can be used for non analytical reports as well! More on that later...:)&lt;br /&gt;&lt;br /&gt;Happy Hunting!!!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8760686654156942303-2035966362098265178?l=bannerreporting.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://bannerreporting.blogspot.com/feeds/2035966362098265178/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://bannerreporting.blogspot.com/2010/04/create-function-library-decrease.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8760686654156942303/posts/default/2035966362098265178'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8760686654156942303/posts/default/2035966362098265178'/><link rel='alternate' type='text/html' href='http://bannerreporting.blogspot.com/2010/04/create-function-library-decrease.html' title='Create a Function Library - Decrease Intellectual Angst and Increase Efficiency'/><author><name>Zach Heath</name><uri>http://www.blogger.com/profile/09253162165797370040</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8760686654156942303.post-496393184428424185</id><published>2009-12-22T15:26:00.000-08:00</published><updated>2009-12-22T15:26:44.205-08:00</updated><title type='text'>Argos - Where to Code your Business Logic</title><content type='html'>If you're anything like me, your first reaction was "wait, isn't 'business logic' an oxymoron!?".  I suppose sometimes it is, but in this case, we're dealing with a bit of a different issue. In this case, business logic refers to the ever present technical representation of business process within the tools we use for reporting.  For example, we might want to be able to report students who have taken a remedial course.  An indicator must be created or found in Banner that will identify those students.  The location of the logic that will be responsible for identifying those students is the topic of this posting.  Because I have the most experience with the Argos reporting tool, I will be using it as an example, but I am positive the same concepts will apply to other tools. In general the goals helping guide this discussion are as follows:&lt;br /&gt;&lt;br /&gt;- Increase Efficiency (both technically and functionally)&lt;br /&gt;- Increase Accuracy&lt;br /&gt;- Maintain Flexibility&lt;br /&gt;&lt;br /&gt;Each of the areas that can "house" business logic will be evaluated using these three criteria. Now that that's out of the way, lets talk terminology. In Argos, there are three main arenas that can be used to "contain" business logic: Reports, DataBlocks, and the Database (in the form of stored functions and procedures). Generally speaking, as your organization matures, you'll want to move further away from storing your business logic at the report level.  &lt;br /&gt;&lt;a href="http://3.bp.blogspot.com/_uwm8xA2XiTg/SzFS-MSHoiI/AAAAAAAAAEI/bPxEdV8V1K8/s1600-h/MaturityChart.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" src="http://3.bp.blogspot.com/_uwm8xA2XiTg/SzFS-MSHoiI/AAAAAAAAAEI/bPxEdV8V1K8/s320/MaturityChart.jpg" /&gt;&lt;/a&gt;&lt;br /&gt;The three goals stated above are distinctly effected the further from the DataBase you are.  For example:&lt;br /&gt;&lt;br /&gt;Let's say we're trying to report current, clerical employees.  Well, it wouldn't be difficult to write a query that retreived all employees:&lt;br /&gt;&lt;br /&gt;select pebempl_pidm&lt;br /&gt;&amp;nbsp;&amp;nbsp;from pebempl;&lt;br /&gt;&lt;br /&gt;Not bad! From a DataBlock design standpoint, this is really easy! However, that puts the responsability for finding current clerical employees on the Report writer, forcing them to add whatever conditions identify such employees at the report level.  For the sake of argument, let's assume that an employee class equal to 'CL' and a status of 'A' indicate a current, clerical employee.  This means the report writer would have to hard code those conditions into the report object to get the appropriate information.&lt;br /&gt;&lt;br /&gt;Now, imagine there are 50 different employee related reports.  Each one of those reports must now be maintained and filtered according to whatever subgroup of employees the report writer is looking for. Each of those reports is now serving a single purpose.  Additionally, the logic loses a performance edge as it has to be run each time the individual reports are invoked, which doesn't allow the code to take advantage of some of Oracle's built in performance engines (buffer, cache, etc).&lt;br /&gt;&lt;br /&gt;So, let's imagine removing that logic from the report level and giving the user a toggle in the datablock that indicates if they want current clerical employees or not.  This is good because we can then use a modified query:&lt;br /&gt;&lt;br /&gt;select pebempl_pidm&lt;br /&gt;  from pebempl&lt;br /&gt; where pebempl_ecls_code = :ecls&lt;br /&gt;   and pebempl_status = :status;&lt;br /&gt;&lt;br /&gt;This now gives the report writer further flexibility, allowing them to choose different options with which to run a single report; a single report "template" can be used for any combination of status and employee class!  This is quite a bit more flexible (as you can see), however, you're still lacking some of the performance benefits of server side code, so the final step would be to move the logic to the database in the form of a stored function! In that case, the query in the DataBlock might look like this:&lt;br /&gt;&lt;br /&gt;select pebempl_pidm&lt;br /&gt;  from pebempl&lt;br /&gt; where f_include_employee(pebempl_pidm,'flag1','flag2') = 'Y';&lt;br /&gt;&lt;br /&gt;In this case, the function f_include_employee() does all the work and simply returns a Y or N to let the system know whether to include the employee or not.  This is extensible, reusable code that only has to be written once.  Additionally, it is pre-compiled and stored in oracle so it fully utilizes oracle's performance enhancing rules! The code inside the function is completely up to the institution and can contain whatever logic you wish! If you'd like an example of some functions that may be useful, let me know! Happy Hunting!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8760686654156942303-496393184428424185?l=bannerreporting.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://bannerreporting.blogspot.com/feeds/496393184428424185/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://bannerreporting.blogspot.com/2009/12/argos-where-to-code-your-business-logic.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8760686654156942303/posts/default/496393184428424185'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8760686654156942303/posts/default/496393184428424185'/><link rel='alternate' type='text/html' href='http://bannerreporting.blogspot.com/2009/12/argos-where-to-code-your-business-logic.html' title='Argos - Where to Code your Business Logic'/><author><name>Zach Heath</name><uri>http://www.blogger.com/profile/09253162165797370040</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://3.bp.blogspot.com/_uwm8xA2XiTg/SzFS-MSHoiI/AAAAAAAAAEI/bPxEdV8V1K8/s72-c/MaturityChart.jpg' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8760686654156942303.post-3972883092909716425</id><published>2009-11-04T14:51:00.001-08:00</published><updated>2009-11-04T15:04:20.428-08:00</updated><title type='text'>Elusive Data: Addresses and Phone Numbers</title><content type='html'>First of all, I must apologize again as I have not updated this BLOG in quite awhile. As it turns out, I'm not very good at regular writing....good to know!:) Anyway, on to today's topic. &lt;br /&gt;&lt;br /&gt;Much of Banner's data is stored in a manner that can make it difficult to find the most up-to-date information. Good examples of this situation are addresses and phone numbers. It can be very difficult to find an individual's most current (let alone correct) address and phone number, especially when substandard data standards are used for the entry of that information. So, firstly, one must put the proper procedures in place to control accurate entry of contact information. To aid in this effort, we can provide several "audit queries" that will help identify common business problems in our address and phone number records.&lt;br /&gt;&lt;br /&gt;&lt;h3&gt;Audit Your Data!&lt;/h3&gt;&lt;br /&gt;In my experience, address and phone errors fall into several different areas:&lt;br /&gt;&lt;ol&gt;&lt;li&gt;Addresses that have a "To" date in the past, but are still marked as active.&lt;/li&gt;&lt;li&gt;More than one active address of the same address type.&lt;/li&gt;&lt;li&gt;The "To" date is NULL, but the address is inactive.&lt;/li&gt;&lt;li&gt;The "From" date is NULL.&lt;/li&gt;&lt;li&gt;More than one active telephone number for the same telephone type.&lt;/li&gt;&lt;li&gt;Having an inactive phone number marked as primary.&lt;/li&gt;&lt;li&gt;More than one primary telephone number per individual.&lt;/li&gt;&lt;/ol&gt;&lt;br /&gt;I'm sure many institutions will have other common "opportunities" when it comes to their contact information, but in any case, after those issues are identified, it simply becomes a matter of querying the database on a regular basis (be it weekly, monthly, etc) to locate those records that are "out of compliance". The following queries will aid in finding the issues mentioned above:&lt;br /&gt;&lt;br /&gt;&lt;code&gt;&lt;br /&gt;--past to dates for active addresses&lt;br /&gt;select spraddr_pidm,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; count(*) address_count&lt;br /&gt;&amp;nbsp; from spraddr&lt;br /&gt;&amp;nbsp;where (spraddr_status_ind is null or spraddr_status_ind = 'A')&lt;br /&gt;&amp;nbsp;&amp;nbsp; and spraddr_to_date &amp;lt; sysdate&lt;br /&gt;&amp;nbsp;group by spraddr_pidm;&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&lt;br /&gt;--more than one active address&lt;br /&gt;select spraddr_pidm,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; spraddr_atyp_code,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; count(*) address_count&lt;br /&gt;&amp;nbsp; from spraddr&lt;br /&gt;&amp;nbsp;where spraddr_status_ind is null or spraddr_status_ind = 'A'&lt;br /&gt;&amp;nbsp;group by spraddr_pidm, spraddr_atyp_code&lt;br /&gt;&amp;nbsp;having count(*) &amp;gt; 1;&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&lt;br /&gt;select spraddr_pidm,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; count(*) address_count&lt;br /&gt;&amp;nbsp; from spraddr&lt;br /&gt;&amp;nbsp;where spraddr_to_date is null&lt;br /&gt;&amp;nbsp;&amp;nbsp; and spraddr_status_ind &amp;lt;&amp;gt; 'A'&lt;br /&gt;&amp;nbsp;&amp;nbsp; and spraddr_status_ind is not null&lt;br /&gt;&amp;nbsp;group by spraddr_pidm;&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&lt;br /&gt;--null from date&lt;br /&gt;select spraddr_pidm,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; count(*) address_count&lt;br /&gt;&amp;nbsp; from spraddr&lt;br /&gt;&amp;nbsp;where spraddr_from_date is null&lt;br /&gt;&amp;nbsp;group by spraddr_pidm;&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&lt;br /&gt;--multiple active phones per phone type&lt;br /&gt;select sprtele_pidm,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; sprtele_tele_code,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; count(*) tele_count&lt;br /&gt;&amp;nbsp; from sprtele&lt;br /&gt;&amp;nbsp;where sprtele_status_ind is null&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; or sprtele_status_ind = 'A'&lt;br /&gt;&amp;nbsp;group by sprtele_pidm, sprtele_tele_code&lt;br /&gt;&amp;nbsp;having count(*) &amp;gt; 1;&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&lt;br /&gt;--individuals with inactive primary tele numbers&lt;br /&gt;select sprtele_pidm,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; count(*) tele_count&lt;br /&gt;&amp;nbsp; from sprtele&lt;br /&gt;&amp;nbsp;where sprtele_status_ind is not null&lt;br /&gt;&amp;nbsp;&amp;nbsp; and sprtele_status_ind &amp;lt;&amp;gt; 'A'&lt;br /&gt;&amp;nbsp;&amp;nbsp; and sprtele_primary_ind = 'Y'&lt;br /&gt;&amp;nbsp;group by sprtele_pidm;&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;&lt;code&gt;&lt;br /&gt;select sprtele_pidm,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; count(*) tele_count&lt;br /&gt;&amp;nbsp; from sprtele&lt;br /&gt;&amp;nbsp;where sprtele_primary_ind = 'Y'&lt;br /&gt;&amp;nbsp;group by sprtele_pidm&lt;br /&gt;&amp;nbsp;having count(*) &amp;gt; 1;&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;As you can see, the queries are all very simple, but provide powerful results. With the list of pidms generated by each query, one could build an automated audit process for addresses and phone numbers. This would obviously need to be modified to include business rules at your institution, but the concept is the same. Additionally, there is an excellent product out there called Clean Address (by Runner Technologies) that is specifically designed to comb your addresses and verify their accuracy. Its moderately priced, but well worth it when you consider how much money is wasted on postage paid for correspondance that never reaches the intended recipient.&lt;br /&gt;&lt;br /&gt;In any case, now that we've made a concerted effort to clean our data and put business processes in place to ensure as consistant and accurate input as possible, it's time to figure out how to query the contact information for an individual and get their most up-to-date information.&lt;br /&gt;&lt;br /&gt;&lt;h3&gt;The info is IN, now how do we get it OUT?!&lt;/h3&gt;&lt;br /&gt;This is a difficult topic to cover in a broad sense because the approach you take depends completely on your goals. For example, if you're simply looking for their most current mailing (typically MA = mailing) address, it's a relatively simple process (assuming you've cleaned things up as described above). At that point, the query simply looks for the active address for each entity with the atyp code equal to MA (see below)!&lt;br /&gt;&lt;br /&gt;&lt;code&gt;&lt;br /&gt;select spraddr_pidm,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; spraddr_street_line1,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; spraddr_street_line2,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; spraddr_city,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; spraddr_stat_code,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; spraddr_zip&lt;br /&gt;&amp;nbsp; from spraddr&lt;br /&gt;&amp;nbsp;where spraddr_atyp_code = 'MA'&lt;br /&gt;&amp;nbsp;&amp;nbsp; and (spraddr_status_ind is null&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; or spraddr_status_ind = 'A');&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;Assuming you've checked to make sure each individual only has ONE active address per address type, this query will work like a charm! You only have to get creative (using from/to dates and sequence numbers) when this isn't the case! NOTE: the same idea holds for telephone numbers.&lt;br /&gt;&lt;br /&gt;Unfortunately, we typically want to bring back an address based on a hierarchy. This can be a bit tricky to make happen, but here are a few ideas for a solution! Firstly, Banner provides a form called GTVSDAX in which you can setup institutional hierarchies for things like addresses and telephone numbers. Once that's done, querying the database using a hierarchy becomes trivial; check it out!&lt;br /&gt;&lt;br /&gt;&lt;code&gt;&lt;br /&gt;select spraddr_pidm,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; spraddr_street_line1,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; spraddr_street_line2,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; spraddr_city,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; spraddr_stat_code,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; spraddr_zip&lt;br /&gt;&amp;nbsp; from spraddr&lt;br /&gt;&amp;nbsp;where (spraddr_status_ind is null&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; or spraddr_status_ind = 'A')&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;and spraddr.rowid = f_get_address_rowid(spraddr_pidm,'ADMDDR','A',SYSDATE,1,'S','');&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;As you can see, it's simply a matter of using the predifined Banner function and hierarchy to find the correct address for each individual. Alas, some schools don't use or know about the GTVSDAX form and as such require more "technical" answers to this dilemma. I happened to work for one such school myself! While there, I developed an Oracle function that would allow me to get the correct address type for each person based on a hierarchy. Bear in mind that I wrote this a long while ago, so it's not my best work, but it should point you in the right direction!&lt;br /&gt;&lt;br /&gt;&lt;code&gt;&lt;br /&gt;create or replace FUNCTION F_GET_ADDR_INFO (V_PIDM NUMBER, V_ATYP1 VARCHAR2, &lt;br /&gt;V_ATYP2 VARCHAR2, V_ATYP3 VARCHAR2, V_ATYP4 VARCHAR2, WHICH VARCHAR2) RETURN VARCHAR2 AS&lt;br /&gt;&lt;br /&gt;&amp;nbsp;V_OUTPUT VARCHAR2(500);&lt;br /&gt;&lt;br /&gt;BEGIN&lt;br /&gt;&amp;nbsp; SELECT DECODE(WHICH,'ADDRESS',SPRADDR_STREET_LINE1 || ' ' || SPRADDR_STREET_LINE2 || ' ' || SPRADDR_STREET_LINE3,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 'CSZ', SPRADDR_CITY || ' ' || SPRADDR_STAT_CODE || ', ' || SPRADDR_ZIP,&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; 'ATYP',SPRADDR_ATYP_CODE) INTO V_OUTPUT&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp; FROM SPRADDR&lt;br /&gt;&amp;nbsp;&amp;nbsp; WHERE SPRADDR_PIDM = V_PIDM&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; AND (SPRADDR_ATYP_CODE = V_ATYP1 OR&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SPRADDR_ATYP_CODE = V_ATYP2 OR&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SPRADDR_ATYP_CODE = V_ATYP4 OR&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; SPRADDR_ATYP_CODE = V_ATYP3)&lt;br /&gt;&amp;nbsp;&amp;nbsp;&amp;nbsp;&amp;nbsp; AND ROWNUM = 1;&lt;br /&gt;&amp;nbsp; RETURN V_OUTPUT;&lt;br /&gt;END F_GET_ADDR_INFO;&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;&lt;br /&gt;For telephone numbers, we're typically looking for a specific telephone type rather than one type in a hierarchy. That being the case, you can use a "pivot" query to produce the desired results (see my article regarding pivot queries) from last month!&lt;br /&gt;&lt;br /&gt;Well, that's all for now. I appreciate your stopping by! Remember, as the old adage goes, "garbage in, garbage out", so always start your data search with an evaluation of your current data and business processes (when possible). This may be painful in the short term, but before long you'll be reaping vast rewards! Happy hunting!!!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8760686654156942303-3972883092909716425?l=bannerreporting.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://bannerreporting.blogspot.com/feeds/3972883092909716425/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://bannerreporting.blogspot.com/2009/11/elusive-data-addresses-and-phone.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8760686654156942303/posts/default/3972883092909716425'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8760686654156942303/posts/default/3972883092909716425'/><link rel='alternate' type='text/html' href='http://bannerreporting.blogspot.com/2009/11/elusive-data-addresses-and-phone.html' title='Elusive Data: Addresses and Phone Numbers'/><author><name>Zach Heath</name><uri>http://www.blogger.com/profile/09253162165797370040</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8760686654156942303.post-4740848827367978572</id><published>2009-09-07T21:31:00.000-07:00</published><updated>2009-09-07T21:31:32.853-07:00</updated><title type='text'>Turning Rows into Columns: Pivot Queries</title><content type='html'>Moving normalized, row based information into usable reports often require the use of what is commonly referred to as a "pivot query" (referring to the "pivot" motion required when changing the data's orientation). For example, Banner stores student test information in a series of rows for each student: &lt;br /&gt;&lt;br /&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;PIDM&lt;/td&gt;&lt;td&gt;TEST&lt;/td&gt;&lt;td&gt;SCORE&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;S1&lt;/td&gt;&lt;td&gt;560&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;S2&lt;/td&gt;&lt;td&gt;650&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;S1&lt;/td&gt;&lt;td&gt;600&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;Unfortunately, this is typically not the way one would prefer to report this information. More often, we'd like to see each person's test scores in a single line with multiple columns for each test: &lt;br /&gt;&lt;br /&gt;&lt;table&gt;&lt;tbody&gt;&lt;tr&gt;&lt;td&gt;PIDM&lt;/td&gt;&lt;td&gt;S1&lt;/td&gt;&lt;td&gt;S2&lt;/td&gt;&lt;/tr&gt;&lt;tr&gt;&lt;td&gt;1&lt;/td&gt;&lt;td&gt;600&lt;/td&gt;&lt;td&gt;650&lt;/td&gt;&lt;/tr&gt;&lt;/tbody&gt;&lt;/table&gt;&lt;br /&gt;The second example is much more accessible, allowing the removal of duplicated students, and provides the reader with a more meaningful way to view institutional data (in this case, test scores). &lt;br /&gt;&lt;br /&gt;&lt;h3&gt;So, How Can this Be Done?!&lt;/h3&gt;Well, SQL provides all the tools necessary to make this happen. The two most important tools from our SQL tool belt that will be used for this particular issue will be the DECODE (or CASE) and MAX functions. Let us first take a look at the raw data: &lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://2.bp.blogspot.com/_uwm8xA2XiTg/SqSMeyXolQI/AAAAAAAAADY/2gn4l2Iep08/s1600-h/SORTESToutput1query.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" lk="true" src="http://2.bp.blogspot.com/_uwm8xA2XiTg/SqSMeyXolQI/AAAAAAAAADY/2gn4l2Iep08/s320/SORTESToutput1query.jpg" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;Query 1 - Basic Test Data&lt;/div&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://1.bp.blogspot.com/_uwm8xA2XiTg/SqSMzc-dQLI/AAAAAAAAADg/yMGkKQ8dyn8/s1600-h/SORTESToutput1.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" lk="true" src="http://1.bp.blogspot.com/_uwm8xA2XiTg/SqSMzc-dQLI/AAAAAAAAADg/yMGkKQ8dyn8/s320/SORTESToutput1.jpg" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;Results 1 - Basic Test Data&lt;/div&gt;&lt;div style="border-bottom: medium none; border-left: medium none; border-right: medium none; border-top: medium none;"&gt;&lt;/div&gt;&lt;br /&gt;As you can see, test information is stored in a manner that is difficult to report, but it won't be for long! The next step is to switch the view of the information from row to columner form. To do this, we will check the test code to create each column: &lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://4.bp.blogspot.com/_uwm8xA2XiTg/SqSOKK5N1tI/AAAAAAAAADo/TLbBVcvDUks/s1600-h/SORTESToutput2query.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" lk="true" src="http://4.bp.blogspot.com/_uwm8xA2XiTg/SqSOKK5N1tI/AAAAAAAAADo/TLbBVcvDUks/s400/SORTESToutput2query.jpg" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;Query&amp;nbsp;2 - Perform the Pivot&lt;/div&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;/div&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;/div&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://3.bp.blogspot.com/_uwm8xA2XiTg/SqSOSmuGlvI/AAAAAAAAADw/fO6a5SG6bD4/s1600-h/SORTESToutput2.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" lk="true" src="http://3.bp.blogspot.com/_uwm8xA2XiTg/SqSOSmuGlvI/AAAAAAAAADw/fO6a5SG6bD4/s400/SORTESToutput2.jpg" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;Results 2 - Performing the Pivot&lt;/div&gt;&lt;br /&gt;The DECODE function, whose syntax is DECODE(column_name,'X','A','Y','B','Z','C','D'), operates as follows: it checks the column_name value to see if it is equal to 'X'. If it is equal to 'X' then it replaces itself with the value 'A'. If it is not equal to 'X' then it checks to see if it is equal to 'Y'. If it is equal to 'Y' then it replaces itself with the value of 'B' and so on until we get to 'D'. As you probably noticed, 'D' is the odd man out; it has no corresponding return value. This is referred to as the "Default". If column_name doesn't equal 'X','Y', OR 'Z', then the decode statement will return 'D' by "Default". As you can see in the above example, this only got us half way there; we still have more than one row per individual. Also, with test codes, it's possible to have more than one score with the same code. This problem is easily solved with the MAX() aggregate function. If we surround each column (other than the PIDM) with the MAX function, it will only return the highest test score for each test, leaving one row per individual. This is exactly what we want! See the following example:&lt;br /&gt;&lt;br /&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://1.bp.blogspot.com/_uwm8xA2XiTg/SqXdoL6JmNI/AAAAAAAAAEA/qkEv6hd6hOo/s1600-h/SORTESToutput3query.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" lk="true" src="http://1.bp.blogspot.com/_uwm8xA2XiTg/SqXdoL6JmNI/AAAAAAAAAEA/qkEv6hd6hOo/s320/SORTESToutput3query.jpg" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;Query 3 - Finalize the Pivot&lt;/div&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;/div&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;&lt;a href="http://1.bp.blogspot.com/_uwm8xA2XiTg/SqXdkbS02XI/AAAAAAAAAD4/VLuLhv1ILIE/s1600-h/SORTESToutput3.jpg" imageanchor="1" style="margin-left: 1em; margin-right: 1em;"&gt;&lt;img border="0" lk="true" src="http://1.bp.blogspot.com/_uwm8xA2XiTg/SqXdkbS02XI/AAAAAAAAAD4/VLuLhv1ILIE/s320/SORTESToutput3.jpg" /&gt;&lt;/a&gt;&lt;/div&gt;&lt;div class="separator" style="clear: both; text-align: center;"&gt;Output 3 - Final Pivot&lt;/div&gt;As we expected, this final output is much easier to read than the first (row style) output. However, this technique does have it's draw backs and limitations: &lt;br /&gt;&lt;br /&gt;&lt;ol&gt;&lt;li&gt;You have to know in advance what you're going to key off to perform the pivot - in this case, we used the test codes.&lt;/li&gt;&lt;li&gt;You have to make a column for every row you want to view as a column. In this case, we only chose 7 tests, but what if you wanted to see every test a student has taken?&lt;/li&gt;&lt;/ol&gt;Thank you for checking out my blog. Be sure to check back next week for "Race and Ethnicity"! Happy hunting!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8760686654156942303-4740848827367978572?l=bannerreporting.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://bannerreporting.blogspot.com/feeds/4740848827367978572/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://bannerreporting.blogspot.com/2009/09/turning-rows-into-columns-pivot-queries.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8760686654156942303/posts/default/4740848827367978572'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8760686654156942303/posts/default/4740848827367978572'/><link rel='alternate' type='text/html' href='http://bannerreporting.blogspot.com/2009/09/turning-rows-into-columns-pivot-queries.html' title='Turning Rows into Columns: Pivot Queries'/><author><name>Zach Heath</name><uri>http://www.blogger.com/profile/09253162165797370040</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://2.bp.blogspot.com/_uwm8xA2XiTg/SqSMeyXolQI/AAAAAAAAADY/2gn4l2Iep08/s72-c/SORTESToutput1query.jpg' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8760686654156942303.post-7980614466329263805</id><published>2009-09-06T20:49:00.000-07:00</published><updated>2009-09-30T10:30:10.991-07:00</updated><title type='text'>Writing Series: Banner Tips of the Month</title><content type='html'>Hello everyone. I apologize for the lack of writing over the last couple months. To help remedy the situation, I will be establishing a monthly series: banner tips of the month. Every first&amp;nbsp;monday, I will post a new entry for each of these topics, so come back soon! Happy hunting!!!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8760686654156942303-7980614466329263805?l=bannerreporting.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://bannerreporting.blogspot.com/feeds/7980614466329263805/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://bannerreporting.blogspot.com/2009/09/writing-series-audit-reporting-and.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8760686654156942303/posts/default/7980614466329263805'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8760686654156942303/posts/default/7980614466329263805'/><link rel='alternate' type='text/html' href='http://bannerreporting.blogspot.com/2009/09/writing-series-audit-reporting-and.html' title='Writing Series: Banner Tips of the Month'/><author><name>Zach Heath</name><uri>http://www.blogger.com/profile/09253162165797370040</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8760686654156942303.post-3123387668915123446</id><published>2009-05-14T19:38:00.000-07:00</published><updated>2009-05-14T19:42:33.404-07:00</updated><title type='text'>Report Query Development Cookbook</title><content type='html'>Report development, independent of tool can be a very daunting task. The following pages are designed to give you a step by step process when developing report queries that (in my experience) has led to fewer errors, a more efficient development process, and more accurate output. Before we get into it though, I apologize for the lack of material over the past couple of months; starting a BLOG is one thing, keeping it up is quite another. I hope that you find the information in this post useful! Happy hunting!&lt;br /&gt;&lt;H1&gt;The Example&lt;/H1&gt;&lt;br /&gt;The following request is a recent request I worked on with a client. It is moderately complex but still lends itself well to the topic at hand. The request was as follows:&lt;br /&gt;&lt;B&gt;Financial Aid Applicant Profile:&lt;/B&gt; All financial aid applicants for a selected term and aid year. Please include the applicant's name, ID, ISIR recieved date, verification message, EFC, college, student level, package complete date, dependency status, campus, offered amount, accepted amount, paid amount, unmet need, gender, ethnicity, total credits, and overall GPA.&lt;br /&gt;As you've probably already surmised, this query was not trivial. However, when approached using the framework in the following pages, it becomes a much less daunting endeavor!&lt;br /&gt;&lt;H3&gt;Step 1 - The Population&lt;/H3&gt;&lt;br /&gt;It is imperative that we always begin any query with the population. Simply (hah! yeah right!) identify a list of PIDMS that exactly corresponds to the number of individuals you're trying to report. In the case above, this means anyone who has applied for financial aid during a given aid year. The following query is what will gather the group in quesiton:&lt;br /&gt;&lt;code font="courier new"&gt;&lt;br /&gt;select spriden_id ID,&lt;br /&gt;  spriden_last_name LastName,&lt;br /&gt;  spriden_first_name FirstName&lt;br /&gt;  from rcrapp1, spriden &lt;br /&gt; where spriden_pidm = rcrapp1_pidm&lt;br /&gt;   and spriden_change_ind is null&lt;br /&gt;   and rcrapp1_aidy_code = :aidy&lt;br /&gt;   and rcrapp1_infc_code = 'ISIR'&lt;br /&gt;   and rcrapp1_curr_rec_ind = 'Y';&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;As you can see, the query is very simple, although it's missing all the attributes (college, level, etc) that make this report useful. However, we now have the foundation against which all additions can be compared. Additionally, we can "spot check" a couple of IDs to make sure that this population is indeed correct (it may even be a good idea to contact the requestor and have them "sniff test" the number in the group before proceeding further. The last thing to do before adding information is to count the number of records returned. Often, this can be done using built in functionality in whatever tool you're using to write your query. For the purposes of this example, let's assume that the query above returned 1200 applicants for 0809. Proceed to step 2!&lt;br /&gt;&lt;H3&gt;Step 2 - Adding Attributes&lt;/H3&gt;&lt;br /&gt;After completing step one, completing the report becomes a matter of "bolting on" additional information. This should be done very methodically and should start with requested attributes that can already be found in the table(s) already being used to gather the population. In this case, we're already using RCRAPP1 and SPRIDEN, so we should go through the request and look for any other attributes that come from either of those two tables. In this case there are a couple of attributes that can be added from the RCRAPP1 table:&lt;br /&gt;&lt;ul&gt;&lt;br /&gt;&lt;li&gt;ISIR Recieved Date (RCRAPP1_RCPT_DATE)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;and Verification Message (RCRAPP1_VERIFICATION_MSG)&lt;/li&gt;&lt;br /&gt;&lt;/ul&gt;&lt;br /&gt;Because the information requested already exists in the table(s) used, there is no need for additional join conditions. This makes adding the data itself trivial:&lt;br /&gt;&lt;code font="courier new"&gt;&lt;br /&gt;select spriden_id ID,&lt;br /&gt;  spriden_last_name LastName,&lt;br /&gt;  spriden_first_name FirstName,&lt;br /&gt;  rcrapp1_rcpt_date ISIR_Recd_Date,&lt;br /&gt;  rcrapp1_verification_msg Ver_Msg&lt;br /&gt;  from rcrapp1, spriden &lt;br /&gt; where spriden_pidm = rcrapp1_pidm&lt;br /&gt;   and spriden_change_ind is null&lt;br /&gt;   and rcrapp1_aidy_code = :aidy&lt;br /&gt;   and rcrapp1_infc_code = 'ISIR'&lt;br /&gt;   and rcrapp1_curr_rec_ind = 'Y';&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;Notice, there have been no changes to the WHERE clause of the statment and no new tables have been joined to the query, so the number of individuals selected should still be the same. SHOULD BE THE SAME. At this point, it's time to check. Rerun the query and make sure that your record count has not changed. This process will be repeated after each new set of attributes are added. Now, let's see what we have left:&lt;br /&gt;&lt;ul&gt;&lt;br /&gt;&lt;li&gt;EFC (RCRAPP4_SAR_EFC)&lt;/li&gt; &lt;br /&gt;&lt;li&gt;college (SGBSTDN_COLL_CODE)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;student level (SGBSTDN_LEVL_CODE)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;package complete date (RORSTAT_PCKG_COMP_DATE)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;dependency status (RCRAPP2_C_DEPEND_STATUS)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;campus (SGBSTDN_CAMP_CODE)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;offered amount (RPRAWRD_OFFER_AMT&lt;/li&gt;&lt;br /&gt;&lt;li&gt;accepted amount (RPRAWRD_ACCEPT_AMT)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;paid amount (RPRAWRD_PAID_AMT)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;unmet need (RORSTAT_UNMET_NEED)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;gender (SPBPERS_SEX)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;ethnicity (SPBPERS_ETHN_CODE)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;total credits (SUM(SFRSTCR_CREDIT_HR)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;and overall GPA (SHRLGPA_GPA)&lt;/li&gt;&lt;br /&gt;&lt;/ul&gt;&lt;br /&gt;Notice in the above list, that I also tabulated the fields in which the data in question can be found. It may be beneficial to do this yourselves as that task can often be a difficult one. Having the list ready and waiting will make things much smoother when it comes to writing the actual SQL.&lt;br /&gt;At this point, I would typically pick out what I feel are the easiest tables to add and start with them. In &lt;i&gt;most&lt;/i&gt; cases, tables with a "B" in the third character are a good place to start. However, SGBSTDN is an exception to that rule (go figure) becuase it is EFFECTIVE DATED. Have no fear though, SPBPERS is very easy to add because the PIDM is the primary key, meaning that only one instance of any given PIDM (student) can be in the table at a time. That being the case, the only thing you have to worry about when adding SPBPERS is joining SPBPERS_PIDM to another PIDM already in the query. In this case, we'll join to SPRIDEN as shown below:&lt;br /&gt;&lt;code&gt;&lt;br /&gt;select spriden_id ID,&lt;br /&gt;  spriden_last_name LastName,&lt;br /&gt;  spriden_first_name FirstName,&lt;br /&gt;  rcrapp1_rcpt_date ISIR_Recd_Date,&lt;br /&gt;  rcrapp1_verification_msg Ver_Msg,&lt;br /&gt;    &lt;b&gt;spbpers_sex Gender,&lt;br /&gt;  spbpers_ethn_code Ethnicity&lt;/b&gt;&lt;br /&gt;  from rcrapp1, spriden, &lt;b&gt;spbpers&lt;/b&gt;&lt;br /&gt; where spriden_pidm = rcrapp1_pidm&lt;br /&gt;&lt;b&gt;and spbpers_pidm = spriden_pidm&lt;/b&gt;&lt;br /&gt;   and spriden_change_ind is null&lt;br /&gt;   and rcrapp1_aidy_code = :aidy&lt;br /&gt;   and rcrapp1_infc_code = 'ISIR'&lt;br /&gt;   and rcrapp1_curr_rec_ind = 'Y';&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;Now, as we did before, we need to run the query again and make sure that the number of records returned is still 1200. Assuming that's the case, it's time to move onto our next set of attributes. This time, I'm going to bite the bullet and add SGBSTDN. See the query below (the additions, shown in &lt;b&gt;bold&lt;/b&gt;).&lt;br /&gt;&lt;code&gt;&lt;br /&gt;select spriden_id ID,&lt;br /&gt;  spriden_last_name LastName,&lt;br /&gt;  spriden_first_name FirstName,&lt;br /&gt;  rcrapp1_rcpt_date ISIR_Recd_Date,&lt;br /&gt;  rcrapp1_verification_msg Ver_Msg,&lt;br /&gt;       spbpers_sex Gender,&lt;br /&gt;  spbpers_ethn_code Ethnicity,&lt;br /&gt;    &lt;b&gt;sgbstdn_coll_code College,&lt;br /&gt;  sgbstdn_levl_code Level,&lt;br /&gt;  sgbstdn_camp_code Campus&lt;/b&gt;&lt;br /&gt;  from rcrapp1, spriden, spbpers&lt;b&gt;, sgbstdn a&lt;/b&gt;&lt;br /&gt; where spriden_pidm = rcrapp1_pidm&lt;br /&gt;   and spbpers_pidm = spriden_pidm&lt;br /&gt;&lt;b&gt;and spriden_pidm = sgbstdn_pidm&lt;br /&gt;   and sgbstdn_term_code_eff = (select max(sgbstdn_term_code_eff)&lt;br /&gt;                                  from sgbstdn b&lt;br /&gt;                                 where a.sgbstdn_pidm = b.sgbstdn_pidm&lt;/b&gt;&lt;br /&gt;   and spriden_change_ind is null&lt;br /&gt;   and rcrapp1_aidy_code = :aidy&lt;br /&gt;   and rcrapp1_infc_code = 'ISIR'&lt;br /&gt;   and rcrapp1_curr_rec_ind = 'Y';&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;The order and tables used for the join conditions are not terribly critical unless you're trying to increase the performance of your query (in which case, there are certain rules that can be applied). That being the case, as shown above I have joined SGBSTDN_PIDM to SPRIDEN. Additionally, note the subquery to find the student's most current SGBSTDN record. Alright, let's take stock of where we're at at this point:&lt;br /&gt;&lt;ul&gt;&lt;br /&gt;&lt;li&gt;EFC (RCRAPP4_SAR_EFC)&lt;/li&gt; &lt;br /&gt;&lt;li&gt;package complete date (RORSTAT_PCKG_COMP_DATE)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;dependency status (RCRAPP2_C_DEPEND_STATUS)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;offered amount (RPRAWRD_OFFER_AMT&lt;/li&gt;&lt;br /&gt;&lt;li&gt;accepted amount (RPRAWRD_ACCEPT_AMT)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;paid amount (RPRAWRD_PAID_AMT)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;unmet need (RORSTAT_UNMET_NEED)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;total credits (SUM(SFRSTCR_CREDIT_HR)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;and overall GPA (SHRLGPA_GPA)&lt;/li&gt;&lt;br /&gt;&lt;/ul&gt;&lt;br /&gt;To help get this article finished (for Pete's sake!!!), I'm going to add everything except the registered credits in the next step. However, they should be added one table at a time and tested at each step to make sure that the record set has not changed. In any case, the query with those fields can be seen below:&lt;br /&gt;&lt;code&gt;&lt;br /&gt;select spriden_id ID,&lt;br /&gt;  spriden_last_name LastName,&lt;br /&gt;  spriden_first_name FirstName,&lt;br /&gt;  rcrapp1_rcpt_date ISIR_Recd_Date,&lt;br /&gt;  rcrapp1_verification_msg Ver_Msg,&lt;br /&gt;       spbpers_sex Gender,&lt;br /&gt;  spbpers_ethn_code Ethnicity,&lt;br /&gt;       sgbstdn_coll_code College,&lt;br /&gt;  sgbstdn_levl_code Level,&lt;br /&gt;  sgbstdn_camp_code Campus,&lt;br /&gt;    &lt;b&gt;rcrapp4_sar_efc EFC,&lt;br /&gt;  rorstat_pckg_comp_date PackgCompDate,&lt;br /&gt;  rcrapp2_c_depend_status DependencyStatus,&lt;br /&gt;  sum(rprawrd_offer_amt) Offered,&lt;br /&gt;  sum(rprawrd_accept_amt) Accepted,&lt;br /&gt;  sum(rprawrd_paid_amt) Paid,&lt;br /&gt;  rorstat_unmet_need UnmetNeed,&lt;br /&gt;  shrlgpa_gpa GPA&lt;/b&gt;&lt;br /&gt;  from rcrapp1, spriden, spbpers, sgbstdn a&lt;br /&gt;  rcrapp4, rorstat, rcrapp2, shrlgpa,&lt;br /&gt;  rprawrd&lt;br /&gt; where spriden_pidm = rcrapp1_pidm&lt;br /&gt;   and spbpers_pidm = spriden_pidm&lt;br /&gt;   and spriden_pidm = sgbstdn_pidm&lt;br /&gt;&lt;b&gt;and spriden_pidm = shrlgpa_pidm&lt;br /&gt;   and sgbstdn_levl_code = shrlgpa_levl_code&lt;br /&gt;   and spriden_pidm = rorstat_pidm&lt;br /&gt;   and rorstat_aidy_code = rcrapp1_aidy_code&lt;br /&gt;   and rcrapp1_pidm = rcrapp2_pidm&lt;br /&gt;   and rcrapp1_aidy_code = rcrapp2_aidy_code&lt;br /&gt;   and rcrapp1_infc_code = rcrapp2_infc_code&lt;br /&gt;   and rcrapp1_pidm = rcrapp4_pidm&lt;br /&gt;   and rcrapp1_aidy_code = rcrapp4_aidy_code&lt;br /&gt;   and rcrapp1_infc_code = rcrapp4_infc_code&lt;br /&gt;   and rorstat_pidm = rprawrd_pidm&lt;/b&gt;&lt;br /&gt;   and rorstat_aidy_code = rprawrd_aidy_code&lt;br /&gt;   and sgbstdn_term_code_eff = (select max(sgbstdn_term_code_eff)&lt;br /&gt;                                  from sgbstdn b&lt;br /&gt;                                 where a.sgbstdn_pidm = b.sgbstdn_pidm&lt;br /&gt;   and spriden_change_ind is null&lt;br /&gt;   and rcrapp1_aidy_code = :aidy&lt;br /&gt;   and rcrapp1_infc_code = 'ISIR'&lt;br /&gt;   and rcrapp1_curr_rec_ind = 'Y'&lt;br /&gt; group by spriden_id, spriden_last_name, spriden_first_name,&lt;br /&gt;     rcrapp1_rcpt_date, rcrapp1_verification_msg, spbpers_sex,&lt;br /&gt;     spbpers_ethn_code, sgbstdn_coll_code, sgbstdn_levl_code,&lt;br /&gt;     sgbstdn_camp_code, rcrapp4_sar_efc, rorstat_pckg_comp_date,&lt;br /&gt;     rorstat_unmet_need, shrlgpa_gpa;&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;Finally, the only thing left is to calculate the total credits for each student. This can be done in two different ways:&lt;br /&gt;&lt;ul&gt;&lt;br /&gt;&lt;li&gt;Join to SFRSTCR and sum(credits)&lt;/li&gt;&lt;br /&gt;&lt;li&gt;Write a SCALAR SUBQUERY to do the calculation&lt;/li&gt;&lt;br /&gt;&lt;/ul&gt;&lt;br /&gt;In cases where only one or two fields are required from a new table, I prefer using the second method (SCALAR SUBQUERY). It acts as an outer join (if required) and performs better in many cases.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8760686654156942303-3123387668915123446?l=bannerreporting.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://bannerreporting.blogspot.com/feeds/3123387668915123446/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://bannerreporting.blogspot.com/2009/05/report-query-development-cookbook.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8760686654156942303/posts/default/3123387668915123446'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8760686654156942303/posts/default/3123387668915123446'/><link rel='alternate' type='text/html' href='http://bannerreporting.blogspot.com/2009/05/report-query-development-cookbook.html' title='Report Query Development Cookbook'/><author><name>Zach Heath</name><uri>http://www.blogger.com/profile/09253162165797370040</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8760686654156942303.post-3615587862720920677</id><published>2009-03-11T09:43:00.000-07:00</published><updated>2009-03-16T14:12:35.937-07:00</updated><title type='text'>To Join or not to Join, that is the Question!</title><content type='html'>One of the most difficult aspects of SQL queries are identifying and accurately completing the relationship identification between database objects. Let me try that again, in English: when querying more than one table at a time (in a relational database), the database needs the developer (you) to identify the relationship between the objects. That identification is difficult at times and leads to many of the problems with report results. In this post, I talk about &lt;a href="#reldb"&gt;Relational Databases&lt;/a&gt;, &lt;a href="#cartesian"&gt;Cartesian Products&lt;/a&gt;, &lt;a href="#Inner"&gt;Inner&lt;/a&gt;, and &lt;a href="#Outer"&gt;Outer&lt;/a&gt; Joins. Before I get into the tips around &lt;a href="http://www.sc.ehu.es/siwebso/KZCC/Oracle_10g_Documentacion/server.101/b10759/queries006.htm"&gt;JOINS&lt;/a&gt;, let's get into a little more depth regarding the theory behind join requirements.&lt;br /&gt;&lt;br /&gt;&lt;a name="reldb"&gt;&lt;h3&gt;Relational Database&lt;/h3&gt;&lt;/a&gt;A relational database is just like it sounds; a database whose contents are related. How they are related is the crux of the join issue. Take the following example:&lt;br /&gt;&lt;br /&gt;SPRIDEN and SPBPERS are two Banner tables containing general person information. SPRIDEN contains ID and Name data, while SPBPERS contains demographic information (such as SSN, Birth Date, Citizenship, Ethnicity, etc). Referencing the first image below, we can see that the data stored in the tables is very organized, but how does the system know which SSN and Gender to associate with Mary Johnson?&lt;br /&gt;&lt;br /&gt;&lt;a href="http://3.bp.blogspot.com/_uwm8xA2XiTg/SbgsFvS2DEI/AAAAAAAAACA/D1m3uLXiloo/s1600-h/JoinEx1.bmp"&gt;&lt;img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 400px; height: 154px;" src="http://3.bp.blogspot.com/_uwm8xA2XiTg/SbgsFvS2DEI/AAAAAAAAACA/D1m3uLXiloo/s400/JoinEx1.bmp" border="0" alt=""id="BLOGGER_PHOTO_ID_5312044237589974082" /&gt;&lt;/a&gt;&lt;br /&gt;As we can see, the individual's PIDM exists in both tables. If we connect (JOIN) the two, we will establish the required relationship; indicating to the system how the two records (and by extension, the two tables) are related. Clear as mud right! Hopefully the image below will help explain:&lt;br /&gt;&lt;br /&gt;&lt;a href="http://4.bp.blogspot.com/_uwm8xA2XiTg/SbguZxvmHWI/AAAAAAAAACI/H4uGQDZUXX4/s1600-h/JoinEx2.bmp"&gt;&lt;img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 400px; height: 154px;" src="http://4.bp.blogspot.com/_uwm8xA2XiTg/SbguZxvmHWI/AAAAAAAAACI/H4uGQDZUXX4/s400/JoinEx2.bmp" border="0" alt=""id="BLOGGER_PHOTO_ID_5312046780868074850" /&gt;&lt;/a&gt;&lt;br /&gt;I don't want to spend too much time on theory, so I'm going to keep this brief. Hopefully you now have an idea about why joins are required. The next step is to understand that there are a few different kind of joins. For more information on the types, how and when they're used, and their syntax, &lt;a href="http://www.sc.ehu.es/siwebso/KZCC/Oracle_10g_Documentacion/server.101/b10759/queries006.htm"&gt;click here&lt;/a&gt;. One thing I do want to discuss before moving onto more practical matters, is the concept of a &lt;b&gt;&lt;i&gt;cartesian join&lt;/b&gt;&lt;/i&gt;, or &lt;b&gt;&lt;i&gt;cartesian product&lt;/b&gt;&lt;/i&gt;.&lt;br /&gt;&lt;br /&gt;&lt;a name="cartesian"&gt;&lt;h3&gt;Cartesian Join/Product&lt;/h3&gt;&lt;/a&gt; You have created a cartesian join when you query more than one object and DO NOT specify the join condition. You would know you've done that by several things:&lt;br /&gt;&lt;ol&gt;&lt;br /&gt;&lt;li&gt;The query may bring down the database&lt;br /&gt;&lt;li&gt;If the results do get returned, there are likely duplicate records&lt;br /&gt;&lt;li&gt;The combination of information from the queried objects will make no sense; Mary Johnson's name will appear with other people's demographic information.&lt;br /&gt;&lt;li&gt;Your query contains more than one object in the FROM clause, with no reference to their relationship in the WHERE clause.&lt;br /&gt;&lt;/ol&gt;&lt;br /&gt;As you probably have guessed, cartesian joins are VERY bad. The problem is that Oracle assumes that each row in the first object should be connected to each row in the second. As an example, reference the two tables below. Assuming that they are displaying all the data in each object, the following query would yeild the results below.&lt;br /&gt;&lt;br /&gt;&lt;a href="http://4.bp.blogspot.com/_uwm8xA2XiTg/SbgzsUa48QI/AAAAAAAAACQ/TGlHtAz8xcg/s1600-h/JoinEx3.bmp"&gt;&lt;img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 400px; height: 216px;" src="http://4.bp.blogspot.com/_uwm8xA2XiTg/SbgzsUa48QI/AAAAAAAAACQ/TGlHtAz8xcg/s400/JoinEx3.bmp" border="0" alt=""id="BLOGGER_PHOTO_ID_5312052596972253442" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;a href="http://1.bp.blogspot.com/_uwm8xA2XiTg/Sbg8JT1NjuI/AAAAAAAAACY/AeQn1PhK2fg/s1600-h/QueryResults1.bmp"&gt;&lt;img style="float:left; margin:0 10px 10px 0;cursor:pointer; cursor:hand;width: 134px; height: 400px;" src="http://1.bp.blogspot.com/_uwm8xA2XiTg/Sbg8JT1NjuI/AAAAAAAAACY/AeQn1PhK2fg/s400/QueryResults1.bmp" border="0" alt=""id="BLOGGER_PHOTO_ID_5312061891123449570" /&gt;&lt;/a&gt;&lt;code&gt;&lt;br /&gt;select ID, Lname, Gender&lt;br /&gt;&amp;nbsp&amp;nbspfrom object1, object2;&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;As we would expect, we have 27 rows of results (3 from Object2 times 9 from Object1). Now, extrapolate; imagine two tables that contain hundreds of thousands of rows!&lt;br /&gt;&lt;br /&gt;200,000 * 300,000 = 60,000,000,000 records returned...ouch!&lt;br /&gt;&lt;br /&gt;It has potential to crash your system!!! What we want to do, is specify the join condition as demonstrated above, but before we do that, we need to talk about the different &lt;i&gt;kinds&lt;/i&gt; of joins.&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;br /&gt;&lt;a name="Inner"&gt;&lt;h3&gt;Inner Joins&lt;/h3&gt;&lt;/a&gt;An &lt;i&gt;inner join&lt;/i&gt; only returns the "SELECTED" information if both sides of the join condition match. Observe the following example:&lt;br /&gt;&lt;code&gt;&lt;br /&gt;SELECT spriden_id, spbpers_ssn&lt;br /&gt;  FROM spriden, spbpers&lt;br /&gt; &lt;i&gt;&lt;b&gt;WHERE spriden_pidm = spbpers_pidm&lt;/i&gt;&lt;/b&gt;;&lt;br /&gt;*NOTE - the case is not important. I use UPPER case here to specify "clauses" of the select statement.&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;In the above example, the ID, and SSN would only return where the SPRIDEN_PIDM and SPBPERS_PIDM are the same. The implication of that is in regards to this questions, "What if they don't have a record in SPBPERS?". In this case, that individual would be REMOVED from the query results! &lt;br /&gt;&lt;br /&gt;&lt;a href="http://3.bp.blogspot.com/_uwm8xA2XiTg/Sb6EqnAMQDI/AAAAAAAAACg/eBzMRP1OVC0/s1600-h/InnerJoinQueryResults.png"&gt;&lt;img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 400px; height: 127px;" src="http://3.bp.blogspot.com/_uwm8xA2XiTg/Sb6EqnAMQDI/AAAAAAAAACg/eBzMRP1OVC0/s400/InnerJoinQueryResults.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5313830477902921778" /&gt;&lt;/a&gt;&lt;br /&gt;To help deal with this issue, we can use what's called an "outer join".&lt;br /&gt;&lt;a name="Outer"&gt;&lt;h3&gt;Outer Joins&lt;/h3&gt;&lt;/a&gt;There are a few different types of outer joins, but for the purposes of this discussion, we really only need to consider the reason you would use an outer vs. an inner join and what the possible repurcussions might be. For more information on outer joins (syntax, etc), &lt;a href="http://www.adp-gmbh.ch/ora/sql/outer_join.html"&gt;click here&lt;/a&gt;. First off, why would you use an outer join? The most important reason is to combat the issue mentioned above under "Inner Joins"; if both sides of the join condition do not equate, then the record is dropped. In many cases (addresses, email, telephone, and others), you don't want to lose the record just because the individual in question doesn't have a record in a certain area. In those cases we use an outer join as in the script below:&lt;br /&gt;&lt;code&gt;&lt;br /&gt;SELECT spriden_id, spbpers_ssn&lt;br /&gt;  FROM spriden, spbpers&lt;br /&gt; &lt;i&gt;&lt;b&gt;WHERE spriden_pidm = spbpers_pidm(+)&lt;/i&gt;&lt;/b&gt;;&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;Notice the "(+)" at the end of SPBPERS_PIDM in the join condition. This indicates that Oracle should return ALL ROWS from spriden as well as the rows from SPBPERS where the PIDM matches. So, again looking at our sample SPRIDEN and SPBPERS tables below, we would get the following results from this query:&lt;br /&gt;&lt;br /&gt;&lt;a href="http://2.bp.blogspot.com/_uwm8xA2XiTg/Sb6Exwu64KI/AAAAAAAAACo/u2p_fPIKeQA/s1600-h/outerJoinQueryResults.png"&gt;&lt;img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 400px; height: 121px;" src="http://2.bp.blogspot.com/_uwm8xA2XiTg/Sb6Exwu64KI/AAAAAAAAACo/u2p_fPIKeQA/s400/outerJoinQueryResults.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5313830600773918882" /&gt;&lt;/a&gt;&lt;br /&gt;One word of caution: if you include a condition on a table that is outer joined, you MUST outer join on that condition as well!!! For example, let's say we only wanted to look at GENDER = 'M'. Normally, we would use the following to do so:&lt;br /&gt;&lt;code&gt;&lt;br /&gt;SELECT spriden_id, spbpers_ssn&lt;br /&gt;  FROM spriden, spbpers&lt;br /&gt; WHERE spriden_pidm = spbpers_pidm(+)&lt;br /&gt;   &lt;i&gt;&lt;b&gt;and spbpers_sex = 'M';&lt;/i&gt;&lt;/b&gt;&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;However, because many individuals have no record in SPBPERS, this query would NOT return ONLY return one row, the row corresponding to the fellow with an 'M' for gender. As such, we must do the following to outer join on the condition as well as the join &lt;i&gt;if we want to include folks with no SPBPERS record&lt;/i&gt;:&lt;br /&gt;&lt;code&gt;&lt;br /&gt;SELECT spriden_id, spbpers_ssn&lt;br /&gt;  FROM spriden, spbpers&lt;br /&gt; WHERE spriden_pidm = spbpers_pidm(+)&lt;br /&gt;   &lt;i&gt;&lt;b&gt;and spbpers_sex(+) = 'M';&lt;/i&gt;&lt;/b&gt;&lt;br /&gt;&lt;/code&gt;&lt;br /&gt;Notice the "(+)" by the spbpers_sex condition as well as the join condition! So as a general rule: &lt;b&gt;any conditions against a table that is outer joined, must be outer join conditions!&lt;/b&gt;&lt;br /&gt;&lt;br /&gt;Now that we have an idea of what an outer join is used for, let's discuss its implications. Basically, using an outer join introduces overhead to your query. That being the case, it's best to stick with inner joins whenever possible. Other than that, good luck! I hope this has given you a little insight into join theory, especially with regard to Banner and Oracle. Please post questions as needed! until next time, happy hunting!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8760686654156942303-3615587862720920677?l=bannerreporting.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://bannerreporting.blogspot.com/feeds/3615587862720920677/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://bannerreporting.blogspot.com/2009/03/to-join-or-not-to-join-that-is-question.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8760686654156942303/posts/default/3615587862720920677'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8760686654156942303/posts/default/3615587862720920677'/><link rel='alternate' type='text/html' href='http://bannerreporting.blogspot.com/2009/03/to-join-or-not-to-join-that-is-question.html' title='To Join or not to Join, that is the Question!'/><author><name>Zach Heath</name><uri>http://www.blogger.com/profile/09253162165797370040</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://3.bp.blogspot.com/_uwm8xA2XiTg/SbgsFvS2DEI/AAAAAAAAACA/D1m3uLXiloo/s72-c/JoinEx1.bmp' height='72' width='72'/><thr:total>0</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8760686654156942303.post-1259930561778231159</id><published>2009-02-27T16:10:00.000-08:00</published><updated>2009-02-27T16:42:16.091-08:00</updated><title type='text'>Effective Dating and Sub Queries</title><content type='html'>Effective dating (no, not the romantic kind!) is one of the most common topics of discussion when reporting against Banner (Baseline, especially: the ODS and EDW go a long way to removing effective date and term code logic requirements). Most people are aware of the issue, but do not fully understand why things are the way they are and even if the &lt;A HREF="#WHY"&gt;"why"&lt;/A&gt; is understood, &lt;A HREF="#HOW"&gt;"how"&lt;/A&gt;&lt;br /&gt;to go about solving the issue can sometimes be very confusing. There are two main examples I like to use when discussing this subject; job history and student records. Job history deals with "effective dates" (NBRJOBS_EFFECTIVE_DATE) while the&lt;br /&gt;student base record deals with "effective term" (SGBSTDN_TERM_CODE_EFF). The following entry discusses both questions of &lt;A HREF="#WHY"&gt;"why"&lt;/A&gt; this situation is important and &lt;A HREF="#HOW"&gt;"how"&lt;/A&gt; it must be dealt with.&lt;br /&gt;&lt;br /&gt;&lt;A NAME="WHY"&gt;&lt;H3&gt;Why is this such a BIG deal!?!?&lt;/H3&gt;&lt;/A&gt;&lt;br /&gt;The most important thing to keep in mind is the two dimensional nature of the way data is stored in Banner (or any database, really). You can think of the Banner database as a really complex and robust series of spreadsheets that hold all your Banner data. Jobs and student information are no different in terms of storage; the difference is in the fact that both store &lt;i&gt;&lt;b&gt;historical information&lt;/i&gt;&lt;/b&gt;. Let's&lt;br /&gt;take a look at a relatively simple example. Assume for a second that John Doe has only one job currently, but has moved around within the school and has been promoted a couple of times. The way his data is stored in the system would look something like this:&lt;br /&gt;&lt;br /&gt;&lt;a href="http://2.bp.blogspot.com/_uwm8xA2XiTg/SaiDirjF22I/AAAAAAAAABA/s0bd8-can1k/s1600-h/JobsPic1.bmp"&gt;&lt;img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 400px; height: 67px;" src="http://2.bp.blogspot.com/_uwm8xA2XiTg/SaiDirjF22I/AAAAAAAAABA/s0bd8-can1k/s400/JobsPic1.bmp" border="0" alt=""id="BLOGGER_PHOTO_ID_5307636792684829538" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;TABLE ALIGN="CENTER"&gt;Figure 1 - NBRJOBS&lt;/TABLE&gt;&lt;br /&gt;As you can see, all job history is stored in the Banner system. If one wishes to look at the most current job information for John, one MUST find that record associated with his most current (or MAX) effective date. As we can see, that would correspond to the last record in the table, for position B2354 with a salary of $45,000.&lt;br /&gt;&lt;br /&gt;Now let's take a look at the student side (SGBSTDN). Even though we are now dealing with effective terms, instead of effective dates, the concept is still the same (and can be applied to any situation in which you're dealing with having to figure out the most current record for a given set of records). As shown in &lt;A HREF="#FIGURE2"&gt;Figure 2&lt;/A&gt; which is very similar to &lt;A HREF="#FIGURE1"&gt;Figure 1&lt;/A&gt;, the student information corresponding to John Doe in SGBSTDN can change a random terms throughout their experience in school. If their major or any other attribute (level, type, etc) changes during their tenure, a new record is added to SGBSTDN reflecting the change &lt;i&gt;&lt;b&gt;with a new effective term&lt;/i&gt;&lt;/b&gt;.&lt;br /&gt;&lt;br /&gt;&lt;a href="http://2.bp.blogspot.com/_uwm8xA2XiTg/SaiELReqRDI/AAAAAAAAABI/pZ4sDEjJy5M/s1600-h/StudPic1.png"&gt;&lt;img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 400px; height: 45px;" src="http://2.bp.blogspot.com/_uwm8xA2XiTg/SaiELReqRDI/AAAAAAAAABI/pZ4sDEjJy5M/s400/StudPic1.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5307637490061558834" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;TABLE ALIGN="CENTER"&gt;Figure 2 - SGBSTDN&lt;/TABLE&gt;&lt;br /&gt;As shown, the problem is the same. Mr. Doe's status changed from new freshmen (N in my simply coded universe) to continuing (C) after his first term. Then, after much thought, he changed his major from psychology (PSYCH) to sociology (SOC) in fall, 2008 (200910). &lt;b&gt;NOTE:&lt;/b&gt; all codes are fictional and do not reflect real Banner data. For your school's codes, reference the validation tables associated with each data element. As with the job records, we would simply need to make sure to isolate John's most current record by looking for the record associated with his most current (or MAX) effective term. In either case, we &lt;i&gt;must&lt;/i&gt; do this through the use of a &lt;a href="http://www.techonthenet.com/oracle/subqueries.php"&gt;&lt;i&gt;&lt;b&gt;correlated subquery&lt;/i&gt;&lt;/b&gt;&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;&lt;A NAME="HOW"&gt;&lt;H3&gt;Alright, alright, now tell me HOW!&lt;/H3&gt;&lt;/A&gt;&lt;br /&gt;Before we get into the details of how a subquery can be used to facilitate our needs, I'd like to briefly discuss &lt;i&gt;why&lt;/i&gt; a subquery is required. This question of "why subquery" is a constant source of confusion among report writers everywhere. Typically, what I hear is, let's just include the MAX effective date or term in our query as in the following script:&lt;br /&gt;&lt;br /&gt;&lt;a href="http://1.bp.blogspot.com/_uwm8xA2XiTg/SaiFaljwFqI/AAAAAAAAABQ/5pRexFRKk8M/s1600-h/Query1.png"&gt;&lt;img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 400px; height: 154px;" src="http://1.bp.blogspot.com/_uwm8xA2XiTg/SaiFaljwFqI/AAAAAAAAABQ/5pRexFRKk8M/s400/Query1.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5307638852661286562" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;a href="http://2.bp.blogspot.com/_uwm8xA2XiTg/SaiFxvplMxI/AAAAAAAAABY/KVWARU-q8rA/s1600-h/JobsPic2.png"&gt;&lt;img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 400px; height: 56px;" src="http://2.bp.blogspot.com/_uwm8xA2XiTg/SaiFxvplMxI/AAAAAAAAABY/KVWARU-q8rA/s400/JobsPic2.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5307639250507084562" /&gt;&lt;/a&gt;&lt;br /&gt;Whenever an aggregate function (such as COUNT, AVG, MAX, or MIN) is used in a SQL query, all fields NOT being aggregated &lt;i&gt;must&lt;/i&gt; exist in the where clause. What that translates into (in english) is that the aggregation occurs around all other fields being selected. So, in the case of the above script, it's looking for the maximum effective date for each combination of ID, Name, Position, Suffix, and Salary. Referencing &lt;A HREF="#FIGURE1"&gt;Figure 1&lt;/A&gt;, we can see that this would be fine for the first position, because it would isolate around the terminated record (all selected fields except the date are the same), indicating the most current record for that position. However, for their currently active position, it would return a record for each salary (because it's looking at the maximum effective date for each &lt;i&gt;different&lt;/i&gt; salary!!!). The crux of the issue, is the fact that Oracle is relatively "dumb", it simply takes the statement you write and returns the results according to pre-established rules. It cannot "guess" what we are trying to do. That being the case, it looks at each row in turn. For this reason, we must use a sub query to match the effective date in the current row to the MAX effective date for the individual, position, and suffix. Now, on to the solution!!!&lt;br /&gt;&lt;br /&gt;A subquery is simply a query within a query. A correlated subquery, as we will see shortly, is a query in the WHERE CLAUSE, usually referencing the same table mroe than once.&lt;br /&gt;&lt;br /&gt;&lt;a href="http://4.bp.blogspot.com/_uwm8xA2XiTg/SaiGniD3tgI/AAAAAAAAABg/20SZDH3WGK0/s1600-h/Query2.png"&gt;&lt;img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 400px; height: 134px;" src="http://4.bp.blogspot.com/_uwm8xA2XiTg/SaiGniD3tgI/AAAAAAAAABg/20SZDH3WGK0/s400/Query2.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5307640174572189186" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;a href="http://1.bp.blogspot.com/_uwm8xA2XiTg/SaiGwqn83CI/AAAAAAAAABo/cZ99bsEwn58/s1600-h/JobsPic3.png"&gt;&lt;img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 400px; height: 34px;" src="http://1.bp.blogspot.com/_uwm8xA2XiTg/SaiGwqn83CI/AAAAAAAAABo/cZ99bsEwn58/s400/JobsPic3.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5307640331489827874" /&gt;&lt;/a&gt;&lt;br /&gt;There are several things that need to be pointed out about this modified query. First, take note of the subquery; it is the key that tells the system we want to keep only the most current record for each &lt;i&gt;&lt;b&gt;PIDM, POSITION, and SUFFIX&lt;/i&gt;&lt;/b&gt; combination. Hence, as the results show, the query shows both the most recent terminated AND active records. Additionally, note that the subquery contains only one table (NBRJOBS with the TABLE2 alias). The fact that two "instances" of the same table are referencing each other is what makes the subquery "correlated". An alias for at least one of the two tables is required, otherwise the system has no way to tell them apart. Now that we have excluded the result set, we can eliminate the individual's terminated positions by simply adding that condition to the MAIN QUERY'S WHERE CLAUSE (not the subquery) as follows:&lt;br /&gt;&lt;br /&gt;&lt;a href="http://4.bp.blogspot.com/_uwm8xA2XiTg/SaiHJ5-uHbI/AAAAAAAAABw/tkoHVdFppPE/s1600-h/Query3.png"&gt;&lt;img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 400px; height: 145px;" src="http://4.bp.blogspot.com/_uwm8xA2XiTg/SaiHJ5-uHbI/AAAAAAAAABw/tkoHVdFppPE/s400/Query3.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5307640765108592050" /&gt;&lt;/a&gt;&lt;br /&gt;&lt;a href="http://2.bp.blogspot.com/_uwm8xA2XiTg/SaiHQV0u8II/AAAAAAAAAB4/vCL8PPEoq9w/s1600-h/JobsPic4.png"&gt;&lt;img style="display:block; margin:0px auto 10px; text-align:center;cursor:pointer; cursor:hand;width: 400px; height: 23px;" src="http://2.bp.blogspot.com/_uwm8xA2XiTg/SaiHQV0u8II/AAAAAAAAAB4/vCL8PPEoq9w/s400/JobsPic4.png" border="0" alt=""id="BLOGGER_PHOTO_ID_5307640875662110850" /&gt;&lt;/a&gt;&lt;br /&gt;Alright, sit back and take a deep breath, I know a whole new world of SQL querying has been opened to you:). Now, it's time to put it to practice. Using the script below, test yourself by running it on your system to get a list of student pidms who have more than one record on SGBSTDN. Then, write a script that will get ALL of one student's records based on one of the PIDMs in the list using the techniques described above. If you need help or want to compare answers, you can get my solution to this vexing problem in the first comment on this blog entry, but don't&lt;br /&gt;be tempted to cheat!!!:)&lt;br /&gt;&lt;br /&gt;SELECT SGBSTDN_PIDM, COUNT(*)&lt;br /&gt;  FROM SGBSTDN &lt;br /&gt;HAVING COUNT(*) &gt; 1&lt;br /&gt; ORDER BY SPRIDEN_PIDM&lt;br /&gt;&lt;br /&gt;I hope this has been helpful, let me know what you think!!!&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8760686654156942303-1259930561778231159?l=bannerreporting.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://bannerreporting.blogspot.com/feeds/1259930561778231159/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://bannerreporting.blogspot.com/2009/02/effective-dating-and-sub-queries.html#comment-form' title='1 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8760686654156942303/posts/default/1259930561778231159'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8760686654156942303/posts/default/1259930561778231159'/><link rel='alternate' type='text/html' href='http://bannerreporting.blogspot.com/2009/02/effective-dating-and-sub-queries.html' title='Effective Dating and Sub Queries'/><author><name>Zach Heath</name><uri>http://www.blogger.com/profile/09253162165797370040</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><media:thumbnail xmlns:media='http://search.yahoo.com/mrss/' url='http://2.bp.blogspot.com/_uwm8xA2XiTg/SaiDirjF22I/AAAAAAAAABA/s0bd8-can1k/s72-c/JobsPic1.bmp' height='72' width='72'/><thr:total>1</thr:total></entry><entry><id>tag:blogger.com,1999:blog-8760686654156942303.post-7967076691402238256</id><published>2009-02-23T19:00:00.000-08:00</published><updated>2009-03-04T10:04:30.573-08:00</updated><title type='text'>Getting Started in Banner Reporting</title><content type='html'>The first thing one must understand before even thinking about building a report is the underlying data structure from which one must extract the information required. In Baseline Banner, there are over 4,000 objects that store various pieces of data. The Operational Data Store (ODS) and Enterprise Data Warehouse (EDW), while they have far fewer objects, contain their own set of storage objects, all of which one must understand before being able to produce a valid and accurate report. So, with close to 5,000 database objects to choose from (assuming one has access to both the ODS and EDW), how does one figure out where to get the data they need? Well, the goal of this posting is to give you a good idea of where to start.&lt;br /&gt;&lt;br /&gt;&lt;h3&gt;Deciding where to get your data.&lt;/h3&gt;&lt;br /&gt;As mentioned above, SunGard provides three areas from which you can draw information for reporting: &lt;a href="#Baseline"&gt;Baseline&lt;/a&gt;, &lt;a href="#ODS"&gt;the ODS&lt;/a&gt;, and &lt;a href="#EDW"&gt;the EDW&lt;/a&gt;. Below, I have included a blurb about why you might choose to use one over another. Happy hunting!&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;a name="Baseline"&gt;Baseline&lt;/a&gt;&lt;/b&gt; is the database architecture that everyone typically thinks of when they say "Banner". It is the thing that stores the hundreds (if not thousands) of transactions that occur daily at your institution and what is typically used for reporting (at least until recently). The key question when deciding to use Baseline vs. the ODS for standard reporting is whether or not the report needs to contain real-time data. The ODS is cloned nightly so any reports drawing data from it are inherently a day old. For things like up-to-the-minute enrollment statistics and current financial statements, Baseline may be required.&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;a name="ODS"&gt;The Operational Data Store (ODS)&lt;/a&gt;&lt;/b&gt; is SunGard's answer to the difficulties that arise when using Baseline for reporting. It is a series of &lt;a href="http://en.wikipedia.org/wiki/View_(database)"&gt;database views&lt;/a&gt; that "flatten" the information stored in Banner Baseline. This makes getting a report of logically related information much easier as one can get all the required information from one reporting view instead of having to grab disparate pieces of data from multiple sources (in the case of Baseline, &lt;a href="http://en.wikipedia.org/wiki/Table_(database)"&gt;database tables&lt;/a&gt;).&lt;br /&gt;&lt;br /&gt;&lt;b&gt;&lt;a name="EDW"&gt;The Enterprise Data Warehouse (EDW)&lt;/a&gt;&lt;/b&gt; is SunGard's solution for analytical reporting. There is a new movement in &lt;a href="http://en.wikipedia.org/wiki/Business_intelligence"&gt;business intelligence&lt;/a&gt; circles towards more dynamic, analytical, database tools for reporting that can be used for real decisions. This new area of reporting has requirements that are completely different from "standard" reporting: aggregated information, data organized in ways that make it easier to analyze, and most importantly, historical data (used for trend analysis).&lt;br /&gt;&lt;br /&gt;&lt;h3&gt;Source ID'd, now for the detail!&lt;/h3&gt;&lt;br /&gt;So, you've figured out the source for your report. Now you have to find the individual objects (tables and/or views) and even column fields within the object that contain the data you need. As with the section above, this one is organized by source, so if you're looking for more information on reporting against Baseline, &lt;a href="#BaselineDetail"&gt;click here&lt;/a&gt;, the ODS, &lt;a href="#ODSDetail"&gt;click here&lt;/a&gt;, and the EDW, &lt;a href="#EDWDetail"&gt;click here&lt;/a&gt;.&lt;br /&gt;&lt;br /&gt;&lt;a name="BaselineDetail"&gt;&lt;h3&gt;Baseline Detail&lt;/h3&gt;&lt;/a&gt;&lt;br /&gt;SunGard uses a naming convention for Baseline objects that is not very useful EXCEPT in the following ways:&lt;br /&gt;&lt;ol&gt;&lt;li&gt;Object names are always 7 characters long&lt;br /&gt;&lt;li&gt;The first letter corresponds to the module&lt;br /&gt;&lt;br /&gt;&lt;ul type="square"&gt;&lt;li&gt;S = Saturn (Student Related Information)&lt;br /&gt;&lt;li&gt;G = General (Information Common Across Modules)&lt;br /&gt;&lt;li&gt;F = Finance&lt;br /&gt;&lt;li&gt;R = Financial Aid&lt;br /&gt;&lt;li&gt;T = Student Accounts&lt;br /&gt;&lt;li&gt;N = Position Control (HR Information)&lt;br /&gt;&lt;li&gt;P = Payroll&lt;br /&gt;&lt;li&gt;A = Advancement (Alumni and Development)&lt;br /&gt;&lt;/ul&gt;&lt;br /&gt;&lt;li&gt;The second two letters if "TV" identify a validation table. So, if you're looking for a validation table with academic term (student information), the first three letters of the object will be "STV = S - Saturn, TV - Validation Table"&lt;br /&gt;&lt;li&gt;The last four characters are a little harder to nail down, but the majority of the time they indicate the type of data stored. For example, the last four characters for the validation table for academic term (as discussed above) are "TERM". Put that together with #3 and you get STVTERM. Likewise, the validation table for student major is STVMAJR, and for financial accounts, FTVACCT.&lt;br /&gt;&lt;li&gt;Another little nugget of information is in regards to the third character. If it's an R, that means that the object has "repeating" records. That means that there is more than one record per person or other key data element. For example, SF&lt;b&gt;R&lt;/b&gt;STCR is the repeating table for course registration information (S=Student, R=Repeating, STCR=STudent Course Registration), and contains a record for each course a student is taking (the student is repeated). On the other Hand, if the third character is a B, that means it's a base table and will contain NO repeating rows for the key data element. NBBPOSN is the base table for position information and contains only one record per position and suffix combination. Student, Employee, and Advancement all have a base table that one must be in to be considered each respective entity type:&lt;br /&gt;&lt;ul type="disc"&gt;&lt;br /&gt;&lt;li&gt;APBCONS - Contains a record for EVERY constitutent&lt;br /&gt;&lt;li&gt;PEBEMPL - Contains a record for EVERY employee&lt;br /&gt;&lt;li&gt;SGBSTDN - Contains a record for EVERY student&lt;br /&gt;&lt;/ul&gt;&lt;br /&gt;If an individual does not have a record in SGBSTDN, they are NOT considered a student, etc.&lt;br /&gt;&lt;/ol&gt;So with the above information, it is my hope that you will have the tools you need to get started reporting against Banner Baseline tables. Additionally, don't forget that the Banner Forms can give you hints regarding the source of the data they contain. To find those hints, click into a data field of interest, click "help", then "dynamic help query". The window that ensues contains the TABLE and the FIELD in which that data element is stored. There is a good chance that related information on the same form resides in the same table. Good luck!!!&lt;br /&gt;&lt;br /&gt;&lt;a name="ODSDetail"&gt;&lt;h3&gt;ODS Detail&lt;/h3&gt;&lt;/a&gt;&lt;br /&gt;The key to the ODS is the metadata provided by SunGard during the purchase process. To locate the documents associated with your institution's metadata, ask your Banner administrator(s). Depending on your starting point, there are a couple different ways to go about navigating the documents contained in the ODS metadata files:&lt;br /&gt;&lt;ol&gt;&lt;br /&gt;&lt;li&gt;If you know the source in Baseline, use the "source reports" to find its location in the ODS:&lt;br /&gt;&lt;ul type="circle"&gt;&lt;br /&gt;&lt;li&gt;On the main page, choose the module that contains the data from the links on the left side.&lt;br /&gt;&lt;li&gt;Scroll to the very bottom of the subsequent page where it says "Source Reports".&lt;br /&gt;&lt;li&gt;In the list of source reports, locate the name of the table in which the data element in question is stored in baseline and click its link.&lt;br /&gt;&lt;/ul&gt;&lt;br /&gt;&lt;li&gt;If you don't have the starting point specified above, start in the ODS Concept Diagrams provided in the ODS metadata:&lt;br /&gt;&lt;ul type="square"&gt;&lt;br /&gt;&lt;li&gt;Starting on the main page of the metadata, click the link to the concept diagrams at the bottom center of the page.&lt;br /&gt;&lt;li&gt;Using the menu on the left, navigate through the different functional modules and select areas related to the data for which you are looking.&lt;br /&gt;&lt;/ul&gt;&lt;br /&gt;&lt;/ol&gt;&lt;br /&gt;&lt;a name="EDWDetail"&gt;&lt;h3&gt;EDW Detail&lt;/h3&gt;&lt;/a&gt;&lt;br /&gt;As I mentioned above, the EDW is built with a completely different reporting methodology in mind. As such, it is organized in what are referred to as &lt;a href="http://en.wikipedia.org/wiki/Star_schema"&gt;"star schemas"&lt;/a&gt;. These schemas are designed to make reporting aggregated, measured data easier. As shown in the article linked above, star schemas are organized in what are known as "fact" and "dimension" tables. The fact tables contain the "measures" while the dimensions contain the "buckets" by which you wish to measure the "measures":). Clear as mud, I know. So, what can &lt;i&gt;you&lt;/i&gt; do with this information? Well, first of all, you should only be using the EDW if you are trying to report measured data. If that's the case, there is a series of metadata documents (very similar to the &lt;a href="#ODSDetail"&gt;ODS metadata&lt;/a&gt;) that will help in figuring out where to get the data you require.&lt;div class="blogger-post-footer"&gt;&lt;img width='1' height='1' src='https://blogger.googleusercontent.com/tracker/8760686654156942303-7967076691402238256?l=bannerreporting.blogspot.com' alt='' /&gt;&lt;/div&gt;</content><link rel='replies' type='application/atom+xml' href='http://bannerreporting.blogspot.com/feeds/7967076691402238256/comments/default' title='Post Comments'/><link rel='replies' type='text/html' href='http://bannerreporting.blogspot.com/2009/02/getting-started-in-banner-reporting.html#comment-form' title='0 Comments'/><link rel='edit' type='application/atom+xml' href='http://www.blogger.com/feeds/8760686654156942303/posts/default/7967076691402238256'/><link rel='self' type='application/atom+xml' href='http://www.blogger.com/feeds/8760686654156942303/posts/default/7967076691402238256'/><link rel='alternate' type='text/html' href='http://bannerreporting.blogspot.com/2009/02/getting-started-in-banner-reporting.html' title='Getting Started in Banner Reporting'/><author><name>Zach Heath</name><uri>http://www.blogger.com/profile/09253162165797370040</uri><email>noreply@blogger.com</email><gd:image rel='http://schemas.google.com/g/2005#thumbnail' width='16' height='16' src='http://img2.blogblog.com/img/b16-rounded.gif'/></author><thr:total>0</thr:total></entry></feed>
