Book a 20-minute Azure review

SQL & reporting · field note

Hardcoded IDs in SQL views: a shortcut with no expiry date

Asif Bhat10 minute read
Hardcoded numeric IDs embedded in a SQL view and separated from the source of truth

The first hardcoded ID is usually written under deadline. The fiftieth is written because the first one taught the codebase that this is where business meaning belongs.

It is easy to understand the temptation. You know the records you need. The query is internal. Joining another table feels like ceremony. So category IDs 3, 7, and 11 go directly into the view, the result looks correct, and the ticket closes.

The database does not attach an expiry date to that assumption. It will keep executing the shortcut long after the person, requirement, and reference data that justified it have changed.

01 / The shortcut

Hardcoded IDs work beautifully—until reality moves

This kind of predicate is concise, fast to write, and completely legal:

CREATE VIEW dbo.vw_completed_activity
AS
SELECT
    a.activity_id,
    a.person_id,
    a.completed_at
FROM dbo.activities AS a
WHERE a.category_id IN (3, 7, 11);

Nothing in SQL Server knows that 3 means “course,” 7 means “assessment,” or 11 was retired last Tuesday. The database sees integers. The business sees categories. The fragility lives in that translation.

If category 14 is introduced, the view will not fail. It will omit 14 with perfect reliability.

02 / The hidden contract

A database ID is identity, not business meaning

An ID answers “which row?” It should not need to answer “what policy does this row belong to?” When a numeric identifier becomes a business rule, application state has leaked into code without a contract for change.

The risk is highest when IDs are generated operationally, differ between environments, can be re-seeded during migrations, or represent categories that product teams can add and retire. A comment helps the next reader but does not keep the query correct.

Not every literal is evil

A database-enforced constant with genuinely immutable meaning can be reasonable. The smell is a literal whose business interpretation lives in someone's memory, a spreadsheet, or whichever production row happened to exist when the view was written.

03 / The multiplication

Copy and paste turns one assumption into a change programme

Reporting code repeats itself because reports repeat concepts: active, completed, eligible, billable, funded. One view embeds the list, a second copies it, and a third wraps both with a slightly different exception.

In a reporting remediation engagement, IDs appeared eight to twelve times per principal report across four main views and another five to seven dependencies—roughly 70 to 130 maintenance points. Six to eight columns had been wrong, some for more than two years, while every query still ran.

That story is unpacked in The reports had been wrong for two years and nothing was broken. The short version is that duplication does more than increase effort. It makes a complete fix difficult to prove.

04 / The inventory

Search for literals, then investigate meaning

Source control is the best starting point because it shows history and review context. In SQL Server, sys.sql_modules can also expose deployed definitions. Search for IN (...) lists, equality checks against numeric literals, repeated CASE expressions, and comments containing category or status names.

SELECT
    OBJECT_SCHEMA_NAME(m.object_id) AS schema_name,
    OBJECT_NAME(m.object_id) AS object_name,
    o.type_desc,
    m.definition
FROM sys.sql_modules AS m
INNER JOIN sys.objects AS o
    ON o.object_id = m.object_id
WHERE m.definition LIKE '%category_id%'
  AND m.definition LIKE '% IN (%';

This query is a lead generator, not a correctness scanner. Dynamic SQL, formatting, aliases, encrypted modules, and other predicate shapes can evade it. Every match still needs human review. Also inspect application queries, ORM raw SQL, ETL jobs, exports, and BI datasets—the database is rarely the only hiding place.

05 / The source of truth

Replace the list with a governed relationship

A common fix is a mapping table that expresses membership explicitly. Give it meaningful keys, database constraints, ownership, and lifecycle fields appropriate to the business. Then the view joins to the rule rather than carrying its own private copy.

CREATE TABLE dbo.reporting_category_map (
    report_group varchar(50) NOT NULL,
    category_id int NOT NULL,
    effective_from date NOT NULL,
    effective_to date NULL,
    CONSTRAINT pk_reporting_category_map
        PRIMARY KEY (report_group, category_id, effective_from),
    CONSTRAINT fk_reporting_category_map_category
        FOREIGN KEY (category_id)
        REFERENCES dbo.categories (category_id),
    CONSTRAINT chk_reporting_category_map_dates
        CHECK (effective_to IS NULL OR effective_to >= effective_from)
);

CREATE VIEW dbo.vw_completed_activity
AS
SELECT
    a.activity_id,
    a.person_id,
    a.completed_at
FROM dbo.activities AS a
INNER JOIN dbo.reporting_category_map AS m
    ON m.category_id = a.category_id
   AND m.report_group = 'completed_activity'
   AND a.completed_at >= m.effective_from
   AND (m.effective_to IS NULL OR a.completed_at < DATEADD(day, 1, m.effective_to));

This is an example, not a universal schema. Some domains need stable natural codes, some need a many-to-many classification table, and some need temporal history. The goal is the same: one explicit definition that can change without hunting through view text.

06 / The safe change

Do not refactor reporting logic without reconciling the output

  1. Document the current and intended business definitions.
  2. Inventory every consumer and every copied implementation.
  3. Create the governed mapping through a versioned migration.
  4. Add constraints so invalid relationships fail visibly.
  5. Run old and new queries side by side over realistic history.
  6. Investigate unmatched rows and material aggregate differences.
  7. Test performance and correctness before switching consumers.
  8. Remove the obsolete path only after the new one is proven.

A hardcoded ID is not dangerous because it is ugly. It is dangerous because it turns a changing business definition into invisible executable history. The fix is not prettier SQL. It is a source of truth with an owner.

Primary references

Sources and further reading

Questions teams ask

Frequently asked questions

Why are hardcoded IDs in SQL views dangerous?

The query remains syntactically valid when reference data changes. New or replaced IDs can be silently excluded, and copied lists create many places that must be updated consistently.

Are hardcoded IDs always wrong in SQL?

Not always. A genuinely immutable, database-enforced constant may be acceptable when its meaning is documented. Most application-generated numeric IDs are not immutable business definitions and should not be treated as constants.

How do I find hardcoded IDs in a SQL Server database?

Search view, procedure, function, and trigger definitions through source control and sys.sql_modules. Review numeric IN lists, repeated predicates, CASE expressions, and joins to literal values. Then validate each match against the business definition.

What should replace a hardcoded ID list?

Usually a governed reference or mapping table with meaningful keys, foreign keys, uniqueness constraints, lifecycle fields, and an owner. The exact design depends on whether the rule is current-state classification, historical mapping, or time-bound policy.

How do you refactor a SQL view safely?

Inventory all consumers, define the intended output, add the governed mapping, compare old and new results over realistic history, investigate every material delta, deploy with versioned migrations, and monitor both performance and correctness.