PHP MySQL SQL

Improving Database Performance and Code Quality: A Review Digest

Introduction

This post summarizes recent code review findings and improvements made to a database migration script within our application. The focus is on enhancing both performance and code quality through addressing issues ranging from index usage to data consistency and coding style.

Addressing Facade Imports

A critical issue identified was the absence of explicit facade imports. According to coding guidelines, it's best practice to use use statements instead of Fully Qualified Class Names (FQCN) inline references. This ensures code clarity and prevents potential issues if facade aliases are disabled. The following facades were missing imports:

  • DB
  • File

By adding use Illuminate\Support\Facades\DB; and use Illuminate\Support\Facades\File; the code now adheres to the established standards.

Optimizing Collation for Index Usage

Applying COLLATE on the column side in WHERE clauses can hinder index usage. To resolve this, the collation should be applied to the parameter side instead. For example, instead of:

WHERE company_id COLLATE utf8mb4_unicode_ci = userCompanyId

Use:

WHERE company_id = userCompanyId COLLATE utf8mb4_unicode_ci

This change allows MySQL to effectively utilize indexes, leading to performance gains.

Ensuring Deterministic Subqueries

Subqueries with LIMIT 1 but without an ORDER BY clause can lead to non-deterministic results. If multiple rows satisfy the WHERE condition, the database may return different results on different executions. To avoid this, an ORDER BY clause should be added to ensure consistent behavior. For example:

SELECT id FROM priority_products WHERE name = 'packaging' LIMIT 1
-- becomes
SELECT id FROM priority_products WHERE name = 'packaging' ORDER BY id ASC LIMIT 1

Adding Tie-Breakers for Deterministic Ordering

When using ORDER BY with a LIMIT clause, it's important to consider tie-breakers. If multiple rows have the same value for the ordering column, the database may return them in an unpredictable order. To ensure deterministic results, add a secondary sort column. For example, ordering by total_cost DESC could be enhanced by also ordering by a unique identifier like product_id:

ORDER BY total_cost DESC, product_id

Coding Style Improvements

Several coding style improvements were identified and addressed:

  • Not Operator Spacing: The ! (not) operator should have a successor space (e.g., !File::exists() instead of !File::exists()).
  • Exception Class Imports: Use imports for exception classes instead of FQCN inline references. For example, instead of throw new \Illuminate\Contracts\Filesystem\FileNotFoundException(...), import the class with use Illuminate\Contracts\Filesystem\FileNotFoundException; and then use throw new FileNotFoundException(...).

Rollback Strategy Documentation

Migrations should ideally follow the expand/contract pattern and be backward-compatible. An empty down() method means the migration cannot be automatically rolled back. When this is intentional (e.g., for procedure updates), it's crucial to document the rollback strategy. This might involve:

  1. Storing the previous procedure version in a separate file.
  2. Adding a comment explaining how to manually restore the previous version.

Example:

    public function down(): void
    {
        // Procedure updates are not rolled back automatically.
        // To restore the previous version, manually execute the prior SQL file
        // or create a new migration with the old procedure definition.
    }

Handling NULL Values in Comparisons

When a column allows NULL values, direct comparisons can lead to unexpected results. In SQL, NULL != value evaluates to UNKNOWN, which can cause rows to be filtered out unintentionally. To correctly handle NULL values, use IS NULL or IS NOT NULL or the coalesce function. For instance, the condition

M.characteristic != 'flexible'

should be updated to

(M.characteristic IS NULL OR M.characteristic != 'flexible')

Grouping by Product ID for Accurate Aggregation

If the products table permits duplicate names across different product IDs, grouping by product_name alone will incorrectly merge distinct products. To prevent this, always include product_id in GROUP BY clauses when product identity is important.

Handling Date Ranges and Rate Calculations

When dealing with date ranges and associated rates, it's crucial to ensure that the correct rates are applied for each transaction date. If a procedure accepts a date range but only uses the year from the start date for rate calculations, multi-year reports will produce inaccurate results. To fix this, the rate join should use the year from the transaction date, or the procedure should enforce single-year ranges.

Preventing Double-Counting in Recipes

Ensure that sales records are linked to the correct recipe version based on the transaction date. Joining aggregated sales data to recipe data without considering the validity period of each recipe can lead to double-counting of costs. The recipe join should occur before aggregation, using the transaction date to determine the valid recipe.

Handling NULL Mass Values

When performing calculations involving nullable columns, such as mass, be mindful of NULL propagation. If mass is NULL, the entire calculation will result in NULL. To avoid this, use COALESCE(mass, 0) to treat NULL values as zero, or add a WHERE clause to filter out rows where mass is NULL.

Ensuring Deterministic Ordering in Final Output

The final SELECT statement should include a tie-breaker in the ORDER BY clause. Without it, the order of rows with identical values in the primary sort column is non-deterministic.

ORDER BY product_total DESC, product_name;

Conclusion

These code review findings highlight the importance of attention to detail when developing database interactions. By addressing issues related to indexing, collation, NULL handling, and coding style, we can significantly improve both the performance and reliability of our application. Key takeaways include:

  • Always index foreign keys and columns used in WHERE and ORDER BY clauses.
  • Handle NULL values explicitly in comparisons and calculations.
  • Ensure deterministic results by adding tie-breakers to ORDER BY clauses.
  • Follow coding guidelines for imports and formatting.
  • Validate multi-year date ranges to avoid incorrect aggregations.
  • Join tables on the appropriate keys to avoid double-counting.
Gerardo Ruiz

Gerardo Ruiz

Author

Share: