File organisation and access

A2 · 20 min

A file of records can be arranged on backing store in different ways, and the arrangement decides how quickly a particular record can be found. File organisation is how the records are placed in the file; file access is how a program gets to them. Paper 3 asks you to describe serial, sequential and random organisation, to choose the right one for a scenario, and to calculate where a record goes using a hashing algorithm; Paper 4 asks you to write the code that reads and writes such files.

Records, fields and keys

A data file is a collection of records, each made up of fields. In a file of students, each record is one student; LastName, YearGroup and StudentID are fields. The records usually all have the same user-defined type.

Definition

A key field is a field whose value uniquely identifies each record in the file, for example a student ID, an account number or a product code. No two records have the same key.

Choosing a good key matters. A surname is a bad key (two students can share it); a generated ID is a good one.

The three methods of file organisation

Serial files

In a serial file, records are stored one after another in the order in which they were added, with each new record appended at the end. There is no ordering by key.

Serial files are the simplest possible arrangement. Writing is fast (always append). Finding a record is slow: there is no way to know where it is, so the file must be read from the start until it is found, or to the end if it is absent.

Typical uses: a log of transactions as they happen during the day, a file of readings from a sensor, a temporary file that will be sorted later, an audit trail. These are all files that are written in time order and later processed from beginning to end.

Sequential files

In a sequential file, records are stored in order of a key field, for example in ascending order of StudentID.

Because the order is known, a search can stop as soon as it reaches a key greater than the one wanted, and two sorted files can be merged efficiently. Adding or deleting a record is harder: to keep the order, the whole file must be copied to a new file, with the new record written in the right place (or the deleted record skipped).

Typical uses: a master file that is processed in full as a batch, such as a payroll file processed once a month or a utility company's customer file used to produce bills. Almost every record is read every time, so reading in order costs nothing extra.

Random files

In a random file (also called a direct-access or hashed file), the position of each record is calculated from its key by a hashing algorithm. The records are not in key order and not in time order; they are wherever the hash puts them.

To read a record, the program hashes the key to get the address and goes straight there. That makes access to an individual record very fast regardless of the size of the file.

Typical uses: any system that must find one record quickly in response to a request, such as a supermarket's product file used at the checkout, a bank's account file used at a cash machine, or a seat-booking system.

Key result
OrganisationRecords storedAdding a recordFinding one recordBest when
Serialin order of arrivalappend to end; fastread from the start until found; slowdata is recorded as it arrives and later processed in full
Sequentialin order of the key fieldcopy file, insert in place; slowread in order, can stop early; slowmost records are processed in a batch (high hit rate)
Randomat an address calculated from the keyhash to find address; fasthash to find address; fastindividual records are needed quickly (low hit rate, fast response)

Methods of file access

Organisation and access are different things. Organisation is the layout; access is how a program reaches records.

Definition

Sequential access: records are read one after another from the beginning of the file until the required record is found or the end of the file is reached. It is used with serial and sequential files.

Direct access: a record is reached without reading the records before it, by going straight to its location. It is used with random files (the location is found by hashing the key) and with sequential files (the location is found from an index).

A sequential file with an index is called an indexed sequential file. The index stores, for groups of keys, the address where that part of the file begins. To find a record, the program looks up the key in the index, jumps to the right part of the file, and reads forward a short distance. The same file can therefore be processed in batch (sequential access, in key order) and queried for single records (direct access via the index).

Key result
Sequential accessDirect access
Serial fileyesno
Sequential fileyesyes, using an index
Random filepossible but pointless (no meaningful order)yes, by hashing the key

Choosing an organisation: hit rate and response time

Two ideas decide most exam answers.

  • The hit rate is the proportion of records that are accessed in one run of a program. Producing monthly bills for every customer is a 100% hit rate; a sequential file read from start to end is ideal. Looking up one customer when they phone is a tiny hit rate; direct access is needed.
  • The response time needed. If a person is waiting (at a checkout, on a phone call), direct access is required. If the job runs overnight, sequential processing is fine.

Volatility (how often records are added and deleted) matters too: a sequential file is costly to change, a random file is cheap to change, and a serial file is cheapest to add to but costly to search.

Hashing algorithms

A hashing algorithm (hash function) takes a record's key and calculates an address in the file. The same key always gives the same address, so the record can be written there and found there again later.

Definition

A hashing algorithm is a calculation performed on the key field of a record that produces the address (position) at which the record is stored in a random file.

The most common algorithm divides by the number of available positions and takes the remainder:

Key result
address=key mod n\text{address} = \text{key} \bmod n

where nn is the number of record positions in the file, numbered 00 to n−1n - 1. A prime nn spreads keys more evenly.

Other algorithms you should be able to describe and use:

  • Folding: split the key into equal parts, add the parts, then (if necessary) take the result MOD the file size. Key 34567812 split into pairs gives 34+56+78+12=18034 + 56 + 78 + 12 = 180; with 97 positions, 180 mod 97=83180 \bmod 97 = 83.
  • Digit extraction: use selected digits of the key, for example the last three digits of 2041763 give address 763 in a 1000-record file.
  • Character keys: convert each character to its character code (ASCII or Unicode), add (or combine) the codes, and take MOD the file size. "CAT" gives 67+65+84=21667 + 65 + 84 = 216.

A good hashing algorithm is quick to calculate, always produces a valid address, and spreads keys evenly across the file so that few keys share an address.

Collisions

A collision occurs when two different keys hash to the same address. It is unavoidable when there are more possible keys than addresses. There are two standard ways to deal with it.

Definition

Open hashing (linear probing): if the calculated address is occupied, the record is stored in the next free address, searching forward one position at a time and wrapping round from the end of the file to the start.

Overflow area (closed hashing, chaining): if the calculated address is occupied, the record is stored in a separate overflow area, searched serially; alternatively, each address holds a pointer to a linked list of all records that hash to it.

Collisions cost time: a record that has been displaced takes extra reads to find. As the file fills up, collisions become more frequent, which is why random files are created with more positions than the expected number of records.

Writing a record to a random file (linear probing)
  1. Apply the hashing algorithm to the record's key to get an address.
  2. Read the record at that address.
  3. If the position is empty, write the new record there and stop.
  4. Otherwise move to the next address (wrapping round from the last address to address 0) and repeat from step 2.
  5. If you return to the starting address, the file is full; report an error.
Reading a record from a random file (linear probing)
  1. Apply the same hashing algorithm to the key being searched for.
  2. Read the record at that address.
  3. If its key matches, the record has been found; stop.
  4. If the position is empty, the record is not in the file; stop.
  5. Otherwise move to the next address (wrapping round) and repeat from step 2, stopping with "not found" if you return to the starting address.

Step 4 of the read is why deleting from an open-hashed file needs care: if you simply empty a position, a search for a record that was displaced past it will stop too early. Deleted positions are therefore marked as "deleted" rather than "empty", so searches continue past them while inserts can reuse them.

Random files in pseudocode

Cambridge pseudocode opens a random file with OPENFILE ... FOR RANDOM, moves the file pointer with SEEK, and reads and writes whole records with GETRECORD and PUTRECORD. The address in SEEK is a record number counted from the start of the file.

TYPE Product
   DECLARE ProductCode : INTEGER     // 0 means the position is empty
   DECLARE Description : STRING
   DECLARE Price : REAL
ENDTYPE

CONSTANT FileSize = 11

// writes NewItem to ProductFile.dat using linear probing
PROCEDURE AddProduct(NewItem : Product)
   DECLARE Address, Start : INTEGER
   DECLARE Current : Product
   DECLARE Placed : BOOLEAN
   Address ← NewItem.ProductCode MOD FileSize
   Start ← Address
   Placed ← FALSE
   OPENFILE "ProductFile.dat" FOR RANDOM
   REPEAT
      SEEK "ProductFile.dat", Address
      GETRECORD "ProductFile.dat", Current
      IF Current.ProductCode = 0 THEN
         SEEK "ProductFile.dat", Address
         PUTRECORD "ProductFile.dat", NewItem
         Placed ← TRUE
      ELSE
         Address ← (Address + 1) MOD FileSize
      ENDIF
   UNTIL Placed = TRUE OR Address = Start
   CLOSEFILE "ProductFile.dat"
   IF Placed = FALSE THEN
      OUTPUT "File is full"
   ENDIF
