File organisation and access
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.
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.
| Organisation | Records stored | Adding a record | Finding one record | Best when |
|---|---|---|---|---|
| Serial | in order of arrival | append to end; fast | read from the start until found; slow | data is recorded as it arrives and later processed in full |
| Sequential | in order of the key field | copy file, insert in place; slow | read in order, can stop early; slow | most records are processed in a batch (high hit rate) |
| Random | at an address calculated from the key | hash to find address; fast | hash to find address; fast | individual 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.
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).
| Sequential access | Direct access | |
|---|---|---|
| Serial file | yes | no |
| Sequential file | yes | yes, using an index |
| Random file | possible 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.
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:
where is the number of record positions in the file, numbered to . A prime 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
MODthe file size. Key34567812split into pairs gives ; with 97 positions, . - Digit extraction: use selected digits of the key, for example the last three digits of
2041763give 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
MODthe file size."CAT"gives .
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.
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.
- Apply the hashing algorithm to the record's key to get an address.
- Read the record at that address.
- If the position is empty, write the new record there and stop.
- Otherwise move to the next address (wrapping round from the last address to address 0) and repeat from step 2.
- If you return to the starting address, the file is full; report an error.
- Apply the same hashing algorithm to the key being searched for.
- Read the record at that address.
- If its key matches, the record has been found; stop.
- If the position is empty, the record is not in the file; stop.
- 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 starts at byte 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).
- Open the old master file and the transaction file for reading, and a new master file for writing.
- Read the first record from each.
- 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.)
- When one file is finished, copy all remaining records from the other.
- 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
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.
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:
| Key | Key MOD 11 | Position used | Reason |
|---|---|---|---|
| 1045 | 0 | 0 | |
| 2391 | 4 | 4 | |
| 4412 | 1 | 1 | |
| 7735 | 2 | 2 | |
| 1001 | 0 | 3 | 0, 1, 2 occupied; 3 free |
| 3335 | 2 | 5 | 2, 3, 4 occupied; 5 free |
| 5442 | 8 | 8 | |
| 6002 | 7 | 7 |
| Position | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Key | 1045 | 4412 | 7735 | 1001 | 2391 | 3335 | 6002 | 5442 |
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) . Read position 2 (7735), 3 (1001), 4 (2391), 5 (3335, found). 4 reads.
(b) . Position 7 holds 6002. 1 read.
(c) , 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.
(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) , and , so the address is 83.
(b) 'L' = 76, 'H' = 72, 'R' = 82 (counting on from 'A' = 65). , and , 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.
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
ENDFUNCTIONMarks 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.
- "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.
- 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.
- 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 nis 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
- Describe the difference between serial and sequential file organisation.
- State what is meant by a key field and explain why a random file must have one.
- A random file has 13 positions (0 to 12) and uses
Key MOD 13with linear probing. Insert, in order: 27, 40, 53, 18, 66, 31. Show the final positions. - Using your answer to question 3, state the number of positions read to (a) find 66, (b) show that 79 is not present.
- Explain why a deleted record in an open-hashed file is marked as deleted rather than simply emptied.
- 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.
- 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. - Write pseudocode to output every record in the random file
ProductFile.dat(positions 0 to 10, record typeProduct) that is not empty. - Write a Python function
hash_name(name, size)that returns the sum of the character codes ofnamemodulosize, 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. - 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
-
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).
-
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.
-
; (collision, goes to 2); (1, 2 taken, goes to 3); ; (1, 2, 3 taken, goes to 4); (5 taken, goes to 6).
Position 1 2 3 4 5 6 Key 27 40 53 66 18 31 All other positions are empty.
-
(a) 66 hashes to 1; read 1, 2, 3, 4: 4 reads. (b) , address 1; read 1, 2, 3, 4, 5, 6 (all other keys) and 7 (empty): 7 reads.
-
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.
-
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.)
-
Codes:
'A'= 65,'B'= 66,'1'= 49,'2'= 50. Sum = 230; . 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.
```pseudocodeDECLARE 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.