SQL Database Design For Developers at Longhorn PHP 2025

ScottKeckWarren 47 views 43 slides Oct 24, 2025
Slide 1
Slide 1 of 206
Slide 1
1
Slide 2
2
Slide 3
3
Slide 4
4
Slide 5
5
Slide 6
6
Slide 7
7
Slide 8
8
Slide 9
9
Slide 10
10
Slide 11
11
Slide 12
12
Slide 13
13
Slide 14
14
Slide 15
15
Slide 16
16
Slide 17
17
Slide 18
18
Slide 19
19
Slide 20
20
Slide 21
21
Slide 22
22
Slide 23
23
Slide 24
24
Slide 25
25
Slide 26
26
Slide 27
27
Slide 28
28
Slide 29
29
Slide 30
30
Slide 31
31
Slide 32
32
Slide 33
33
Slide 34
34
Slide 35
35
Slide 36
36
Slide 37
37
Slide 38
38
Slide 39
39
Slide 40
40
Slide 41
41
Slide 42
42
Slide 43
43
Slide 44
44
Slide 45
45
Slide 46
46
Slide 47
47
Slide 48
48
Slide 49
49
Slide 50
50
Slide 51
51
Slide 52
52
Slide 53
53
Slide 54
54
Slide 55
55
Slide 56
56
Slide 57
57
Slide 58
58
Slide 59
59
Slide 60
60
Slide 61
61
Slide 62
62
Slide 63
63
Slide 64
64
Slide 65
65
Slide 66
66
Slide 67
67
Slide 68
68
Slide 69
69
Slide 70
70
Slide 71
71
Slide 72
72
Slide 73
73
Slide 74
74
Slide 75
75
Slide 76
76
Slide 77
77
Slide 78
78
Slide 79
79
Slide 80
80
Slide 81
81
Slide 82
82
Slide 83
83
Slide 84
84
Slide 85
85
Slide 86
86
Slide 87
87
Slide 88
88
Slide 89
89
Slide 90
90
Slide 91
91
Slide 92
92
Slide 93
93
Slide 94
94
Slide 95
95
Slide 96
96
Slide 97
97
Slide 98
98
Slide 99
99
Slide 100
100
Slide 101
101
Slide 102
102
Slide 103
103
Slide 104
104
Slide 105
105
Slide 106
106
Slide 107
107
Slide 108
108
Slide 109
109
Slide 110
110
Slide 111
111
Slide 112
112
Slide 113
113
Slide 114
114
Slide 115
115
Slide 116
116
Slide 117
117
Slide 118
118
Slide 119
119
Slide 120
120
Slide 121
121
Slide 122
122
Slide 123
123
Slide 124
124
Slide 125
125
Slide 126
126
Slide 127
127
Slide 128
128
Slide 129
129
Slide 130
130
Slide 131
131
Slide 132
132
Slide 133
133
Slide 134
134
Slide 135
135
Slide 136
136
Slide 137
137
Slide 138
138
Slide 139
139
Slide 140
140
Slide 141
141
Slide 142
142
Slide 143
143
Slide 144
144
Slide 145
145
Slide 146
146
Slide 147
147
Slide 148
148
Slide 149
149
Slide 150
150
Slide 151
151
Slide 152
152
Slide 153
153
Slide 154
154
Slide 155
155
Slide 156
156
Slide 157
157
Slide 158
158
Slide 159
159
Slide 160
160
Slide 161
161
Slide 162
162
Slide 163
163
Slide 164
164
Slide 165
165
Slide 166
166
Slide 167
167
Slide 168
168
Slide 169
169
Slide 170
170
Slide 171
171
Slide 172
172
Slide 173
173
Slide 174
174
Slide 175
175
Slide 176
176
Slide 177
177
Slide 178
178
Slide 179
179
Slide 180
180
Slide 181
181
Slide 182
182
Slide 183
183
Slide 184
184
Slide 185
185
Slide 186
186
Slide 187
187
Slide 188
188
Slide 189
189
Slide 190
190
Slide 191
191
Slide 192
192
Slide 193
193
Slide 194
194
Slide 195
195
Slide 196
196
Slide 197
197
Slide 198
198
Slide 199
199
Slide 200
200
Slide 201
201
Slide 202
202
Slide 203
203
Slide 204
204
Slide 205
205
Slide 206
206

About This Presentation

