Week 7 · Backend systems: indexing, search, caching and logging
The one-minute story
A busy restaurant kitchen. Your Flask app is the kitchen. When it works you want to know what happened (the logbook: "7:02 table 4 ordered, 7:05 oven failed"). When something burns you need to stop time and look inside (debugging). The pantry is huge, so finding the rice quickly needs a label system (an index), or a cook will search every shelf. A guest who wants "anything with mango" needs a search catalogue (full-text search). The same popular dish ordered 500 times an hour should not be cooked from scratch each time: keep a few ready on the counter (a cache). And when the queue at the door gets long, you either buy a bigger stove (scale up) or open more kitchens behind a manager at the door (scale out with a load balancer).
Backend systems are the habits that keep a working app observable, fast and able to grow.
By the end of this week you can:
- predict the output of Python
loggingprograms for any level and format; - debug a Flask app with debug mode,
pdb, the Flask shell and tracebacks; - create an index, read
EXPLAIN QUERY PLAN, and say when an index helps and when it hurts; - build a full-text search with SQLite FTS5 and keep it in sync with triggers;
- explain caching (client vs server, TTL, keys, invalidation,
Cache-Control,ETag) and use Flask-Caching; - calculate latency, throughput, bandwidth, data volume and requests per second;
- explain scaling, load balancers, stateless servers, batch jobs, N+1 queries and pagination.
Words you need
- Log - a time-ordered record of events written by the program while it runs.
- Log level - the importance of a message: DEBUG, INFO, WARNING, ERROR, CRITICAL.
- Handler - decides where a log record goes (console, file, email).
- Formatter - decides how a log record looks (time, level, message).
- Traceback - the list of function calls that led to an error, printed when it crashes.
- Index - an extra sorted structure that lets the database find rows without scanning the whole table.
- Full-text search (FTS) - searching words inside long text, not exact column values.
- Cache - a small fast store of copies of results so they need not be recomputed or refetched.
- TTL (time to live) - how long a cached item may be used before it expires.
- Latency - the delay before something starts or arrives (milliseconds).
- Throughput - how much work or data is completed per unit time (requests/s, MB/s).
- Bandwidth - the maximum data rate of a link or memory (bits or bytes per second).
- Scale up / scale out - make one machine bigger / add more machines.
- Stateless server - a server that keeps no per-user memory between requests, so any copy can answer any request.
- N+1 problem - one query for a list plus one extra query for every item in it.
- Pagination - returning big results one page at a time.
Idea 1: Logging (the app's diary)
print() is fine on day one. A real app needs levels, timestamps, files, and the ability to switch noise on and off without editing every line. That is the logging module.
Five levels, in increasing severity (the number is what Python stores):
| Level | Number | Typical use |
|---|---|---|
| DEBUG | 10 | detailed values for developers |
| INFO | 20 | normal events ("user logged in") |
| WARNING | 30 | something odd but the app continues |
| ERROR | 40 | an operation failed |
| CRITICAL | 50 | the app may not continue |
Rule: a message is shown only if its level is greater than or equal to the configured level. The default level is WARNING, so DEBUG and INFO are silent unless you lower the level.
import logging
logging.basicConfig(level=logging.INFO,
format='%(levelname)s:%(name)s:%(message)s')
log = logging.getLogger("shop")
log.debug("hidden") # 10 < 20, suppressed
log.info("hello %s", "ravi") # shown
log.warning("low stock")
log.error("fail")
log.critical("down")
Output (checked by running it):
INFO:shop:hello ravi
WARNING:shop:low stock
ERROR:shop:fail
CRITICAL:shop:down
Handlers, formatters, loggers. A logger (named, e.g. "shop") creates records. A handler sends them somewhere (StreamHandler = console, FileHandler("app.log")). A formatter turns the record into text using fields like %(asctime)s, %(levelname)s, %(message)s, %(name)s. Each handler can have its own level, so you can print everything to the screen but save only errors to a file:
fh = logging.FileHandler("app.log")
fh.setLevel(logging.ERROR)
fh.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s"))
log.addHandler(fh)
log.warning("seen on console only")
log.error("seen on console and in app.log")
basicConfig() is a one-line setup of the root logger. Other details that are asked:
logging.exception("msg")iserrorplus the full traceback; use it insideexcept.- Use lazy formatting:
log.info("user %s", name), not an f-string, so the string is built only if the level is on. basicConfigdoes nothing if the root logger already has handlers; call it once, early.- Flask's logger is
app.logger(named after your app). It logs unhandled exceptions as ERROR with a traceback and returns 500 to the browser. In debug mode Flask also shows werkzeug's request lines.
@app.route("/")
def home():
app.logger.info("visited home")
return "ok"
Question. log.py:
import logging, sys
logging.basicConfig(level=logging.INFO,
format='%(levelname)s - %(message)s')
num1, num2 = int(sys.argv[1]), int(sys.argv[2])
logging.info("This operation checks factors")
if num1 % num2 == 0:
logging.debug(f"{num2} is a factor of {num1}")
else:
logging.warning(f"{num2} is not the factor of {num1}")
What is printed for python log.py 25 5?
Solution. Level is INFO (20). The first call logs INFO: shown. 25 % 5 == 0, so the if branch runs and logs DEBUG (10), which is below INFO and suppressed. Output: INFO - This operation checks factors.
Same style with level=logging.WARNING and a critical call: CRITICAL (50) is always shown, in the format CRITICAL - text.
Question. Level is WARNING, format %(asctime)s - %(levelname)s - %(message)s. The code calls logging.info("Value added") when the value is fine, but raises ValueError for negative numbers and handles it with logging.exception("Exception occurred: %s", str(ve)). What happens for python log.py -34 and for python log.py 34?
Solution. For -34: the ValueError is caught, logging.exception logs at ERROR (40 >= 30) so a line such as 2026-10-06 11:51:07,727 - ERROR - Exception occurred: Invalid value: Please enter a positive value. appears, followed by the traceback. For 34: only logging.info runs (20 < 30), so nothing at all is printed. (We ran both.)
sys.argvsplits on spaces:python code.py mad 1givesargv[1] == 'mad', not'mad 1'. A real paper used this to make theelsebranch run.- A lower level number means more output.
level=DEBUGshows everything;level=ERRORshows only ERROR and CRITICAL. - Without
basicConfig, the default format isLEVEL:logger:message, and the default level is WARNING. - The timestamp in
asctimehas milliseconds after a comma:2024-07-29 10:15:32,125.
Idea 2: Debugging
Debug mode. app.run(debug=True) (or flask --app app run --debug) does two things: it auto-reloads when you save a file, and it shows an interactive error page with the traceback in the browser. Never use it in production, because it lets anyone who sees the page run code on your server.
Reading a traceback. Read it from the bottom: the last line is the exception type and message; the line above shows the exact line of your code that failed; the lines above show who called whom.
Traceback (most recent call last):
File "app.py", line 11, in b
return 1/0
ZeroDivisionError: division by zero
pdb, the Python debugger. Put breakpoint() (or import pdb; pdb.set_trace()) where you want to stop. Execution pauses with a (Pdb) prompt:
| Command | Meaning |
|---|---|
n (next) | run the next line, do not enter functions |
s (step) | step into a function call |
c (continue) | run until the next breakpoint |
p expr | print an expression, e.g. p user.name |
l (list) | show the code around the current line |
q | quit |
Flask shell. flask --app app shell opens a Python prompt with the app context already pushed, so db, models and queries work right away. It is the fastest way to test a query or insert test data.
flask --app app shell
>>> User.query.count()
Debugging recipe: reproduce it, read the last line of the traceback, add a log line or breakpoint() just before the failure, check the values, fix, and re-run. Do not edit five things at once.
Add a logging.basicConfig at the top of your project, log every login, every failed login and every database error at the right level, and keep one app.log file. In the viva you may be asked "how would you know if a user complained about a failure yesterday?" The answer is your log file.
Idea 3: Database indexing
Without help, SELECT * FROM student WHERE city='c7' makes SQLite read every row (a full table scan, cost proportional to n). An index is a separate sorted structure, like the index at the back of a book: it maps a column value to the rows that hold it.
B-tree intuition. Real indexes are B-trees: a short, wide tree whose nodes hold many sorted keys. To find a value you go from the root down a few levels, choosing the right branch at each node. For a million rows a binary search needs about 20 comparisons; a B-tree with 100 keys per node needs only about 3 levels. Cost grows with the logarithm of the table size, not the size itself.
import sqlite3
c = sqlite3.connect(":memory:")
c.execute("create table student(id integer primary key, name text, city text, marks int)")
c.executemany("insert into student(name,city,marks) values(?,?,?)",
[("s%d" % i, "c%d" % (i % 50), i % 100) for i in range(10000)])
q = "select * from student where city='c7'"
print(c.execute("explain query plan " + q).fetchall())
c.execute("create index idx_city on student(city)")
print(c.execute("explain query plan " + q).fetchall())
Real output (the middle numbers differ by version):
[(2, 0, 216, 'SCAN student')]
[(3, 0, 62, 'SEARCH student USING INDEX idx_city (city=?)')]
SCAN = reads all rows. SEARCH ... USING INDEX = uses the index. EXPLAIN QUERY PLAN shows what SQLite plans to do without running the query.
Index facts
- Syntax:
CREATE INDEX idx_city ON student(city);andCREATE UNIQUE INDEX ...(also enforces uniqueness). - The primary key is already indexed (
SEARCH student USING INTEGER PRIMARY KEY (rowid=?)). - An index can cover several columns:
CREATE INDEX idx_cm ON student(city, marks)helpsWHERE city=? AND marks>?. - An index can be on a column with duplicate values (city repeats 200 times above and still helps).
- Helps:
WHERE,JOIN ... ON,ORDER BYon the indexed column; and selective conditions (few rows match). - Cost: extra disk space, and every INSERT, UPDATE and DELETE must also update the index, so writes get slower. Index the columns you search on, not everything.
- Does not help: tiny tables; conditions like
name LIKE '%5'(a leading wildcard cannot use a sorted order; we ran it and it still said SCAN); columns where most rows match.
Question. After create index idx_city on student(city), what does SQLite do for select * from student where city='c7' and marks>50? And after also create index idx_cm on student(city,marks)?
Solution (run on the database above). First: SEARCH student USING INDEX idx_city (city=?): it uses the city index then checks marks on those rows. Second: SEARCH student USING INDEX idx_cm (city=? AND marks>?): both conditions are answered from the index. The two-column index is better for this query.
In Flask-SQLAlchemy: city = db.Column(db.String, index=True) creates the index with the table. Foreign key columns used in joins are good candidates.
Two statements from a real End Term were both false: "an index can only be created on one column" and "indexes cannot be created on columns having duplicate values". Also remember the one-line purpose: an index speeds up data retrieval; it does not encrypt, replicate, or describe the schema.
Idea 4: Full-text search with SQLite FTS5
WHERE body LIKE '%flask%' scans everything and cannot rank results. FTS5 is an SQLite extension that builds an inverted index (word -> list of rows containing it). You create a virtual table: it looks like a table but is stored in special search structures.
c.execute("create virtual table notes using fts5(title, body)")
c.execute("insert into notes values('Flask intro','Flask is a micro framework'),"
"('Jinja','Templates for Flask apps'),('SQL','Indexes make queries fast')")
print(c.execute("select title from notes where notes match 'flask'").fetchall())
print(c.execute("select title from notes where notes match 'micro OR index*'").fetchall())
[('Flask intro',), ('Jinja',)]
[('Flask intro',), ('SQL',)]
MATCHsearches words;'micro OR index*'usesORand the prefix starindex*(matches indexes). AlsoAND,NOTand quoted phrases'"micro framework"'.- Matching is by whole words (the default tokenizer does no stemming):
cachedoes not matchcaching; usecache*. - Order by relevance with
ORDER BY rank; highlight withsnippet(...)orhighlight(...).
Keeping it in sync. If your real data lives in a normal table post, make the FTS table an external-content table and add triggers (automatic actions that run after insert, update, delete) so the search index never goes stale:
create table post(id integer primary key, title text, body text);
create virtual table post_fts using fts5(title, body, content='post', content_rowid='id');
create trigger post_ai after insert on post begin
insert into post_fts(rowid,title,body) values (new.id,new.title,new.body);
end;
create trigger post_ad after delete on post begin
insert into post_fts(post_fts,rowid,title,body) values('delete',old.id,old.title,old.body);
end;
create trigger post_au after update on post begin
insert into post_fts(post_fts,rowid,title,body) values('delete',old.id,old.title,old.body);
insert into post_fts(rowid,title,body) values (new.id,new.title,new.body);
end;
Search and join back to the real rows: select p.id,p.title from post_fts f join post p on p.id=f.rowid where post_fts match 'cache*'. We tested all three triggers: after an update the new words match, after a delete the row disappears from results.
A search box over your project's items (courses, trackers, posts) is a good "extra feature". Say in the viva: "plain LIKE scans every row; FTS5 uses an inverted index and can rank, and triggers keep it in sync."
Idea 5: Caching
A cache stores the result of expensive work (a database query, a rendered page, an API call) so the next identical request is answered instantly. Why: lower latency for the user, less load on the server and database, lower cost.
| Client-side cache | Server-side cache | |
|---|---|---|
| Lives in | browser (or a proxy/CDN in between) | server memory, Redis, Memcached |
| Saves | network trip and server work | database and computation work |
| Controlled by | HTTP headers | your code (Flask-Caching) |
Server-side with Flask-Caching. (Install flask-caching.)
from flask_caching import Cache
cache = Cache(app, config={"CACHE_TYPE": "SimpleCache", "CACHE_DEFAULT_TIMEOUT": 60})
@app.route("/slow")
@cache.cached(timeout=30) # whole response cached for 30 s
def slow():
...
@cache.memoize(timeout=30) # cache by function arguments
def square(x): ...
We ran this: two calls of /slow ran the function once (the second came from the cache). square(4) twice ran once, but square(5) would compute again, because the cache key includes the arguments. A cache key is the label under which the result is stored: for @cache.cached it is the request path, for memoize it is the function name plus arguments.
Three problems every cache has
- Expiry (TTL): after
timeoutseconds the item is dropped and recomputed, so data can be stale for up to the TTL. - Invalidation: when the real data changes (a new post is saved) you must delete the affected cache keys (
cache.delete(key),cache.delete_memoized(fn, args),cache.clear()), or users keep seeing old data. "There are only two hard things in computing: cache invalidation..." - Cache hit / miss: a hit is served from the cache; a miss goes to the real source and then fills the cache.
Cache things that are read often and change rarely. Never cache per-user private pages under a shared key.
Client-side caching with HTTP headers.
Cache-Control: public, max-age=60- any cache may reuse this response for 60 s without asking.private= only the user's browser.no-store= never keep it.- ETag - a fingerprint of the response ("v1"). Next time the browser sends
If-None-Match: "v1". If unchanged the server replies 304 Not Modified with no body: the browser reuses its copy and saves bandwidth. If changed: 200 with the new body.
@app.route("/data")
def data():
resp = make_response('{"v": 1}')
resp.set_etag("v1")
resp.headers["Cache-Control"] = "public, max-age=60"
return resp.make_conditional(request)
Tested: first request 200 with ETag: "v1"; second request with If-None-Match: "v1" gave 304 and an empty body.
REST APIs and caching. Only safe, idempotent reads (GET) are meant to be cached. POST, PUT, DELETE change data and are not cached by default. A paper asked which statements about REST caching were false; the false ones were: "caching degrades service and increases server load", "browsers treat all GET and POST requests as cacheable by default", and "caching optimises the network by increasing latency". Caching reduces latency and load; only GET is cacheable by default.
- Caching helps reads. After a write you must invalidate, otherwise users see stale data.
- A 304 response has no body; the data comes from the browser's own copy.
- A bigger TTL means fewer misses but staler data.
Idea 6: Performance vocabulary and arithmetic
Latency is "how long until the first byte" (delay). Throughput is "how much per second" (rate). Bandwidth is the maximum rate a link or memory can carry. A highway analogy: latency is the travel time of one car; bandwidth is the number of lanes.
Memory hierarchy (fast and small at the top, slow and large at the bottom):
| Level | Latency | Capacity per cost | Throughput |
|---|---|---|---|
| Registers | lowest | smallest | highest |
| SRAM (CPU cache) | very low | small | very high |
| DRAM (main memory) | low | medium | high |
| SSD | higher | large | medium |
| HDD | highest | largest | lowest |
So "highest latency" = HDD, "lowest density" = registers, "lowest throughput" = HDD. For small, hot data wanting very low latency (such as frequently used records), the answer in the papers is SRAM-like cache memory.
The arithmetic. Convert units first: 1 byte = 8 bits; use the question's own definition (usually 1 KB = 1000 B, 1 MB = 1000 KB).
Question. A DRAM module has bus width 64 bits, clock 400 MHz, DDR (two transfers per clock). Maximum bandwidth in dual-channel mode?
Solution. Bus = 64 bits = 8 bytes. Transfers per second = 400 million x 2 = 800 million. One channel = 8 x 800 million = 6.4 GB/s. Dual channel = 2 x 6.4 = 12.8 GB/s. (At 800 MHz dual channel: 25.6. At 1 GHz single channel DDR: 8 bytes x 2 billion = 16 GB/s. These three appeared in real papers.)
Question. Speed of signal in cable is 2 x 10^8 m/s. Distance to the data centre is 2000 km. Find the one-way and round-trip latency, and the maximum requests per second one client can finish one after another.
Solution. One way: 2,000,000 m / 2x10^8 = 0.01 s = 10 ms. Round trip = 20 ms. If each request waits for its reply, requests per second = 1000 / 20 = 50. (For 1000 km: 5 ms one way.) A bigger response adds transfer time = size / bandwidth.
Question. A phone moves away at 60 km/h from a tower with range 40 km, using all of 120 Mbps the whole time. How many GB are used?
Solution. Time = 40/60 h = 40 min = 2400 s. Data = 120 Mbit/s x 2400 s = 288,000 Mbit = 36,000 MB (divide by 8) = 36 GB. For a graph of bandwidth against time, split into steps and add up: 8 Mbps for 4 h = 8 x 14,400 s = 115,200 Mbit = 14.4 GB, and so on.
Other quick formulas: disk worst-case rotational latency = one full rotation = 60000 / RPM milliseconds (6000 RPM gives 10 ms, 5000 RPM gives 12 ms; the average is half). HDD throughput = data per revolution x revolutions per second: to match an SSD of 180 MB/s with 4.8 Mbit (0.6 MB) per revolution you need 300 rev/s = 18000 RPM.
Idea 7: Scaling, stateless servers, background jobs
- Scale up (vertical): a bigger machine (more RAM, CPU). Simple, but has a ceiling and one point of failure.
- Scale out (horizontal): more machines behind a load balancer, which spreads requests (round robin, least connections). Practically unlimited, and one machine failing does not stop the site.
- Stateless servers make scale-out easy: if user data lives in the database or a shared store (Redis) and not in one server's memory, any server can answer any request. This is why a login can be carried by a token or a shared session store instead of a local variable.
- Redundancy and replication: keeping copies (extra servers, database replicas) for availability and fault tolerance. A paper had two statements, "redundancy improves performance and reduces load" and "replication ensures data is not lost", and marked both false. The official wording favours: redundancy is for reliability, not speed, and "ensures" is too strong. Check your course notes for the exact phrasing.
- ACID vs BASE: ACID (atomic, consistent, isolated, durable) suits financial transactions; BASE (basically available, soft state, eventually consistent) suits social media feeds. In the paper: scale up = increasing RAM, scale out = adding physical servers.
Session memory is also a scaling calculation: 1.5 million sessions x 4 KB = 6,000,000,000 bytes = 6 GB of RAM (a paper asked this for a Redis session store).
Background (batch) jobs. Work that is slow and does not need an instant answer (emailing reports, resizing images, nightly exports) should not block the web request. The request puts a job on a queue and returns at once; a separate worker takes jobs from the queue. Cron runs a task on a schedule ("every night at 2"). The tool for this in Flask is Celery, which you meet in MAD2; here you only need the idea.
Idea 8: N+1 queries and pagination
N+1 problem. One query fetches N authors, then touching author.books fires one more query per author: N+1 queries. We counted queries with 5 authors and 3 books each:
for a in Author.query.all(): # 1 query
len(a.books) # +5 lazy queries -> 6 total
for a in Author.query.options(selectinload(Author.books)).all(): # 2 queries
len(a.books)
for a in Author.query.options(joinedload(Author.books)).all(): # 1 query (JOIN)
len(a.books)
Measured: 6, 2 and 1 queries. Fix: eager loading (selectinload, joinedload) or a single JOIN. Add an index on foreign keys.
Pagination. Never return a million rows. Return a page at a time with LIMIT and OFFSET (or db.paginate):
p = db.paginate(db.select(Book).order_by(Book.id), page=2, per_page=4)
# p.items = [b11, b12, b20, b21]; p.page=2, p.pages=4, p.total=15, p.has_next, p.has_prev
Always add ORDER BY so pages are stable. API shape: GET /books?page=2&per_page=4.
Your turn
logging.basicConfig(level=logging.WARNING, format='%(levelname)s - %(message)s'). Then logging.debug("a"), logging.warning("b"), logging.critical("c"), logging.info("d"). What prints?
Need a hint?
Which levels are 30 or higher?
Show the full solution
WARNING - b and CRITICAL - c. DEBUG (10) and INFO (20) are below 30.
Table orders has 2 million rows. For each query say whether an index on the column helps: (a) WHERE customer_id = 17, (b) WHERE note LIKE '%gift%', (c) a table of 20 rows, (d) a column status where 99 percent of rows are 'done'.
Need a hint?
Helps when few rows match and the condition can use sorted order.
Show the full solution
(a) yes; (b) no, leading wildcard forces a scan (use FTS5); (c) no benefit; (d) little for 'done' (almost every row matches), but useful for the rare values.
Dual-channel DDR, 64-bit bus, 200 MHz clock. Maximum bandwidth in GB/s?
Need a hint?
8 bytes x transfers per second x channels.
Show the full solution
8 x (200 million x 2) = 3.2 GB/s per channel; dual = 6.4 GB/s.
(a) A disk turns at 7500 RPM. Worst-case rotational latency? (b) Distance 3000 km, speed in cable 2 x 10^8 m/s: round-trip latency?
Need a hint?
60000 / RPM in ms; distance divided by speed, then double.
Show the full solution
(a) 60000/7500 = 8 ms. (b) 3,000,000 / 2x10^8 = 15 ms one way, 30 ms round trip.
Your home page shows "top 10 courses", recomputed from 1 million rows every time, but changes only when an admin edits courses. Design the caching.
Need a hint?
Cache key, TTL, and what happens on an edit.
Show the full solution
Cache the rendered page or the result with @cache.cached(timeout=300) (or a key like top10). On every admin edit, call cache.delete("top10") so the next visitor rebuilds it. A TTL alone would show stale data for up to 5 minutes; deleting on edit fixes that.
Exam radar
Counts are about (keyword-matched, so rough). Of roughly 692 Quiz 1 questions about 27 touch this week; of 624 Quiz 2 about 55; of 1600 End Term about 119. A sample of about 62 distinct questions shows these repeating:
- Predict the output of a
loggingprogram (the most reliable one: about 6 such questions across quizzes and End Terms, withINFO,WARNING,logging.exception, andargvvalues). - Memory and bandwidth arithmetic: DRAM bandwidth (64-bit, DDR, dual channel: 12.8, 25.6, 16 GB/s variants), disk RPM latency (6000 RPM and 5000 RPM), SSD vs HDD throughput, data in a network graph, mobile data in GB.
- Latency and requests per second (speed of light in cable, 1 KB responses, 20 ms round trip gives 50 per second).
- Indexes: purpose of an index; the two false index statements.
- Caching in REST APIs (find the false statements) and scale up vs scale out, ACID vs BASE, redundancy and replication.
- Memory hierarchy match-the-column (latency, density, throughput).
- The debug line vanishes when the level is higher; read the level first, then each call.
- Do not forget
DDRdoubles transfers anddual channeldoubles again. - Round trip is twice the one-way delay; requests/second = 1000 / (RTT in ms) for one at a time.
- "Index" is not encryption, replication or metadata; it is speed.
- Caching is for reads; false options say it raises load or latency.
Quick quiz
basicConfig(level=logging.INFO, format='%(levelname)s - %(message)s'). The code calls logging.info("start") then, for python log.py 25 5, logging.debug("factor"). The terminal shows...
INFO - startandDEBUG - factorINFO - startonlyDEBUG - factoronly- nothing
Level is WARNING. The program calls logging.warning("a"), logging.info("b"), logging.error("c"). Which lines print?
- a, b, c
- only c
- a and c
- b and c
DRAM, 64-bit bus, 800 MHz, DDR, dual channel. Maximum bandwidth?
- 12.8 GB/s
- 25.6 GB/s
- 51.2 GB/s
- 102.4 GB/s
Select all FALSE statements about indexes. (select all that apply)
- An index can be created on only one column.
- Indexes cannot be created on columns with duplicate values.
- An index speeds up data retrieval.
- Indexes slow down inserts and updates.
A browser sends If-None-Match: "v1" and the resource is unchanged. The server replies...
- 200 with the full body
- 304 with no body
- 404
- 201
A disk spins at 6000 RPM. Worst-case wait before data transfer starts?
- 5 ms
- 10 ms
- 20 ms
- 60 ms
Viva: say it out loud
Why use logging instead of print?
Logging has levels, timestamps and handlers, so I can send errors to a file, silence debug noise in production and still keep a history. With print I would edit the code to change any of that. In my project I log logins and failures with app.logger.
What is an index and what does it cost?
It is a separate sorted structure (a B-tree) that lets the database find rows by a column without scanning the table, so lookups take about logarithmic time. The cost is extra disk space and slower writes, because every insert, update or delete must also update the index. So I index only the columns I filter, join or sort on.
How would you check that a query uses your index?
I run EXPLAIN QUERY PLAN before the query. SCAN means a full table scan; SEARCH ... USING INDEX means the index is used.
Explain caching and the main risk.
A cache stores results so repeat requests skip the work: lower latency and less server load. The risk is stale data, so I set a TTL and invalidate the key whenever the underlying data changes.
What is the difference between latency and throughput?
Latency is the delay for one request to complete, in milliseconds. Throughput is how many requests or bytes complete per second. A system can have high throughput and still high latency, like a long pipe.
Why do you want stateless servers when you scale out?
If no user state is kept in a server's own memory, any server behind the load balancer can handle any request, so I can add or remove servers freely. State goes into the database or a shared store.
What is the N+1 problem and how do you fix it?
Fetching a list with one query and then one more query per item to load related rows. I fix it with eager loading (selectinload or joinedload) or a JOIN, so the number of queries stays small.
Why paginate an API?
Returning every row wastes memory, bandwidth and time. I return a page at a time with page and per_page, with a stable ORDER BY.
Recap
- Logging: levels 10/20/30/40/50, default WARNING; a message prints only if its level is at or above the configured one;
logging.exceptionadds a traceback. - Debugging: debug mode (reload plus error page, never in production), read the traceback from the bottom,
breakpoint()andpdb,flask shell. - Indexes speed up reads (B-tree, about logarithmic) and slow writes; check with
EXPLAIN QUERY PLAN; multi-column and duplicates are fine. - FTS5: virtual table,
MATCH, whole-word matching, triggers keep it in sync. - Caching: key, TTL, invalidation; client side with
Cache-ControlandETag(304); server side with Flask-Caching; only GET is cacheable by default. - Numbers: bandwidth = bytes x transfers x channels; latency = distance / speed; RPM latency = 60000 / RPM ms; data = rate x time.
- Scale out with load balancer and stateless servers; push slow work to a queue; avoid N+1; paginate.
- Levels: DEBUG 10, INFO 20, WARNING 30 (default), ERROR 40, CRITICAL 50
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')breakpoint(); pdb:n,s,c,p x,l,q;flask --app app shellCREATE INDEX idx ON t(col);EXPLAIN QUERY PLAN SELECT ...;SCAN vs SEARCH USING INDEXCREATE VIRTUAL TABLE t USING fts5(a, b);WHERE t MATCH 'word OR pre*'@cache.cached(timeout=60),@cache.memoize(),cache.delete(key),cache.clear()Cache-Control: public, max-age=60,ETagandIf-None-Matchgive 304- DDR bandwidth = (bus bytes) x (2 x clock) x channels; worst disk latency = 60000 / RPM ms
- Round trip = 2 x distance / speed; requests per second = 1000 / RTT ms
- Data = rate (Mbit/s) x seconds / 8 (MB); 1.5 million x 4 KB = 6 GB
- MAD2 week 9 - Asynchronous jobs: Redis and Celery: MAD1's backend topics (logging, queues of work) grow into Redis and Celery.
- MAD2 week 11 - Performance: measuring and caching: Caching and indexing from MAD1 are measured and tuned in MAD2's performance week.
Practise with real exam questions
These come straight from past papers (with the real figures and the official answer key). Open 60 real questions on this week All Quiz 2 papers