Showing posts with label views. Show all posts
Showing posts with label views. Show all posts

06 December 2025

💎💫SQL Reloaded: Schema Differences between Database Versions - Part I: INFORMATION_SCHEMA version

During data migrations and other similar activities it's important to check what changed in the database at the various levels. Usually, it's useful to check when schemas, object names or table definitions changed, even if the changes are thoroughly documented. One can write a script to point out all the differences in one output, though it's recommended to check the differences at each level of detail

For this purpose one can use the INFORMATION_SCHEMA available for many of the RDBMS implementing it. This allows to easily port the scripts between platforms. The below queries were run on SQL Server 2025 in combination with Dynamics 365 schemas, though they should run on the earlier versions, incl. (Azure) SQL Databases. 

Such comparisons must be done from the both sides, this implying a FULL OUTER JOIN when writing a single SELECT statement, however the results can become easily hard to read and even interpret when the number of columns in output increases. Therefore, it's recommended to keep the number of columns at a minimum while addressing the scope, respectively break the FULL OUTER JOIN in two LEFT JOINs.

The simplest check is at schema level, and this can be easily done from both sides (note that database names needed to be replaced accordingly):

-- difference schemas (objects not available in the new schema)
SELECT *
FROM ( -- comparison
	SELECT DB1.CATALOG_NAME
	, DB1.SCHEMA_NAME
	, DB1.SCHEMA_OWNER
	, DB1.DEFAULT_CHARACTER_SET_NAME
	, DB2.SCHEMA_OWNER NEW_SCHEMA_OWNER
	, DB2.DEFAULT_CHARACTER_SET_NAME NEW_DEFAULT_CHARACTER_SET_NAME
	, CASE 
		WHEN DB2.SCHEMA_NAME IS NULL THEN 'schema only in old db'
		WHEN DB1.SCHEMA_OWNER <> IsNull(DB2.SCHEMA_OWNER, '') THEN 'different table type'
	  END Comment
        , CASE WHEN DB1.DEFAULT_CHARACTER_SET_NAME <> DB2.DEFAULT_CHARACTER_SET_NAME THEN 'different character sets' END Character_sets
	FROM [old database_name].INFORMATION_SCHEMA.SCHEMATA DB1
	     LEFT JOIN [new database name].INFORMATION_SCHEMA.SCHEMATA DB2
	       ON DB1.SCHEMA_NAME = DB2.SCHEMA_NAME
 ) DAT
WHERE DAT.Comment IS NOT NULL
ORDER BY DAT.CATALOG_NAME
, DAT.SCHEMA_NAME


-- difference schemas (new objects)
SELECT *
FROM ( -- comparison
	SELECT DB1.CATALOG_NAME
	, DB1.SCHEMA_NAME
	, DB1.SCHEMA_OWNER
	, DB1.DEFAULT_CHARACTER_SET_NAME
	, DB2.SCHEMA_OWNER OLD_SCHEMA_OWNER
	, DB2.DEFAULT_CHARACTER_SET_NAME OLD_DEFAULT_CHARACTER_SET_NAME
	, CASE 
		WHEN DB2.SCHEMA_NAME IS NULL THEN 'schema only in old db'
		WHEN DB1.SCHEMA_OWNER <> IsNull(DB2.SCHEMA_OWNER, '') THEN 'different table type'
	  END Comment
        , CASE WHEN DB1.DEFAULT_CHARACTER_SET_NAME <> DB2.DEFAULT_CHARACTER_SET_NAME THEN 'different character sets' END Character_sets
	FROM [new database name].INFORMATION_SCHEMA.SCHEMATA DB1
	     LEFT JOIN [old database name].INFORMATION_SCHEMA.SCHEMATA DB2
	       ON DB1.SCHEMA_NAME = DB2.SCHEMA_NAME
 ) DAT
WHERE DAT.Comment IS NOT NULL
ORDER BY DAT.CATALOG_NAME
, DAT.SCHEMA_NAME

Comments:
1) The two queries can be easily combined via a UNION ALL, though it might be a good idea then to add a column to indicate the direction of the comparison. 

The next step would be to check which objects has been changed:

