Showing posts with label TEXT Helps in MySQL. Show all posts
Showing posts with label TEXT Helps in MySQL. Show all posts

Saturday, October 17, 2026

Case Study: How Foreign Keys are Useful in SQL

Consider a growing training company that offers courses to individual learners and to employees of business clients. It stores information about learners, courses, and course registrations in a relational database.

At first, the company keeps these records in a single spreadsheet. Each row contains a learner’s name and email, the course title, the instructor, the registration date, and the learner’s status. This works while the company has a small number of registrations. As the business expands, however, the spreadsheet becomes difficult to manage.

The same learner’s contact information is copied into many rows. If that learner changes their email address, staff must find and correct every occurrence. Course titles and instructor details are repeated, too. Mistakes are inevitable: a course may be entered with slightly different names, or a registration may refer to a learner who does not exist in the learner list. These problems are examples of inconsistent and unreliable data.

The company decides to move its information into a SQL database and separate the records into related tables. Foreign keys help connect those tables while ensuring that their relationships remain valid.

## Designing the tables

The database uses three main tables:

- `Learners` stores one record for each learner. - `Courses` stores one record for each course. - `Registrations` records which learner has registered for which course.

A simplified schema might look like this:

```sql CREATE TABLE Learners ( learner_id INT PRIMARY KEY, full_name VARCHAR(100) NOT NULL, email VARCHAR(255) NOT NULL UNIQUE );

CREATE TABLE Courses ( course_id INT PRIMARY KEY, course_title VARCHAR(150) NOT NULL );

CREATE TABLE Registrations ( registration_id INT PRIMARY KEY, learner_id INT NOT NULL, course_id INT NOT NULL, registration_date DATE NOT NULL, status VARCHAR(20) NOT NULL,

FOREIGN KEY (learner_id) REFERENCES Learners(learner_id),

FOREIGN KEY (course_id) REFERENCES Courses(course_id) ); ```

The `learner_id` and `course_id` columns are primary keys in their respective tables. Each primary key identifies one row uniquely. In `Registrations`, the same columns are foreign keys: they refer to primary keys in `Learners` and `Courses`.

A registration can therefore connect a learner to a course without copying the learner’s name, email address, or course title into every registration row. The database stores each person and course once, then uses identifiers to link them to registrations.

## Preventing invalid registrations

The most immediate benefit of the foreign keys is that they prevent registrations from referring to records that do not exist.

Suppose an employee attempts to add this registration:

```sql INSERT INTO Registrations ( registration_id, learner_id, course_id, registration_date, status ) VALUES (501, 9999, 12, '2025-04-10', 'confirmed'); ```

If there is no learner with `learner_id` 9999, the database rejects the insert. The `learner_id` foreign key requires the value in `Registrations` to match an existing value in `Learners`. The same rule applies to `course_id`: a registration cannot refer to a course that is absent from the `Courses` table.

Without this rule, the database could accept an entry that appears to represent a registration but cannot be connected to a real learner. Such a row is often called an orphan record. It may cause problems in reports, billing, attendance lists, and customer support. A report that joins registrations to learners, for example, might omit the orphan record because there is no matching learner row.

Foreign keys make this a database-level rule rather than a reminder for staff or an assumption built into application code. Any application, script, or person that writes to the database must follow the same constraint.

## Keeping information consistent

Separating learners and registrations also reduces repeated information. A learner’s email is stored in one place rather than copied into every course registration. When the learner changes their email, the company updates the learner row once:

```sql UPDATE Learners SET email = 'alex.morgan@example.com' WHERE learner_id = 104; ```

Registrations continue to point to learner 104, so they remain associated with that person. The database does not need to update every registration row because those rows store the stable identifier, not a copy of the email address.

This design helps avoid update anomalies. If the email were copied into ten registration rows, changing only nine of them would leave conflicting information. A foreign key does not itself prevent every possible data-quality issue, but it supports a structure in which related facts are stored in appropriate places and connected reliably.

The company can retrieve a registration together with its related details using a join:

```sql SELECT l.full_name, l.email, c.course_title, r.registration_date, r.status FROM Registrations AS r JOIN Learners AS l ON r.learner_id = l.learner_id JOIN Courses AS c ON r.course_id = c.course_id; ```

The foreign keys establish the relationships that these joins use. They also help make the schema easier for developers and database users to understand: `Registrations.learner_id` points to a learner, and `Registrations.course_id` points to a course.

## Controlling deletions

Foreign keys are also useful when records are deleted. Suppose the company tries to delete a learner who still has registrations. With the default restrictive behavior in many database systems, the database refuses the deletion. This prevents the company from removing the person’s main record while leaving registrations that refer to it.

