Bad table design reflected using Severely damaged urban buildings after a landslide in Mocoa, Colombia, showcasing destruction

Bad vs Good Table design

Introduction

As a DBA, data modeling is part of the job; working with developers, reviewing table design, implementing proper constraints and much more. In an earlier post about performance tuning, I talk about some practical steps that can be taken to fix performance problems. However, sometimes those problems could be “self-inflicted” and I ended that post with the heading “No Amount of Tuning can fix that Schema” which is relevant to the content of this post.

In the following lines, I will share a recent lived experience about bad table design.

What happened?

I got a request from a developer to build a table that looked something like this:

Table name : project_approvals

COLUMN_NAME                    DATA_TYPE
------------                   -------------
 project_id                    NUMBER Primary key
,project_type                  VARCHAR2(32 CHAR)
,Project_role_1                VARCHAR2(32 CHAR)
,Project_role_2                VARCHAR2(32 CHAR)
,Project_role_3                VARCHAR2(32 CHAR)
,Project_role_4                VARCHAR2(32 CHAR)
,appr_order_1                  VARCHAR2(32 CHAR)
,appr_order_2                  VARCHAR2(32 CHAR)
,appr_order_3                  VARCHAR2(32 CHAR)
,appr_order_4                  VARCHAR2(32 CHAR)
,appr_active_1                 VARCHAR2(02 CHAR)
,appr_active_2                 VARCHAR2(02 CHAR)
,appr_active_3                 VARCHAR2(02 CHAR)
,appr_active_4                 VARCHAR2(02 CHAR)
,active                        VARCHAR2(01 CHAR)

At the time, I had just watched a presentation referencing this exact same scenario. Obviously I was not okay with this design because of the following reasons:

  • It is modeling a repeating group of up to 4 appr-role assignments in separate columns.
  • It is storing the same kind of data in multiple numbered columns.
  • It will cause difficult maintenance as Business rules could change, and the requirement could be for 5,6,7 or even 10 roles. Every time this happens, the table structure would need to change!
  • Probably the biggest one, Messy queries! To get count of active roles, we’d need to unpivot columns or write repetitive SQL.

Example problem; Compute number of active roles

Suppose we want to answer a supposedly “simple” question from this table like “How many roles are currently active?”. Let’s create the table as requested and see how we can write this query.

We create the table as requested:

CREATE TABLE project_approvals
( project_id       NUMBER
, project_type     VARCHAR2(32)
, project_role_1   VARCHAR2(32)
, project_role_2   VARCHAR2(32)
, project_role_3   VARCHAR2(32)
, project_role_4   VARCHAR2(32)
, appr_order_1     VARCHAR2(32)
, appr_order_2     VARCHAR2(32)
, appr_order_3     VARCHAR2(32)
, appr_order_4     VARCHAR2(32)
, appr_active_1    VARCHAR2(2)
, appr_active_2    VARCHAR2(2)
, appr_active_3    VARCHAR2(2)
, appr_active_4    VARCHAR2(2)
, active           VARCHAR2(1)
, CONSTRAINT project_approvals_pk PRIMARY KEY (project_id)
)
/

Table created.

We insert 5 rows:

INSERT INTO project_approvals 
 VALUES 
         (1,'FINANCE','APPROVER','MANAGER','DIRECTOR',NULL,'1','2','3',NULL,'Y','Y','N',NULL,'Y')
        ,(2,'HR','LEAD','MANAGER','DIRECTOR',NULL,'1','2','3',NULL,'Y','Y','Y',NULL,'Y')
        ,(3,'IT','APPROVER','LEAD','VP',NULL,'1','2','3',NULL,'Y','N','Y',NULL,'Y')
        ,(4,'OPS','MANAGER','DIRECTOR','VP',NULL,'1','2','3',NULL,'Y','N','N',NULL,'N')
        ,(5,'SALES','APPROVER','MANAGER','VP',NULL,'1','2','3',NULL,'Y','Y','Y', NULL,'Y' );

COMMIT;

Notice the use of the values Constructor clause, new in Oracle AI Database 26ai.

If we want to write a query to see the number of active roles, we can use a CASE statement:

SELECT
    SUM(
        CASE WHEN appr_active_1 = 'Y' THEN 1 ELSE 0 END +
        CASE WHEN appr_active_2 = 'Y' THEN 1 ELSE 0 END +
        CASE WHEN appr_active_3 = 'Y' THEN 1 ELSE 0 END +
        CASE WHEN appr_active_4 = 'Y' THEN 1 ELSE 0 END
    ) AS active_role_count
FROM project_approvals;

   ACTIVE_ROLE_COUNT 
____________________ 
                  11 

Another way would be to use UNPIVOT:

SELECT
      SUM(COUNT(appr_active)) AS active_role_count
FROM (
    SELECT project_id,
           project_type,
           appr_active
    FROM project_approvals
    UNPIVOT (
        appr_active FOR role_col IN (
            appr_active_1 AS '1',
            appr_active_2 AS '2',
            appr_active_3 AS '3',
            appr_active_4 AS '4'
        )
    )
)
WHERE appr_active = 'Y'
GROUP BY project_id, project_type
ORDER BY project_id ;

   ACTIVE_ROLE_COUNT 