-- table-based objects only in the old schema (tables & views)
SELECT *
FROM ( -- comparison
	SELECT DB1.TABLE_CATALOG
	, DB1.TABLE_SCHEMA
	, DB1.TABLE_NAME
	, DB1.TABLE_TYPE
	, DB2.TABLE_CATALOG NEW_TABLE_CATALOG
	, DB2.TABLE_TYPE NEW_TABLE_TYPE
	, CASE 
		WHEN DB2.TABLE_NAME IS NULL THEN 'objects only in old db'
		WHEN DB1.TABLE_TYPE <> IsNull(DB2.TABLE_TYPE, '') THEN 'different table type'
		--WHEN DB1.TABLE_CATALOG <> IsNull(DB2.TABLE_CATALOG, '') THEN 'different table catalog'
	  END Comment
	FROM [old database name].INFORMATION_SCHEMA.TABLES DB1
	    LEFT JOIN [new database name].INFORMATION_SCHEMA.TABLES DB2
	      ON DB1.TABLE_SCHEMA = DB2.TABLE_SCHEMA
	     AND DB1.TABLE_NAME = DB2.TABLE_NAME
 ) DAT
WHERE DAT.Comment IS NOT NULL
ORDER BY DAT.TABLE_SCHEMA
, DAT.TABLE_NAME

Comments:
1) If the database was imported under another name, then the TABLE_CATALOG will have different values as well.

At column level, the query increases in complexity, given the many aspects that must be considered:

-- difference columns (columns not available in the new scheam, respectively changes in definitions)
SELECT *
FROM ( -- comparison
	SELECT DB1.TABLE_CATALOG
	, DB1.TABLE_SCHEMA
	, DB1.TABLE_NAME
	, DB1.COLUMN_NAME 
	, DB2.TABLE_CATALOG NEW_TABLE_CATALOG
	, CASE WHEN DB2.TABLE_NAME IS NULL THEN 'column only in old db' END Comment
	, DB1.DATA_TYPE
	, DB2.DATA_TYPE NEW_DATA_TYPE
	, CASE WHEN DB2.TABLE_NAME IS NOT NULL AND IsNull(DB1.DATA_TYPE, '') <> IsNull(DB2.DATA_TYPE, '') THEN 'Yes' END Different_data_type
	, DB1.CHARACTER_MAXIMUM_LENGTH
	, DB2.CHARACTER_MAXIMUM_LENGTH NEW_CHARACTER_MAXIMUM_LENGTH
	, CASE WHEN DB2.TABLE_NAME IS NOT NULL AND IsNull(DB1.CHARACTER_MAXIMUM_LENGTH, '') <> IsNull(DB2.CHARACTER_MAXIMUM_LENGTH, '') THEN 'Yes' END Different_maximum_length
	, DB1.NUMERIC_PRECISION
	, DB2.NUMERIC_PRECISION NEW_NUMERIC_PRECISION
	, CASE WHEN DB2.TABLE_NAME IS NOT NULL AND IsNull(DB1.NUMERIC_PRECISION, '') <> IsNull(DB2.NUMERIC_PRECISION, '') THEN 'Yes' END Different_numeric_precision
	, DB1.NUMERIC_SCALE
	, DB2.NUMERIC_SCALE NEW_NUMERIC_SCALE
	, CASE WHEN DB2.TABLE_NAME IS NOT NULL AND IsNull(DB1.NUMERIC_SCALE, '') <> IsNull(DB2.NUMERIC_SCALE,'') THEN 'Yes' END Different_numeric_scale
	, DB1.CHARACTER_SET_NAME
	, DB2.CHARACTER_SET_NAME NEW_CHARACTER_SET_NAME
	, CASE WHEN DB2.TABLE_NAME IS NOT NULL AND IsNull(DB1.CHARACTER_SET_NAME, '') <> IsNull(DB2.CHARACTER_SET_NAME, '') THEN 'Yes' END Different_character_set_name 
	, DB1.COLLATION_NAME
	, DB2.COLLATION_NAME NEW_COLLATION_NAME
	, CASE WHEN DB2.TABLE_NAME IS NOT NULL AND IsNull(DB1.COLLATION_NAME, '') <> IsNull(DB2.COLLATION_NAME, '') THEN 'Yes' END Different_collation_name
	, DB1.ORDINAL_POSITION
	, DB2.ORDINAL_POSITION NEW_ORDINAL_POSITION
	, DB1.COLUMN_DEFAULT
	, DB2.COLUMN_DEFAULT NEW_COLUMN_DEFAULT
	, DB1.IS_NULLABLE
	, DB2.IS_NULLABLE NEW_IS_NULLABLE
	FROM [old database name].INFORMATION_SCHEMA.COLUMNS DB1
	    LEFT JOIN [new database name].INFORMATION_SCHEMA.COLUMNS DB2
	      ON DB1.TABLE_SCHEMA = DB2.TABLE_SCHEMA
	     AND DB1.TABLE_NAME = DB2.TABLE_NAME
	     AND DB1.COLUMN_NAME = DB2.COLUMN_NAME
 ) DAT