Databases are the magic box in a lot of our workflows and in this presentation I show you my rules for better database design,


Slide Content

SQL Database Design For Developers Scott Keck-Warren Longhorn PHP 2025 https://tinyurl.com/todo https://scott.keck-warren.com/

Never Worked With a DBA On a Project Every Project Has At Least One SQL Database

Never Worked With a DBA On a Project Every Project Has At Least One SQL Database

My (Early) Relationship to SQL Database: Less Than Ideal

How Did I Get Better?

Trial By Fire

Problems “Random” slowness Data inconsistencies Weird Bugs

SQL Database Design For Developers

Scott Keck-Warren

Partner “Best Dad Ever”

PHP Developer

Educator

YouTube Videos Articles TikToks Conference Speaker

Podcaster

Community Corner Podcast

https://scott.keck-warren.com

Scott’s Rules For Database Design

Never Worked With a DBA On a Project

Scott’s Rules For Database Design Backup Your Database and Test It Normalize Your Database For Data Deduplication Use The Database Engine to Keep Data Clean Proactively Add Indexes to Keep Queries Performant Denormalize Your Database For Speed

Backup Your Database and Test It

“Backups are unimportant” “Backups are the most important thing in the world” One “Lost” File

mysql> delete from users where id = 1;

mysql> delete from users where id > 1;

Backup Your Database and Test It 3 - 2 - 1 Backups 3 copies 2 different mediums 1 “offsite”

Backup Your Database and Test It Router Router Internet Internet Internet Switch Switch Application Backups

Backup Your Database and Test It Router Router Internet Internet Internet Switch Switch Application Backups

Backup Your Database and Test It Router Router Internet Internet Internet Switch Switch Application Backups

Backup Your Database and Test It Router Router Internet Internet Internet Switch Switch Application Backups

Backup Your Database and Test It Internet Internet Internet Switch Switch Application Backups

Backup Your Database and Test It At Least Monthly Test Restoring It Ideally as part of DR plan

How Should I Backup My Data?

Backup Your Database and Test It 4 Types of Backups Continuous Data Protection - Constantly backing up changes Full - All Data Differential - All changes from last full Incremental - All changes from previous backup

Backup Your Database and Test It Continuous Data Protection Ideal method to backup data Database don’t generally support this

Backup Your Database and Test It Always Full Backups Makes it really easy to do a restore When DB is small - perfect When DB is not so small?

Grandfather-Father-Son Backups

Grandfather-Father-Son Backups Grandfather - full offsite backup for long term storage Father - full onsite backup for quick restores Son - differential backup for quick backup

Full Backups In MySQL mysqldump —single-transaction > full_backup.sql

Partial Backups In MySQL mysqldump —single-transaction \ —-where=“updated_at>={last_full}” > partial_backup.sql

Partial Backups In MySQL # in / etc / mysql / mysql.conf.d / mysqld.cnf Log_bin = /var/log/mysql/mysql-bin.log expire_logs_days = 10

Partial Backups In MySQL update users set x=“y” where id = 1 users /var/ log /mysql/mysql-bin.######

Partial Backups In MySQL # Create full backup and # 1. Start a new binary log # 2. Export the current binary log state mysqldump —single-transaction --flush-logs \ --master-data=2 > full_backup.sql

Partial Backups In MySQL # replay each “new” binary log mysqlbinlog /var/ log /mysql/mysql-bin.000002 | \ mysql -uroot -p mydb

Normalize Your Database For Data Deduplication

Users Table

Users Table Email address Password Active state Hire Date Listing of previous passwords Office Name Office City Office Zip

Users Table Email address (string) Password (string) Active state (string) Hire Date (string) Listing of previous passwords (string) Office Name (string) Office City (string) Office Zip (string)

Users Table email password active hire_date previous_password office_name office_phone office_city office_zip [email protected] hash1 1 1/1/2024 hash1
hash5
hash6 Main Office 555-555-5555 Saginaw 48609 [email protected] NULL 1 8/11/2024 hash2
hash7
hash8 main office 5555555555 Saginaw 48609 [email protected] hash3 1 May 11th, 23 hash3 Man office (555)555-5555 Saginaw 48609 [email protected] hash4 1 Tuesday hash4 Main 555/555/5555 Saginaw 48609