____________________ 
                  11 

This query should be a simple count! However, In both solutions shown, the queries are unnecessarily complex. This is because our table design is not great.

A better Design

Instead of having one table, we can create 3 tables named:

  • project_approval_header
  • project_roles
  • project_approval_details

The code below creates the tables:

CREATE TABLE project_approval_header
( project_approval_header_id NUMBER
 ,project_type               VARCHAR2(32 CHAR)
 ,active                     CHAR(1)
 ,CONSTRAINT project_approval_header_pk PRIMARY KEY (project_approval_header_id)
);

Table PROJECT_APPROVAL_HEADER created.

CREATE TABLE project_roles (
 role_id   NUMBER
,role_name VARCHAR2(32 CHAR)
,CONSTRAINT project_roles_pk PRIMARY KEY (role_id)
);

Table PROJECT_ROLES created.

CREATE TABLE project_approval_details (
  project_approval_details_id   NUMBER
 ,project_approval_header_id    NUMBER
 ,role_id                       NUMBER
 ,appr_order                    NUMBER
 ,active                        CHAR(1)
 ,CONSTRAINT project_approval_details_pk  PRIMARY KEY (project_approval_details_id)
 ,CONSTRAINT project_approval_details_fk1 FOREIGN KEY (project_approval_header_id)
   REFERENCES project_approval_header(project_approval_header_id)
 ,CONSTRAINT project_approval_details_fk2 FOREIGN KEY (role_id)
   REFERENCES project_roles(role_id)
,CONSTRAINT project_approval_details_uk1
    UNIQUE (project_approval_header_id, appr_order)
 );

Table PROJECT_APPROVAL_DETAILS created.

Why is this better?

  • With this design, we can insert any number of roles without modifying the table structure.
  • Proper referential Integrity Constraints can actually be defined on this model.
  • It’s much simpler to query from these tables.
  • Whenever business rules change, the table does not need to be modified and thus it’s easier to maintain.

Compute number of active roles again

Let’s insert data into the tables:

--project_approval_header
INSERT INTO project_approval_header
(project_approval_header_id, project_type, active)
VALUES (1, 'FINANCE', 'Y')
      ,(2, 'HR', 'Y')
      ,(3, 'IT', 'Y')
      ,(4, 'OPS', 'N')
      ,(5, 'SALES', 'Y');

--roles
INSERT INTO project_roles (role_id, role_name) VALUES (1, 'APPROVER');
INSERT INTO project_roles (role_id, role_name) VALUES (2, 'MANAGER');
INSERT INTO project_roles (role_id, role_name) VALUES (3, 'DIRECTOR');
INSERT INTO project_roles (role_id, role_name) VALUES (4, 'VP');
INSERT INTO project_roles (role_id, role_name) VALUES (5, 'LEAD');

--project_approval details
INSERT INTO project_approval_details
(project_approval_details_id, project_approval_header_id, role_id, appr_order, active)
VALUES (1, 1, 1, 1, 'Y')
      ,(2, 1, 2, 2, 'Y')
      ,(3, 1, 3, 3, 'N')
      ,(4, 2, 5, 1, 'Y')
      ,(5, 2, 2, 2, 'Y')
      ,(6, 2, 3, 3, 'Y')
      ,(7, 3, 1, 1, 'Y')
      ,(8, 3, 5, 2, 'N')
      ,(9, 3, 4, 3, 'Y')
      ,(10, 4, 2, 1, 'Y')
      ,(11, 4, 3, 2, 'N')
      ,(12, 4, 4, 3, 'N')
      ,(13, 5, 1, 1, 'Y')
      ,(14, 5, 2, 2, 'Y')
      ,(15, 5, 4, 3, 'Y');

COMMIT;

Now say we wanted to run the query to see the total number of active roles again. We only need to query the project_approval_details table:

SELECT COUNT(*) AS active_role_count
FROM project_approval_details
WHERE active = 'Y';

   ACTIVE_ROLE_COUNT 
____________________ 
                  11 

It’s that simple; A straightforward COUNT(*) to get the same results as the queries with the CASE statement and UNPIVOT. With these second set of tables, it’s much easier to query our data with much simpler SQL.

Conclusion

In the world of AI we live in today, everyone seems to want fancy tools. While this is great, the fundamentals are still very… fundamental!

Bad database design like the one shown in this example could lead to complex queries to extract basic information. Consequently, this could cause poor performance if the entire application is made up of these complex queries. Additionally, perhaps a lot of tuning efforts could go into fixing or rewriting the resulting queries down the road. This is all wasted cost in terms of time and resources meanwhile the real issue is poor database design.

Normalization, a fundamental principle of database design and data modelling, is sometimes seen as one more thing to do. However, if you’re using a relational database like Oracle, properly designing schemas and following best practices for normalization could be the difference between your project succeeding and it failing woefully. Don’t forget the fundamentals, they matter… a lot!

Leave a Comment

Your email address will not be published. Required fields are marked *