{"id":18597569,"url":"https://github.com/lealre/northwind-analytics-sql","last_synced_at":"2026-04-17T12:33:09.109Z","repository":{"id":239053571,"uuid":"796857442","full_name":"lealre/northwind-analytics-sql","owner":"lealre","description":"An analytical business report from a sales database with SQL","archived":false,"fork":false,"pushed_at":"2024-05-10T17:38:27.000Z","size":791,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-05-16T13:52:48.882Z","etag":null,"topics":["docker","docker-compose","postgresql","sql"],"latest_commit_sha":null,"homepage":"","language":null,"has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lealre.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-05-06T18:52:39.000Z","updated_at":"2024-05-10T17:38:30.000Z","dependencies_parsed_at":null,"dependency_job_id":"78649408-eedf-487a-97a5-3723e8f1a040","html_url":"https://github.com/lealre/northwind-analytics-sql","commit_stats":null,"previous_names":["lealre/northwind-analytics-sql"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/lealre/northwind-analytics-sql","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lealre%2Fnorthwind-analytics-sql","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lealre%2Fnorthwind-analytics-sql/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lealre%2Fnorthwind-analytics-sql/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lealre%2Fnorthwind-analytics-sql/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lealre","download_url":"https://codeload.github.com/lealre/northwind-analytics-sql/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lealre%2Fnorthwind-analytics-sql/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31929704,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-17T10:35:34.458Z","status":"ssl_error","status_checked_at":"2026-04-17T10:35:09.472Z","response_time":62,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["docker","docker-compose","postgresql","sql"],"created_at":"2024-11-07T01:28:37.953Z","updated_at":"2026-04-17T12:33:09.082Z","avatar_url":"https://github.com/lealre.png","language":null,"funding_links":[],"categories":[],"sub_categories":[],"readme":"# Analytic Report with SQL - Northwind database\n\nThis project aims to generate an analytical business report from a sales database. \n\nThe Northwind database contains sales data from a company called Northwind Traders, which imports and exports specialty foods from around the world. In this report, we will primarily focus on extracting insights from revenue, product, and customer data using SQL operations in a PostgreSQL database.\n\nThe analyses provided here can benefit companies of all sizes looking to enhance their analytical capabilities. Through these reports, organizations can strategically position themselves in the market, leveraging data-driven decisions to improve their future results.\n\nIt is possible to run this project using only Docker, as it builds both the PostgreSQL database and the client pgAdmin. All instructions are provided in [How to run this project](#how-to-run-this-project) section.\n\n## Table of Contents\n- [Questions we want to answer](#questions-we-want-to-answer)\n    - [Operational Revenue](#operational-revenue)\n        - [How can we observe the operational revenue over the years?](#how-can-we-observe-the-operational-revenue-over-the-years)\n        - [How can we observe the trends of the operational revenue within each year?](#how-can-we-observe-the-trends-of-the-operational-revenue-within-each-year)\n    - [Customers Analysis](#customers-analysis)\n        - [From which customers do we have the main operational revenue?](#from-which-customers-do-we-have-the-main-operational-revenue)\n        - [How can we classify customers to give specific approaches based on their level of demand?](#how-can-we-classify-customers-to-give-specific-approaches-based-on-their-level-of-demand)\n    - [Products Analysis](#products-analysis)\n        - [Which products have the highest demand and revenue?](#which-products-have-the-highest-demand-and-revenue)\n    - [Conclusion](#conlusion)\n\n- [Database context](#database-context)\n- [How to run this project](#how-to-run-this-project)\n\n-------------------------------\n\n## Questions we want to answer\n\n### Operational Revenue\n\n#### How can we observe the operational revenue over the years?\n\nThe query below aggregates the operational revenue by year and calculates the cumulative operational revenue over the same years. It is useful for obtaining an overall trend of the results.\n\n```sql\nCREATE VIEW annual_revenues_analysis AS\nWITH annual_revenues AS (\n    SELECT \n        EXTRACT(YEAR FROM o.order_date) AS year,\n        ROUND(SUM((od.unit_price * od.quantity) * (1 - od.discount))::numeric, 2) as revenue\n    FROM \n        order_details AS od\n    LEFT JOIN \n        orders AS o \n        ON od.order_id = o.order_id\n    GROUP BY \n        EXTRACT(YEAR FROM o.order_date)\n)\nSELECT \n    year,\n    revenue,\n    SUM(revenue) OVER (ORDER BY year) AS cumulative_revenue\nFROM annual_revenues;\n```\n\n| year | revenue | cumulative_revenue |\n|------|---------------|--------------------------|\n| 1996 | 208083.97     | 208083.97                |\n| 1997 | 617085.20     | 825169.17                |\n| 1998 | 440623.87     | 1265793.04               |\n\n#### How can we observe the trends of the operational revenue within each year?\n\nBy using a similar approach to the previous query, we can aggregate the operational revenue by month, calculate the cumulative operational revenue by year (year-to-date), and obtain the total and relative difference between each month. This can be useful for observing trends in small time windows and identifying patterns specific to the business, such as certain parts of the year yielding better results than others.\n\n```sql\nCREATE VIEW ytd_revenue_analysis AS\nWITH monthly_revenue_table AS (\n    SELECT\n        EXTRACT(YEAR FROM o.order_date) AS year,\n        EXTRACT(MONTH FROM o.order_date) AS month,\n        ROUND(SUM(od.unit_price * od.quantity * (1.0 - od.discount))::numeric,2) AS monthly_revenue\n    FROM \n        order_details AS od\n    LEFT JOIN \n        orders AS o \n        ON od.order_id = o.order_id\n    GROUP BY\n        EXTRACT(YEAR FROM o.order_date),\n        EXTRACT(MONTH FROM o.order_date)\n),\ncumulative_revenue_table AS (\n    SELECT\n        year,\n        month,\n        monthly_revenue,\n        SUM(monthly_revenue) OVER (PARTITION BY year ORDER BY month) AS ytd_revenue\n    FROM \n        monthly_revenue_table\n)\nSELECT\n    year,\n    month,\n    monthly_revenue,\n\tytd_revenue,\n    monthly_revenue - LAG(monthly_revenue) OVER (PARTITION BY year ORDER BY month) AS monthly_difference,\n    ROUND((monthly_revenue - LAG(monthly_revenue) OVER (PARTITION BY year ORDER BY month)) / LAG(monthly_revenue) OVER (PARTITION BY year ORDER BY month) * 100::numeric,2) AS percentage_monthly_difference\nFROM \n    cumulative_revenue_table\nORDER BY \n    year, month;\n```\n\n| year | month | monthly_revenue | ytd_revenue | monthly_difference | percentage_monthly_difference |\n|------|-------|---------|--------------------|----------------|--------------------|\n| 1996 | 7     | 27861.90 | 27861.90           |                |                    |\n| 1996 | 8     | 25485.28 | 53347.18           | -2376.62       | -8.53              |\n| 1996 | 9     | 26381.40 | 79728.58           | 896.12         | 3.52               |\n| ... | ...     | ... | ...        | ...          | ...               |\n| 1998 | 3     | 104854.16| 298491.56          | 5438.87        | 5.47               |\n| 1998 | 4     | 123798.68| 422290.24          | 18944.52       | 18.07              |\n| 1998 | 5     | 18333.63 | 440623.87          | -105465.05     | -85.19             |\n\n\n\n### Customers Analysis\n\n#### From which customers do we have the main operational revenue?\n\nThe query below orders the customers by the total and relative operational revenue they were responsible for over the total time. This is very useful for understanding the concentration of operational revenue and forecasting future results. \n\n```sql\nCREATE VIEW customers_analysis AS\nSELECT\n    c.company_name,\n    ROUND(SUM((od.unit_price * od.quantity) * (1 - od.discount))::numeric, 2) AS total_revenue,\n    ROUND((SUM((od.unit_price * od.quantity) * (1 - od.discount)) / SUM(SUM((od.unit_price * od.quantity) * (1 - od.discount))) OVER() * 100)::numeric, 2) AS percentage_of_total_revenue\nFROM \n    order_details AS od\nLEFT JOIN \n    orders AS o \n    ON od.order_id = o.order_id\nLEFT JOIN \n    customers AS c \n    ON c.customer_id = o.customer_id\nGROUP BY \n    c.company_name\nORDER BY \n    total_revenue DESC\n```\n\n| company_name                       | total_revenue | percentage_of_total_revenue |\n|--------------------------------|---------------|------------|\n| QUICK-Stop                      | 110277.31     | 8.71       |\n| Ernst Handel                   | 104874.98     | 8.29       |\n| Save-a-lot Markets              | 104361.95     | 8.24       |\n| Rattlesnake Canyon Grocery      | 51097.80      | 4.04       |\n| ...                              | ...           | ...        |\n| Lazy K Kountry Store            | 357.00        | 0.03       |\n| Centro comercial Moctezuma      | 100.80        | 0.01       |\n\n\n#### How can we classify customers to give specific approaches based on their level of demand?\n\nAfter classifying customers based on operational revenue, we can categorize them by executing the following query.\n\n```sql\nCREATE VIEW revenue_groups AS \nSELECT\n    c.company_name,\n    ROUND(SUM((od.unit_price * od.quantity) * (1 - od.discount))::numeric, 2) AS total_revenue,\n    ROUND((SUM((od.unit_price * od.quantity) * (1 - od.discount)) / SUM(SUM((od.unit_price * od.quantity) * (1 - od.discount))) OVER() * 100)::numeric, 2) AS percentage_of_total_revenue,\n    NTILE(5) OVER (ORDER BY SUM((od.unit_price * od.quantity) * (1 - od.discount)) DESC) AS revenue_group\nFROM \n    order_details AS od\nLEFT JOIN \n    orders AS o \n    ON od.order_id = o.order_id\nLEFT JOIN \n    customers AS c \n    ON c.customer_id = o.customer_id\nGROUP BY \n    c.company_name\nORDER BY \n    total_revenue DESC\n```\n\n| company_name                       | total_revenue | percentage_of_total_revenue | revenue_group |\n|-------------------------------|---------------|-----------------------------|---------------|\n| QUICK-Stop                    | 110277.31     | 8.71                        | 1             |\n| Ernst Handel                 | 104874.98     | 8.29                        | 1             |\n| ...                           | ...           | ...                         | ...           |\n| Lazy K Kountry Store         | 357.00        | 0.03                        | 5             |\n| Centro comercial Moctezuma   | 100.80        | 0.01                        | 5             |\n\n\nNow only the customers who are in groups 3, 4, and 5 will be selected for a special marketing analysis with them, for example.\n\n```sql\nCREATE VIEW revenue_groups_filtered AS\nWITH companies_revenue_groups AS (\n    SELECT\n        c.company_name,\n        ROUND(SUM((od.unit_price * od.quantity) * (1 - od.discount))::numeric, 2) AS total_revenue,\n        ROUND((SUM((od.unit_price * od.quantity) * (1 - od.discount)) / SUM(SUM((od.unit_price * od.quantity) * (1 - od.discount))) OVER() * 100)::numeric, 2) AS percentage_of_total_revenue,\n        NTILE(5) OVER (ORDER BY SUM((od.unit_price * od.quantity) * (1 - od.discount)) DESC) AS revenue_group\n    FROM \n        order_details AS od\n    LEFT JOIN \n        orders AS o \n        ON od.order_id = o.order_id\n    LEFT JOIN \n        customers AS c \n        ON c.customer_id = o.customer_id\n    GROUP BY \n        c.company_name\n    ORDER BY \n        total_revenue DESC\n)\nSELECT \n    *   \nFROM \n    companies_revenue_groups\nWHERE \n    revenue_group IN (3,4,5);\n```\n\n| company_name                       | total_revenue | percentage_of_total_revenue | revenue_group |\n|------------------------------------|---------------|-----------------------------|---------------|\n| Split Rail Beer \u0026 Ale              | 11441.63      | 0.90                        | 3             |\n| Tortuga Restaurante                | 10812.15      | 0.85                        | 3             |\n| ...                                | ...           | ...                         | ...           |\n| Lazy K Kountry Store               | 357.00        | 0.03                        | 5             |\n| Centro comercial Moctezuma         | 100.80        | 0.01                        | 5             |\n\n\nWe can also filter customers by specific criteria, like filtering for only UK customers who paid more than 1000 dollars, for example.\n\n```sql\nCREATE VIEW uk_customers_who_payed_more_than_1000 AS \nSELECT \n    c.company_name, \n    ROUND(SUM(od.unit_price * od.quantity * (1.0 - od.discount))::numeric, 2) AS revenue\nFROM \n    order_details AS od\nLEFT JOIN \n    orders AS o \n    ON od.order_id = o.order_id\nLEFT JOIN \n    customers AS c\n    ON o.customer_id = c.customer_id\nWHERE \n    LOWER(c.country) = 'uk'\nGROUP BY \n    c.company_name\nHAVING \n    SUM(od.unit_price * od.quantity * (1.0 - od.discount)) \u003e 1000\nORDER BY\n    revenue DESC;\n```\n\n| company_name           | revenue |\n|-------------------------|---------------|\n| Seven Seas Imports      | 16215.33      |\n| Eastern Connection      | 14761.03      |\n| Around the Horn         | 13390.65      |\n| Island Trading          | 6146.30       |\n| B's Beverages           | 6089.90       |\n| Consolidated Holdings   | 1719.10       |\n\n\n### Products Analysis\n\n#### Which products have the highest demand and revenue?\n\nThe query below orders the products responsible for generating more operational revenue, as well as their total quantity sold.\n\n```sql\nCREATE VIEW products_analysis AS\nSELECT \n    DISTINCT p.product_name, \n    ROUND((SUM(od.unit_price * od.quantity * (1.0 - od.discount)) OVER (PARTITION BY p.product_name))::numeric, 2) AS revenue,\n    SUM(od.quantity) OVER (PARTITION BY p.product_name) AS quantity_sold\nFROM \n    order_details AS od\nLEFT JOIN \n    products AS p\n    ON od.product_id = p.product_id\nORDER BY \n    revenue DESC;\n```\n\n| product_name                       | revenue   | quantity_sold |\n|------------------------------------|-----------|---------------|\n| Côte de Blaye                     | 141396.74 | 623           |\n| Thüringer Rostbratwurst           | 80368.67  | 746           |\n| Raclette Courdavault              | 71155.70  | 1496          |\n|...|...|...|\n| Genen Shouyu                      | 1784.82   | 122           |\n| Geitost                           | 1648.12   | 755           |\n| Chocolade                         | 1368.71   | 138           |\n\n### Conlusion\n\nIn this report, we performed some queries to obtain specific business insights to assist the business. There are always more filters and approaches we can explore, as well as extending the monthly analysis conducted on operational revenue to include customers and products.\n\n## Database context\n\nThe Northwind database contains sales data for a company called Northwind Traders, which imports and exports specialty foods from around the world.\n\nThe Northwind database is an ERP with data on customers, orders, inventory, purchases, suppliers, shipments, employees, and accounting.\n\nThe Northwind dataset includes sample data for the following:\n\n* **Suppliers**: Northwind's suppliers and vendors\n* **Customers**: Customers who purchase products from Northwind\n* **Employees**: Details of Northwind Traders' employees\n* **Products**: Product information\n* **Shippers**: Details of carriers that ship the traders' products to end customers\n* **Orders** and **Order Details**: Sales order transactions occurring between customers and the company\n\nThe Northwind database includes 14 tables, and the relationships between the tables are shown in the following entity relationship diagram.\n\n![](pics/northwind-er-diagram.png)\n\n## How to run this project\n\n### Manually\n\nAfter connecting to your own database, use the [`northwind.sql`](northwind.sql) file to populate the database by copying the script, pasting it into the query tool, and running it.\n\n### With Docker\n\nIts is required to have docker and docker compose intalled to be able to run this project.\n\n- [Start with Docker](https://www.docker.com/get-started/)\n- [Install Docker Compose](https://docs.docker.com/compose/install/)\n\nOnce we have docker avaiable, we do the following steps:\n\n1. Clone the repository locally.\n```bash \ngit clone https://github.com/lealre/northwind-analytics-sql.git\n```\n\n2. Access the project folder.\n```bash\ncd northwind-analytics-sql\n```\n\n3. Build the Docker container.\n```bash\ndocker compose up -d\n```\n\nThe `-d` flag is used to run the container detached from the terminal.\n\n4. Access pgAdmin at http://localhost:5050/\n\n5. Set the master password (when accessing for the first time).\n\n\u003cimg src=\"pics/1.png\"  width=500\u003e\n\n6. Right-click on the server to connect pgAdmin to the database.\n\n\u003cimg src=\"pics/2.png\"  width=500\u003e\n\n\n7. Set the server name (it can be any name you want).\n\n\u003cimg src=\"pics/3.png\"  width=500\u003e\n\n8. Connect to the database using the credentials we set in the [`docker-compose.yaml`](docker-compose.yaml) file.\n\n`Host name`: `db`\n\n`Password`: `postgres`\n\n\u003cimg src=\"pics/4.png\"  width=500\u003e\n\nAfter completing this final step, you will be able to access the Northwind database, as well as the views created in the report.\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flealre%2Fnorthwind-analytics-sql","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flealre%2Fnorthwind-analytics-sql","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flealre%2Fnorthwind-analytics-sql/lists"}