ENDPROCEDURE

The second SEEK before PUTRECORD is good practice: it makes clear the record is written at the position just examined rather than wherever the pointer moved after the read.

Random files in Python

Python has no record type and no GETRECORD, but genuine direct access is possible with fixed-length records. The struct module packs a record into a fixed number of bytes, so record number pp starts at byte p×record sizep \times \text{record size} and seek can jump straight to it.

import struct

FILE_SIZE = 11                       # record positions 0 to 10
RECORD = struct.Struct("<i20sd")     # int code, 20-byte description, float price
FILENAME = "ProductFile.dat"

def create_file():
    with open(FILENAME, "wb") as f:
        for _ in range(FILE_SIZE):
            f.write(RECORD.pack(0, b"", 0.0))   # code 0 marks an empty position

def read_record(f, address):
    f.seek(address * RECORD.size)
    code, desc, price = RECORD.unpack(f.read(RECORD.size))
    return code, desc.rstrip(b"\0").decode(), price

def write_record(f, address, code, desc, price):
    f.seek(address * RECORD.size)
    f.write(RECORD.pack(code, desc.encode(), price))

def add_product(code, desc, price):
    with open(FILENAME, "r+b") as f:
        address = start = code % FILE_SIZE
        while True:
            if read_record(f, address)[0] == 0:
                write_record(f, address, code, desc, price)
                return address
            address = (address + 1) % FILE_SIZE
            if address == start:
                raise IOError("File is full")

def find_product(code):
    with open(FILENAME, "rb") as f:
        address = start = code % FILE_SIZE
        reads = 0
        while True:
            record = read_record(f, address)
            reads += 1
            if record[0] == code:
                return address, record, reads
            if record[0] == 0:
                return None, None, reads          # empty slot: not in file
            address = (address + 1) % FILE_SIZE
            if address == start:
                return None, None, reads

create_file()
for code, desc in [(1045, "Kettle"), (2391, "Toaster"), (4412, "Lamp"),
                   (7735, "Fan"), (1001, "Iron"), (3335, "Mixer")]:
    print(code, "stored at", add_product(code, desc, 9.99))

print(find_product(3335))
print(find_product(9999))

Output:

1045 stored at 0
2391 stored at 4
4412 stored at 1
7735 stored at 2
1001 stored at 3
3335 stored at 5
(5, (3335, 'Mixer', 9.99), 4)
(None, None, 7)

Updating a sequential file

Because a sequential file must stay in key order, records cannot simply be inserted. Instead, the changes (the transaction file) are sorted into the same key order and merged with the old master file to produce a new master file. The old master is kept as a backup (the "grandfather–father–son" idea).

Merging a sorted transaction file into a sequential master file
  1. Open the old master file and the transaction file for reading, and a new master file for writing.
  2. Read the first record from each.
  3. While neither file is finished: write whichever record has the smaller key to the new file and read the next record from that file. (If the keys are equal, apply the transaction, for example an update or deletion.)
  4. When one file is finished, copy all remaining records from the other.
  5. Close all three files.
# merging two lists of records already sorted by key (key, name)
old_master = [(101, "Ahmed"), (104, "Bella"), (110, "Chen"), (115, "Dara")]
new_records = [(102, "Eve"), (112, "Femi"), (120, "Gita")]

new_master = []
i = j = 0
while i < len(old_master) and j < len(new_records):
    if old_master[i][0] < new_records[j][0]:
        new_master.append(old_master[i]); i += 1
    else:
        new_master.append(new_records[j]); j += 1
new_master.extend(old_master[i:])      # copy whatever is left
new_master.extend(new_records[j:])

print(new_master)

Output: [(101, 'Ahmed'), (102, 'Eve'), (104, 'Bella'), (110, 'Chen'), (112, 'Femi'), (115, 'Dara'), (120, 'Gita')].

Worked examples

Choosing a file organisation

For each application, state the most appropriate file organisation and access method, and justify your choice.

(a) A power company's customer file, used once a quarter to print every customer's bill. (b) A hotel's room file, used by the receptionist to check whether a particular room is free while a guest waits. (c) A file that records every login attempt to a server, with the date and time, for later analysis.

