├── LICENSE └── README.md /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Gorilla Logic, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Clean-code-sql 2 | 3 | The final guide to write best SQL queries. 4 | 5 | ## Table of Contents 6 | 7 | - [Clean-code-sql](#clean-code-sql) 8 | - [Table of Contents](#table-of-contents) 9 | - [Introduction](#introduction) 10 | - [Indenting](#indenting) 11 | - [Select](#select) 12 | - [Joins](#joins) 13 | - [Tables](#tables) 14 | - [Columns](#columns) 15 | - [Procedures](#procedures) 16 | - [Views](#views) 17 | - [Comments](#comments) 18 | - [Modularize](#modularize) 19 | - [Temporary tables](#temporary-tables) 20 | - [Looping](#looping) 21 | - [When troubleshooting](#when-troubleshooting) 22 | 23 | ## Introduction 24 | 25 | These are guidelignes about good practices to write cleaner and better SQL queries. 26 | 27 | ## Indenting 28 | 29 | Indenting makes SQL query visually structured and easier to follow it. 30 | 31 | Bad: 32 | 33 | ```sql 34 | SELECT d.DepartmentName,e.Name, e.LastName, e.Address, e.State, e.City, e.Zip FROM Departments AS d JOIN Employees AS e ON d.ID = e.DepartmentID WHERE d.DepartmentName != 'HR'; 35 | ``` 36 | 37 | Good: 38 | 39 | ```sql 40 | SELECT d.DepartmentName 41 | , e.Name 42 | , e.LastName 43 | , e.Address 44 | , e.State 45 | , e.City 46 | , e.Zip 47 | FROM Departments AS d 48 | JOIN Employees AS e ON d.ID = e.DepartmentID 49 | WHERE d.Name != 'HR'; 50 | ``` 51 | 52 | [Back to top](#table-of-contents) 53 | 54 | ## Select 55 | 56 | SELECT \* 57 | 58 | When using SELECT \*, all the columns will be returned in the same order as they are defined, so the data returned can change whenever the table definitions change. 59 | 60 | Bad: 61 | 62 | ```sql 63 | SELECT * 64 | FROM Departments 65 | ``` 66 | 67 | Good: 68 | 69 | ```sql 70 | SELECT d.DepartmentName 71 | , d.Location 72 | FROM Departments AS d 73 | ``` 74 | 75 | **Table Aliases** 76 | 77 | It is more readable to use aliases instead of writing columns with no table information. 78 | 79 | Bad: 80 | 81 | ```sql 82 | SELECT Name, 83 | DepartmentName 84 | FROM Departments 85 | JOIN Employees ON ID = DepartmentID; 86 | ``` 87 | 88 | Good: 89 | 90 | ```sql 91 | SELECT e.Name 92 | , d.DepartmentName 93 | FROM Departments AS d 94 | JOIN Employees AS e ON d.ID = e.DepartmentID; 95 | ``` 96 | 97 | **SELECT DISTINCT** 98 | 99 | SELECT DISTINCT is a practical way to remove duplicates from a query, however its use requires a high processing cost. Better select more fields to create unique results. 100 | 101 | Bad: 102 | 103 | ```sql 104 | SELECT DISTINCT Name, 105 | LastName, 106 | Address 107 | FROM Employees; 108 | ``` 109 | 110 | Good: 111 | 112 | ```sql 113 | SELECT Name 114 | , LastName 115 | , Address 116 | , State 117 | , City 118 | , Zip 119 | FROM Employees; 120 | ``` 121 | 122 | **EXISTS** 123 | 124 | Prefer EXISTS to IN. Exists process exits as soon it finds the value, whereas IN scan the entire table. 125 | 126 | Bad: 127 | 128 | ```sql 129 | SELECT e.name 130 | , e.salary 131 | FROM Employee AS e 132 | WHERE e.EmployeeID IN (SELECT p.IdNumber 133 | FROM Population AS p 134 | WHERE p.country = "Canada" 135 | AND p.city = "Toronto"); 136 | ``` 137 | 138 | Good: 139 | 140 | ```sql 141 | SELECT e.name 142 | , e.salary 143 | FROM Employee AS e 144 | WHERE e.EmployeeID EXISTS (SELECT p.IdNumber 145 | FROM Population AS p 146 | WHERE p.country = "Canada" 147 | AND p.city = "Toronto"); 148 | ``` 149 | 150 | [Back to top](#table-of-contents) 151 | 152 | ## Joins 153 | 154 | Use the ANSI-Standard Join clauses instead of the old style joins. 155 | 156 | Bad: 157 | 158 | ```sql 159 | SELECT e.Name, 160 | d.DepartmentName 161 | FROM Departments AS d, 162 | Employees AS e 163 | WHERE d.ID = e.DepartmentID; 164 | ``` 165 | 166 | Good: 167 | 168 | ```sql 169 | SELECT e.Name, 170 | d.DepartmentName 171 | FROM Departments AS d 172 | JOIN Employees AS e ON d.ID = e.DepartmentID; 173 | ``` 174 | 175 | [Back to top](#table-of-contents) 176 | 177 | ## Tables 178 | 179 | For naming tables take into consideration the following advices: 180 | 181 | - Use singular names 182 | - Use schema name prefix 183 | - Use Pascal case 184 | 185 | Bad: 186 | 187 | ```sql 188 | CREATE TABLE Addresses 189 | ``` 190 | 191 | Good: 192 | 193 | ```sql 194 | CREATE TABLE [Person].[Address] 195 | ``` 196 | 197 | [Back to top](#table-of-contents) 198 | 199 | ## Columns 200 | 201 | For naming columns take into consideration the following advices: 202 | 203 | - Use singular names 204 | - Use Pascal case 205 | - Name your primary keys using "[TableName]ID" format 206 | - Be descriptive 207 | - Be consistent 208 | 209 | Bad: 210 | 211 | ```sql 212 | CREATE TABLE [Person].[Address]( 213 | [Address] [int] NOT NULL, 214 | [AddressLine1] [nvarchar](60) NOT NULL, 215 | [Address2] [nvarchar](60) NULL, 216 | [city] [nvarchar](30) NOT NULL, 217 | [State_ProvinceID] [int] NOT NULL, 218 | [postalCode] [nvarchar](15) NOT NULL, 219 | [ModifiedDate] [datetime] NOT NULL, 220 | CONSTRAINT [PK_Address_AddressID] PRIMARY KEY CLUSTERED ([Address]) 221 | ); 222 | ``` 223 | 224 | Good: 225 | 226 | ```sql 227 | CREATE TABLE [Person].[Address]( 228 | [AddressID] [int] NOT NULL, 229 | [AddressLine1] [nvarchar](60) NOT NULL, 230 | [AddressLine2] [nvarchar](60) NULL, 231 | [City] [nvarchar](30) NOT NULL, 232 | [StateProvinceID] [int] NOT NULL, 233 | [PostalCode] [nvarchar](15) NOT NULL, 234 | [ModifiedDate] [datetime] NOT NULL, 235 | CONSTRAINT [PK_Address_AddressID] PRIMARY KEY CLUSTERED ([AddressID]) 236 | ); 237 | ``` 238 | 239 | [Back to top](#table-of-contents) 240 | 241 | ## Procedures 242 | 243 | To write incredible stored procedures, take into consideration the following advices: 244 | 245 | - Use the SET NOCOUNT ON option in the beginning of the stored procedure to prevent the server from sending row counts of data affected by some statement or stored procedure to the client. 246 | - Try to write the DECLARATION and initialization (SET) at the beginning of the stored procedure. 247 | - Write all the SQL Server keywords in the CAPS letter. 248 | - Avoid to use 'sp\_' at the begining of the stored procedure name. 249 | - Take into consideration the previous sections, [Indenting](#indenting) - [Select](#select) - [Joins](#joins). 250 | 251 | Bad: 252 | 253 | ```sql 254 | CREATE OR ALTER PROCEDURE [HumanResources].[sp_UpdateEmployeePersonalInfo] 255 | @BusinessEntityID [int], 256 | @NationalIDNumber [nvarchar](15), 257 | @BirthDate [datetime], 258 | @MaritalStatus [nchar](1), 259 | @Gender [nchar](1) 260 | AS 261 | BEGIN 262 | --some code 263 | END; 264 | ``` 265 | 266 | Good: 267 | 268 | ```sql 269 | CREATE OR ALTER PROCEDURE [HumanResources].[UpdateEmployeePersonalInfo] 270 | @BusinessEntityID [int], 271 | @NationalIDNumber [nvarchar](15), 272 | @BirthDate [datetime], 273 | @MaritalStatus [nchar](1), 274 | @Gender [nchar](1) 275 | AS 276 | BEGIN 277 | SET NOCOUNT ON; 278 | 279 | DECLARE @messageLog nvarchar(100); 280 | 281 | SET @messageLog = 'Updating the employee personal info'; 282 | 283 | --some code 284 | END; 285 | ``` 286 | 287 | [Back to top](#table-of-contents) 288 | 289 | ## Views 290 | 291 | Don't use the word 'view' in the view's name. 292 | 293 | Bad: 294 | 295 | ```sql 296 | - ViewEmployeesDepartments 297 | - EmployeeDeparmentsView 298 | ``` 299 | 300 | Good: 301 | 302 | ```sql 303 | - EmployeeDeparments 304 | ``` 305 | 306 | Don't include Order by and Where conditions into the view. 307 | 308 | Bad: 309 | 310 | ```sql 311 | SELECT EmployeeId, 312 | Name, 313 | LastName, 314 | Address, 315 | State 316 | FROM Employees 317 | WHERE EmployeeId > 0 ORDER BY Name 318 | ``` 319 | 320 | Good: 321 | 322 | ```sql 323 | SELECT Name, 324 | LastName, 325 | Address, 326 | State, 327 | City, 328 | Zip 329 | FROM Employees; 330 | ``` 331 | 332 | Use Alias with table + column to specify when the values come from. 333 | 334 | Bad: 335 | 336 | ```sql 337 | SELECT e.Name, 338 | d.Name 339 | FROM Departments AS d 340 | JOIN Employees AS e ON d.ID = e.DepartmentID; 341 | ``` 342 | 343 | Good: 344 | 345 | ```sql 346 | SELECT e.Name as EmployeeName, 347 | d.Name as DeparmentName 348 | FROM Departments AS d 349 | JOIN Employees AS e ON d.ID = e.DepartmentID; 350 | ``` 351 | 352 | [Back to top](#table-of-contents) 353 | 354 | ## Comments 355 | 356 | Write helpful comments and only when necessary. Do not write comments trying to explain the code that we will understand by reading the code itself. 357 | 358 | Bad: 359 | 360 | ```sql 361 | CREATE OR ALTER PROCEDURE [dbo].[uspLogError] 362 | @ErrorLogID [int] = 0 OUTPUT 363 | AS 364 | BEGIN 365 | SET NOCOUNT ON; 366 | -- Setting ErrorLogID to 0 367 | SET @ErrorLogID = 0; 368 | 369 | //some code 370 | END; 371 | ``` 372 | 373 | Good: 374 | 375 | ```sql 376 | CREATE OR ALTER PROCEDURE [dbo].[uspLogError] 377 | @ErrorLogID [int] = 0 OUTPUT 378 | AS 379 | BEGIN 380 | SET NOCOUNT ON; 381 | 382 | -- Output parameter value of 0 indicates that error 383 | -- information was not logged 384 | SET @ErrorLogID = 0; 385 | 386 | //some code 387 | END; 388 | ``` 389 | 390 | [Back to top](#table-of-contents) 391 | 392 | ## Modularize 393 | 394 | When writing a long query, thinking about modularizing is a good idea. Using common table expressions (CTEs) you can break down your code and make it easy to understand. 395 | 396 | Bad: 397 | 398 | ```sql 399 | SELECT e.name 400 | , e.salary 401 | FROM Employee AS e 402 | WHERE e.EmployeeID EXISTS (SELECT p.IdNumber 403 | FROM Population AS p 404 | WHERE p.country = "Canada" 405 | AND p.city = "Toronto") 406 | AND e.salary >= (SELECT AVG(s.salary) 407 | FROM salaries AS s 408 | WHERE s.gender = "Female") 409 | ``` 410 | 411 | Good: 412 | 413 | ```sql 414 | WITH toronto_ppl AS ( 415 | SELECT p.IdNumber 416 | FROM Population AS p 417 | WHERE p.country = "Canada" 418 | AND p.city = "Toronto" 419 | ) 420 | , avg_female_salary AS ( 421 | SELECT AVG(s.salary) AS avgSalary 422 | FROM salaries AS s 423 | WHERE s.gender = "Female" 424 | ) 425 | SELECT e.name 426 | , e.salary 427 | FROM Employee AS e 428 | WHERE e.EmployeeID EXISTS (SELECT IdNumber FROM toronto_ppl) 429 | AND e.salary >= (SELECT avgSalary FROM avg_female_salary) 430 | ``` 431 | 432 | [Back to top](#table-of-contents) 433 | 434 | ## Temporary tables 435 | 436 | Prefer to use a Table variable when the result set is small instead of a Temporary table. A temp table will be created on the temp database and that will make your query slower. 437 | 438 | Bad: 439 | 440 | ```sql 441 | DECLARE TABLE #ListOWeekDays 442 | ( 443 | DyNumber INT, 444 | DayAbb VARCHAR(40), 445 | WeekName VARCHAR(40) 446 | ) 447 | ``` 448 | 449 | Good: 450 | 451 | ```sql 452 | DECLARE @ListOWeekDays TABLE 453 | ( 454 | DyNumber INT, 455 | DayAbb VARCHAR(40), 456 | WeekName VARCHAR(40) 457 | ) 458 | ``` 459 | 460 | [Back to top](#table-of-contents) 461 | 462 | ## Looping 463 | 464 | Avoid running queries using a loop. Coding SQL queries in loops slows down the entire sequence. 465 | 466 | Bad: 467 | 468 | ```sql 469 | WHILE @date <= @endMonth 470 | BEGIN 471 | SET @date = DATEADD(d, 1, @date) 472 | END 473 | ``` 474 | 475 | Good: 476 | 477 | ```sql 478 | DECLARE @StartDate DateTime = '2021-06-01' 479 | DECLARE @EndDate DateTime = '2021-06-29' 480 | 481 | ;WITH populateDates (dates) AS ( 482 | 483 | SELECT @StartDate as dates 484 | UNION ALL 485 | SELECT DATEADD(d, 1, dates) 486 | FROM populateDates 487 | WHERE DATEADD(d, 1, dates)<=@EndDate 488 | 489 | ) 490 | SELECT * 491 | INTO dbo.SomeTable 492 | FROM populateDates 493 | ``` 494 | 495 | [Back to top](#table-of-contents) 496 | 497 | ## When troubleshooting 498 | 499 | - Use the Execution Plan tool if you have it available, with it you can see the statistics and warnings, which can help you find improvement areas. 500 | - Use the comma before the column, it is useful when for some reason you have to comment out columns. 501 | 502 | [Back to top](#table-of-contents) 503 | --------------------------------------------------------------------------------