Normalize Your Database For Data Deduplication “[T]he process of structuring a relational database  in accordance with a series of so-called normal forms in order to reduce data redundancy and improve data integrity .” -“Database normalization” on Wikipedia

Normalize Your Database For Data Deduplication UNF: Unnormalized form 1NF: First normal form 2NF: Second normal form 3NF: Third normal form EKNF: Elementary key normal form BCNF: Boyce–Codd normal form 4NF: Fourth normal form ETNF: Essential tuple normal form 5NF: Fifth normal form DKNF: Domain-key normal form 6NF: Sixth normal form

Normalize Your Database For Data Deduplication UNF: Unnormalized form 1NF: First normal form 2NF: Second normal form 3NF: Third normal form EKNF: Elementary key normal form BCNF: Boyce–Codd normal form 4NF: Fourth normal form ETNF: Essential tuple normal form 5NF: Fifth normal form DKNF: Domain-key normal form 6NF: Sixth normal form

Normalize Your Database For Data Deduplication Boyce–Codd Normal Form: X should be a superkey for every functional dependency (FD) X−>Y in a given relation.

Unnormalized Form

Unnormalized Form A table doesn’t meet any of the conditions of normalization Essentially a spreadsheet email password active hire_date previous_password office_name office_phone office_city office_zip [email protected] hash1 1 1/1/2024 hash1
hash5
hash6 Main Office 555-555-5555 Saginaw 48609 [email protected] NULL 1 8/11/2024 hash2
hash7
hash8 main office 5555555555 Saginaw 48609 [email protected] hash3 1 May 11th, 23 hash3 Man office (555)555-5555 Saginaw 48609 [email protected] hash4 1 Tuesday hash4 Main 555/555/5555 Saginaw 48609

First Normal Form (1NF)

First Normal Form (1NF) The table contains a unique identifier, also called the primary key, that is used to identify the row. Each column contains atomic values (values that can not be broken down)

1NF - users email password active hire_date previous_password office_name office_phone office_city office_zip [email protected] hash1 1 1/1/2024 hash1
hash5
hash6 Main Office 555-555-5555 Saginaw 48609 [email protected] NULL 1 8/11/2024 hash2
hash7
Hash8 main office 5555555555 Saginaw 48609 [email protected] hash3 1 May 11th, 23 hash3 Man office (555)555-5555 Saginaw 48609 [email protected] hash4 1 Tuesday hash4 Main 555/555/5555 Saginaw 48609

1NF - users In MOST cases, a unique identifier should be: Auto-incrementing int -> good “fast” solution UUID -> slower but better for data shared across systems

1NF - users id email password active hire_date previous_password office_name office_phone office_city office_zip 1 [email protected] hash1 1 1/1/2024 hash1
hash5
hash6 Main Office 555-555-5555 Saginaw 48609 2 [email protected] NULL 1 8/11/2024 hash2
hash7
Hash8 main office 5555555555 Saginaw 48609 3 [email protected] hash3 1 May 11th, 23 hash3 Man office (555)555-5555 Saginaw 48609 4 [email protected] hash4 1 Tuesday hash4 Main 555/555/5555 Saginaw 48609

1NF - users id email password active hire_date previous_password office_name office_phone office_city office_zip 1 [email protected] hash1 1 1/1/2024 hash1
hash5
hash6 Main Office 555-555-5555 Saginaw 48609 2 [email protected] NULL 1 8/11/2024 hash2
hash7
hash8 main office 5555555555 Saginaw 48609 3 [email protected] hash3 1 May 11th, 23 hash3 Man office (555)555-5555 Saginaw 48609 4 [email protected] hash4 1 Tuesday hash4 Main 555/555/5555 Saginaw 48609

1NF - user_password_histories

1NF - user_password_histories id user_id password

1NF - user_password_histories id user_id password 1 1 hash1 2 1 hash5 3 1 hash6 4 2 hash2 5 2 hash7 6 2 hash8 7 3 hash3 8 4 hash4

1NF - users id email password active hire_date previous_password office_name office_phone office_city office_zip 1 [email protected] hash1 1 1/1/2024 hash1
hash5
hash6 Main Office 555-555-5555 Saginaw 48609 2 [email protected] NULL 1 8/11/2024 hash2
hash7
Hash8 main office 5555555555 Saginaw 48609 3 [email protected] hash3 1 May 11th, 23 hash3 Man office (555)555-5555 Saginaw 48609 4 [email protected] hash4 1 Tuesday hash4 Main 555/555/5555 Saginaw 48609