Solution

(a) Sequential organisation (in order of customer number), sequential access. Every customer gets a bill, so the hit rate is 100%; reading each record in order is the fastest way to process them all, and the bills come out in customer-number order. There is no need for fast access to one record.

(b) Random organisation, direct access by hashing the room number. Only one record is needed at a time, the guest is waiting so the response must be fast, and hashing finds the record without reading any others.

(c) Serial organisation, sequential access. Records are created in time order and simply appended, which is quick; the data is analysed later by reading the whole file, so there is no need for key order or direct access.

Each justification refers to the hit rate or response time of the scenario, which is what the mark scheme looks for.

Storing records with a hashing algorithm

A random file has 11 record positions, numbered 0 to 10. The hashing algorithm is Key MOD 11. Collisions are dealt with by storing the record in the next free position. Records with these keys are added in this order:

1045, 2391, 4412, 7735, 1001, 3335, 5442, 6002

Show the contents of the file after all eight records are added.

Solution

Calculate each address, checking whether it is already taken:

KeyKey MOD 11Position usedReason
1045001045=11×951045 = 11 \times 95
2391442391=11×217+42391 = 11 \times 217 + 4
4412114412=11×401+14412 = 11 \times 401 + 1
7735227735=11×703+27735 = 11 \times 703 + 2
1001030, 1, 2 occupied; 3 free
3335252, 3, 4 occupied; 5 free
5442885442=11×494+85442 = 11 \times 494 + 8
6002776002=11×545+76002 = 11 \times 545 + 7
Position012345678910
Key10454412773510012391333560025442
Searching a hashed file

Using the file from the previous example, state how many record positions must be read to (a) find 3335, (b) find 6002, (c) discover that 9999 is not in the file.

Solution

(a) 3335 mod 11=23335 \bmod 11 = 2. Read position 2 (7735), 3 (1001), 4 (2391), 5 (3335, found). 4 reads.

(b) 6002 mod 11=76002 \bmod 11 = 7. Position 7 holds 6002. 1 read.

(c) 9999=11×9099999 = 11 \times 909, so the address is 0. Read positions 0, 1, 2, 3, 4, 5 (all occupied by other keys) and then 6, which is empty. An empty position means the key would have been placed there, so 9999 is not in the file. 7 reads.

This shows the cost of collisions: records that hash into a "cluster" of occupied positions take several reads to find, and unsuccessful searches are the worst of all.

Folding and character keys

(a) A file has 97 positions (0 to 96). The hashing algorithm splits an eight-digit key into four two-digit numbers, adds them, and takes the result MOD 97. Find the address for key 34567812. (b) A different file uses three-letter airport codes as keys. The algorithm adds the ASCII codes of the three letters and takes the result MOD 50. Given that 'A' is 65, find the address of "LHR".

Solution

(a) 34+56+78+12=18034 + 56 + 78 + 12 = 180, and 180=97+83180 = 97 + 83, so the address is 83.

(b) 'L' = 76, 'H' = 72, 'R' = 82 (counting on from 'A' = 65). 76+72+82=23076 + 72 + 82 = 230, and 230=4×50+30230 = 4 \times 50 + 30, so the address is 30.

Note that "RHL" would give the same address; any algorithm that just adds character codes gives collisions for anagrams, which is a reason to weight each character by its position.

Writing a search for a random file

Write a pseudocode function FindProduct(Code : INTEGER) RETURNS INTEGER that returns the record position of the product with key Code in the 11-record file ProductFile.dat used above, or −1 if it is not present. Use the record type Product and the hashing algorithm Code MOD 11.

Solution
FUNCTION FindProduct(Code : INTEGER) RETURNS INTEGER
   DECLARE Address, Start, Result : INTEGER
   DECLARE Current : Product
   DECLARE Finished : BOOLEAN
   Address ← Code MOD 11
   Start ← Address
   Result ← -1
   Finished ← FALSE
   OPENFILE "ProductFile.dat" FOR RANDOM
   REPEAT
      SEEK "ProductFile.dat", Address
      GETRECORD "ProductFile.dat", Current
      IF Current.ProductCode = Code THEN
         Result ← Address
         Finished ← TRUE
      ELSE
         IF Current.ProductCode = 0 THEN
            Finished ← TRUE                 // empty position: not present
         ELSE
            Address ← (Address + 1) MOD 11
            IF Address = Start THEN
               Finished ← TRUE              // whole file searched
            ENDIF
         ENDIF
      ENDIF
   UNTIL Finished = TRUE
   CLOSEFILE "ProductFile.dat"
   RETURN Result
