Database Fundamentals

A database is an organised, persistent collection of data: it survives after the program that wrote it stops, which is why your orders are still there when you reopen a shopping app. The software that stores the data, enforces its rules and answers queries is the database management system (DBMS), such as PostgreSQL, MySQL, SQL Server or SQLite; people often call both "the database".

In a relational database, data lives in tables: each table holds one kind of thing, each row is one record and each column is one attribute with a declared data type. When you sign up in an online shop, the application adds one row to a customers table — customer 317, Sara Ahmed, phone 0550000001 — and every screen that later shows your profile reads that row.

Why it matters

The screen shows what the application chose to display; the database holds what was actually stored. Many defects live only there: a phone number that lost its leading zero, an Arabic name saved as question marks, a description cut at 50 characters, a timestamp shifted by three hours. They surface weeks later in an invoice, a report or another system.

Knowing how a table is built turns guessing into test design. Each column's type and length are limits the requirement rarely mentions, and each optional column raises the question of what "empty" means.

Tables, rows and columns

The customers table below shows three rows and five of its columns: one row per customer, one column per attribute, and a single value where a row meets a column.

Two properties surprise beginners. A column means the same thing in every row: phone cannot hold a phone number in one row and an email address in another. And rows have no built-in order: without ORDER BY the database may return them in any order, so "the last row" is no way to find the record you just created. Look it up by its identifier — here customer_id, a key, the subject of the lesson on keys and relationships.

The customers table: each row is one customer, each column one attribute with one data type, and NULL in birth_date means no value was stored, unlike the 0.00 balance.
customer_id · INTEGERColumns: one attribute each, with its data type
full_nameVARCHAR(100)phoneVARCHAR(20)birth_dateDATEwallet_balanceNUMERIC(10,2)
317Sara Ahmed0550000001text: the leading 0 stays1994-03-18250.00
318Ravi Kumar+919800000002text: the + staysNULLno value stored0.00zero is a value
319Lina Haddad07900000032001-11-021250.50
  • Rowone customer, with a value or NULL in every column
  • NULL: no value, which is not 0 and not empty text

Fictional rows. Rows have no built-in order, and two columns of the worked example’s definition are left out.

  • Other names: a table is a relation in relational theory, a row is a record or tuple, and a column is a field or attribute.
  • Schema: the definition of all tables, columns, types and rules (in PostgreSQL and SQL Server, also a namespace that groups tables).
Text version
Table: customers

customer_id | full_name   | phone         | birth_date | wallet_balance
------------+-------------+---------------+------------+---------------
        317 | Sara Ahmed  | 0550000001    | 1994-03-18 |         250.00
        318 | Ravi Kumar  | +919800000002 | NULL       |           0.00
        319 | Lina Haddad | 0790000003    | 2001-11-02 |        1250.50

Data types: what a column may hold

Every column declares a data type, which decides what values it accepts, how they sort and compare, and how large they can be. Names and strictness differ between engines: SQL Server uses BIT instead of BOOLEAN; PostgreSQL rejects text that is too long or a number too large for its column but rounds extra decimal places without an error, MySQL's behaviour depends on its SQL mode, and SQLite by default stores almost any value in any column.

  • Whole numbers — INTEGER, BIGINT: counts, quantities, identifiers. Each has a fixed range; a 4-byte INTEGER stops at 2147483647.
  • Exact decimals — NUMERIC(p,s) or DECIMAL(p,s): money. NUMERIC(10,2) has 10 digits in total, 2 of them after the point: it holds at most 99999999.99, and PostgreSQL stores 0.125 as 0.13.
  • Approximate numbers — REAL, DOUBLE PRECISION: measurements. Never money: in binary floating point, 0.1 + 0.2 is not exactly 0.3.
  • Text — VARCHAR(n), TEXT: n is a maximum length. Most engines count characters, but some types count bytes, and in UTF-8 an Arabic letter takes 2 bytes and a Devanagari letter 3.
  • Date and time — DATE, TIMESTAMP: a day or a moment. Check whether a timestamp keeps its time zone: 01:30 in Riyadh (UTC+3) is still the previous day in UTC.
  • Boolean — BOOLEAN: true or false; MySQL stores it as a small integer, so you see 1 and 0.
  • NULL — not a type but a marker for "no value" in any column. It differs from 0, from empty text '' and from the word "NULL" typed by a user. A NOT NULL column is required.