1NF - users id email password active hire_date office_name office_phone office_city office_zip 1 [email protected] hash1 1 1/1/2024 Main Office 555-555-5555 Saginaw 48609 2 [email protected] NULL 1 8/11/2024 main office 5555555555 Saginaw 48609 3 [email protected] hash3 1 May 11th, 23 Man office (555)555-5555 Saginaw 48609 4 [email protected] hash4 1 Tuesday Main 555/555/5555 Saginaw 48609

Second Normal Form (2NF)

Second Normal Form (2NF) Is already in 1NF All the non-key columns are dependent on the primary key of the table

Second Normal Form (2NF) id email password active hire_date office_name office_phone office_city office_zip 1 [email protected] hash1 1 1/1/2024 Main Office 555-555-5555 Saginaw 48609 2 [email protected] NULL 1 8/11/2024 main office 5555555555 Saginaw 48609 3 [email protected] hash3 1 May 11th, 23 Man office (555)555-5555 Saginaw 48609 4 [email protected] hash4 1 Tuesday Main 555/555/5555 Saginaw 48609

2nd - offices id name phone city zip 1 Main Office 555-555-5555 Saginaw 48609 2 main office 5555555555 Saginaw 48609 3 Man office (555)555-5555 Saginaw 48609 4 Main 555/555/5555 Saginaw 48609

2NF - users id email password active hire_date office_name office_phone office_city office_zip 1 [email protected] hash1 1 1/1/2024 Main Office 555-555-5555 Saginaw 48609 2 [email protected] NULL 1 8/11/2024 main office 5555555555 Saginaw 48609 3 [email protected] hash3 1 May 11th, 23 Man office (555)555-5555 Saginaw 48609 4 [email protected] hash4 1 Tuesday Main 555/555/5555 Saginaw 48609

2NF - users id email password active hire_date office_name office_phone office_city office_zip office_id 1 [email protected] hash1 1 1/1/2024 Main Office 555-555-5555 Saginaw 48609 1 2 [email protected] NULL 1 8/11/2024 main office 5555555555 Saginaw 48609 2 3 [email protected] hash3 1 May 11th, 23 Man office (555)555-5555 Saginaw 48609 3 4 [email protected] hash4 1 Tuesday Main 555/555/5555 Saginaw 48609 4

2NF - users id email password active hire_date office_id 1 [email protected] hash1 1 1/1/2024 1 2 [email protected] NULL 1 8/11/2024 2 3 [email protected] hash3 1 May 11th, 23 3 4 [email protected] hash4 1 Tuesday 4

Third Normal Form (3NF)

Third Normal Form (3NF) Is already in 2NF It contains columns that are non-transitively dependent on the primary key

3NF - offices id name phone city zip 1 Main Office 555-555-5555 Saginaw 48609 2 main office 5555555555 Saginaw 48609 3 Man office (555)555-5555 Saginaw 48609 4 Main 555/555/5555 Saginaw 48609

3NF - zips id city 48609 Saginaw 48640 Midland 48642 Midland 48901 Lansing

3NF - zips id city state 48609 Saginaw MI 48640 Midland MI 48642 Midland MI 48901 Lansing MI

Old Table Structure

New Table Structure

Use The Database Engine to Keep Data Clean

Why?

Why? $validated = $request ->validate([ "email" => "required|unique:users|max:255" , ]);