WHERE DAT.Comment IS NOT NULL
  OR IsNull(DAT.Different_data_type,'') = 'Yes'
  OR IsNull(DAT.Different_maximum_length,'') = 'Yes'
  OR IsNull(DAT.Different_numeric_precision,'') = 'Yes'
  OR IsNull(DAT.Different_numeric_scale,'') = 'Yes'
  OR IsNull(DAT.Different_character_set_name,'') = 'Yes'
  OR IsNull(DAT.Different_collation_name,'') = 'Yes'
ORDER BY DAT.TABLE_SCHEMA
, DAT.TABLE_NAME
, DAT.COLLATION_NAME

Comments:
1) The query targets only the most common scenarios, therefore must be changed to handle further cases (e.g. different column defaults, different attributes like nullable, etc.)!
2) The other perspective can be obtained by inverting the table names (without aliases) and changing the name of the columns from "NEW_' to "OLD_" (see the queries for schemas).
3) One can move the column-based conditions for the differences in the main query, though then is needed to duplicate the logic, which will make the code more challenging to change and debug. 

Happy coding!

24 May 2025

🏭🗒️Microsoft Fabric: Materialized Lake Views (MLV) [Notes] 🆕🗓️

Disclaimer: This is work in progress intended to consolidate information from various sources for learning purposes. For the latest information please consult the documentation (see the links below)! 

Last updated: 27-Jul-2025

-- create schema
CREATE SCHERA IF NOT EXISTS <lakehouse_name>.<schema_name>

