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.
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 · INTEGER | Columns: one attribute each, with its data type | |||
|---|---|---|---|---|
full_nameVARCHAR(100) | phoneVARCHAR(20) | birth_dateDATE | wallet_balanceNUMERIC(10,2) | |
317 | Sara Ahmed | 0550000001text: the leading 0 stays | 1994-03-18 | 250.00 |
318 | Ravi Kumar | +919800000002text: the + stays | NULLno value stored | 0.00zero is a value |
319 | Lina Haddad | 0790000003 | 2001-11-02 | 1250.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.50Data 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-byteINTEGERstops at 2147483647. - Exact decimals —
NUMERIC(p,s)orDECIMAL(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.2is not exactly0.3. - Text —
VARCHAR(n),TEXT:nis 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. ANOT NULLcolumn 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 of0550000001and the+of+919800000002. Compare the stored value character by character.birth_date DATE NULL: leave the field empty. ExpectNULLin the row, not a placeholder such as1900-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.
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
UPDATEorDELETEon 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
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.
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- Classify each column: a number you calculate with, a code made of digits, text, a date or yes/no.
- Choose a type for each column, with a length or precision where needed, and mark it required or optional.
- Pick the column that identifies a row, then write one test idea per column with its expected result.
Show the 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
- ISO/IEC 9075-1:2023 — Information technology — Database languages SQL — Part 1: Framework (SQL/Framework)The framework part of the international SQL standard, which the dialects of PostgreSQL, MySQL, SQLite and others implement to different degrees.
- PostgreSQL Documentation: SQL SyntaxLexical structure of SQL in PostgreSQL: identifiers, quoting, string constants and escaping.
- SQL As Understood By SQLiteThe SQL grammar accepted by SQLite, statement by statement, with railroad diagrams.
Practise with Mutqan tools
Convert CSV data into CREATE TABLE and INSERT statements with inferred column types.
Generate SQL seed scripts (CREATE TABLE + INSERTs) with realistic test rows for any dialect.
Generate Unicode test strings across scripts (Arabic, Devanagari, CJK, Cyrillic), emoji, combining marks and normalisation forms.
Measure string length in characters, code points, UTF-8 bytes and UTF-16 units.
Generate boundary values around limits: min-1, min, min+1, max-1, max, max+1 for integers of any width.
Convert a JSON array of objects into SQL INSERT statements (with optional CREATE TABLE).
Go deeper
Your progress on this topic
Not started
Mark the topic complete when you have studied it and done the exercise.
Sign in to track your progress
Mark topics complete, continue where you left off and unlock specialisation exams.