{"id":91,"date":"2025-11-04T18:38:08","date_gmt":"2025-11-04T18:38:08","guid":{"rendered":"http:\/\/vargas-solar.com\/data-ware-lake-houses-bi\/?page_id=91"},"modified":"2025-11-04T18:53:26","modified_gmt":"2025-11-04T18:53:26","slug":"olap","status":"publish","type":"page","link":"https:\/\/vargas-solar.com\/data-ware-lake-houses-bi\/olap\/","title":{"rendered":"First touch into OLAP"},"content":{"rendered":"\n<h1 class=\"wp-block-heading\">Student Handout \u2014 OLAP Ludic Exercise + Snowflake DW<\/h1>\n\n\n\n<h3 class=\"wp-block-heading has-medium-font-size\">Context<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">This hands-on exercise bridges the tangible and the analytical dimensions of OLAP learning. Students begin by&nbsp;<em>building and manipulating a physical cube<\/em>&nbsp;using moulding paste to represent three analytical dimensions \u2014&nbsp;<strong>Product<\/strong>,&nbsp;<strong>Region<\/strong>, and&nbsp;<strong>Time<\/strong>&nbsp;\u2014 exploring how slicing, dicing, roll-ups, and drill-downs modify the cube. This tactile step supports conceptual understanding of multidimensional aggregation before transitioning to the digital environment.&nbsp;<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In the second part, students&nbsp;<em>instantiate the same cube virtually<\/em>&nbsp;by creating and populating a&nbsp;<strong>snowflake data warehouse schema<\/strong>&nbsp;in PostgreSQL. They then run advanced SQL queries&nbsp;(<code>GROUPING SETS<\/code>,&nbsp;<code>ROLLUP<\/code>,&nbsp;<code>CUBE<\/code>,&nbsp;and window functions) to compute summaries, growth indicators, and category shares.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Learning Objective<\/em>: Practice advanced SQL aggregation and analytics techniques (GROUPING SETS, ROLLUP, CUBE, and window functions) on a dimensional model.<\/p>\n\n\n\n<h3 class=\"wp-block-heading has-medium-font-size\">Material<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Modelling paste <\/li>\n\n\n\n<li>Spatula<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading has-medium-font-size\">Part I: Build an OLAP Cube with moulding paste (Product \u00d7 Region \u00d7 Time). Then map each physical operation to an SQL statement<\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Consider the DW setting draw the snowflake schema of the proposed DW.<\/li>\n\n\n\n<li>Using the modelling paste sticks of different colours, model the smallest cube that can be materialised of the snowflake schema and using the data used in the DW setting.<\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-image size-large is-resized\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"1024\" src=\"http:\/\/vargas-solar.com\/data-ware-lake-houses-bi\/wp-content\/uploads\/sites\/64\/2025\/11\/image-1024x1024.png\" alt=\"\" class=\"wp-image-92\" style=\"width:391px;height:auto\" srcset=\"https:\/\/vargas-solar.com\/data-ware-lake-houses-bi\/wp-content\/uploads\/sites\/64\/2025\/11\/image-1024x1024.png 1024w, https:\/\/vargas-solar.com\/data-ware-lake-houses-bi\/wp-content\/uploads\/sites\/64\/2025\/11\/image-300x300.png 300w, https:\/\/vargas-solar.com\/data-ware-lake-houses-bi\/wp-content\/uploads\/sites\/64\/2025\/11\/image-150x150.png 150w, https:\/\/vargas-solar.com\/data-ware-lake-houses-bi\/wp-content\/uploads\/sites\/64\/2025\/11\/image-768x768.png 768w, https:\/\/vargas-solar.com\/data-ware-lake-houses-bi\/wp-content\/uploads\/sites\/64\/2025\/11\/image.png 1200w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><em>Figure 1. OLAP Cube representing Product \u00d7 Region \u00d7 Time dimensions and aggregation operations (slice, dice, drill-down, roll-up).<\/em><\/p>\n\n\n\n<h3 class=\"wp-block-heading has-medium-font-size\"><strong>Part II \u2014 Snowflake DW: Build, Populate, Query<\/strong><\/h3>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Use the schema diagram and the provided DDL to create dimension tables and the fact table. Populate with the sample inserts and run the OLAP queries. Take a photo<\/li>\n\n\n\n<li>Using your initial cube and the remaining modelling paste, illustrate the results of applying OLAP operators to the cube. For every result, take a photo.<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading has-medium-font-size\">OLAP Queries to Try<\/h3>\n\n\n\n<h4 class=\"wp-block-heading\"><strong><em>Setting up a querying environment<\/em><\/strong><\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">There are two alternatives to have access to externalized PostgreSQL SQL environments with no installation burden required:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Aurora PostgreSQL or Aurora MySQL (version \u22658)&nbsp;<a href=\"https:\/\/sqliteonline.com\/\">https:\/\/sqliteonline.com<\/a>\n<ul class=\"wp-block-list\">\n<li>Create an account (attention!&nbsp;<strong>limited number<\/strong>&nbsp;of queries allowed! so avoid testing too much).<\/li>\n\n\n\n<li>Choose the PostgreSQL option.<\/li>\n\n\n\n<li>DW setting in PostgreSQL available here:\u00a0<a href=\"https:\/\/drive.google.com\/file\/d\/1Togs0mZytWXhtvmvCMjDJ1rue7ttiTZD\/view?usp=sharing\">https:\/\/drive.google.com\/file\/d\/1Togs0mZytWXhtvmvCMjDJ1rue7ttiTZD\/view?usp=sharing<\/a><\/li>\n\n\n\n<li>The SQL queries to test script is available here:\u00a0<a href=\"https:\/\/drive.google.com\/file\/d\/1FxAvalT0rLNtrOt1aTthMPVTEqVWYKEd\/view?usp=sharing\">https:\/\/drive.google.com\/file\/d\/1FxAvalT0rLNtrOt1aTthMPVTEqVWYKEd\/view?usp=sharing<\/a><\/li>\n<\/ul>\n<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">2. PostgreSQL externalized cloud (<strong>recommended<\/strong>)<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Create a&nbsp;<strong>free trial<\/strong>&nbsp;account on the PostgreSQL cloud&nbsp;<a href=\"https:\/\/aiven.io\/\">https:\/\/aiven.io<\/a><\/li>\n\n\n\n<li>Create a PostgreSQL instance&nbsp;<\/li>\n\n\n\n<li>Access from Python<\/li>\n\n\n\n<li>Open in Colab the following GIST:&nbsp;<a href=\"https:\/\/gist.github.com\/gevargas\/17cebe04b147553df81abd4931eee944\">https:\/\/gist.github.com\/gevargas\/17cebe04b147553df81abd4931eee944<\/a><\/li>\n\n\n\n<li>Use the connection information provided by your Aiven instance to connect from the notebook.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><strong><em>Analyse and test the following query expressions<\/em><\/strong><\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Q1 \u2013 Multilevel Summary with GROUPING SETS<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">This query calculates total sales revenue at different aggregation levels: by Year + Quarter + Region, by Year + Region, and by Year only.&nbsp;GROUPING SETS&nbsp;lets you produce all these summaries in one query instead of running several&nbsp;GROUP BYstatements.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">SELECT<br>&nbsp;&nbsp;dt.year,<br>&nbsp;&nbsp;dt.quarter,<br>&nbsp;&nbsp;dr.region_name,<br>&nbsp;&nbsp;SUM(fs.revenue) AS revenue<br>FROM fact_sales fs<br>JOIN dim_time&nbsp;&nbsp;&nbsp;dt ON fs.time_id = dt.time_id<br>JOIN dim_region dr ON fs.region_id = dr.region_id<br>GROUP BY GROUPING SETS (<br>&nbsp;&nbsp;(dt.year, dt.quarter, dr.region_name),<br>&nbsp;&nbsp;(dt.year, dr.region_name),<br>&nbsp;&nbsp;(dt.year)<br>)<br>ORDER BY dt.year, dt.quarter NULLS LAST, dr.region_name NULLS LAST;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Q2 \u2013 Hierarchical Rollup by Quarter and Category<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">This query computes total revenue for each quarter and category, including subtotals per quarter and a grand total. The&nbsp;ROLLUP&nbsp;operator automatically adds &#8216;All categories&#8217; and &#8216;All quarters&#8217; rows.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">SELECT<br>&nbsp;&nbsp;dt.quarter,<br>&nbsp;&nbsp;dc.category_name,<br>&nbsp;&nbsp;SUM(fs.revenue) AS revenue<br>FROM fact_sales fs<br>JOIN dim_time&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dt ON fs.time_id = dt.time_id<br>JOIN dim_product&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dp ON fs.product_id = dp.product_id<br>JOIN dim_subcategory ds ON dp.subcategory_id = ds.subcategory_id<br>JOIN dim_category&nbsp;&nbsp;&nbsp;&nbsp;dc ON ds.category_id = dc.category_id<br>GROUP BY ROLLUP (dt.quarter, dc.category_name)<br>ORDER BY dt.quarter NULLS LAST, dc.category_name NULLS LAST;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Q3 \u2013 Two-Dimensional Cube of Region \u00d7 Channel<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">This builds a cross-tab combining regions and sales channels. The&nbsp;CUBE&nbsp;operator creates totals per region, totals per channel, and a global total.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">SELECT<br>&nbsp;&nbsp;dr.region_name,<br>&nbsp;&nbsp;ch.channel_name,<br>&nbsp;&nbsp;SUM(fs.revenue) AS revenue<br>FROM fact_sales fs<br>JOIN dim_region dr ON fs.region_id = dr.region_id<br>JOIN dim_channel ch ON fs.channel_id = ch.channel_id<br>GROUP BY CUBE (dr.region_name, ch.channel_name)<br>ORDER BY dr.region_name NULLS LAST, ch.channel_name NULLS LAST;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Q4 \u2013 Top-3 Products per Region and Quarter<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">Using window functions, this query ranks products by total revenue within each region and quarter.&nbsp;DENSE_RANK()&nbsp;assigns ranks, and we filter to keep only the top three per group.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">WITH revenue_p AS (<br>&nbsp;&nbsp;SELECT dr.region_name, dt.year, dt.quarter, dp.name AS product_name,<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SUM(fs.revenue) AS revenue<br>&nbsp;&nbsp;FROM fact_sales fs<br>&nbsp;&nbsp;JOIN dim_region dr ON fs.region_id = dr.region_id<br>&nbsp;&nbsp;JOIN dim_time&nbsp;&nbsp;&nbsp;dt ON fs.time_id = dt.time_id<br>&nbsp;&nbsp;JOIN dim_product dp ON fs.product_id = dp.product_id<br>&nbsp;&nbsp;GROUP BY dr.region_name, dt.year, dt.quarter, dp.name<br>),<br>ranked AS (<br>&nbsp;&nbsp;SELECT *, DENSE_RANK() OVER (PARTITION BY region_name, year, quarter ORDER BY revenue DESC) AS rnk<br>&nbsp;&nbsp;FROM revenue_p<br>)<br>SELECT region_name, year, quarter, product_name, revenue, rnk<br>FROM ranked<br>WHERE rnk &lt;= 3<br>ORDER BY region_name, year, quarter, rnk;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Q5 \u2013 Quarter-over-Quarter (QoQ) Growth<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">This measures how revenue changes from one quarter to the next for each region and category.&nbsp;LAG()&nbsp;retrieves the previous quarter\u2019s value to compute absolute and percentage change.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">WITH base AS (<br>&nbsp;&nbsp;SELECT dr.region_name, dc.category_name, dt.year, dt.quarter,<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SUM(fs.revenue) AS revenue<br>&nbsp;&nbsp;FROM fact_sales fs<br>&nbsp;&nbsp;JOIN dim_region dr ON fs.region_id = dr.region_id<br>&nbsp;&nbsp;JOIN dim_time&nbsp;&nbsp;&nbsp;dt ON fs.time_id = dt.time_id<br>&nbsp;&nbsp;JOIN dim_product dp ON fs.product_id = dp.product_id<br>&nbsp;&nbsp;JOIN dim_subcategory ds ON dp.subcategory_id = ds.subcategory_id<br>&nbsp;&nbsp;JOIN dim_category dc ON ds.category_id = dc.category_id<br>&nbsp;&nbsp;GROUP BY dr.region_name, dc.category_name, dt.year, dt.quarter<br>)<br>SELECT region_name, category_name, year, quarter, revenue,<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;revenue &#8211; LAG(revenue) OVER (PARTITION BY region_name, category_name ORDER BY year, quarter) AS qoq_change,<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;CASE WHEN LAG(revenue) OVER (PARTITION BY region_name, category_name ORDER BY year, quarter) = 0<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;THEN NULL<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ELSE ROUND(100.0 * (revenue &#8211; LAG(revenue) OVER (PARTITION BY region_name, category_name ORDER BY year, quarter)) \/<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;NULLIF(LAG(revenue) OVER (PARTITION BY region_name, category_name ORDER BY year, quarter),0), 2)<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;END AS qoq_pct<br>FROM base<br>ORDER BY region_name, category_name, year, quarter;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Q6 \u2013 Year-to-Date (YTD) Running Total<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">This tracks cumulative revenue for each category within a year.&nbsp;SUM() OVER (&#8230;)&nbsp;adds up sales month by month to show how revenue accumulates.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">WITH monthly AS (<br>&nbsp;&nbsp;SELECT dt.year, dt.month, dc.category_name,<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SUM(fs.revenue) AS revenue<br>&nbsp;&nbsp;FROM fact_sales fs<br>&nbsp;&nbsp;JOIN dim_time&nbsp;&nbsp;&nbsp;dt ON fs.time_id = dt.time_id<br>&nbsp;&nbsp;JOIN dim_product dp ON fs.product_id = dp.product_id<br>&nbsp;&nbsp;JOIN dim_subcategory ds ON dp.subcategory_id = ds.subcategory_id<br>&nbsp;&nbsp;JOIN dim_category dc ON ds.category_id = dc.category_id<br>&nbsp;&nbsp;GROUP BY dt.year, dt.month, dc.category_name<br>)<br>SELECT year, month, category_name, revenue,<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SUM(revenue) OVER (PARTITION BY year, category_name ORDER BY month<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS ytd_revenue<br>FROM monthly<br>ORDER BY year, month, category_name;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Q7 \u2013 Category Market Share<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">This calculates how much each category contributes to total sales within each region and quarter, showing each category\u2019s share as a percentage of regional revenue.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">WITH base AS (<br>&nbsp;&nbsp;SELECT dr.region_name, dt.year, dt.quarter, dc.category_name,<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SUM(fs.revenue) AS revenue<br>&nbsp;&nbsp;FROM fact_sales fs<br>&nbsp;&nbsp;JOIN dim_region dr ON fs.region_id = dr.region_id<br>&nbsp;&nbsp;JOIN dim_time&nbsp;&nbsp;&nbsp;dt ON fs.time_id = dt.time_id<br>&nbsp;&nbsp;JOIN dim_product dp ON fs.product_id = dp.product_id<br>&nbsp;&nbsp;JOIN dim_subcategory ds ON dp.subcategory_id = ds.subcategory_id<br>&nbsp;&nbsp;JOIN dim_category dc ON ds.category_id = dc.category_id<br>&nbsp;&nbsp;GROUP BY dr.region_name, dt.year, dt.quarter, dc.category_name<br>),<br>tot AS (<br>&nbsp;&nbsp;SELECT region_name, year, quarter, SUM(revenue) AS total_rev<br>&nbsp;&nbsp;FROM base<br>&nbsp;&nbsp;GROUP BY region_name, year, quarter<br>)<br>SELECT b.region_name, b.year, b.quarter, b.category_name, b.revenue,<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ROUND(100.0 * b.revenue \/ t.total_rev, 2) AS pct_share<br>FROM base b<br>JOIN tot&nbsp;&nbsp;t USING (region_name, year, quarter)<br>ORDER BY b.region_name, b.year, b.quarter, b.category_name;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Q8 \u2013 Channel Mix per Quarter<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">For the year 2025, this query measures what percentage of sales came from each channel (Online, Store) within every quarter.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">WITH q AS (<br>&nbsp;&nbsp;SELECT dt.quarter, ch.channel_name, SUM(fs.revenue) AS revenue<br>&nbsp;&nbsp;FROM fact_sales fs<br>&nbsp;&nbsp;JOIN dim_time&nbsp;&nbsp;&nbsp;dt ON fs.time_id = dt.time_id<br>&nbsp;&nbsp;JOIN dim_channel ch ON fs.channel_id = ch.channel_id<br>&nbsp;&nbsp;WHERE dt.year = 2025<br>&nbsp;&nbsp;GROUP BY dt.quarter, ch.channel_name<br>),<br>tot AS (<br>&nbsp;&nbsp;SELECT quarter, SUM(revenue) AS total_rev FROM q GROUP BY quarter<br>)<br>SELECT q.quarter, q.channel_name, q.revenue,<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;ROUND(100.0 * q.revenue \/ t.total_rev, 2) AS pct_in_quarter<br>FROM q JOIN tot t USING (quarter)<br>ORDER BY q.quarter, q.channel_name;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Q9 \u2013 Detect Revenue Declines<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">This identifies all region\u2013category pairs that experienced at least one drop in revenue between consecutive quarters.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">WITH series AS (<br>&nbsp;&nbsp;SELECT dr.region_name, dc.category_name, dt.year, dt.quarter,<br>&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;SUM(fs.revenue) AS revenue<br>&nbsp;&nbsp;FROM fact_sales fs<br>&nbsp;&nbsp;JOIN dim_region dr ON fs.region_id = dr.region_id<br>&nbsp;&nbsp;JOIN dim_time&nbsp;&nbsp;&nbsp;dt ON fs.time_id = dt.time_id<br>&nbsp;&nbsp;JOIN dim_product dp ON fs.product_id = dp.product_id<br>&nbsp;&nbsp;JOIN dim_subcategory ds ON dp.subcategory_id = ds.subcategory_id<br>&nbsp;&nbsp;JOIN dim_category dc ON ds.category_id = dc.category_id<br>&nbsp;&nbsp;GROUP BY dr.region_name, dc.category_name, dt.year, dt.quarter<br>),<br>lagged AS (<br>&nbsp;&nbsp;SELECT *, LAG(revenue) OVER (PARTITION BY region_name, category_name ORDER BY year, quarter) AS prev_rev<br>&nbsp;&nbsp;FROM series<br>)<br>SELECT DISTINCT region_name, category_name<br>FROM lagged<br>WHERE prev_rev IS NOT NULL AND revenue &lt; prev_rev<br>ORDER BY region_name, category_name;<\/p>\n\n\n\n<h4 class=\"wp-block-heading\">Q10 \u2013 Filtered Rollup<\/h4>\n\n\n\n<p class=\"wp-block-paragraph\">This variation of&nbsp;ROLLUP&nbsp;filters results to show only groups with total revenue greater than 200.&nbsp;COALESCE&nbsp;replaces&nbsp;NULLtotals with zero for safe comparison.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">SELECT dt.year, dc.category_name, SUM(fs.revenue) AS revenue<br>FROM fact_sales fs<br>JOIN dim_time&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dt ON fs.time_id = dt.time_id<br>JOIN dim_product&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;dp ON fs.product_id = dp.product_id<br>JOIN dim_subcategory ds ON dp.subcategory_id = ds.subcategory_id<br>JOIN dim_category&nbsp;&nbsp;&nbsp;&nbsp;dc ON ds.category_id = dc.category_id<br>GROUP BY ROLLUP (dt.year, dc.category_name)<br>HAVING COALESCE(SUM(fs.revenue),0) &gt; 200<br>ORDER BY dt.year NULLS LAST, dc.category_name NULLS LAST;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Student Handout \u2014 OLAP Ludic Exercise + Snowflake DW Context This hands-on exercise bridges the tangible and the analytical dimensions of OLAP learning. Students begin by&nbsp;building and manipulating a physical cube&nbsp;using moulding paste to represent three analytical dimensions \u2014&nbsp;Product,&nbsp;Region, and&nbsp;Time&nbsp;\u2014 &hellip; <a href=\"https:\/\/vargas-solar.com\/data-ware-lake-houses-bi\/olap\/\">Continue reading <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":11,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-91","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/vargas-solar.com\/data-ware-lake-houses-bi\/wp-json\/wp\/v2\/pages\/91","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/vargas-solar.com\/data-ware-lake-houses-bi\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/vargas-solar.com\/data-ware-lake-houses-bi\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/vargas-solar.com\/data-ware-lake-houses-bi\/wp-json\/wp\/v2\/users\/11"}],"replies":[{"embeddable":true,"href":"https:\/\/vargas-solar.com\/data-ware-lake-houses-bi\/wp-json\/wp\/v2\/comments?post=91"}],"version-history":[{"count":6,"href":"https:\/\/vargas-solar.com\/data-ware-lake-houses-bi\/wp-json\/wp\/v2\/pages\/91\/revisions"}],"predecessor-version":[{"id":105,"href":"https:\/\/vargas-solar.com\/data-ware-lake-houses-bi\/wp-json\/wp\/v2\/pages\/91\/revisions\/105"}],"wp:attachment":[{"href":"https:\/\/vargas-solar.com\/data-ware-lake-houses-bi\/wp-json\/wp\/v2\/media?parent=91"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}