mysql> insert into users (password) values (“just a password?"); Query OK, 1 row affected (0.01 sec)

mysql> insert into users (password) values (“just a password?"); Query OK, 1 row affected (0.01 sec)

Garbage In -> Garbage Out

Garbage Data -> Bugs

To Prevent Bugs: Make The Database Work For Us

id email password active hire_date office_id 1 [email protected] hash1 1 1/1/2024 1 2 [email protected] NULL 1 8/11/2024 2 3 [email protected] hash3 1 May 11th, 23 3 4 [email protected] hash4 1 Tuesday 4 5 Hash12 2 2024-04-01 1000

Use Correct Column Types

Use Correct Column Types id email password active hire_date office_id 1 [email protected] hash1 1 1/1/2024 1 2 [email protected] NULL 1 8/11/2024 2 3 [email protected] hash3 1 May 11th, 23 3 4 [email protected] hash4 1 Tuesday 4 5 Hash12 2 2024-04-01 1000

Use Correct Column Types Numeric: INT, TINYINT, BIGINT, FLOAT, REAL, etc. Date/Time: DATE, TIME, DATETIME, etc. String: CHAR, VARCHAR, TEXT, etc. Binary data types such as: BLOB, etc.

Use Correct Column Types create table users ( id int primary key auto_increment , email char ( 255 ), password char ( 255 ), active char ( 255 ), hire_date char ( 255 ), office_id int );

Use Correct Column Types create table users ( id int primary key auto_increment , email char ( 255 ), password char ( 255 ), active tinyint ( 1 ), hire_date date , office_id int );

Use Correct Column Types mysql> insert into users (hire_date) values ("tuesday"); ERROR 1292 (22007): Incorrect date value: 'tuesday' for column 'hire_date' at row 1 mysql> insert into users (hire_date) values ("May 11th, 23"); ERROR 1292 (22007): Incorrect date value: 'May 11th, 23' for column 'hire_date' at row 1

Use NOT NULL for Required Fields

Use NOT NULL for Required Fields mysql> insert into users (password) values (“just a password?"); Query OK, 1 row affected (0.01 sec)

Use NOT NULL for Required Fields create table users ( id int primary key auto_increment , email char ( 255 ), password char ( 255 ), active tinyint ( 1 ), hire_date date , office_id int );

Use NOT NULL for Required Fields create table users ( id int primary key auto_increment , email char ( 255 ) not null , password char ( 255 ) not null , active tinyint ( 1 ) not null , hire_date date not null, office_id int not null );

Use NOT NULL for Required Fields mysql> insert into users (password) values (“just a password?”); ERROR 1364 (HY000): Field ‘email' doesn't have a default value

Use NOT NULL for Required Fields mysql> insert into users (password) values (“just a password?”); ERROR 1364 (HY000): Field ‘email' doesn't have a default value

Use NOT NULL for Required Fields mysql> insert into users (email, password) values (“s@s”, "just a password?"); ERROR 1364 (HY000): Field 'active' doesn't have a default value

Use UNIQUE for Unique Values

Use UNIQUE for Unique Values mysql> insert into users (email) values ("[email protected]"); Query OK, 1 row affected (0.01 sec) mysql> insert into users (email) values ("[email protected]"); Query OK, 1 row affected (0.02 sec)

create table users ( id int primary key auto_increment , email char ( 255 ), password char ( 255 ), active tinyint ( 1 ), hire_date date , office_id int ); Use UNIQUE for Unique Values

Use UNIQUE for Unique Values create table users ( id int primary key auto_increment , email char ( 255 ), password char ( 255 ), active tinyint ( 1 ), hire_date date , office_id int, unique (email) );

Use UNIQUE for Unique Values mysql> insert into users (email) values ("[email protected]"); ERROR 1062 (23000): Duplicate entry '[email protected]' for key 'users.email'

Use UNIQUE for Unique Values create table users ( id int primary key auto_increment , email char ( 255 ), password char ( 255 ), active tinyint ( 1 ), hire_date date , office_id int, unique (email, office_id) );

Use Foreign Keys For References To Other Tables

Use Foreign Keys For References To Other Tables id name phone city zip_id 1 Main Office 555-555-5555 Saginaw 48609 2 main office 5555555555 Saginaw 48609 3 Man office (555)555-5555 Saginaw 48609 4 Main 555/555/5555 Saginaw 48609

Use Foreign Keys For References To Other Tables id name phone city zip_id 1 Main Office 555-555-5555 Saginaw 48609 2 main office 5555555555 Saginaw 48609 3 Man office (555)555-5555 Saginaw 48609

Use Foreign Keys For References To Other Tables id name phone city zip_id 1 Main Office 555-555-5555 Saginaw 48609 2 main office 5555555555 Saginaw 48609

Use Foreign Keys For References To Other Tables id name phone city zip_id 1 Main Office 555-555-5555 Saginaw 48609

Constraints - Foreign Keys

select count(*) from users; select name, email, offices.name from users inner join offices on users.office_id = offices.id;

Orphaned Rows

create table users ( id int primary key auto_increment , email char ( 255 ), password char ( 255 ), active tinyint ( 1 ), hire_date date , office_id int );

mysql> insert into users (email, office_id) values ("[email protected]", 1000000); Query OK, 1 row affected (0.03 sec)

mysql> insert into users (email, office_id ) values ("[email protected]", 1000000 ); Query OK, 1 row affected (0.03 sec)

Foreign Key Constraints

create table users ( id int primary key auto_increment , email char ( 255 ), password char ( 255 ), active char ( 255 ), hire_date date , office_id int, ); Foreign Key Constraints create table offices ( id int primary key auto_increment , name char ( 255 ), phone char ( 255 ), zip_id int );

create table users ( id int primary key auto_increment , email char ( 255 ), password char ( 255 ), active char ( 255 ), hire_date date , office_id int, foreign key ); Foreign Key Constraints create table offices ( id int primary key auto_increment , name char ( 255 ), phone char ( 255 ), zip_id int );

create table users ( id int primary key auto_increment , email char ( 255 ), password char ( 255 ), active char ( 255 ), hire_date date , office_id int, foreign key (office_id) ); Foreign Key Constraints create table offices ( id int primary key auto_increment , name char ( 255 ), phone char ( 255 ), zip_id int );

create table users ( id int primary key auto_increment , email char ( 255 ), password char ( 255 ), active char ( 255 ), hire_date date , office_id int, foreign key (office_id) references offices(id) ); Foreign Key Constraints create table offices ( id int primary key auto_increment , name char ( 255 ), phone char ( 255 ), zip_id int );

Foreign Key Constraints mysql> insert into users (email, office_id) values ("[email protected]", 1000000); ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`databasetalk`.`users`, CONSTRAINT `users_ibfk_1` FOREIGN KEY (`office_id`) REFERENCES `offices` (`id`))

Foreign Key Constraints mysql> insert into users (email, office_id) values ("[email protected]", 1000000); ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails (`databasetalk`.`users`, CONSTRAINT `users_ibfk_1` FOREIGN KEY (`office_id`) REFERENCES `offices` (`id`))

Foreign Key Constraints mysql> insert into users (email, office_id ) values ("[email protected]", 1 ); Query OK, 1 row affected (0.01 sec)

Foreign Key Constraints mysql> delete from offices where id = 1; ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails (`databasetalk`.`users`, CONSTRAINT `users_ibfk_1` FOREIGN KEY (`office_id`) REFERENCES `offices` (`id`))

Foreign Key Constraints mysql> delete from offices where id = 1; ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails (`databasetalk`.`users`, CONSTRAINT `users_ibfk_1` FOREIGN KEY (`office_id`) REFERENCES `offices` (`id`))

create table users ( id int primary key auto_increment , email char ( 255 ), password char ( 255 ), active char ( 255 ), hire_date date , office_id int ); Foreign Key Constraints create table user_password_histories ( id int primary key auto_increment , user_id int , password char ( 255 ), foreign key (user_id) references users(id) on delete cascade );

create table users ( id int primary key auto_increment , email char ( 255 ), password char ( 255 ), active char ( 255 ), hire_date date , office_id int ); Foreign Key Constraints create table user_password_histories ( id int primary key auto_increment , user_id int , password char ( 255 ), foreign key (user_id) references users(id) on delete cascade );

create table users ( id int primary key auto_increment , email char ( 255 ), password char ( 255 ), active char ( 255 ), hire_date date , office_id int ); Foreign Key Constraints create table user_password_histories ( id int primary key auto_increment , user_id int , password char ( 255 ), foreign key (user_id) references users(id) on delete cascade );

Foreign Key Constraints mysql> insert into user_password_histories (user_id, password) values (2, "test1"); Query OK, 1 row affected (0.01 sec) mysql> insert into user_password_histories (user_id, password) values (2, "test2"); Query OK, 1 row affected (0.01 sec)

Foreign Key Constraints mysql> delete from users where id = 2; Query OK, 1 row affected (0.01 sec) mysql> select * from user_password_histories where user_id = 2; Empty set (0.00 sec)

Foreign Key Constraints mysql> delete from users where id = 2; Query OK, 1 row affected (0.01 sec) mysql> select * from user_password_histories where user_id = 2; Empty set (0.00 sec)

create table users ( id int primary key auto_increment , email char ( 255 ), password char ( 255 ), active char ( 255 ), hire_date date , office_id int ); Foreign Key Constraints create table user_password_histories ( id int primary key auto_increment , user_id int , password char ( 255 ), foreign key (user_id) references users(id) on delete cascade );

create table users ( id int primary key auto_increment , email char ( 255 ), password char ( 255 ), active char ( 255 ), hire_date date , office_id int ); Foreign Key Constraints create table user_password_histories ( id int primary key auto_increment , user_id int , password char ( 255 ), foreign key (user_id) references users(id) on delete set null );

Downsides to Constraints

Performance

Use Triggers For Complex Requirements

Use Triggers For Complex Requirements id email password active hire_date office_id 1 [email protected] hash1 1 1/1/2024 1 2 [email protected] NULL 1 8/11/2024 2 3 [email protected] hash3 1 May 11th, 23 3 4 [email protected] hash4 1 Tuesday 4 5 Hash12 2 2024-04-01 1000

Use Triggers For Complex Requirements Triggers add additional power to DB Operate based on create, update, or delete

Use Triggers For Complex Requirements CREATE TRIGGER users_before_insert_valid_active BEFORE INSERT ON users FOR EACH ROW BEGIN IF NEW.active < || NEW.active > 1 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'active must be 0 or 1' ; END IF ; END ;

Use Triggers For Complex Requirements CREATE TRIGGER users_before_insert_validate_active BEFORE INSERT ON users FOR EACH ROW BEGIN IF NEW.active < || NEW.active > 1 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'active must be 0 or 1' ; END IF ; END ;

Use Triggers For Complex Requirements CREATE TRIGGER users_before_insert_validate_active BEFORE INSERT ON users FOR EACH ROW BEGIN IF NEW.active < || NEW.active > 1 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'active must be 0 or 1' ; END IF ; END ;

Use Triggers For Complex Requirements CREATE TRIGGER users_before_insert_validate_active BEFORE INSERT ON users FOR EACH ROW BEGIN IF NEW.active < || NEW.active > 1 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'active must be 0 or 1' ; END IF ; END ;

Use Triggers For Complex Requirements CREATE TRIGGER users_before_insert_validate_active BEFORE INSERT ON users FOR EACH ROW BEGIN IF NEW.active < || NEW.active > 1 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'active must be 0 or 1' ; END IF ; END ;

Use Triggers For Complex Requirements CREATE TRIGGER users_before_insert_validate_active BEFORE INSERT ON users FOR EACH ROW BEGIN IF NEW.active < || NEW.active > 1 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'active must be 0 or 1' ; END IF ; END ;

Use Triggers For Complex Requirements CREATE TRIGGER users_before_update_validate_active BEFORE UPDATE ON users FOR EACH ROW BEGIN IF NEW.active < || NEW.active > 1 THEN SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'active must be 0 or 1' ; END IF ; END ;

Use Triggers For Complex Requirements mysql> insert into users (active) values (2); ERROR 1644 (45000): active must be 0 or 1

Use Triggers For Complex Requirements mysql> insert into users (active) values (2); ERROR 1644 (45000): active must be 0 or 1

type = ? Require fields 1,2,3 1 Require fields 2,5,11 2

Downsides to Triggers

Performance

“Magic”

Proactively Add Indexes to Keep Queries Performant

Indexes in Databases

select * from users where hire_date = "2023-12-03"

Indexes in Databases

Indexes in Databases

Indexes in Databases 2023-12-03 Index: hire_date 2023-12-04 2023-12-05 2023-12-06

create table users ( id int primary key auto_increment , email char ( 255 ), password char ( 255 ), active char ( 255 ), hire_date date , office_id int , );

create table users ( id int primary key auto_increment , email char ( 255 ), password char ( 255 ), active char ( 255 ), hire_date date , office_id int , index (hire_date), );

create table users ( id int primary key auto_increment , email char ( 255 ), password char ( 255 ), active char ( 255 ), hire_date date , office_id int , index (hire_date), index (active), );

create table users ( id int primary key auto_increment , email char ( 255 ), password char ( 255 ), active char ( 255 ), hire_date date , office_id int , index (hire_date), index (active), index (hire_date, active) );

Start Out Simple

Start Out Simple We “automatically” create indexes for: All foreign key relationships Any column that’s searched

Start Out Simple Every month or so run database tools to find missing indexes set global log_queries_not_using_indexes = “On”; mysqldumpslow to check results after an hour, a day, and a week Determine IF we feel it’s necessary

Denormalize Your Database For Speed

Denormalize Your Database For Speed “[D]enormalization is the process of trying to improve the read performance of a database, at the expense of losing some write performance, by adding redundant copies of data or by grouping data.” -“Denormalization” on Wikipedia

Denormalize Your Database For Speed Office Jan Feb March … Nov Dec Charges (USD) 10 7 8 … 8 7 Credits (USD) 2 … 1 1 Costs (USD) 2 2 2 … 2 2 Profit 6 5 6 … 5 4

Denormalize Your Database For Speed Office Jan Feb March … Nov Dec Charges (USD) 10 7 8 … 8 7 Credits (USD) 2 … 1 1 Costs (USD) 2 2 2 … 2 2 Profit 6 5 6 … 5 4

Denormalize Your Database For Speed What we found: 10s of millions charges 100s of thousand credits 10s of thousand costs

Methods of Denormalization Pre-joining Table Splitting Adding derived and redundant columns Mirrored tables

Pre-joining create table charges ( id int primary key auto_increment , amount not null int , office_id not null int , created_at datetime );

Pre-joining create table charges ( id int primary key auto_increment , amount not null int , office_id not null int , office_name not null char ( 255 ), created_at datetime );

Table Splitting create table charges ( id int primary key auto_increment , amount not null int , office_id not null int , created_at datetime ); create table charges_office_1 ( id int primary key auto_increment , amount not null int , office_id not null int , created_at datetime ); create table charges_office_2 ( id int primary key auto_increment , amount not null int , office_id not null int , created_at datetime ); create table charges_office_3 ( id int primary key auto_increment , amount not null int , office_id not null int , created_at datetime );

Table Splitting create table charges ( id int primary key auto_increment , amount not null int , office_id not null int , description text , notes text , file_upload_1 blob , file_upload_2 blob , file_upload_3 blob , file_upload_4 blob , file_upload_5 blob , created_at datetime ); create table charges_billing ( id int primary key , amount not null int , office_id not null int , created_at datetime ); create table charges_support ( id int primary key , description text , notes text , file_upload_1 blob , file_upload_2 blob , file_upload_3 blob , file_upload_4 blob , file_upload_5 blob , created_at datetime );

Adding Derived and Redundant Columns create table charges ( id int primary key auto_increment , amount not null int , office_id not null int , invoice_month int not null, created_at datetime );

Adding Derived and Redundant Columns create table charges ( id int primary key auto_increment , amount not null int , office_id not null int , invoice_month int not null , created_at datetime );

Mirrored Tables Office Jan Feb March … Nov Dec Charges (USD) 10 7 8 … 8 7 Credits (USD) 2 … 1 1 Costs (USD) 2 2 2 … 2 2 Profit 6 5 6 … 5 4

Mirrored Tables create table monthly_stats ( id int primary key auto_increment , month_and_year varchar ( 255 ) not null , charges int not null , credits int not null , costs int not null );

Mirrored Tables Office Jan Feb March … Nov Dec Charges (USD) 10 7 8 … 8 7 Credits (USD) 2 … 1 1 Costs (USD) 2 2 2 … 2 2 Profit 6 5 6 … 5 4

Generating Denormalize Data Delayed Generate using cron job (every day or hour) “Instantly” Internal logic inside the application (event sourcing) Use a trigger inside the db

What You Need to Know

What You Need to Know Backup Your Database and Test It Normalize Your Database For Data Deduplication Use The Database Engine to Keep Data Clean Proactively Add Indexes to Keep Queries Performant Denormalize Your Database For Speed

What You Need to Know The table contains a unique identifier, also called the primary key, that is used to identify the row. Each column contains atomic values (values that can not be broken down) All the non-key columns are dependent on the primary key of the table It contains columns that are non-transitively dependent on the primary key

What You Need to Know Make the DB Work With You Correct Column Types NOT NULL for Required Fields UNIQUE for Unique Values Foreign Keys For References To Other Tables Triggers For Complex Requirements

What You Need to Know Use indexes on commonly searched columns Start simple

What You Need to Know Pre-joining Table Splitting Adding derived and redundant columns Mirrored tables

Thank The Sponsors

Thank The Organizers

Thank The Speakers

Questions/Follow Me Questions? Please rate the talk https://tinyurl.com/todo