That behavior may be appropriate when registrations must be preserved for financial or historical records. Staff can first decide what should happen to the registrations, rather than accidentally creating broken references.

SQL also allows a database designer to specify what happens when a referenced row is deleted. For example, `ON DELETE CASCADE` automatically deletes dependent rows:

```sql FOREIGN KEY (learner_id) REFERENCES Learners(learner_id) ON DELETE CASCADE ```

If the learner is deleted, their registrations are deleted as well. This may suit temporary data or a system where dependent records have no independent value. It can be dangerous in a training company, however. Deleting one learner might remove years of registration history. The choice should reflect business rules, not just convenience.

Another option is `ON DELETE SET NULL`, which sets the foreign-key column to `NULL` when the referenced row is deleted. This requires the column to allow `NULL`, and it may not make sense when every registration must have a learner. Some databases also support additional referential actions. Their exact behavior and syntax can vary, so the team should confirm the rules for its database system.

The company might choose to keep historical registrations but mark a learner as inactive instead of deleting the learner record. That approach preserves the relationship and history while preventing new registrations. A foreign key does not decide the organization’s retention policy; it ensures that whatever policy is chosen does not silently leave invalid links.

## Preventing duplicate registrations

The first schema guarantees that every registration refers to a valid learner and course, but it still allows the same learner to register for the same course multiple times. Whether that is valid depends on the company’s rules. If learners may register only once per course, a composite uniqueness constraint can enforce that:

```sql ALTER TABLE Registrations ADD CONSTRAINT unique_learner_course UNIQUE (learner_id, course_id); ```

This constraint is not itself a foreign key, but it complements the foreign keys. The foreign keys ensure that both referenced records exist; the unique constraint ensures that the pair does not appear more than once.

If learners are allowed to retake courses, the design may need a different rule, such as including a course session or attempt number in the unique key. This illustrates an important point: foreign keys protect relationships, while other constraints protect other aspects of the data. A reliable schema usually combines primary keys, foreign keys, `NOT NULL`, `UNIQUE`, and appropriate validation rules.

## Supporting reports and daily operations

With valid relationships in place, the company can rely on its reports more confidently. For instance, it can count registrations by course:

```sql SELECT c.course_title, COUNT(r.registration_id) AS registration_count FROM Courses AS c LEFT JOIN Registrations AS r ON r.course_id = c.course_id GROUP BY c.course_id, c.course_title; ```

Because the foreign key prevents registrations from pointing to nonexistent courses, the company is less likely to encounter unexplained registration rows that cannot be assigned to a course. The `LEFT JOIN` also includes courses with no registrations, which can help staff identify offerings that have not attracted learners.

Foreign keys can support operational workflows as well. A staff member can add a new course before accepting registrations, then add registrations that refer to it. If the course has not yet been entered, the database rejects the registration. This signals that the process is incomplete and directs staff to correct the missing record.

The constraint is useful even when the company has a web application that checks whether a learner and course exist before submitting a registration. Application checks can improve the user experience by displaying a helpful message. But they are not a substitute for database constraints: another application, an import process, or a direct SQL command might bypass them. The foreign key provides a final safeguard at the point where the data is stored.

## Practical considerations and limitations

Foreign keys need to be designed with care. The columns on both sides must have compatible data types, and the referenced column must normally be a primary key or otherwise uniquely constrained. The names and exact requirements depend on the SQL database system.

The team should also consider performance. A foreign key check involves verifying that a referenced row exists, and deletes or updates may require checking dependent rows. Indexing foreign-key columns can help queries and certain database operations, especially when tables grow large. Some database systems create indexes automatically in particular circumstances, while others do not, so the behavior should be checked rather than assumed.

Foreign keys do not guarantee that all information is accurate. They cannot determine whether a person typed the correct email address, whether a course description is current, or whether the learner is eligible for a particular course. Those concerns require additional validation, business rules, or review processes. A valid reference means that the referenced row exists—not necessarily that the overall business decision is correct.

The company must also account for differences among database products. Referential actions, constraint enforcement settings, and migration procedures can vary. For example, some systems allow foreign-key enforcement to be configured, and bulk imports may require special planning. The database team should test the intended behavior in the system it actually uses.

## The result

After adopting the relational design, the training company has a clearer and more dependable record of its operations. Learners and courses are stored once, registrations refer to them through identifiers, and the database blocks references to nonexistent records. Updates are easier because contact details are not duplicated across registrations. Deletions follow deliberate rules, and reports can connect related information through reliable joins.

The central value of a foreign key is simple: it turns a relationship that might otherwise exist only in people’s assumptions or application code into a rule the database can enforce. In this case study, that rule prevents invalid registrations, protects historical records, and supports consistent reporting. Used alongside other constraints and thoughtful data policies, foreign keys help organizations keep their SQL data connected, understandable, and trustworthy.

