Buenas practicas en SQL

Estas son recomendaciones sobre buenas prácticas para escribir consultas SQL más limpias y con mejor rendimiento.

Indenting 

Indenting hace que la consulta SQL esté visualmente estructurada y sea más fácil de seguir.

Malo:

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';

Bien:

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.Name != 'HR';

Select

SELECT*

Al utilizar SELECT *, todas las columnas se devolverán en el mismo orden en que están definidas, por lo que los datos devueltos pueden cambiar cada vez que cambien las definiciones de la tabla.

Malo:

SELECT   *
FROM     Departments
Bien:
SELECT    d.DepartmentName
        , d.Location
FROM     Departments AS d

Bien:

SELECT    d.DepartmentName
        , d.Location
FROM     Departments AS d

Table Aliases

Es más legible usar alias en lugar de escribir columnas sin información de tabla.

Malo:

SELECT   Name,
         DepartmentName
FROM     Departments
JOIN     Employees  ON ID = DepartmentID;

Bien:

SELECT   e.Name
       , d.DepartmentName
FROM     Departments AS d
JOIN     Employees AS e ON d.ID = e.DepartmentID;

select distinct

SELECT DISTINCT es una forma práctica de eliminar duplicados de una consulta, sin embargo, su uso requiere un alto costo de procesamiento. En lugar de eso seleccionemos más campos para crear resultados únicos.

Malo:

SELECT   DISTINCT Name,
         LastName,
         Address
FROM     Employees;

Bien:

SELECT   Name
       , LastName
       , Address
       , State
       , City
       , Zip 
FROM     Employees;

Exists

Preferir EXISTS a IN. Existe el proceso sale tan pronto como encuentra el valor, mientras que IN escanea toda la tabla.

Malo:

SELECT   e.name
       , e.salary
FROM Employee AS e
WHERE e.EmployeeID IN (SELECT   p.IdNumber
			FROM   Population AS p
			WHERE  p.country = "Canada"
			       AND   p.city = "Toronto");

Bien:

SELECT   e.name
       , e.salary
FROM Employee AS e
WHERE e.EmployeeID EXISTS (SELECT   p.IdNumber
			   FROM   Population AS p
			   WHERE  p.country = "Canada"
				  AND   p.city = "Toronto");

Joins

Utilice las cláusulas ANSI-Standard Join en lugar de las uniones de estilo antiguo.

Malo:

SELECT   e.Name,
         d.DepartmentName
FROM     Departments AS d,
         Employees   AS e
WHERE    d.ID = e.DepartmentID;

Bien:

SELECT   e.Name,
         d.DepartmentName
FROM     Departments AS d
JOIN     Employees AS e ON d.ID = e.DepartmentID;

Tables

Para la nomenclatura de las tablas, tengamos en cuenta los siguientes consejos:

  • Usar nombres singulares
  • Usar prefijo de nombre de esquema
  • Usar PascalCase

Malo:

CREATE TABLE Addresses

Bien:

CREATE TABLE [Person].[Address]

Columns

Para nombrar columnas, tengamos en cuenta los siguientes consejos:

  • Usar nombres singulares
  • Usar PascalCase
  • Asignemos un nombre a las claves principales con el formato “[TableName]ID”
  • Seamos descriptivos
  • Seamos consistentes

Malo:

CREATE TABLE [Person].[Address](
	[Address] [int] NOT NULL,
	[AddressLine1] [nvarchar](60) NOT NULL,
	[Address2] [nvarchar](60) NULL,
	[city] [nvarchar](30) NOT NULL,
	[State_ProvinceID] [int] NOT NULL,
	[postalCode] [nvarchar](15) NOT NULL,
	[ModifiedDate] [datetime] NOT NULL,
    CONSTRAINT [PK_Address_AddressID] PRIMARY KEY CLUSTERED ([Address])
);

Bien:

CREATE TABLE [Person].[Address](
	[AddressID] [int] NOT NULL,
	[AddressLine1] [nvarchar](60) NOT NULL,
	[AddressLine2] [nvarchar](60) NULL,
	[City] [nvarchar](30) NOT NULL,
	[StateProvinceID] [int] NOT NULL,
	[PostalCode] [nvarchar](15) NOT NULL,
	[ModifiedDate] [datetime] NOT NULL,
    CONSTRAINT [PK_Address_AddressID] PRIMARY KEY CLUSTERED ([AddressID])
);

Procedures

Para escribir buenos procedimientos almacenados, tengamos en cuenta los siguientes consejos:

  • Utilicemos la opción SET NOCOUNT ON al principio del procedimiento almacenado para evitar que el servidor envíe al cliente recuentos de filas de datos afectados por alguna instrucción o procedimiento almacenado.
  • Intentemos escribir la DECLARACIÓN y la inicialización (SET) al principio del procedimiento almacenado.
  • Escribamos todas las palabras clave de SQL Server en la letra CAPS.
  • Evitemos usar ‘sp_’ al principio del nombre del procedimiento almacenado.
  • Tengamos en cuenta las secciones anteriores, indenting – Select- joins.

Malo:

CREATE OR ALTER PROCEDURE [HumanResources].[sp_UpdateEmployeePersonalInfo]
    @BusinessEntityID [int],
    @NationalIDNumber [nvarchar](15),
    @BirthDate [datetime],
    @MaritalStatus [nchar](1),
    @Gender [nchar](1)