-- create a materialized view
CREATE MATERIALIZED VIEW IF NOT EXISTS <lakehouse_name>.<schema_name>.<view_name> 
[(
    CONSTRAINT <constraint_name> CHECK (<constraint>) ON MISMATCH DROP 
)] 
[PARTITIONED BY (col1, col2, ... )] 
[COMMENT “description or comment”] 
[TBLPROPERTIES (“key1”=”val1”, “key2”=”val2”, 
AS 
SELECT ...
FROM ...
-- WHERE ...
--GROUP BY ...

[Microsoft Fabric] Materialized Lake Views (MLVs)

  • {def} persisted, continuously updated view of data [1]
    • {benefit} allows to build declarative data pipelines using SQL, complete with built-in data quality rules and automatic monitoring of data transformations
      • simplifies the implementation of multi-stage Lakehouse processing [1]
        • ⇐ aids in the creation, management, and monitoring of views [3]
        • ⇐ improves transformations through a declarative approach [3]
        • streamline data workflows
        • enable developers to focus on business logic [1]
          • ⇐ not on infrastructural or data quality-related issues [1]
        • the views can be created in a notebook [2]
    • {benefit} allows developers visualize lineage across all entities in lakehouse, view the dependencies, and track its execution progress [3]
      • can have data quality constraints enforced and visualized for every run, showing completion status and conformance to data quality constraints defined in a single view [1]
      • empowers developers to set up complex data pipelines with just a few SQL statements and then handle the rest automatically [1]
        • faster development cycles 
        • trustworthy data
        • quicker insights
  • {goal} process only the new or changed data instead of reprocessing everything each time [1]
    • ⇐  leverages Delta Lake’s CDF under the hood
      • ⇒ it can update just the portions of data that changed rather than recompute the whole view from scratch [1]
  • {operation} creation
    • allows defining transformations at each layer [1]
      • e.g. aggregation, projection, filters
    • allows specifying certain checks that the data must meet [1]
      • incorporate data quality constraints directly into the pipeline definition
    • via CREATE MATERIALIZED LAKE VIEW
      • the SQL syntax is declarative and Fabric figures out how to produce and maintain it [1]
  • {operation} refresh
    • refreshes only when its source has new data [1]
      • if there’s no change, it can skip running entirely (saving time and resources) [1]
    • via REFRESH MATERIALIZED LAKE VIEW [workspace.lakehouse.schema].MLV_Identifier [FULL];
  • {operation} list views from schema [3]
    • via SHOW MATERIALIZED LAKE VIEWS <IN/FROM> Schema_Name;
  • {opetation} retrieve definition
    • via SHOW CREATE MATERIALIZED LAKE VIEW MLV_Identifier;
  • {operstion} update definition
    • via ALTER MATERIALIZED LAKE VIEW MLV_Identifier RENAME TO MLV_Identifier_New;
  • {operstion} drop view
    • via DROP MATERIALIZED LAKE VIEW MLV_Identifier;
    • {warning} dropping or renaming a materialized lake view affects the lineage view and scheduled refresh [3]
    • {recommendation} update the reference in all dependent materialized lake views [3]
  • {operation} schedule view run
    • lets users set how often the MLV should be refreshed based on business needs and lineage execution timing [5]
    • depends on
      • data update frequency: the frequency with which the data is updated [5]
      • query performance requirements: Business requirement to refresh the data in defined frequent intervals [5]
      • system load: optimizing the time to run the lineage without overloading the system [5]
  • {operation} view run history
    • users can access the last 25 runs including lineage and run metadata
      • available from the dropdown for monitoring and troubleshooting
  • {concept} lineage
    • the sequence of MLV that needs to be executed to refresh the MLV once new data is available [5]
  • {feature} automatically generate a visual report that shows trends on data quality constraints 
    • {benefit} allows to easily identify the checks that introduce maximum errors and the associated MLVs for easy troubleshooting [1]
  • {feature} can be combined with Shortcut Transformation feature for CSV ingestion 
    • {benefit} facilitate the building of end-to-end Medallion architectures
  • {feature} dependency graph
    • allows to see the dependencies existing between the various objects [2]
      • ⇐ automatically generated [2]
  • {feature} data quality
    • {benefit} allows to compose precise queries to exclude poor quality data from the source tables [5]
    • [medallion architecture] ensuring data quality is essential at every stage of the architecture [5]
    • maintained by setting constraints when defining the MLVs [5]
    • {action} FAIL
      • stops refreshing an MLV if any constraint is violated [5]
      • {default} halt is at the first instance
        • even without specifying the FAIL keyword [5]
        • takes precedence over DROP
    • {action} DROP
      • processes the MLV and removes records that don't meet the specified constrain [5]
        • provides the count of removed records in the lineage view [5]
    • {constraint} updating data quality constraints after creating an MLV isn't supported [5]
      • ⇐ the MLV must be recreated
    • {constraint} the use of functions and pattern search with operators in constraint condition is restricted [5]
      • e.g. LIKE, regex 
    • {known issue} the creation and refresh of an MLV with a FAIL action in constraint may result in a "delta table not found" error
      • {recommendation} recreate the MLV and avoid using the FAIL action [5] 
      • {feature} data quality report
        • built-in Power BI dashboard that shows several aggregated metrics [2]
    • {feature} monitor hub
      • centralized portal to browse MLV runs in the lakehouse [7]
      • {operation} view runs' status [7]
      • {operation} search and filter the runs [7]
        • based on different criteria
      • {operation} cancel in-progress run [7]
      • {operation} drill down run execution details [7]
    • doesn't support
      • {feature|planned} PySpark [3]
      • {feature|planned} incremental refresh [3]
      • {feature|planned} integration with Data Activator [3]
      • {feature|planned} API [3]
      • {feature|planned} cross-lakehouse lineage and execution [3]
      • {limitation} Spark properties set at the session level aren't applied during scheduled lineage refresh [4]
      • {limitation} creation with delta time-travel [4]
      • {limitation} DML statements [4]
      • {limitation} UDFs in CTAS [4] 
      • {limitation} temporary views can't be used to define MLVs [4]
    •  {recommendation} create all MLVs for a workspace within a single Lakehouss [8]
      • allows to ensure a consolidated and clear representation in the DAG view [8]

    References:
    [1] Microsoft Fabric Update Blog (2025) Simplifying Medallion Implementation with Materialized Lake Views in Fabric [link|aka]
    [2] Power BI Tips (2025) Microsoft Fabric Notebooks with Materialized Views - Quick Tips [link]
    [3] Microsoft Learn (2025) What are materialized lake views in Microsoft Fabric? [link]
    [4] Microsoft Learn (2025) Materialized lake views Spark SQL reference [link]
    [5] Microsoft Learn (2025) Manage Fabric materialized lake views lineage [link] 
    [6] Microsoft Learn (2025) Data quality in materialized lake views [link]
    [7] Microsoft Learn (2025) Monitor materialized lake views [link
    [8] Microsoft Learn (2025) Development and Monitoring [link]

    Resources:
    [R1] Databricks (2025) Use materialized views in Databricks SQL [link]
    [R2] Microsoft Learn (2025) Implement medallion architecture with materialized lake views [link]
    [R3] Mastering Declarative Data Transformations with Materialized Lake Views [link]

    Acronyms:
    API - Application Programming Interface
    CDF - Change Data Feed
    CTAS - Create Tables As Select
    DML - Data Manipulation Language 
    ETL - Extract, Transfer, Load
    MF - Microsoft Fabric
    MLV - Materialized Lake views
    UDF - User-defined functions

    24 April 2025

    💎🏭SQL Reloaded: Microsoft Fabric's Lakehouses at Work (Part I: Proof-of-Concept)


    Introduction

    One way to work with the data files existing in organization is to import them into a lakehouse and build a data model based on them that can be reused in the various solutions (incl. Power BI). As a reminder, a lakehouse is a data architecture platform for storing, managing, and analyzing structured and unstructured data in a single location.

    The tutorials available on lakehouses are pretty useful for getting an idea how to start, though probably each seasoned professional has his/her way of doing things, at least for testing the capabilities before building a proper solution. The target is thus, to create the minimum for testing the capabilities needed for a proof-of-concept solution. 

    The files used in this post are available on Microsoft's GitHub. Download the files and store them to be easily available for the next steps. The following files were considered for the current post: customers.csv, orders.csv and products.csv.

    Create the Lakehouse

    It's recommended to have a naming convention in place for the various items created in a workspace, e.g. a combination between item type (lakehouse, warehouse), system type (Prod, UAT, Dev, PoC) and eventually department (e.g. FIN, SCM, HR, etc.). One should try to balance between meaning and usefulness. Ideally, one should use 2 maximum 4 letters for each part encoded in the name. For example, the following scripts were created in the LH_SCM_PoC lakehouse. 

    More complex naming conventions can include the system (e.g. D365, CRM, EBS) or the company. The target is to easily identify the systems, independently how complex the rules are. Given that it can become challenging to rename the schemas afterwards, ideally the naming convention should be available from the early stages. 

    Create the Schema

    A lakehouse comes with a dbo schema available by default, though it's recommended to create meaningful schema(s) as needed. The schemas should ideally reflect the domain of the data (e.g. departments or other key areas) and the schemas shouldn't change when the objects are deployed between the different environments. Upon case, one should consider creating multiple schemas that reflect the model's key areas. The names should be simple and suggestive.

    -- create schema
    CREATE Schema Orders
    

    Create a Staging Area

    The next step is to create a staging area where the files in scope can be made available and then further loaded in the lakehouse. One needs to compromise between creating a deep hierarchical structure that reflects the business structure and the need to easily identify, respectively manage the files. An hierarchical structure with 1-2 level could provide the needed compromise, though each additional level tends to increase the complexity. 

    One should also consider rules for archiving or even deleting the files.

    Upload the Files

    Microsoft Fabric allows users to upload multiple files together into a single step. Ideally the files should have proper names for each column, otherwise overheads deriving from this may appear later in the process. 

    When the files are available in multiple folders in a complex hierarchical structure, a set of shortcuts could help in their management.

    Load the Data

    A file's data can be loaded into the lakehouse on the fly by providing a valid table name:
    Files >> SCM_Orders >> (select file) >> Load to Tables >> new table >> Load file to new table >> (provide information) >> Load

    Load file to new table


    Of course, the table's name must be unique within the Schema and the further properties must define files' definition. 

    One should consider loading first a couple of tables, performing a rough validation of the data imported, and only after that the remaining tables can be imported. This allows to identify the issues that typically lead to reimports of the data (wrong formatting, invalid column names, duplicated files, etc.) or rework.

    If the files have different characteristics (e.g. delimiters, number of attributes/records, special data types), one should consider this in the initial scope and have at least one example from each category. 

    Review the Metadata

    Once the files were made available, there's the tendency to start directly with the development without analyzing the data, or equally important, the metadata available. To review the metadata of the tables newly created, one can use the objects from the standard INFORMATION_SCHEMA (see post):

    -- retrieve the list of tables
    SELECT * 
    FROM INFORMATION_SCHEMA.TABLES
    
    WHERE TABLE_SCHEMA = 'orders'
    ORDER BY TABLE_SCHEMA  

    Further on, one can review columns' definition:
     
    -- retrieve column metadata
    SELECT TABLE_CATALOG
    , TABLE_SCHEMA
    , TABLE_NAME
    , COLUMN_NAME
    , ORDINAL_POSITION
    , DATA_TYPE
    , CHARACTER_MAXIMUM_LENGTH
    , NUMERIC_PRECISION
    , NUMERIC_SCALE
    , DATETIME_PRECISION
    , CHARACTER_SET_NAME
    FROM INFORMATION_SCHEMA.COLUMNS
    WHERE TABLE_SCHEMA = 'orders'
    ORDER BY ORDINAL_POSITION

    It's a good idea to save the metadata to a file and use it later for reviews, respectively for metadata management, when no other solution is in place for the same (e.g. Purview). That's useful also for the people with limited or no access to the workspace. 

    Alternatively, one can use a notebook with the following SQL-based commands: 

    %%sql
    
    DESCRIBE TABLE LH_SCM_PoC.orders.sales;
    
    DESCRIBE TABLE EXTENDED LH_SCM_PoC.orders.sales;
    

    One can even provide meaningful descriptions for each table and its attributes via scripts like the ones below, however it might be a good idea to do this in the later phases of the PoC, when the logic become stable:

    %%sql
    
    -- modify a table's COMMENT
    COMMENT ON TABLE LH_SCM_PoC.orders.sales IS 'Customer sales orders';
    
    -- modify columns' COMMENT for an existing table
    ALTER TABLE LH_SCM_DWH.orders.sales  
    ALTER COLUMN SalesOrderNumber COMMENT 'Sales Order Number';
    

    Data Validation

    Before diving into building any business logic, besides identifying the primary, foreign keys and further attributes used in bringing the data together, it's recommended to get an overview of data's intrinsic and extrinsic characteristics relevant to the analysis. Some of the rules used typically for studying the quality of data apply to some extent also in here, though one needs to prioritize accordingly, otherwise one replicates the effort that's typically part of the Data Management initiatives. 

    In addition, it's important to check how much the identified issues impact the business logic, respectively on whether the issues can be corrected to match the expectations. Often, no logic can compensate for major  data quality issues, and this can also affect  PoC's results as soon as the outcomes are validated against the expectations! 

    Data Understanding 

    Further on, it makes sense to get a high-level understanding of the data by looking at the distribution of values, respectively at the records participating in the joins. Of course, more similar queries can be built, though again, one should try to focus on the most important aspects!

    The analysis could for example consider the following points:

    /* validation of Products */
    
    -- review duplicated product numbers (should be 0)
    SELECT ProductName
    , count(*) RecordCount
    FROM orders.products
    GROUP BY ProductName
    HAVING count(*)>1
    
    -- review most (in)expensive products
    SELECT top 100 ProductID
    , ProductName
    , Category
    , ListPrice 
    FROM orders.products
    ORDER BY ListPrice DESC --ASC
    
    -- review category distribution
    SELECT Category
    , count(*) RecordCount 
    FROM orders.products
    GROUP BY Category
    ORDER BY RecordCount DESC
    
    -- review price ranges (
    SELECT Len(floor(ListPrice)) RangeCount
    , count(*) RecordCount 
    FROM orders.products
    GROUP BY Len(floor(ListPrice)) 
    ORDER BY RangeCount DESC
    
    /* validation of Customers */
    
    -- duplicated email address 
    SELECT CST.CustomerID
    , CST.FirstName
    , CST.LastName 
    , CST.EmailAddress 
    , DUP.RecordCount
    FROM (-- duplicates
    	SELECT EmailAddress
    	, count(*) RecordCount 
    	FROM orders.customers 
    	GROUP BY EmailAddress 
    	HAVING count(*)>1
    	) DUP
    	JOIN orders.customers CST
    	   ON DUP.EmailAddress = CST.EmailAddress
    ORDER BY DUP.RecordCount DESC
    , DUP.EmailAddress 
    
    -- duplicated Customer names (not necessarily duplicates)
    SELECT CST.CustomerID
    , CST.FirstName
    , CST.LastName 
    , CST.EmailAddress 
    , DUP.RecordCount
    FROM (-- duplicates
    	SELECT FirstName
    	, LastName
    	, count(*) RecordCount 
    	FROM orders.customers 
    	GROUP BY FirstName
    	, LastName 
    	HAVING count(*)>1
    	) DUP
    	JOIN orders.customers CST
    	   ON DUP.FirstName = CST.FirstName
          AND DUP.LastName = CST.LastName
    ORDER BY DUP.RecordCount DESC
    , DUP.FirstName
    , DUP.LastName
    
    /* validation of Orders */
    
    -- review a typical order
    SELECT SalesOrderID
    , OrderDate
    , CustomerID
    , LineItem
    , ProductID
    , OrderQty
    , LineItemTotal
    FROM orders.orders
    WHERE SalesOrderID = 71780
    ORDER BY SalesOrderID 
    , LineItem
    
    -- review orders' distribution by month
    SELECT Year(OrderDate) Year
    , Month(OrderDate) Month
    , count(*) RecordCount
    FROM orders.orders
    GROUP BY Year(OrderDate) 
    , Month(OrderDate) 
    ORDER BY Year
    , Month
    
    -- checking for duplicates
    SELECT SalesOrderID
    , LineItem
    , count(*) RecordCount
    FROM orders.orders ord 
    GROUP BY SalesOrderID
    , LineItem
    HAVING count(*)>1
    
    -- checking for biggest orders
    SELECT SalesOrderID
    , count(*) RecordCount
    FROM orders.orders ord 
    GROUP BY SalesOrderID
    HAVING count(*) > 10
    ORDER BY NoRecords DESC
    
    -- checking for most purchased products
    SELECT ProductID
    , count(*) NoRecords
    FROM orders.orders ord 
    GROUP BY ProductID
    HAVING count(*) > 8
    ORDER BY NoRecords DESC
    
    -- checking for most active customers
    SELECT CustomerID
    , count(*) RecordCount
    FROM orders.orders ord 
    GROUP BY CustomerID
    HAVING count(*) > 10
    ORDER BY RecordCount DESC
    
    /* join checks */
    
    -- Prders without Product (should be 0)
    SELECT count(*) RecordCount
    FROM orders.orders ord 
    	 LEFT JOIN orders.products prd
    	   ON ord.ProductID = prd.ProductID
    WHERE prd.ProductID IS NULL
    
    -- Prders without Customer (should be 0)
    SELECT count(*) RecordCount
    FROM orders.orders ORD 
    	 LEFT JOIN orders.customers CST
    	   ON ORD.CustomerID = CST.CustomerID
    WHERE CST.CustomerID IS NULL
    
    -- Products without Orders (153 records)
    SELECT count(*) RecordCount
    FROM orders.products prd
    	 LEFT JOIN orders.orders ord 
    	   ON prd.ProductID = ord.ProductID 
    WHERE ord.ProductID IS NULL
    
    
    -- Customers without Orders (815 records)
    SELECT count(*) RecordCount
    FROM orders.customers CST
    	 LEFT JOIN orders.orders ORD
    	   ON ORD.CustomerID = CST.CustomerID
    WHERE ORD.CustomerID IS NULL
    

    The more tables are involved, the more complex the validation logic can become. One should focus on the most important aspects.

    Building the Logic

    Once one has an acceptable understanding of the data entities involved and the relation between them, it's time to build the needed business logic by joining the various tables at the various levels of detail. One can focus on the minimum required, respectively attempt to build a general model that can address a broader set of requirements. For the PoC it's usually recommended to start small by addressing the immediate requirements, though some flexibility might be needed for exploring the data and preparing the logic for a broader set of requirements. Independently of the scope, one should consider a set of validations. 

    Usually, it makes sense to encapsulate the logic in several views or table-valued functions that reflect the logic for the main purposes and which allow a high degree of reuse (see [1]). Of course, one can use the standard approach for modelling the bronze, silver, respectively the gold layers adopted by many professionals. For a PoC, even if  that's not mandatory, it might still be a good idea to make steps in the respective direction. 

    In this case, dealing with only three tables - a fact table and two dimensions table - there are several perspectives that can be built:

    a) all records from fact table + dimension records

    The following view provides the lowest level of details for the fact table, allowing thus to look at the data from different perspectives as long as focus is only the values used is Sales Orders:

    -- create the view
    CREATE OR ALTER VIEW orders.vSalesOrders
    -- Sales Orders with Product & Customer information
    AS
    SELECT ORD.SalesOrderID
    , ORD.OrderDate
    , ORD.CustomerID
    , CST.FirstName 
    , CST.LastName
    , CST.EmailAddress
    , ORD.LineItem
    , ORD.ProductID
    , PRD.ProductName 
    , PRD.Category
    , ORD.OrderQty
    , ORD.LineItemTotal
    , PRD.ListPrice 
    , ORD.OrderQty * PRD.ListPrice ListPriceTotal
    FROM orders.orders ORD 
    	 JOIN orders.products PRD
    	   ON ORD.ProductID = PRD.ProductID
    	 JOIN orders.customers CST
    	   ON ORD.CustomerID = CST.CustomerID
    
    -- test the view   
    SELECT *
    FROM orders.vSalesOrders
    WHERE SalesOrderID = 71780
    

    One can use full joins unless some of the references dimensions are not available.  

    b) aggregated data for all dimension combinations

    The previous view allows to aggregate the data at the various levels of details:

    -- Sales volume by Customer & Product
    SELECT ORD.EmailAddress
    , ORD.ProductName 
    , ORD.Category
    , SUM(ORD.OrderQty) OrderQty
    , SUM(ORD.LineItemTotal) LineItemTotal
    FROM orders.vSalesOrders ORD 
    WHERE ORD.OrderDate >= '2022-06-01'
      AND ORD.OrderDate < '2022-07-01'
    GROUP BY ORD.EmailAddress
    , ORD.ProductName 
    , ORD.Category
    ORDER BY ORD.EmailAddress
    , ORD.ProductName 
    

    One can comment out the dimensions not needed. The query can be included in a view as well. 

    c) all records from each dimension table + aggregated fact records

    Sometimes, it's useful to look at the data from a dimension's perspective, though it might be needed to create such an object for each dimension, like in the below examples. For the maximum of flexibility the logic can be included in a table-valued function:

    -- create the user-defined function
    CREATE OR ALTER FUNCTION orders.tvfProductsSalesVolume(
        @StartDate date NULL,
        @EndDate date NULL
    )
    RETURNS TABLE
    -- Sales volume by Product
    AS
    RETURN (
    SELECT PRD.ProductID
    , PRD.ProductName 
    , PRD.Category
    , ORD.FirstOrderDate
    , ORD.LastOrderDate 
    , IsNull(ORD.TotalSalesQty, 0) TotalSalesQty 
    , IsNull(ORD.TotalSalesValue, 0) TotalSalesValue
    , IsNull(ORD.OrderCount, 0) OrderCount
    , IsNull(ORD.LineCount, 0) LineCount
    FROM orders.products PRD
         OUTER APPLY (
    		SELECT Min(ORD.OrderDate) FirstOrderDate
    		, Max(ORD.OrderDate) LastOrderDate 
    		, SUM(ORD.OrderQty) TotalSalesQty
    		, SUM(ORD.LineItemTotal) TotalSalesValue
    		, count(DISTINCT SalesOrderID) OrderCount
    		, count(*) LineCount
    		FROM orders.orders ORD 
    		WHERE ORD.ProductID = PRD.ProductID
    		  AND ORD.OrderDate >= @StartDate 
    		  AND ORD.OrderDate < @EndDate 
    	 ) ORD
    );
    
    -- test the user-defined function
    SELECT *
    FROM orders.tvfProductsSalesVolume('2022-06-01','2022-07-01') PRD
    WHERE TotalSalesValue <> 0
    ORDER BY TotalSalesValue DESC
    , LastOrderDate DESC
    
    
    -- create the user-defined function
    CREATE OR ALTER FUNCTION orders.tvfCustomersSalesVolume(
        @StartDate date NULL,
        @EndDate date NULL
    )
    RETURNS TABLE
    -- Sales volume by Customer
    AS
    RETURN (
    SELECT CST.CustomerID
    , CST.FirstName 
    , CST.LastName
    , CST.EmailAddress
    , ORD.FirstOrderDate
    , ORD.LastOrderDate 
    , IsNull(ORD.TotalSalesValue, 0) TotalSalesValue
    , IsNull(ORD.OrderCount, 0) OrderCount
    , IsNull(ORD.LineCount, 0) LineCount
    FROM orders.customers CST
         OUTER APPLY (
    		SELECT Min(ORD.OrderDate) FirstOrderDate
    		, Max(ORD.OrderDate) LastOrderDate 
    		, SUM(ORD.LineItemTotal) TotalSalesValue
    		, count(DISTINCT SalesOrderID) OrderCount
    		, count(*) LineCount
    		FROM orders.orders ORD 
    		WHERE ORD.CustomerID = CST.CustomerID
    		  AND ORD.OrderDate >= @StartDate 
    		  AND ORD.OrderDate < @EndDate 
    	 ) ORD
    );
    
    -- test the user-defined function
    SELECT *
    FROM orders.tvfCustomersSalesVolume('2022-06-01','2022-07-01') PRD
    WHERE TotalSalesValue <> 0
    ORDER BY TotalSalesValue DESC
    , LastOrderDate DESC
    

    When restructuring the queries in similar ways, there's always a compromise between the various factors: (re)usability, performance or completeness. 

    Further Comments

    The above database objects should allow users to address most of the requirements, though, as usual, there can be also exceptions, especially when the data needs to be aggregated at a different level of detail that requires the query to be structured differently.

    The number of perspectives can increase also with the number of fact tables used to model a certain entity (e.g. Sales order headers vs. lines). For example, 

    In theory, one can also find ways to automate the process of creating database objects, though one must choose the relevant attributes, respectively include logic that makes sense only within a certain perspective. 

    No matter the data, respectively systems used as source, expect surprises and test your assumptions! For example, in the file used to create the orders.customers table, there seem to be duplicated entities with the same name and email address. One must clarify how such entities must be handled in data analysis, respectively in data modeling. For example, a person can appear twice because of the roles associated with the name or can be other entitled reasons. 

    The files in scope of this post are small compared with the files existing in organizations. In many scenarios files' size could range from GB to TB and thus require partitioning and different other strategies. 

    |>> Next Post

    References
    [1] sql-troubles (2023) Architecture Part IV: Building a Modern Data Warehouse with Azure Synapse [link]

    Resources
    [1] Microsoft Learn (2024) Fabric: Lakehouse and Delta Lake tables [link]

    Related Posts Plugin for WordPress, Blogger...

    About Me

    My photo
    Koeln, NRW, Germany
    IT Professional with more than 25 years experience in IT in the area of full life-cycle of Web/Desktop/Database Applications Development, Software Engineering, Consultancy, Data Management, Data Quality, Data Migrations, Reporting, ERP implementations & support, Team/Project/IT Management, etc.