ENDFUNCTION

Marks go for: using the same hash as the write; reading at the hashed address with SEEK and GETRECORD; comparing the key; stopping at an empty position; moving to the next position with wrap-round (MOD 11); stopping after a full circuit; closing the file; returning the result.

Watch out
  • "Sequential" is not "serial". Serial means in order of arrival; sequential means in order of a key field. Mixing them up loses the mark every time.
  • "Random" does not mean the records are placed randomly. Their positions are calculated from the key, which is the opposite of random.
  • After a collision, a search must not stop just because the first position held a different key. It stops only on a match, an empty position or a full circuit.
  • Do not confuse file organisation (serial, sequential, random) with file access (sequential, direct). A sequential file can be accessed directly through an index.
Exam tip
  • Describing a file organisation earns marks for: how records are placed (order of arrival / key order / hashed address), how a record is added, and how a record is found.
  • Justifications must use the scenario: "100% hit rate, every record processed in a batch" or "one record needed quickly while the customer waits". Generic statements ("it is faster") earn little.
  • In hashing questions, show the arithmetic for each key and state clearly where each colliding record ends up. Wrap round from the last position to position 0.
  • If a question asks how a hashed file could be improved, good answers are: increase the number of positions (lower load), choose a prime number of positions, use a better hash that spreads keys, or use an overflow area / chaining.
Summary
  • Serial: order of arrival, append to add, read from the start to find. Sequential: key order, copy-and-insert to add, read in order to find. Random: position calculated from the key by hashing.
  • Sequential access is used for serial and sequential files; direct access is used for random files (by hashing) and sequential files (by an index).
  • Choose by hit rate, required response time and volatility.
  • A hashing algorithm turns a key into an address; Key MOD n is the standard one, with folding, digit extraction and character-code sums as alternatives.
  • A collision is two keys with the same address; deal with it by linear probing (next free position) or an overflow area / chaining.
  • In pseudocode: OPENFILE ... FOR RANDOM, SEEK, GETRECORD, PUTRECORD, CLOSEFILE.
  • Sequential files are updated by merging a sorted transaction file with the old master file into a new master file.

Practice questions

Question
  1. Describe the difference between serial and sequential file organisation.
  2. State what is meant by a key field and explain why a random file must have one.
  3. A random file has 13 positions (0 to 12) and uses Key MOD 13 with linear probing. Insert, in order: 27, 40, 53, 18, 66, 31. Show the final positions.
  4. Using your answer to question 3, state the number of positions read to (a) find 66, (b) show that 79 is not present.
  5. Explain why a deleted record in an open-hashed file is marked as deleted rather than simply emptied.
  6. A school library's file of book records is used (i) every night to produce a list of all overdue books, and (ii) during the day to look up a book when it is borrowed. Recommend a file organisation that supports both uses, explaining how each use would access the file.
  7. A hashing algorithm adds the character codes of a four-character product code and takes the result MOD 100. Given 'A' = 65 and '0' = 48, find the address for "AB12". Explain one weakness of this hashing algorithm.
  8. Write pseudocode to output every record in the random file ProductFile.dat (positions 0 to 10, record type Product) that is not empty.
  9. Write a Python function hash_name(name, size) that returns the sum of the character codes of name modulo size, and use it to show that "STOP" and "POTS" collide in a file of 50 positions. Suggest and implement a change that removes this collision.
  10. A bank stores 2 million account records. Customers use cash machines, which need one account in under a second; every night, interest is added to every account. Evaluate random organisation and sequential organisation for this file, and recommend a solution.