Limitations of VARCHAR and How TEXT Helps in MySQL

MySQL offers several data types for storing text, but choosing between `VARCHAR` and the `TEXT` family can be confusing. Both hold variable-length character data, yet they differ in capacity, storage behavior, indexing, and practical use. Understanding these differences helps you avoid row-size errors, unnecessary storage costs, and performance problems.

`TEXT` can accommodate larger values than many practical `VARCHAR` definitions, but it is not a universal replacement. The right choice depends on how much data a column may contain, how the application uses it, and which MySQL version and storage engine are involved.

## How `VARCHAR` Works

A `VARCHAR(M)` column stores a variable-length string, where `M` is the maximum number of characters permitted. For example:

```sql title VARCHAR(200) ```

This column can hold strings of up to 200 characters. A shorter value does not use space for all 200 characters; MySQL stores the value’s data along with a small length indicator. The storage used by the characters depends on the character set. With `utf8mb4`, for instance, a character may require up to four bytes.

This distinction between characters and bytes is important. A declaration such as `VARCHAR(200)` specifies a character limit, but the maximum byte size depends on the character set. The declared limit must also fit within MySQL’s row-size constraints.

## The Main Limitations of `VARCHAR`

### Row-size constraints

MySQL has a maximum row size of 65,535 bytes for columns stored in a table row, although the precise practical limit depends on the storage engine and table definition. This limit applies to the combined row, not independently to each `VARCHAR` column.

As a result, a table with many wide `VARCHAR` columns may exceed the row-size limit even if none of the individual columns appears especially large. For example, defining several long `VARCHAR` fields using a multibyte character set can require far more potential bytes than expected.

A table can therefore fail to be created, or an attempt to add a column can fail, with an error about row size. In InnoDB tables, some variable-length data may be stored outside the main row, depending on the row format and circumstances. Nevertheless, the table definition and the amount of data retained inline still matter.

### The maximum length is not a promise of convenient use

A `VARCHAR` declaration can have a large character limit, but that does not mean a very large value is a good fit for the column. A large maximum may make the table harder to design within row-size constraints, particularly when the table contains many other columns. It can also obscure the intended purpose of the field: a short label and a potentially large article are not the same kind of data.

Setting every string column to a generous maximum “just in case” is rarely a good design strategy. Limits should reflect reasonable application requirements.

### Character sets affect storage

The number in `VARCHAR(M)` counts characters, not bytes. When a character set uses more than one byte per character, the maximum possible storage is larger than the number in the declaration.

For example, a 1,000-character string in a multibyte character set may require several thousand bytes. This affects row-size calculations and can make a wide table exceed its limits sooner than expected. The actual stored value may be smaller, but MySQL must still account for the column’s possible size when evaluating the table definition.

### Oversized values can be awkward to index

MySQL can index `VARCHAR` columns, but index key length is limited. The maximum key length varies by storage engine, row format, character set, and MySQL version. A long `VARCHAR` using `utf8mb4` may not fit fully into an index.

When only part of a long value needs to be indexed, MySQL supports prefix indexes, such as:

```sql CREATE INDEX idx_summary ON articles (summary(100)); ```

A prefix index uses only the first specified number of characters. It can help with certain lookups, but it does not behave exactly like an index on the full value. Queries that depend on the whole string may require additional checks.

## What the `TEXT` Family Provides

MySQL provides four principal text types, with capacities measured in bytes:

| Type | Maximum capacity | |---|---:| | `TINYTEXT` | 255 bytes | | `TEXT` | 65,535 bytes | | `MEDIUMTEXT` | 16,777,215 bytes | | `LONGTEXT` | 4,294,967,295 bytes |

These figures are theoretical type limits, not guarantees that an application can send or retrieve a value of that size. Practical limits can be lower because of settings such as `max_allowed_packet`, available memory, client libraries, application limits, and storage constraints. Character set encoding also affects how many characters fit in a given number of bytes.

For example, `TEXT` holds up to about 64 KiB of data in bytes. If the text uses a multibyte character set, that may correspond to fewer than 65,535 characters.

A column can be defined as:

```sql description TEXT ```

Unlike `VARCHAR`, a `TEXT` column does not include a character-length limit in its declaration. Its maximum capacity is determined by the particular `TEXT` type.

## How `TEXT` Helps with Large Values

### It accommodates longer content

`TEXT` is useful when a field may contain paragraphs, descriptions, comments, or other content whose length varies substantially. If a value might exceed a reasonable `VARCHAR` limit, using an appropriate `TEXT` type avoids having to define an unusually wide character column.