Try it: CSV to SQL Try it: Boundary Number Dataset Generator Try it: String Length Counter

Worked example: test ideas from a table definition

A new profile page lets customers enter a name, a mobile number and an optional birth date, and shows their wallet balance. Before any screen exists, read the developer's table definition below as part of the test basis (everything you derive tests from) and turn its columns into test conditions (the aspects worth testing) or questions:

  • full_name VARCHAR(100): save names of exactly 100 and 101 characters (the limit and one past it) in English, Arabic and Hindi. Expect 100 stored unchanged and 101 rejected with a clear message, not silently cut.
  • phone VARCHAR(20): text is right; a number type would drop the leading 0 of 0550000001 and the + of +919800000002. Compare the stored value character by character.
  • birth_date DATE NULL: leave the field empty. Expect NULL in the row, not a placeholder such as 1900-01-01.
  • wallet_balance NUMERIC(10,2): top up 0.10 three times and expect exactly 0.30. Ask what happens when a top-up would take the balance above 99999999.99, the column's maximum.
  • created_at TIMESTAMP: in PostgreSQL this type keeps no time zone. Sign up at 01:30 Riyadh time and ask whether UTC or local time was stored; every daily report depends on it.
CREATE TABLE customers (
  customer_id     INTEGER        NOT NULL,
  full_name       VARCHAR(100)   NOT NULL,
  phone           VARCHAR(20)    NOT NULL,
  birth_date      DATE           NULL,
  wallet_balance  NUMERIC(10,2)  NOT NULL DEFAULT 0,
  is_active       BOOLEAN        NOT NULL DEFAULT TRUE,
  created_at      TIMESTAMP      NOT NULL,
  CONSTRAINT pk_customers PRIMARY KEY (customer_id)
);

Try it: SQL Dataset Generator Try it: Unicode Dataset Generator

Relational and other kinds of databases

Relational databases, queried with SQL, are widely used for business records and are the focus of this module. SQL is standardised as ISO/IEC 9075, but each engine adds its own dialect. Non-relational ("NoSQL") databases organise data differently and often leave checks to the application:

  • Document databases, such as MongoDB, store JSON-like documents whose fields can differ within one collection unless a validation rule forbids it; test for missing or misspelt fields and numbers stored as text.
  • Key-value stores, such as Redis, keep a value under a key, often as a cache that can fall out of date with the database behind it.
  • Wide-column stores, such as Apache Cassandra, spread very large tables across many servers.
  • Graph databases, such as Neo4j, store nodes and the relationships between them, such as who follows whom.

Try it: JSON to SQL

Common mistakes

  • Treating the screen as proof of what was stored; a page can echo what you typed while the row holds a cut or garbled value.
  • Finding a new record by taking "the last row" instead of looking it up by its key.
  • Treating NULL, 0 and empty text as the same thing in test data and expected results.
  • Accepting a number type for anything made of digits: phone numbers, barcodes and national ids are text, because leading zeros and formatting matter.
  • Running UPDATE or DELETE on a shared test database to "fix" data without its owner's agreement.

The QA perspective

In backlog refinement or a design review, ask for the table definition of a new feature and read it as part of the test basis. Each column raises the same questions — required or optional, which type, what length or range, which time zone, what default — and a mismatch such as a form that accepts 150 characters for a column of 100 is best raised before coding.