Answers
  1. In a serial file records are stored in the order they were added (chronologically), each new record appended to the end, with no ordering by key. In a sequential file records are stored in order of a key field, so a new record must be inserted in its correct position (usually by copying to a new file).

  2. A key field is a field whose value is unique to each record and identifies it. A random file needs one because the record's address is calculated by applying the hashing algorithm to the key; without a unique key there is nothing to hash and no way to find a particular record again.

  3. 27 mod 13=127 \bmod 13 = 1; 40 mod 13=140 \bmod 13 = 1 (collision, goes to 2); 53 mod 13=153 \bmod 13 = 1 (1, 2 taken, goes to 3); 18 mod 13=518 \bmod 13 = 5; 66 mod 13=166 \bmod 13 = 1 (1, 2, 3 taken, goes to 4); 31 mod 13=531 \bmod 13 = 5 (5 taken, goes to 6).

    Position123456
    Key274053661831

    All other positions are empty.

  4. (a) 66 hashes to 1; read 1, 2, 3, 4: 4 reads. (b) 79=6×13+179 = 6 \times 13 + 1, address 1; read 1, 2, 3, 4, 5, 6 (all other keys) and 7 (empty): 7 reads.

  5. A search stops when it reaches an empty position, because an empty position means no displaced record could have been placed beyond it. If a deleted record's position were emptied, a search for a record that had collided and been stored further on would stop at the gap and wrongly report "not found". Marking it "deleted" lets searches continue past it, while inserts may still reuse it.

  6. An indexed sequential file: records stored in order of the book's key (for example the accession number) with an index. (i) The overnight report reads the whole file in key order with sequential access, which suits a 100% hit rate. (ii) A daytime lookup uses the index to find the block containing the key and goes directly there (direct access), so the librarian gets a fast response. (A random file with a separate nightly serial read is acceptable if justified, but the order of the report would then be arbitrary.)

  7. Codes: 'A' = 65, 'B' = 66, '1' = 49, '2' = 50. Sum = 230; 230 mod 100=30230 \bmod 100 = 30. Weakness: any codes with the same characters in a different order ("BA21", "1A2B") give the same sum and collide; also sums of four characters cluster in a narrow range (roughly 192 to 360), so addresses are unevenly spread.

```pseudocode
DECLARE Address : INTEGER
DECLARE Current : Product
OPENFILE "ProductFile.dat" FOR RANDOM
FOR Address ← 0 TO 10
   SEEK "ProductFile.dat", Address
   GETRECORD "ProductFile.dat", Current
   IF Current.ProductCode <> 0 THEN
      OUTPUT Current.ProductCode, " ", Current.Description, " ", Current.Price
   ENDIF
NEXT Address
CLOSEFILE "ProductFile.dat"
```

9.

```python
def hash_name(name, size):
    total = 0
    for ch in name:
        total += ord(ch)
    return total % size

print(hash_name("STOP", 50), hash_name("POTS", 50))   # 26 26: collision

def hash_name_weighted(name, size):
    total = 0
    for position, ch in enumerate(name, start=1):
        total += position * ord(ch)      # weight each code by its position
    return total % size

print(hash_name_weighted("STOP", 50), hash_name_weighted("POTS", 50))   # 8 22
```

`S`, `T`, `O`, `P` have codes 83, 84, 79, 80. Both words total $83 + 84 + 79 + 80 = 326$, and $326 \bmod 50 = 26$, so they collide. Weighting each code by its position gives $1(83) + 2(84) + 3(79) + 4(80) = 808$ for `"STOP"` (address 8) and $1(80) + 2(79) + 3(84) + 4(83) = 822$ for `"POTS"` (address 22), so the anagrams no longer collide.

10. Random organisation gives direct access: hashing the account number finds one record in about one read, meeting the cash machine requirement regardless of file size, and records can be added without rewriting the file. But the nightly interest run must touch every record; reading a hashed file position by position works but is in no useful order and includes empty positions. Sequential organisation suits the nightly batch run (100% hit rate, read in order) but a cash-machine lookup would need, on average, a million reads, which is far too slow. Recommendation: an indexed sequential file (or a random file plus a nightly full scan), so that single lookups use direct access through the index or hash, and the interest run uses sequential access through the whole file.

How well do you know this?

Builds on

Console

Search notes, courses and tools, or run an action