For data larger than the `TEXT` limit, `MEDIUMTEXT` or `LONGTEXT` may be appropriate. The smallest type that safely accommodates the expected data is generally preferable. Choosing `LONGTEXT` for a field that will contain a few paragraphs adds no practical benefit.

### It can reduce pressure on the main row

With InnoDB, large variable-length values can be stored partly or mostly outside the main row, depending on the row format and the circumstances. The row retains information needed to locate that data. This can make `TEXT` useful when a table has many other columns and large content would otherwise contribute to row-size problems.

However, the exact storage behavior is not simply “all `TEXT` data is always stored elsewhere.” Details depend on the storage engine, row format, and value size. A `TEXT` column also has storage and retrieval costs; moving data off the main row does not make it free.

### It makes the intended data size clearer

Using `TEXT` signals that a field is intended for potentially substantial text rather than a short, tightly bounded value. That distinction can improve schema readability. A title, country code, or email address usually has a meaningful maximum and is often better represented by a suitably sized `VARCHAR`. An article body or lengthy user comment is more naturally represented by a `TEXT` type.

## Important Trade-offs of `TEXT`

### Indexing is more limited

MySQL does not allow a conventional full-column index on a `TEXT` value in the same way it does for a short string. A prefix length is generally required:

```sql CREATE INDEX idx_excerpt ON articles (excerpt(150)); ```

The index covers only the chosen prefix. This may be sufficient for some searches, but it is not equivalent to indexing every character. If the application needs to search large bodies of text, MySQL full-text indexes may be more suitable:

```sql CREATE FULLTEXT INDEX idx_body ON articles (body); ```

Full-text search has its own behavior and limitations; it is not a substitute for every kind of exact-match or substring lookup.

### Defaults and version differences matter

Support for default values on `TEXT` columns has varied across MySQL versions. Older versions generally did not allow ordinary non-`NULL` defaults for `TEXT` and `BLOB` columns, while newer versions provide more flexibility. Applications that need to run across different MySQL versions should verify the relevant behavior rather than assume that a declaration accepted on one server will work on another.

Where defaults are important, a bounded `VARCHAR` may be simpler and more portable. The application can also explicitly provide a value when inserting a row.

### Large values have operational costs

Long text consumes storage, can increase backup size, and may take longer to transfer between the server and client. Queries that retrieve every column can also fetch large values unnecessarily. For example, listing article titles should not require loading the full article body.

Selecting only the needed columns helps:

```sql SELECT id, title FROM articles; ```

rather than:

```sql SELECT * FROM articles; ```

This matters especially when a table contains large `TEXT` fields and the application frequently displays summaries or lists.

## Choosing Between `VARCHAR` and `TEXT`

Use `VARCHAR` when the value has a natural, reasonably small maximum and is frequently used for filtering, sorting, or indexing. Examples include names, titles, short labels, and identifiers. A meaningful limit also helps prevent accidental storage of unexpectedly large input.

Use `TEXT` when the value may be substantially longer or has no practical small maximum, such as a user comment, product description, or article body. Choose `MEDIUMTEXT` or `LONGTEXT` only when the expected content exceeds the capacity of the smaller types.

A common design might look like this:

```sql CREATE TABLE articles ( id BIGINT PRIMARY KEY, title VARCHAR(250) NOT NULL, summary TEXT, body MEDIUMTEXT ); ```

The title has a clear bound, while the summary and body can hold longer content. The exact types should be based on the application’s actual requirements, not just on the largest size MySQL permits.

## Common Misunderstandings

A larger declared `VARCHAR` is not always equivalent to `TEXT`. Both can store variable-length data, but their limits, indexing behavior, and storage characteristics differ.

Likewise, changing a column to `TEXT` does not eliminate all row-size or system limits. MySQL still has limits on packets, indexes, table definitions, and available resources. Very large content may also be better stored in object storage or a file system, with only a reference kept in the database, depending on access patterns and operational needs.

Finally, `TEXT` does not automatically improve performance. It can help fit large values into a table design, but it may increase I/O if large fields are frequently retrieved. Performance depends on how the application queries and stores the data.

## Conclusion

`VARCHAR` is well suited to bounded strings and often works best for values that need regular indexing or comparison. Its practical limits arise from the combined row-size budget, character-set byte usage, and index restrictions. The `TEXT` family provides larger capacities and can reduce the pressure that large strings place on the main row, making it useful for long-form content.

The choice is not simply between “small” and “large.” It should reflect the expected size of the data, the way the application searches and retrieves it, and the capabilities of the MySQL version and storage engine in use. Use `VARCHAR` for bounded text, use the smallest suitable `TEXT` type for longer content, and avoid retrieving or indexing large values unless the application needs to do so.