During testing, ask for read-only access to the test database and confirm what each action stored. In a bug report, name the table, column, row key, expected value and actual value: "customers.phone for customer_id 318 is 919800000002, expected +919800000002".

Practice exercise

Choose the data types for a product table

Your task

A shop wants to import the product list below into a new products table. The barcode is a 13-digit EAN-13 code that may start with 0. Design the table, then write one test idea per column.

Afterwards, compare your choices with the types the CSV to SQL tool infers from the sample: a guess from values, not a decision about meaning.

Material
sku,name_en,name_ar,price_sar,stock_qty,weight_kg,launch_date,online,barcode
TEA-001,Green tea,شاي أخضر,18.50,240,0.125,2026-03-01,yes,0614141000012
CUP-014,Glass cup,كوب زجاجي,9.95,0,0.250,2025-11-20,no,0614141000029
Steps
  1. Classify each column: a number you calculate with, a code made of digits, text, a date or yes/no.
  2. Choose a type for each column, with a length or precision where needed, and mark it required or optional.
  3. Pick the column that identifies a row, then write one test idea per column with its expected result.
Show the model answer
Model answer

One reasonable design in PostgreSQL types: sku VARCHAR(20) NOT NULL (identifies the row), name_en VARCHAR(200) NOT NULL, name_ar VARCHAR(200) NOT NULL, price_sar NUMERIC(10,2) NOT NULL, stock_qty INTEGER NOT NULL, weight_kg NUMERIC(8,3) NULL, launch_date DATE NULL, is_online BOOLEAN NOT NULL (the online column, renamed to read as yes/no), barcode VARCHAR(13) NULL.

The traps: the barcode is a code, not a number — as BIGINT, 0614141000012 would lose its leading zero, so it must be text. The price is money, so an exact decimal, never REAL. The yes/no column becomes BOOLEAN, and the import must define what "Yes" or "Y" mean.

Test ideas: a duplicate SKU is rejected; 200-character names are stored unchanged; 18.50 stays exactly 18.50; stock 0 is stored as 0, an empty stock is rejected; weight 0.125 keeps three decimals; an empty launch date becomes NULL; "no" becomes false; the barcode keeps its leading zero, and a 12-digit barcode is rejected — VARCHAR(13) alone would accept it, so ask whether a check in the database or the import enforces exactly 13 digits.

The CSV to SQL tool sizes its types from these two rows only: it proposes DECIMAL(4,2) for the price, which stops at 99.99, and VARCHAR(20) for both names, too short for real product names. It does keep the barcode as text, because of the leading zero. Treat inferred types as a first draft to review, never as the design.

Questions for the product owner: is the weight used for shipping fees? Can two products share a barcode? How long can a real product name be?

Frequently asked questions

Do I need SQL to test a database?

At a basic level, yes. A SELECT with a WHERE condition answers most verification questions, and later lessons in this module teach it. You do not need to design databases, but you do need to read a table definition and query the rows your test created.

What is the difference between a database and a table?

A database is the whole collection: usually many tables, plus the rules, users and permissions around them. A table is one structure inside it that holds one kind of record, such as customers or orders.

Is NoSQL better than a relational database?

Neither is better in general. They make different trade-offs in structure, querying and scaling, and many products use both. As a tester, find out which store holds which data, which rules each store enforces itself and which it leaves to the application.

References

Practise with Mutqan tools

Convert CSV data into CREATE TABLE and INSERT statements with inferred column types.

Database

Generate SQL seed scripts (CREATE TABLE + INSERTs) with realistic test rows for any dialect.

Test Data

Generate Unicode test strings across scripts (Arabic, Devanagari, CJK, Cyrillic), emoji, combining marks and normalisation forms.

Test Data

Measure string length in characters, code points, UTF-8 bytes and UTF-16 units.

Formatting

Convert a JSON array of objects into SQL INSERT statements (with optional CREATE TABLE).

Database

Go deeper

Your progress on this topic

Part of the QA Roadmap