AS
BEGIN
   --some code
END;

Bien:

    @BusinessEntityID [int],
    @NationalIDNumber [nvarchar](15),
    @BirthDate [datetime],
    @MaritalStatus [nchar](1),
    @Gender [nchar](1)
AS
BEGIN
    SET NOCOUNT ON;
 
    DECLARE @messageLog nvarchar(100);
 
    SET @messageLog = 'Updating the employee personal info';
 
    --some code
END;

Views

No usemos la palabra ‘View’ en el nombre de la vista.

Malo:

– ViewEmployeesDepartments

– EmployeeDeparmentsView

Bien:

– EmployeeDeparments

No incluyamos las condiciones OrderBy y Where en la vista.

Malo:

SELECT  EmployeeId,
        Name,
        LastName,
        Address,
        State
FROM    Employees
WHERE   EmployeeId > 0 ORDER BY Name

Bien:

SELECT   Name,
         LastName,
         Address,
         State,
         City,
         Zip
FROM     Employees;

Utilicemos Alias con tabla + columna para especificar de dónde provienen los valores.

Malo:

SELECT   e.Name,
         d.Name
FROM     Departments AS d
JOIN     Employees AS e ON d.ID = e.DepartmentID;

Bien:

SELECT   e.Name as EmployeeName,
         d.Name as DeparmentName
FROM     Departments AS d
JOIN     Employees AS e ON d.ID = e.DepartmentID;

Comments

Escribamos comentarios útiles y solo cuando sea necesario. No escribamos comentarios intentando explicar el código que deberíamos entender leyendo el propio código.

Malo:

CREATE OR ALTER PROCEDURE [dbo].[uspLogError]
    @ErrorLogID [int] = 0 OUTPUT
AS
BEGIN
    SET NOCOUNT ON;
    -- Setting ErrorLogID to 0
    SET @ErrorLogID = 0;
 
	//some code
END;

Bien:

CREATE OR ALTER PROCEDURE [dbo].[uspLogError]
    @ErrorLogID [int] = 0 OUTPUT
AS
BEGIN
    SET NOCOUNT ON;
 
    -- Output parameter value of 0 indicates that error
    -- information was not logged
    SET @ErrorLogID = 0;
 
	//some code
END;

Modularize

Al escribir una consulta larga, pensemos en modularizar. Con las expresiones de tabla comunes (CTEs) podemos desglosar el código y facilitar su comprensión.

Malo:

SELECT   e.name
       , e.salary
FROM Employee AS e
WHERE e.EmployeeID EXISTS (SELECT   p.IdNumber
			   FROM   Population AS p
			   WHERE  p.country = "Canada"
				AND p.city = "Toronto")
      AND e.salary >= (SELECT	AVG(s.salary)
			FROM	salaries AS s
			WHERE	s.gender = "Female")

Bien:

WITH toronto_ppl AS (
         SELECT   p.IdNumber
	 FROM   Population AS p
	 WHERE  p.country = "Canada"
		AND p.city = "Toronto"
)
, avg_female_salary AS (
        SELECT	AVG(s.salary) AS avgSalary
	 FROM	salaries AS s
	WHERE	s.gender = "Female"
)
SELECT   e.name
       , e.salary
FROM Employee AS e
WHERE e.EmployeeID EXISTS (SELECT IdNumber FROM toronto_ppl)
      AND e.salary >= (SELECT avgSalary FROM avg_female_salary)

Temporary tables

Prioricemos usar una variable Table cuando el conjunto de resultados es pequeño en lugar de una tabla Temporal. Se creará una tabla temporal en la memoria temporal de la base de datos y esto hará que su consulta sea más lenta.

Malo:

DECLARE TABLE #ListOWeekDays
(
  DyNumber INT,
  DayAbb   VARCHAR(40),
  WeekName VARCHAR(40)
)

Bien:

DECLARE @ListOWeekDays TABLE
(
  DyNumber INT,
  DayAbb   VARCHAR(40),
  WeekName VARCHAR(40)
)

Looping

Evitemos ejecutar consultas mediante un bucle. La codificación de consultas SQL en bucles ralentiza toda la ejecución.

Malo:

WHILE @date <= @endMonth
BEGIN
    SET @date = DATEADD(d, 1, @date)
END

Bien:

DECLARE @StartDate DateTime = '2021-06-01'
DECLARE @EndDate DateTime = '2021-06-29'
 
;WITH populateDates (dates) AS (
 
    SELECT @StartDate as dates
    UNION ALL
    SELECT DATEADD(d, 1, dates)
    FROM populateDates
    WHERE DATEADD(d, 1, dates)<=@EndDate
 
)
SELECT *
INTO dbo.SomeTable
FROM populateDates

Al solucionar problemas 

  • Utilicemos la herramienta Plan de Ejecución si la tenemos disponible, con ella podríamos ver las estadísticas y advertencias, que pueden ayudarnos a encontrar áreas de mejora.
  • Utilicemos la coma antes de la columna, es útil cuando por alguna razón tenemos que comentar columnas.

Referencia:

https://github.com/gorillalogic/clean-code-sql