The Story That Explains Databases 📚Databases समझाने वाली कहानी 📚一个讲清数据库的故事 📚
Now imagine the same data in a library with a card catalogue. Every book has a shelf. Every card is sorted by author, title and genre. Ask "sci-fi bought in October" and the librarian hands you a stack in seconds. Nothing moved — just organised questions against organised data.
That is a database. Data lives in named tables. Each row is a record. You ask questions in SQL. Python has two main tools for this:
sqlite3 (built in, zero setup) and SQLAlchemy (the popular toolkit that works with many databases). This tutorial covers both.अब वही data एक library में सोचिए जिसमें card catalogue है। हर किताब की एक shelf है। हर card लेखक, शीर्षक और genre से क्रमित है। पूछिए "अक्टूबर में ख़रीदी गई sci-fi" और librarian सेकंडों में ढेर थमा देता है। कुछ नहीं हिला — बस व्यवस्थित data पर व्यवस्थित सवाल।
यही database है। Data नाम वाली tables में रहता है। हर row एक record है। आप SQL में सवाल पूछते हैं। Python में इसके दो मुख्य औज़ार हैं:
sqlite3 (built-in, कोई setup नहीं) और SQLAlchemy (लोकप्रिय toolkit जो कई databases के साथ चलता है)। यह tutorial दोनों सिखाता है।现在想象同样的数据放在一个有卡片目录的 图书馆 里。每本书都有书架,每张卡片按作者、书名和类别排好。问一句"十月卖出的科幻书",图书管理员几秒钟就递给你一叠。什么都没搬动 — 只是对有序的数据提出 有序的问题。
这就是 数据库。数据存放在有名字的表中,每一行是一条记录。你用 SQL 提问。Python 为此有两个主要工具:
sqlite3(内置,零配置)和 SQLAlchemy(流行的工具包,支持多种数据库)。本教程两者都讲。A database is organised storage you can query. Python ships with sqlite3 — a full SQL engine stored in a single file. For bigger work — PostgreSQL, MySQL, an ORM, migrations — most Python developers reach for SQLAlchemy. Together they cover most of what everyday Python programs need.
Database व्यवस्थित storage है जिससे सवाल पूछे जा सकते हैं। Python के साथ sqlite3 आता है — एक file में रखा पूरा SQL engine। बड़े काम के लिए — PostgreSQL, MySQL, ORM, migrations — ज़्यादातर Python developers SQLAlchemy लेते हैं। दोनों मिलकर रोज़ के Python programs की ज़्यादातर ज़रूरतें पूरी करते हैं।
数据库就是 可以查询的有序存储。Python 自带 sqlite3 — 一个存放在单个文件里的完整 SQL 引擎。更大的工作 — PostgreSQL、MySQL、ORM、数据库迁移 — 大多数 Python 开发者会选择 SQLAlchemy。二者合起来能满足日常 Python 程序的大部分需求。
You do not have to master SQL before doing useful work. Four verbs — SELECT, INSERT, UPDATE, DELETE — plus filters (WHERE) and joins take you a very long way.
काम की चीज़ें करने से पहले SQL में माहिर होना ज़रूरी नहीं। चार क्रियाएँ — SELECT, INSERT, UPDATE, DELETE — साथ में filters (WHERE) और joins आपको बहुत दूर ले जाते हैं।
不必先精通 SQL 才能做有用的事。四个动词 — SELECT、INSERT、UPDATE、DELETE — 加上过滤(WHERE)和连接(JOIN),就能走得很远。
Look for the interactive boxes: the SQL Injection Lab (Section 04), the Query Playground (05), the Transaction Lab (07), the JOIN Lab (08), the N+1 Query Counter (13) and the Bug Doctor (15). Every output on this page is from a real run on Python 3.13.16 with SQLite 3.45.1 and SQLAlchemy 2.1.5.
Interactive डिब्बे खोजें: SQL Injection Lab (Section 04), Query Playground (05), Transaction Lab (07), JOIN Lab (08), N+1 Query Counter (13) और Bug Doctor (15)। इस page का हर output Python 3.13.16, SQLite 3.45.1 और SQLAlchemy 2.1.5 पर असली run से है।
请留意这些互动框:SQL 注入实验室(第 04 节)、查询游乐场(05)、事务实验室(07)、JOIN 实验室(08)、N+1 查询计数器(13)和 Bug 医生(15)。本页每个输出都来自 Python 3.13.16、SQLite 3.45.1 和 SQLAlchemy 2.1.5 上的真实运行。
Visual Diagram — How Your Program Reaches the Database 🔌चित्र — आपका Program Database तक कैसे पहुँचता है 🔌图解 — 程序如何到达数据库 🔌
SQL goes in; rows come back.
SQL अंदर जाता है; rows वापस आती हैं।
SQL 发出去,行返回来。
The Three Building Blocks of Any Database 🧱हर Database की तीन ईंटें 🧱任何数据库的三块基石 🧱
CREATE TABLE. It names each column, its type (INTEGER, TEXT, REAL) and its rules (PRIMARY KEY, NOT NULL).CREATE TABLE से एक बार बताया गया नक़्शा। यह हर column का नाम, उसका type (INTEGER, TEXT, REAL) और नियम (PRIMARY KEY, NOT NULL) बताता है।CREATE TABLE 一次性声明的蓝图。它规定每一列的名字、类型(INTEGER、TEXT、REAL)和约束(PRIMARY KEY、NOT NULL)。INSERT adds one row. Each row is a record — a customer, a book, an order — matching the columns of its table.INSERT एक row जोड़ता है। हर row एक record है — एक ग्राहक, किताब, order — जो अपनी table के columns से मेल खाता है।INSERT 添加一行。每行是一条记录 — 一位顾客、一本书、一个订单 — 与所在表的列相对应。SELECT … FROM … WHERE … — filter rows, join tables, add things up. Every answer you get from a database starts as a query.SELECT … FROM … WHERE … — rows छाँटिए, tables जोड़िए, जोड़-घटाव कीजिए। Database से मिलने वाला हर जवाब एक query से शुरू होता है।SELECT … FROM … WHERE … — 筛选行、连接表、做汇总。你从数据库得到的每个答案都始于一条查询。Your First Database — Nothing to Installआपका पहला Database — कुछ install नहीं करना你的第一个数据库 — 无需安装
import sqlite3
# Creates the file if it doesn't exist; opens it if it does
conn = sqlite3.connect("bookstore.db")
cur = conn.cursor()
# Schema - declared ONCE. IF NOT EXISTS makes it safe to re-run.
cur.execute("""
CREATE TABLE IF NOT EXISTS books (
id INTEGER PRIMARY KEY,
title TEXT NOT NULL,
author TEXT NOT NULL,
price REAL NOT NULL,
stock INTEGER DEFAULT 0
)
""")
conn.commit() # save the schema
conn.close() # always close when done
print("database ready")
The original used INTEGER PRIMARY KEY AUTOINCREMENT. In SQLite, INTEGER PRIMARY KEY already gives each new row the next id; the SQLite docs advise against AUTOINCREMENT unless you must never reuse an id, because it adds extra work. The original output block also contained a comment ("a file … now exists") that Python never prints — above, a real ls shows the file.
मूल में INTEGER PRIMARY KEY AUTOINCREMENT था। SQLite में INTEGER PRIMARY KEY पहले से हर नई row को अगला id देता है; SQLite docs AUTOINCREMENT से बचने की सलाह देते हैं जब तक id को कभी दोबारा न लेने की ज़रूरत न हो, क्योंकि यह अतिरिक्त काम जोड़ता है। मूल output block में एक टिप्पणी ("a file … now exists") भी थी जिसे Python कभी print नहीं करता — ऊपर असली ls file दिखाता है।
原文使用了 INTEGER PRIMARY KEY AUTOINCREMENT。在 SQLite 中,INTEGER PRIMARY KEY 本身就会给每一新行分配下一个 id;SQLite 文档建议除非绝对不能复用 id,否则不要用 AUTOINCREMENT,因为它会带来额外开销。原输出框里还有一句 Python 根本不会打印的注释("a file … now exists")— 上面用真实的 ls 展示了这个文件。
SQLite is one file. No server to run, no user accounts. Copy the .db file and you have copied the whole database. It is built into every Android and iPhone and into most web browsers, which makes it one of the most widely used databases in the world. Perfect for learning, prototypes, single-user tools and tests.
SQLite एक file है। चलाने को कोई server नहीं, कोई user accounts नहीं। .db file copy कीजिए और पूरा database copy हो गया। यह हर Android और iPhone और ज़्यादातर web browsers में बना हुआ है, इसलिए यह दुनिया के सबसे ज़्यादा इस्तेमाल होने वाले databases में से है। सीखने, prototypes, एक-user वाले tools और tests के लिए बढ़िया।
SQLite 就是 一个文件。不用运行服务器,不用用户账号。复制 .db 文件,就复制了整个数据库。它内置于每部 Android 手机和 iPhone 以及大多数浏览器中,是世界上使用最广泛的数据库之一。非常适合学习、原型、单用户工具和测试。
SQLite ships inside every iPhone and Android phone and most browsers — but not every app uses it, and not every browser exposes it. The callout above keeps the true part.
SQLite हर iPhone और Android फ़ोन और ज़्यादातर browsers में आता है — पर हर app इसे इस्तेमाल नहीं करता, और हर browser इसे उपलब्ध नहीं कराता। ऊपर का callout सच्चा हिस्सा रखता है।
SQLite 内置于每部 iPhone 和 Android 手机以及大多数浏览器中 — 但并非每个应用都使用它,也不是每个浏览器都对外开放它。上面的提示保留了真实的部分。
INSERT — Adding Rows ➕INSERT — Rows जोड़ना ➕INSERT — 添加行 ➕
Never put values into SQL with f-strings. That is the classic way SQL injection gets in. Use parameterized queries with ? placeholders: the values travel separately from the SQL text, so they can never be read as SQL.
f-strings से values SQL में कभी मत डालिए। SQL injection घुसने का यही पुराना रास्ता है। ? placeholders वाली parameterized queries लीजिए: values SQL text से अलग जाती हैं, इसलिए उन्हें कभी SQL की तरह पढ़ा नहीं जा सकता।
永远不要用 f-string 把值塞进 SQL。这正是 SQL 注入的经典入口。请使用带 ? 占位符的 参数化查询:值与 SQL 文本 分开 传送,因此永远不会被当作 SQL 来解读。
import sqlite3
with sqlite3.connect("bookstore.db") as conn:
cur = conn.cursor()
# Single insert - parameterized with ? placeholders
cur.execute(
"INSERT INTO books (title, author, price, stock) VALUES (?, ?, ?, ?)",
("Dune", "Frank Herbert", 15.99, 12),
)
print("new id:", cur.lastrowid)
# Bulk insert - one call for many rows
rows = [
("1984", "George Orwell", 9.99, 30),
("Brave New World", "Aldous Huxley", 11.50, 18),
("Foundation", "Isaac Asimov", 13.75, 22),
("Neuromancer", "William Gibson", 14.20, 8),
]
cur.executemany(
"INSERT INTO books (title, author, price, stock) VALUES (?, ?, ?, ?)",
rows,
)
print("executemany rowcount:", cur.rowcount)
conn.commit()
total = conn.execute("SELECT COUNT(*) FROM books").fetchone()[0]
print(f"books in the table: {total}")
The original printed f"inserted {cur.rowcount + 1} books". After executemany(), rowcount is the rows of that call (4); the "+1" silently guessed the earlier single insert. Add one more single insert and the message is wrong. Count what you need directly, as above.
मूल ने f"inserted {cur.rowcount + 1} books" print किया। executemany() के बाद rowcount उसी call की rows (4) है; "+1" ने चुपचाप पहले वाले एक insert का अंदाज़ा लगाया। एक और insert जोड़िए और message ग़लत हो जाएगा। जो चाहिए उसे सीधे गिनिए, जैसा ऊपर।
原文打印 f"inserted {cur.rowcount + 1} books"。executemany() 之后,rowcount 只是 这一次 调用影响的行数(4);"+1"是在悄悄猜测前面那次单条插入。再加一条插入,消息就错了。请像上面那样直接统计你需要的数字。
cur.execute(f"SELECT … WHERE name = '{name}'") is a SQL injection hole. A user who types x' OR '1'='1 as a name gets every row. Always use ? placeholders.
cur.execute(f"SELECT … WHERE name = '{name}'") एक SQL injection छेद है। नाम में x' OR '1'='1 लिखने वाले user को हर row मिल जाती है। हमेशा ? placeholders लीजिए।
cur.execute(f"SELECT … WHERE name = '{name}'") 是一个 SQL 注入漏洞。用户把名字填成 x' OR '1'='1 就能拿到所有行。始终使用 ? 占位符。
import sqlite3
conn = sqlite3.connect(":memory:") # a throw-away database in RAM
conn.execute("CREATE TABLE users (name TEXT, secret TEXT)")
conn.executemany("INSERT INTO users VALUES (?, ?)",
[("Ada", "ada-pin-1111"), ("Grace", "grace-pin-2222")])
name = "x' OR '1'='1" # typed into a login form
# WRONG - the input becomes part of the SQL text
sql = f"SELECT name, secret FROM users WHERE name = '{name}'"
print("SQL sent :", sql)
print("f-string :", conn.execute(sql).fetchall())
# RIGHT - the input is sent separately, as a value
print("? param :", conn.execute("SELECT name, secret FROM users WHERE name = ?",
(name,)).fetchall())
🛡️ Simulator: SQL Injection Lab🛡️ Simulator: SQL Injection Lab🛡️ 模拟器:SQL 注入实验室
Pick what a user types into a login box. You see the SQL an f-string builds (the user's text is in red) and what really came back — next to the safe ? version. Every result is a real run.
चुनिए user login box में क्या लिखता है। आप f-string से बना SQL देखते हैं (user का text लाल में) और असल में क्या लौटा — साथ में सुरक्षित ? version। हर result असली run है।
选择用户在登录框里输入的内容。你会看到 f-string 拼出的 SQL(用户的文字标红)以及真实返回的结果 — 旁边是安全的 ? 版本。每个结果都来自真实运行。
SELECT — Asking Questions ❓SELECT — सवाल पूछना ❓SELECT — 提出问题 ❓
import sqlite3
with sqlite3.connect("bookstore.db") as conn:
# Row factory: rows behave like dicts as well as tuples
conn.row_factory = sqlite3.Row
cur = conn.cursor()
# All books
cur.execute("SELECT id, title, author, price FROM books")
for row in cur.fetchall():
print(f"#{row['id']} {row['title']:20s} £{row['price']:.2f}")
# With a WHERE filter - parameterized (safe against injection)
cur.execute("SELECT * FROM books WHERE price < ? AND stock > ?", (14, 0))
affordable = cur.fetchall()
print("affordable:", [r["title"] for r in affordable])
# Aggregate query
cur.execute("SELECT COUNT(*), AVG(price), MAX(stock) FROM books")
count, avg_price, max_stock = cur.fetchone()
print(f"total={count} avg=£{avg_price:.2f} most-stocked={max_stock}")
fetchone() returns the next row (or None). fetchall() loads every remaining row into memory — fine for small results, risky for huge ones. For big results, loop over the cursor: for row in cur: reads rows bit by bit.
fetchone() अगली row लौटाता है (या None)। fetchall() बची हर row memory में ले आता है — छोटे results के लिए ठीक, बहुत बड़े के लिए जोखिम। बड़े results के लिए cursor पर loop चलाइए: for row in cur: rows थोड़ा-थोड़ा पढ़ता है।
fetchone() 返回下一行(或 None)。fetchall() 把剩下的所有行都读进内存 — 结果少时没问题,结果巨大时有风险。结果很多时,直接遍历游标:for row in cur: 会一点点地读取行。
🔎 Simulator: Query Playground🔎 Simulator: Query Playground🔎 模拟器:查询游乐场
Click a query. Rows that match the WHERE stay bright in the table on the left; the result comes in on the right. All results came from running the query on the real bookstore.db built above.
एक query पर click कीजिए। WHERE से मेल खाने वाली rows बाईं table में चमकती रहती हैं; result दाईं ओर आता है। हर result ऊपर बने असली bookstore.db पर query चलाकर आया है।
点击一条查询。符合 WHERE 条件的行在左侧表中保持高亮;结果出现在右侧。所有结果都来自在上面建好的真实 bookstore.db 上运行查询。
UPDATE and DELETE — Changing Data ✏️UPDATE और DELETE — Data बदलना ✏️UPDATE 与 DELETE — 修改数据 ✏️
import sqlite3
with sqlite3.connect("bookstore.db") as conn:
cur = conn.cursor()
# Reduce stock when a book is sold - the WHERE clause is essential!
cur.execute("UPDATE books SET stock = stock - 1 WHERE id = ?", (1,))
print(f"rows updated: {cur.rowcount}")
# Price rise on cheap books
cur.execute("UPDATE books SET price = ROUND(price * 1.1, 2) WHERE price < ?", (12,))
print(f"prices raised: {cur.rowcount}")
# Delete out-of-stock, cheap books
cur.execute("DELETE FROM books WHERE stock = 0 AND price < ?", (10,))
print(f"rows deleted: {cur.rowcount}")
conn.commit()
for row in conn.execute("SELECT id, title, price, stock FROM books"):
print(row)
cur.rowcount tells you how many rows each statement touched. Here the DELETE matched nothing: no book had stock = 0. Checking rowcount is a cheap way to catch a WHERE that matched less (or more) than you expected.
cur.rowcount बताता है कि हर statement ने कितनी rows छुईं। यहाँ DELETE किसी से मेल नहीं खाया: किसी किताब का stock = 0 नहीं था। rowcount जाँचना उस WHERE को पकड़ने का सस्ता तरीका है जो उम्मीद से कम (या ज़्यादा) rows से मेल खाए।
cur.rowcount 告诉你每条语句影响了多少行。这里的 DELETE 一行也没匹配到:没有哪本书 stock = 0。检查 rowcount 是发现 WHERE 匹配得比预期少(或多)的廉价办法。
The original did price = price * 1.1. With floating point, 9.99 × 1.1 is stored as 10.989000000000001 — prices with long tails. We used ROUND(price * 1.1, 2). For real money, store whole cents as INTEGER.
मूल में price = price * 1.1 था। Floating point में 9.99 × 1.1, 10.989000000000001 बनकर रखा जाता है — लंबी पूँछ वाले दाम। हमने ROUND(price * 1.1, 2) लिया। असली पैसों के लिए पूरे पैसे (cents) INTEGER में रखिए।
原文写的是 price = price * 1.1。由于浮点数,9.99 × 1.1 会被存成 10.989000000000001 — 价格带着长尾巴。我们改用 ROUND(price * 1.1, 2)。真正涉及金钱时,应把"分"作为 INTEGER 存储。
DELETE FROM books with no WHERE deletes every row. UPDATE books SET price = 0 makes every book free. Run a SELECT with the same filter first to see exactly what will change. (The Bug Doctor in Section 15 shows this with a real run.)
WHERE के बिना DELETE FROM books हर row मिटा देता है। UPDATE books SET price = 0 हर किताब मुफ़्त कर देता है। पहले उसी filter से SELECT चलाइए ताकि ठीक-ठीक दिखे क्या बदलेगा। (Section 15 का Bug Doctor इसे असली run से दिखाता है।)
不带 WHERE 的 DELETE FROM books 会删除 所有行。UPDATE books SET price = 0 会让每本书免费。先用相同的条件跑一次 SELECT,看清到底会改动哪些行。(第 15 节的 Bug 医生用真实运行展示了这一点。)
Does "with sqlite3.connect()" close the connection?क्या "with sqlite3.connect()" connection बंद करता है?"with sqlite3.connect()" 会关闭连接吗?
import sqlite3
from contextlib import closing
with sqlite3.connect("bookstore.db") as conn:
conn.execute("UPDATE books SET stock = stock + 1 WHERE id = 1")
# the with-block COMMITTED... but did it close the connection?
print("still open:", conn.execute("SELECT COUNT(*) FROM books").fetchone())
# closing() really closes it; the inner "with conn" handles the transaction
with closing(sqlite3.connect("bookstore.db")) as conn:
with conn:
conn.execute("UPDATE books SET stock = stock + 1 WHERE id = 1")
try:
conn.execute("SELECT 1")
except sqlite3.ProgrammingError as e:
print("after closing():", e)
The original pitfalls table and Rule 2 say with sqlite3.connect(...) closes the connection. It does not: for a sqlite3 connection, with only commits (or rolls back on an error). The run above shows the connection still working after the block. To close it as well, wrap it in contextlib.closing(), or call conn.close().
मूल pitfalls table और नियम 2 कहते हैं कि with sqlite3.connect(...) connection बंद करता है। ऐसा नहीं है: sqlite3 connection के लिए with सिर्फ़ commit करता है (या error पर rollback)। ऊपर का run दिखाता है कि block के बाद भी connection चल रहा है। उसे बंद भी करना हो तो contextlib.closing() में लपेटिए, या conn.close() बुलाइए।
原陷阱表和规则 2 说 with sqlite3.connect(...) 会关闭连接。其实不会:对 sqlite3 连接来说,with 只负责 提交(出错时回滚)。上面的运行显示语句块结束后连接仍可使用。如果还要关闭,就用 contextlib.closing() 包起来,或调用 conn.close()。
Transactions — All-or-Nothing Changes 🔒Transactions — सब या कुछ नहीं 🔒事务 — 要么全成,要么全不成 🔒
A transaction is a group of changes treated as one unit. Either every change lands, or none does. Classic example: moving money from one account to another must change both sides — or neither.
Transaction बदलावों का ऐसा समूह है जिसे एक इकाई माना जाता है। या तो हर बदलाव होता है, या कोई नहीं। पुराना उदाहरण: एक खाते से दूसरे में पैसे भेजने पर दोनों तरफ़ बदलाव होना चाहिए — या किसी भी तरफ़ नहीं।
事务 是被当作一个整体的一组修改。要么全部生效,要么一个都不生效。经典例子:从一个账户转钱到另一个账户,两边都必须改动 — 或者都不改。
Both paths end with a consistent database.
दोनों रास्ते संगत database पर ख़त्म होते हैं।
两条路都以一致的数据库结束。
import sqlite3
def setup(path="bank.db"):
with sqlite3.connect(path) as conn:
conn.executescript("""
DROP TABLE IF EXISTS accounts;
CREATE TABLE accounts (
id INTEGER PRIMARY KEY,
owner TEXT NOT NULL,
balance REAL NOT NULL CHECK (balance >= 0) -- no overdrafts
);
INSERT INTO accounts VALUES (1, 'Ada', 500), (2, 'Grace', 100);
""")
def balances():
with sqlite3.connect("bank.db") as conn:
return dict(conn.execute("SELECT owner, balance FROM accounts"))
def transfer(from_id: int, to_id: int, amount: float) -> None:
conn = sqlite3.connect("bank.db")
try:
cur = conn.cursor()
cur.execute("UPDATE accounts SET balance = balance + ? WHERE id = ?",
(amount, to_id))
if cur.rowcount != 1:
raise ValueError(f"no account {to_id}")
cur.execute("UPDATE accounts SET balance = balance - ? WHERE id = ?",
(amount, from_id))
conn.commit() # both updates land together
except Exception:
conn.rollback() # any error -> undo BOTH updates
raise
finally:
conn.close()
setup()
print("start :", balances())
transfer(1, 2, 100)
print("after 100 :", balances())
for args in [(2, 1, 999), (1, 7, 50)]:
try:
transfer(*args)
except Exception as e:
print(f"transfer{args} failed: {type(e).__name__}: {e}")
print(" :", balances())
The original used a bank.db with an accounts table that was never created, so it failed with "no such table". We added the setup. More important: an UPDATE whose WHERE matches nothing is not an error — it just changes 0 rows. Sending money to account 7 would take it from Ada and give it to no one, and the transaction would commit happily. Check rowcount and raise, as above. The CHECK (balance >= 0) rule makes the database itself refuse overdrafts.
मूल ने bank.db की accounts table इस्तेमाल की जो कभी बनाई ही नहीं गई, इसलिए वह "no such table" से fail होता। हमने setup जोड़ा। ज़्यादा ज़रूरी: जिस UPDATE का WHERE किसी से मेल न खाए, वह error नहीं है — बस 0 rows बदलता है। Account 7 को पैसे भेजने पर Ada से पैसे कटते और किसी को नहीं मिलते, और transaction ख़ुशी से commit हो जाता। ऊपर की तरह rowcount जाँचिए और raise कीजिए। CHECK (balance >= 0) नियम database से ही overdraft मना करवाता है।
原文用了 bank.db 里一个从未创建的 accounts 表,所以会因"no such table"而失败。我们补上了建表步骤。更重要的是:WHERE 一行都没匹配到的 UPDATE 不算错误 — 只是改动了 0 行。转账给 7 号账户,会从 Ada 那里扣钱却谁也没收到,而事务照样愉快地提交。请像上面那样检查 rowcount 并抛出异常。CHECK (balance >= 0) 规则让数据库自己拒绝透支。
🏦 Simulator: Transaction Lab🏦 Simulator: Transaction Lab🏦 模拟器:事务实验室
Six transfers, each really run against a fresh bank.db. Watch the statements, then compare the balances. The total money in the bank must never change.
छह transfers, हर एक नए bank.db पर सच में चलाया गया। Statements देखिए, फिर balances मिलाइए। बैंक में कुल पैसा कभी नहीं बदलना चाहिए।
六次转账,每次都在全新的 bank.db 上真实运行。观察执行的语句,再比较余额。银行里的总金额绝不应该改变。
Multiple Tables — Foreign Keys and JOINs 🔗कई Tables — Foreign Keys और JOINs 🔗多张表 — 外键与 JOIN 🔗
Real data has relationships. A customer places orders; an order names a book. We keep them in separate tables and connect them with a foreign key: a column that holds the id of a row in another table.
असली data में रिश्ते होते हैं। ग्राहक orders देता है; order में किताब का नाम होता है। हम उन्हें अलग tables में रखते हैं और foreign key से जोड़ते हैं: ऐसा column जो दूसरी table की किसी row का id रखता है।
真实数据之间有关系。顾客下订单,订单写着书名。我们把它们放在不同的表里,用 外键 连接:一个保存另一张表中某行 id 的列。
import sqlite3
with sqlite3.connect("shop.db") as conn:
conn.execute("PRAGMA foreign_keys = ON") # enforce FKs (SQLite has them OFF by default)
conn.executescript("""
CREATE TABLE IF NOT EXISTS customers (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
);
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL,
book_title TEXT NOT NULL,
qty INTEGER NOT NULL,
FOREIGN KEY (customer_id) REFERENCES customers(id)
);
""")
# Insert customers
conn.execute("INSERT OR IGNORE INTO customers (id, name, email) VALUES (?, ?, ?)",
(1, "Ada", "ada@example.com"))
conn.execute("INSERT OR IGNORE INTO customers (id, name, email) VALUES (?, ?, ?)",
(2, "Grace", "grace@example.com"))
# Insert orders that reference customers
conn.executemany("INSERT INTO orders (customer_id, book_title, qty) VALUES (?, ?, ?)",
[(1, "Dune", 2), (1, "1984", 1), (2, "Foundation", 3)])
# JOIN - combine rows from both tables
rows = conn.execute("""
SELECT c.name, o.book_title, o.qty
FROM orders o
JOIN customers c ON c.id = o.customer_id
ORDER BY c.name, o.id
""").fetchall()
for name, title, qty in rows:
print(f"{name:8s} {title:12s} x{qty}")
Run it a second time…इसे दूसरी बार चलाइए…再运行一次…
import sqlite3
with sqlite3.connect("shop.db") as conn:
conn.execute("PRAGMA foreign_keys = ON") # enforce FKs (SQLite has them OFF by default)
conn.executescript("""
CREATE TABLE IF NOT EXISTS customers (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT UNIQUE NOT NULL
);
CREATE TABLE IF NOT EXISTS orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER NOT NULL,
book_title TEXT NOT NULL,
qty INTEGER NOT NULL,
FOREIGN KEY (customer_id) REFERENCES customers(id)
);
""")
# Insert customers
conn.execute("INSERT OR IGNORE INTO customers (id, name, email) VALUES (?, ?, ?)",
(1, "Ada", "ada@example.com"))
conn.execute("INSERT OR IGNORE INTO customers (id, name, email) VALUES (?, ?, ?)",
(2, "Grace", "grace@example.com"))
# Insert orders that reference customers
conn.executemany("INSERT INTO orders (customer_id, book_title, qty) VALUES (?, ?, ?)",
[(1, "Dune", 2), (1, "1984", 1), (2, "Foundation", 3)])
# JOIN - combine rows from both tables
rows = conn.execute("""
SELECT c.name, o.book_title, o.qty
FROM orders o
JOIN customers c ON c.id = o.customer_id
ORDER BY c.name, o.id
""").fetchall()
for name, title, qty in rows:
print(f"{name:8s} {title:12s} x{qty}")
The tables use IF NOT EXISTS and the customers use INSERT OR IGNORE, so they are safe to re-run — but the orders are a plain INSERT, so every run adds three more. Seed data should be inserted once (check first, or give the rows fixed ids with INSERT OR IGNORE). We also added o.id to ORDER BY: without it, the order of Ada's two rows is not guaranteed.
Tables IF NOT EXISTS और customers INSERT OR IGNORE लेते हैं, इसलिए दोबारा चलाना सुरक्षित है — पर orders सादा INSERT हैं, इसलिए हर run तीन और जोड़ता है। Seed data एक बार डालना चाहिए (पहले जाँचिए, या rows को INSERT OR IGNORE के साथ तय ids दीजिए)। हमने ORDER BY में o.id भी जोड़ा: उसके बिना Ada की दो rows का क्रम पक्का नहीं है।
表使用了 IF NOT EXISTS,顾客使用了 INSERT OR IGNORE,所以可以安全重跑 — 但订单是普通的 INSERT,每运行一次就多三条。初始数据应只插入一次(先检查,或用 INSERT OR IGNORE 给行固定 id)。我们还在 ORDER BY 中加了 o.id:没有它,Ada 那两行的顺序是不确定的。
Foreign keys are OFF by default in SQLiteSQLite में Foreign keys default रूप से OFF हैंSQLite 默认关闭外键约束
import sqlite3
for fk in ("OFF", "ON"):
conn = sqlite3.connect(":memory:")
conn.execute(f"PRAGMA foreign_keys = {fk}")
conn.executescript("""
CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT);
CREATE TABLE orders (id INTEGER PRIMARY KEY,
customer_id INTEGER REFERENCES customers(id));
INSERT INTO customers VALUES (1, 'Ada');
""")
try:
conn.execute("INSERT INTO orders (customer_id) VALUES (99)") # no customer 99!
print(f"foreign_keys={fk}: orphan order saved ->",
conn.execute("SELECT * FROM orders").fetchall())
except sqlite3.IntegrityError as e:
print(f"foreign_keys={fk}: IntegrityError: {e}")
conn.close()
With foreign_keys OFF, SQLite happily saved an order for customer 99, who does not exist. Turn it ON for every connection — it is a per-connection setting, not saved in the file.
foreign_keys OFF होने पर SQLite ने ग्राहक 99 का order ख़ुशी से save कर लिया, जो मौजूद ही नहीं। इसे हर connection के लिए ON कीजिए — यह हर connection की setting है, file में save नहीं होती।
foreign_keys 关闭时,SQLite 照样为并不存在的 99 号顾客保存了订单。要为 每一个 连接都打开它 — 这是连接级的设置,不会保存在文件里。
🔗 Simulator: JOIN Lab🔗 Simulator: JOIN Lab🔗 模拟器:JOIN 实验室
Three customers, three orders — and Linus has no orders. Pick a query and watch which rows get matched. All results are from real queries.
तीन ग्राहक, तीन orders — और Linus का कोई order नहीं। एक query चुनिए और देखिए कौन-सी rows मेल खाती हैं। सारे results असली queries से हैं।
三位顾客,三个订单 — Linus 没有订单。选一条查询,看看哪些行被匹配。所有结果都来自真实查询。
Enter SQLAlchemy — The Toolkit for Bigger Apps 🧰SQLAlchemy — बड़े Apps का Toolkit 🧰走进 SQLAlchemy — 大型应用的工具包 🧰
sqlite3 is perfect for a single script. Bigger applications often need PostgreSQL or MySQL support, connection pooling, schema migrations (with Alembic), an object mapper and a query builder. That is SQLAlchemy — first released in 2006 and today one of the most widely used database toolkits in Python. Flask and FastAPI projects commonly use it (Flask-SQLAlchemy, SQLModel); Django is the main exception, with its own ORM.
sqlite3 एक script के लिए बढ़िया है। बड़े applications को अक्सर PostgreSQL या MySQL support, connection pooling, schema migrations (Alembic से), object mapper और query builder चाहिए। यही SQLAlchemy है — 2006 में पहली बार आया और आज Python के सबसे ज़्यादा इस्तेमाल होने वाले database toolkits में से एक। Flask और FastAPI projects आम तौर पर इसे लेते हैं (Flask-SQLAlchemy, SQLModel); Django मुख्य अपवाद है, जिसका अपना ORM है।
sqlite3 非常适合单个脚本。更大的应用通常需要 PostgreSQL 或 MySQL 支持、连接池、数据库迁移(配合 Alembic)、对象映射器和查询构建器。这就是 SQLAlchemy — 2006 年首次发布,如今是 Python 中使用最广泛的数据库工具包之一。Flask 和 FastAPI 项目常用它(Flask-SQLAlchemy、SQLModel);Django 是主要例外,它有自己的 ORM。
The original said SQLAlchemy is "the backend of every major Python framework (FastAPI, Flask …)". FastAPI and Flask do not include a database layer at all; many of their projects choose SQLAlchemy. Django uses its own ORM. The comparison table below is also softened: switching databases is often small but not always "one string", and editors autocomplete column names — they do not make queries fully type-safe.
मूल में कहा गया कि SQLAlchemy "हर बड़े Python framework (FastAPI, Flask …) का backend" है। FastAPI और Flask में database layer है ही नहीं; उनके कई projects SQLAlchemy चुनते हैं। Django अपना ORM लेता है। नीचे की तुलना table भी नरम की गई है: database बदलना अक्सर छोटा काम है पर हमेशा "एक string" नहीं, और editors column नामों का autocomplete करते हैं — वे queries को पूरी तरह type-safe नहीं बनाते।
原文说 SQLAlchemy 是"所有主流 Python 框架(FastAPI、Flask …)的后端"。FastAPI 和 Flask 本身根本不带数据库层;只是它们的许多项目 选择 了 SQLAlchemy。Django 用的是自己的 ORM。下面的对比表也做了修正:切换数据库通常改动不大,但并不总是"只改一个字符串";编辑器能自动补全列名 — 但这并不能让查询做到完全类型安全。
| Aspectपहलू方面 | Behaviourव्यवहार表现 |
|---|---|
| QueryQuery查询 | Hand-written SQL stringsहाथ से लिखी SQL strings手写 SQL 字符串 |
| TyposTypos拼写错误 | Found when the SQL runsSQL चलने पर पता चलते हैंSQL 执行时才发现 |
| Switch databasesDatabase बदलना切换数据库 | Rewrite queries that use SQLite-only featuresSQLite-विशेष features वाली queries दोबारा लिखें重写使用了 SQLite 特有功能的查询 |
| RowsRows行 | Tuples / sqlite3.RowTuples / sqlite3.Row元组 / sqlite3.Row |
| Aspectपहलू方面 | Behaviourव्यवहार表现 |
|---|---|
| QueryQuery查询 | Python expressions (Core) or classes (ORM)Python expressions (Core) या classes (ORM)Python 表达式(Core)或类(ORM) |
| TyposTypos拼写错误 | A wrong column name fails in Python, before SQL is sent; editors autocompleteग़लत column नाम SQL भेजने से पहले Python में fail; editors autocomplete करते हैं列名写错会在发送 SQL 之前就在 Python 中报错;编辑器可自动补全 |
| Switch databasesDatabase बदलना切换数据库 | Change the URL — plus fix any database-specific SQLURL बदलिए — और database-विशेष SQL ठीक कीजिए改 URL — 再修正与特定数据库相关的 SQL |
| RowsRows行 | Row objects, or your own Python classesRow objects, या आपकी अपनी Python classesRow 对象,或你自己的 Python 类 |
SetupSetup安装
pip install sqlalchemy
SQLAlchemy Core — SQL, but Composable 🧩SQLAlchemy Core — SQL, पर जोड़ने लायक 🧩SQLAlchemy Core — 可组合的 SQL 🧩
The Core layer lets you build SQL as Python expressions. It is still SQL — you can even print it — but values are always sent as parameters, and a wrong column name fails at once.
Core layer आपको SQL को Python expressions की तरह बनाने देती है। यह अब भी SQL है — आप इसे print भी कर सकते हैं — पर values हमेशा parameters की तरह जाती हैं, और ग़लत column नाम तुरंत fail होता है।
Core 层让你用 Python 表达式构建 SQL。它仍然是 SQL — 甚至可以打印出来 — 但值总是作为参数发送,列名写错会立刻报错。
from sqlalchemy import create_engine, MetaData, Table, Column, Integer, String, Float, select
# The URL tells SQLAlchemy which database to use:
# sqlite:///bookstore_core.db <- SQLite file
# postgresql+psycopg://user:pw@host/db
# mysql+pymysql://user:pw@host/db
engine = create_engine("sqlite:///bookstore_core.db", echo=False)
metadata = MetaData()
books = Table(
"books", metadata,
Column("id", Integer, primary_key=True),
Column("title", String, nullable=False),
Column("author", String, nullable=False),
Column("price", Float, nullable=False),
Column("stock", Integer, default=0),
)
metadata.create_all(engine) # CREATE TABLE only for tables that don't exist yet
# Insert
with engine.begin() as conn: # begin() commits on exit
conn.execute(books.insert(), [
{"title": "Dune", "author": "Frank Herbert", "price": 15.99, "stock": 12},
{"title": "1984", "author": "George Orwell", "price": 9.99, "stock": 30},
{"title": "Foundation", "author": "Isaac Asimov", "price": 13.75, "stock": 22},
])
# SELECT - as a Python expression
with engine.connect() as conn:
stmt = (select(books.c.title, books.c.price)
.where(books.c.price < 14)
.order_by(books.c.price))
print(stmt) # the SQL SQLAlchemy builds
print()
for row in conn.execute(stmt):
print(row.title, row.price)
# A column-name typo fails in Python, before any SQL is sent
try:
select(books.c.titel)
except AttributeError as e:
print("\ntypo caught:", type(e).__name__, e)
Look at the printed SQL: books.price < :price_1. The number 14 was not pasted into the text — it travels as a parameter, just like ? in sqlite3. We also gave this example its own file, bookstore_core.db, so it does not add duplicate books to the bookstore.db from Sections 03–06.
Print हुआ SQL देखिए: books.price < :price_1। संख्या 14 text में चिपकाई नहीं गई — यह parameter की तरह जाती है, बिल्कुल sqlite3 के ? जैसे। हमने इस example को अपनी file bookstore_core.db भी दी, ताकि यह Sections 03–06 के bookstore.db में दोहरी किताबें न जोड़े।
看打印出来的 SQL:books.price < :price_1。数字 14 没有被粘贴进文本 — 它作为参数传送,就像 sqlite3 中的 ?。我们还给这个示例单独用了文件 bookstore_core.db,这样它就不会往第 03–06 节的 bookstore.db 里添加重复的书。
The same Core and ORM code runs on SQLite, PostgreSQL, MySQL and others — you change the create_engine("…") URL and install the driver. Differences remain (types, some functions, how strict each database is), so test on the database you will really use.
वही Core और ORM code SQLite, PostgreSQL, MySQL और दूसरों पर चलता है — आप create_engine("…") URL बदलते हैं और driver install करते हैं। कुछ अंतर रहते हैं (types, कुछ functions, हर database कितना सख़्त है), इसलिए उसी database पर test कीजिए जिसे सच में इस्तेमाल करेंगे।
同一套 Core 和 ORM 代码可以运行在 SQLite、PostgreSQL、MySQL 等数据库上 — 只需更改 create_engine("…") 的 URL 并安装驱动。差异依然存在(类型、部分函数、各数据库的严格程度),所以要在真正要用的数据库上测试。
SQLAlchemy ORM — Rows Become Python Objects 🧬SQLAlchemy ORM — Rows बनती हैं Python Objects 🧬SQLAlchemy ORM — 行变成 Python 对象 🧬
The ORM (Object-Relational Mapper) lets you treat rows as normal Python objects. Each class is a table. Each instance is a row. The Session tracks your changes and writes them to the database when you commit.
ORM (Object-Relational Mapper) आपको rows को सामान्य Python objects की तरह इस्तेमाल करने देता है। हर class एक table है। हर instance एक row है। Session आपके बदलाव याद रखता है और commit करने पर database में लिखता है।
ORM(对象关系映射器)让你把行当作普通的 Python 对象来使用。每个类是一张表,每个实例是一行。Session 会跟踪你的修改,并在提交时写入数据库。
Each attribute becomes a column; each instance becomes a row.
हर attribute एक column बनता है; हर instance एक row।
每个属性成为一列;每个实例成为一行。
from sqlalchemy import create_engine, String, Integer, Float, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, Session
# 1) Base class for all models
class Base(DeclarativeBase):
pass
# 2) A model - one class = one table
class Book(Base):
__tablename__ = "books"
id: Mapped[int] = mapped_column(Integer, primary_key=True)
title: Mapped[str] = mapped_column(String(200), nullable=False)
author: Mapped[str] = mapped_column(String(100), nullable=False)
price: Mapped[float] = mapped_column(Float, nullable=False)
stock: Mapped[int] = mapped_column(Integer, default=0)
def __repr__(self) -> str:
return f"Book({self.title!r}, £{self.price})"
# 3) Create the engine and tables
engine = create_engine("sqlite:///bookstore_orm.db")
Base.metadata.create_all(engine)
# 4) Insert - just create Python objects
with Session(engine) as session:
session.add_all([
Book(title="Dune", author="Frank Herbert", price=15.99, stock=12),
Book(title="1984", author="George Orwell", price=9.99, stock=30),
Book(title="Foundation", author="Isaac Asimov", price=13.75, stock=22),
])
session.commit()
# 5) Query - Python style
with Session(engine) as session:
stmt = select(Book).where(Book.price < 14).order_by(Book.price)
for book in session.scalars(stmt):
print(book) # uses __repr__
# Update - change the object, then commit
dune = session.scalar(select(Book).where(Book.title == "Dune"))
dune.stock -= 1 # plain Python attribute assignment
session.commit() # SQL UPDATE runs automatically
print("Dune stock now:", dune.stock)
The original used bookstore.db here too. If you ran Sections 03–06 and 10 first, this query would also return the earlier copies of 1984, Foundation and Brave New World, and the output would not match. A separate file (bookstore_orm.db) keeps the example honest.
मूल ने यहाँ भी bookstore.db लिया था। अगर आपने पहले Sections 03–06 और 10 चलाए हों, तो यह query 1984, Foundation और Brave New World की पहले वाली copies भी लौटाती, और output मेल नहीं खाता। अलग file (bookstore_orm.db) example को सच्चा रखती है।
原文这里也用了 bookstore.db。如果你先运行过第 03–06 节和第 10 节,这条查询还会返回之前的 1984、Foundation 和 Brave New World 副本,输出就对不上了。使用单独的文件(bookstore_orm.db)让示例保持真实。
Relationships — One-to-Many Made Easy 👨👩👧Relationships — One-to-Many आसान 👨👩👧关系 — 轻松实现一对多 👨👩👧
Two related tables — users and their posts — connect naturally in the ORM. You can move in both directions: user.posts gives a list; post.author gives the user.
दो जुड़ी tables — users और उनके posts — ORM में स्वाभाविक रूप से जुड़ती हैं। आप दोनों दिशाओं में जा सकते हैं: user.posts एक list देता है; post.author user देता है।
两张相关的表 — 用户和他们的帖子 — 在 ORM 中自然地连接起来。你可以双向访问:user.posts 给出列表;post.author 给出用户。
The foreign key points back to the parent; the ORM walks both ways.
Foreign key parent की ओर इशारा करती है; ORM दोनों ओर चलता है।
外键指回父对象;ORM 可以双向访问。
from sqlalchemy import ForeignKey, create_engine, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session
class Base(DeclarativeBase): pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
email: Mapped[str] = mapped_column(unique=True)
# A User has many Posts - this line creates the collection
posts: Mapped[list["Post"]] = relationship(back_populates="author")
class Post(Base):
__tablename__ = "posts"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str]
author_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
# Back-reference - post.author gives you the User
author: Mapped[User] = relationship(back_populates="posts")
engine = create_engine("sqlite:///blog.db")
Base.metadata.create_all(engine)
# Create a user with posts, all in one go
with Session(engine) as s:
ada = User(name="Ada", email="ada@x.io", posts=[
Post(title="Hello, world"),
Post(title="Learning SQLAlchemy"),
Post(title="Why relationships rule"),
])
s.add(ada)
s.commit()
# Navigate BOTH ways - no SQL written by you
with Session(engine) as s:
ada = s.scalar(select(User).where(User.name == "Ada"))
print(f"{ada.name} has {len(ada.posts)} posts:")
for p in ada.posts:
print(f" - {p.title:24s} (by {p.author.name})")
Run it a second time…इसे दूसरी बार चलाइए…再运行一次…
from sqlalchemy import ForeignKey, create_engine, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session
class Base(DeclarativeBase): pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
email: Mapped[str] = mapped_column(unique=True)
# A User has many Posts - this line creates the collection
posts: Mapped[list["Post"]] = relationship(back_populates="author")
class Post(Base):
__tablename__ = "posts"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str]
author_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
# Back-reference - post.author gives you the User
author: Mapped[User] = relationship(back_populates="posts")
engine = create_engine("sqlite:///blog.db")
Base.metadata.create_all(engine)
# Create a user with posts, all in one go
with Session(engine) as s:
ada = User(name="Ada", email="ada@x.io", posts=[
Post(title="Hello, world"),
Post(title="Learning SQLAlchemy"),
Post(title="Why relationships rule"),
])
s.add(ada)
s.commit()
# Navigate BOTH ways - no SQL written by you
with Session(engine) as s:
ada = s.scalar(select(User).where(User.name == "Ada"))
print(f"{ada.name} has {len(ada.posts)} posts:")
for p in ada.posts:
print(f" - {p.title:24s} (by {p.author.name})")
email is unique=True, and the script always inserts Ada again, so the second run raises IntegrityError: UNIQUE constraint failed. Seed only when the user is missing (Section 14 shows the pattern). Also, the original output showed the titles neatly lined up, but its print had no padding; we added :24s so the real output matches.
email unique=True है, और script हर बार Ada को फिर डालती है, इसलिए दूसरा run IntegrityError: UNIQUE constraint failed देता है। Seed तभी कीजिए जब user मौजूद न हो (Section 14 यह pattern दिखाता है)। साथ ही, मूल output में titles सीधी कतार में दिखाए गए थे, पर उसके print में padding नहीं थी; हमने :24s जोड़ा ताकि असली output मेल खाए।
email 设置了 unique=True,而脚本每次都会再插入 Ada,所以第二次运行会抛出 IntegrityError: UNIQUE constraint failed。应只在用户不存在时才插入初始数据(第 14 节展示了这种写法)。另外,原输出中标题排得整整齐齐,但它的 print 并没有填充;我们加上了 :24s,让真实输出与之相符。
Lazy loading and the N+1 problemLazy loading और N+1 समस्या延迟加载与 N+1 问题
By default user.posts is loaded lazily: the first time you touch it, the ORM runs one more query. Loop over 20 users and touch each user's posts, and you run 1 + 20 queries. That is the N+1 problem. selectinload() fetches all the posts in one extra query.
Default रूप से user.posts lazy load होता है: पहली बार छूने पर ORM एक और query चलाता है। 20 users पर loop चलाइए और हर user के posts छुइए, तो 1 + 20 queries चलती हैं। यही N+1 समस्या है। selectinload() सारे posts एक अतिरिक्त query में ले आता है।
默认情况下 user.posts 是 延迟 加载的:第一次访问时,ORM 会再执行一条查询。遍历 20 个用户并访问每个人的帖子,就会执行 1 + 20 条查询。这就是 N+1 问题。selectinload() 只需一条额外查询就能取回所有帖子。
from sqlalchemy import ForeignKey, create_engine, select, event
from sqlalchemy.orm import (DeclarativeBase, Mapped, mapped_column, relationship,
Session, selectinload)
class Base(DeclarativeBase): pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str]
posts: Mapped[list["Post"]] = relationship(back_populates="author")
class Post(Base):
__tablename__ = "posts"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str]
author_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
author: Mapped[User] = relationship(back_populates="posts")
engine = create_engine("sqlite://") # in-memory database
Base.metadata.create_all(engine)
with Session(engine) as s:
s.add_all([User(name=f"user{i}", posts=[Post(title="p1"), Post(title="p2")])
for i in range(20)])
s.commit()
queries = []
event.listen(engine, "before_cursor_execute",
lambda conn, cur, sql, *a: queries.append(sql))
for label, stmt in [("lazy (default)", select(User)),
("selectinload ", select(User).options(selectinload(User.posts)))]:
queries.clear()
with Session(engine) as s:
total = sum(len(u.posts) for u in s.scalars(stmt))
print(f"{label}: {total} posts loaded with {len(queries)} SQL queries")
🔢 Simulator: N+1 Query Counter🔢 Simulator: N+1 Query Counter🔢 模拟器:N+1 查询计数器
Each dot is one SQL query that SQLAlchemy really sent (counted with an event listener). Change the number of users and the loading strategy.
हर बिंदु एक SQL query है जो SQLAlchemy ने सच में भेजी (event listener से गिनी गई)। Users की संख्या और loading strategy बदलिए।
每个圆点代表 SQLAlchemy 真实发出的一条 SQL 查询(用事件监听器统计)。改变用户数量和加载策略试试。
Common Pitfalls (and Fixes) 🕳️आम ग़लतियाँ (और सुधार) 🕳️常见陷阱(及修正)🕳️
| Mistakeग़लती错误 | What happensक्या होता है会发生什么 | Fixसुधार修正 |
|---|---|---|
String-built SQL: f"WHERE name='{n}'"String से बना SQL: f"WHERE name='{n}'"字符串拼接 SQL:f"WHERE name='{n}'" | SQL injection; names like O'Brien crashSQL injection; O'Brien जैसे नाम crashSQL 注入;O'Brien 这样的名字会出错 | ? placeholders or SQLAlchemy expressions? placeholders या SQLAlchemy expressions? 占位符或 SQLAlchemy 表达式 |
Forgetting commit()commit() भूलना忘记 commit() | Changes lost when the connection closesConnection बंद होने पर बदलाव खो जाते हैं连接关闭时修改丢失 | with conn: (commits) or conn.commit()with conn: (commit करता है) या conn.commit()with conn:(会提交)或 conn.commit() |
| Never closing the connectionConnection कभी बंद न करना从不关闭连接 | Open handles; file locks; pool runs outखुले handles; file locks; pool ख़त्म句柄未释放;文件锁;连接池耗尽 | conn.close() or contextlib.closing() — the sqlite3 with does not closeconn.close() या contextlib.closing() — sqlite3 का with बंद नहीं करताconn.close() 或 contextlib.closing() — sqlite3 的 with 不会关闭 |
| UPDATE / DELETE without WHEREWHERE के बिना UPDATE / DELETE没有 WHERE 的 UPDATE / DELETE | Every row changed / deletedहर row बदली / मिटी所有行被修改 / 删除 | Run SELECT with the same filter first; check rowcountपहले उसी filter से SELECT; rowcount जाँचें先用相同条件运行 SELECT;检查 rowcount |
| UPDATE that matches no rowकोई row न मिलने वाला UPDATE一行都没匹配到的 UPDATE | No error — 0 rows change silentlyकोई error नहीं — चुपचाप 0 rows बदलती हैं不报错 — 悄悄改动 0 行 | Check cur.rowcount and raisecur.rowcount जाँचें और raise करें检查 cur.rowcount 并抛出异常 |
fetchall() on a million rowsलाखों rows पर fetchall()对上百万行调用 fetchall() | Memory blows upMemory फट जाती है内存暴涨 | Loop over the cursor: for row in cur:Cursor पर loop: for row in cur:遍历游标:for row in cur: |
No PRAGMA foreign_keys = ON (SQLite)PRAGMA foreign_keys = ON नहीं (SQLite)没有 PRAGMA foreign_keys = ON(SQLite) | Foreign keys declared but not enforcedForeign keys घोषित पर लागू नहीं声明了外键却不强制执行 | Turn it on after every connectहर connect के बाद चालू करें每次连接后都打开它 |
| N+1 queries (ORM)N+1 queries (ORM)N+1 查询(ORM) | One extra query per parent rowहर parent row पर एक अतिरिक्त query每个父行多一次查询 | selectinload() / joinedload()selectinload() / joinedload()selectinload() / joinedload() |
| Using ORM objects after the Session closedSession बंद होने के बाद ORM objects इस्तेमाल करना在 Session 关闭后使用 ORM 对象 | DetachedInstanceError on lazy attributesLazy attributes पर DetachedInstanceError访问延迟属性时报 DetachedInstanceError | Load what you need inside the Session (e.g. selectinload)जो चाहिए उसे Session के अंदर load करें (जैसे selectinload)在 Session 内加载所需数据(如 selectinload) |
| One Session shared across threadsThreads के बीच साझा एक Session多个线程共享一个 Session | Race conditions, broken stateRace conditions, बिगड़ी हालत竞态条件,状态错乱 | One Session per request / taskहर request / task के लिए एक Session每个请求 / 任务一个 Session |
The original fix for a connection leak was "use with sqlite3.connect(...)" — which does not close the connection (Section 06). We also added three real mistakes shown on this page: an UPDATE that silently matches nothing (Section 07), O'Brien-style quotes breaking string-built SQL (Section 04), and using ORM objects after their Session closed (Bug Doctor).
Connection leak का मूल सुधार था "with sqlite3.connect(...) लीजिए" — जो connection बंद नहीं करता (Section 06)। हमने इस page पर दिखाई गई तीन असली ग़लतियाँ भी जोड़ीं: चुपचाप किसी से मेल न खाने वाला UPDATE (Section 07), string से बने SQL को तोड़ने वाले O'Brien जैसे quotes (Section 04), और Session बंद होने के बाद ORM objects इस्तेमाल करना (Bug Doctor)।
原文对连接泄漏给出的修正是"使用 with sqlite3.connect(...)"— 而它并不会关闭连接(第 06 节)。我们还补充了本页展示过的三个真实错误:悄悄一行都没匹配到的 UPDATE(第 07 节)、O'Brien 这类引号会破坏拼接的 SQL(第 04 节),以及在 Session 关闭后使用 ORM 对象(Bug 医生)。
Real-World Example — A Full Task Tracker 📋असली उदाहरण — पूरा Task Tracker 📋实战示例 — 完整的任务跟踪器 📋
A complete mini-app: users have many tasks, tasks have a status, and we can query, update and report — all with the ORM and typed models.
एक पूरा mini-app: users के कई tasks हैं, tasks का status है, और हम query, update और report कर सकते हैं — सब ORM और typed models से।
一个完整的小应用:用户有多个任务,任务有状态,我们可以查询、更新和生成报告 — 全部使用 ORM 和带类型的模型。
from __future__ import annotations
from datetime import datetime, timezone
from sqlalchemy import create_engine, String, ForeignKey, func, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session
class Base(DeclarativeBase): pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(80))
tasks: Mapped[list[Task]] = relationship(
back_populates="owner", cascade="all, delete-orphan"
)
class Task(Base):
__tablename__ = "tasks"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(200))
status: Mapped[str] = mapped_column(String(20), default="open")
created: Mapped[datetime] = mapped_column(default=lambda: datetime.now(timezone.utc))
owner_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
owner: Mapped[User] = relationship(back_populates="tasks")
engine = create_engine("sqlite:///tracker.db", echo=False)
Base.metadata.create_all(engine)
# -- Seed data -----------------------------------
with Session(engine) as s:
if not s.scalar(select(User)): # only on the first run
s.add_all([
User(name="Ada", tasks=[
Task(title="Design schema", status="done"),
Task(title="Write ORM models", status="done"),
Task(title="Add tests", status="open"),
]),
User(name="Grace", tasks=[
Task(title="Draft docs", status="open"),
Task(title="Ship v1", status="blocked"),
]),
])
s.commit()
# -- Report: open tasks per user -----------------
with Session(engine) as s:
stmt = (
select(User.name, func.count(Task.id).label("n_open"))
.join(Task, Task.owner_id == User.id)
.where(Task.status == "open")
.group_by(User.name)
)
print("Open tasks by user:")
for name, n in s.execute(stmt):
print(f" {name:8s} {n} open")
# Mark one task as done - the ORM notices the change
t = s.scalar(select(Task).where(Task.title == "Add tests", Task.status == "open"))
if t:
t.status = "done"
s.commit()
print(f"\nmarked done: {t.title}")
Run it a second time…इसे दूसरी बार चलाइए…再运行一次…
from __future__ import annotations
from datetime import datetime, timezone
from sqlalchemy import create_engine, String, ForeignKey, func, select
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship, Session
class Base(DeclarativeBase): pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
name: Mapped[str] = mapped_column(String(80))
tasks: Mapped[list[Task]] = relationship(
back_populates="owner", cascade="all, delete-orphan"
)
class Task(Base):
__tablename__ = "tasks"
id: Mapped[int] = mapped_column(primary_key=True)
title: Mapped[str] = mapped_column(String(200))
status: Mapped[str] = mapped_column(String(20), default="open")
created: Mapped[datetime] = mapped_column(default=lambda: datetime.now(timezone.utc))
owner_id: Mapped[int] = mapped_column(ForeignKey("users.id"))
owner: Mapped[User] = relationship(back_populates="tasks")
engine = create_engine("sqlite:///tracker.db", echo=False)
Base.metadata.create_all(engine)
# -- Seed data -----------------------------------
with Session(engine) as s:
if not s.scalar(select(User)): # only on the first run
s.add_all([
User(name="Ada", tasks=[
Task(title="Design schema", status="done"),
Task(title="Write ORM models", status="done"),
Task(title="Add tests", status="open"),
]),
User(name="Grace", tasks=[
Task(title="Draft docs", status="open"),
Task(title="Ship v1", status="blocked"),
]),
])
s.commit()
# -- Report: open tasks per user -----------------
with Session(engine) as s:
stmt = (
select(User.name, func.count(Task.id).label("n_open"))
.join(Task, Task.owner_id == User.id)
.where(Task.status == "open")
.group_by(User.name)
)
print("Open tasks by user:")
for name, n in s.execute(stmt):
print(f" {name:8s} {n} open")
# Mark one task as done - the ORM notices the change
t = s.scalar(select(Task).where(Task.title == "Add tests", Task.status == "open"))
if t:
t.status = "done"
s.commit()
print(f"\nmarked done: {t.title}")
(1) The original used default=datetime.utcnow. Since Python 3.12 utcnow() is deprecated (it returns a time with no time zone). Use lambda: datetime.now(timezone.utc). (2) On the second run Ada is missing from the report: the inner JOIN + WHERE status = "open" drops users who have no open tasks. Use an outer join with the condition in the ON clause if you want "Ada 0 open". (3) We added Task.status == "open" to the lookup, so a re-run does not "mark done" a task that is already done.
(1) मूल ने default=datetime.utcnow लिया। Python 3.12 से utcnow() deprecated है (यह बिना time zone वाला समय लौटाता है)। lambda: datetime.now(timezone.utc) लीजिए। (2) दूसरे run में Ada report से ग़ायब है: inner JOIN + WHERE status = "open" उन users को हटा देता है जिनका कोई open task नहीं। "Ada 0 open" चाहिए तो outer join लीजिए और शर्त ON में रखिए। (3) हमने lookup में Task.status == "open" जोड़ा, ताकि दोबारा चलाने पर पहले से done task फिर "mark done" न हो।
(1) 原文用了 default=datetime.utcnow。从 Python 3.12 起 utcnow() 已弃用(它返回不带时区的时间)。请使用 lambda: datetime.now(timezone.utc)。(2) 第二次运行时,Ada 从报告中 消失 了:内连接 JOIN + WHERE status = "open" 会去掉没有未完成任务的用户。如果想看到"Ada 0 open",请用外连接并把条件放进 ON 子句。(3) 我们在查找时加了 Task.status == "open",这样重跑时不会把已完成的任务再"标记完成"一次。
Typed models with Mapped[]. A two-way relationship with back_populates. cascade="all, delete-orphan", so deleting a user also deletes their tasks. An aggregate query with func.count(). Seed data inserted only once. Changes tracked automatically — change an attribute and commit.
Mapped[] वाले typed models। back_populates वाला दो-तरफ़ा relationship। cascade="all, delete-orphan", ताकि user मिटाने पर उसके tasks भी मिटें। func.count() वाली aggregate query। Seed data सिर्फ़ एक बार। बदलाव अपने-आप track — attribute बदलिए और commit कीजिए।
使用 Mapped[] 的带类型模型。通过 back_populates 建立的双向关系。cascade="all, delete-orphan",删除用户时连同其任务一起删除。使用 func.count() 的聚合查询。初始数据只插入一次。修改自动跟踪 — 改属性然后提交即可。
Bug Doctor, Golden Rules and Quiz 🏆Bug Doctor, सुनहरे नियम और Quiz 🏆Bug 医生、黄金法则与小测验 🏆
🩺 Simulator: Bug Doctor🩺 Simulator: Bug Doctor🩺 模拟器:Bug 医生
Six real database mistakes. Buggy code on the left, fixed code on the right, changed lines marked. Every output is from a real run.
छह असली database ग़लतियाँ। बाईं ओर buggy code, दाईं ओर ठीक किया हुआ, बदली lines चिह्नित। हर output असली run से है।
六个真实的数据库错误。左边是有 bug 的代码,右边是修正后的代码,改动的行已标出。每个输出都来自真实运行。
How much faster is executemany()?executemany() कितना तेज़ है?executemany() 到底快多少?
import sqlite3, time, os
ROWS = [(f"Book {i}", "Author", 9.99, 5) for i in range(2000)]
def fresh():
if os.path.exists("speed.db"): os.remove("speed.db")
conn = sqlite3.connect("speed.db")
conn.execute("CREATE TABLE books (title TEXT, author TEXT, price REAL, stock INT)")
return conn
SQL = "INSERT INTO books VALUES (?, ?, ?, ?)"
conn = fresh(); t0 = time.perf_counter()
for r in ROWS:
conn.execute(SQL, r); conn.commit() # commit after EVERY row
a = time.perf_counter() - t0; conn.close()
conn = fresh(); t0 = time.perf_counter()
for r in ROWS:
conn.execute(SQL, r) # loop, ONE commit at the end
conn.commit(); b = time.perf_counter() - t0; conn.close()
conn = fresh(); t0 = time.perf_counter()
conn.executemany(SQL, ROWS); conn.commit() # executemany, one commit
c = time.perf_counter() - t0; conn.close()
print(f"loop + commit per row : {a * 1000:7.1f} ms")
print(f"loop + one commit : {b * 1000:7.1f} ms")
print(f"executemany + commit : {c * 1000:7.1f} ms")
The big win is not executemany() itself — it is committing once. Committing after every row forces SQLite to save to disk each time. Inside one transaction, a loop of execute() is already fast, and executemany() is a little faster and shorter to write.
बड़ा फ़ायदा ख़ुद executemany() नहीं — एक बार commit करना है। हर row के बाद commit करने से SQLite को हर बार disk पर save करना पड़ता है। एक transaction के अंदर execute() का loop पहले से तेज़ है, और executemany() थोड़ा और तेज़ और लिखने में छोटा है।
真正的提速不在 executemany() 本身 — 而在于 只提交一次。每插一行就提交一次,会迫使 SQLite 每次都写盘。在同一个事务中,循环 execute() 本来就很快,executemany() 只是更快一点、写起来更短。
? in sqlite3, expressions in SQLAlchemy. This is your main defence against SQL injection, and it also handles quotes like O'Brien.?, SQLAlchemy में expressions। यह SQL injection से आपका मुख्य बचाव है, और O'Brien जैसे quotes भी संभालता है।?,SQLAlchemy 用表达式。这是防御 SQL 注入的主要手段,也能处理 O'Brien 这样的引号。with conn: commits (or rolls back) in sqlite3 but does not close — use conn.close() or contextlib.closing(). In SQLAlchemy, with Session(engine) and with engine.begin() clean up for you.with conn: commit (या rollback) करता है पर बंद नहीं करता — conn.close() या contextlib.closing() लीजिए। SQLAlchemy में with Session(engine) और with engine.begin() आपके लिए सफ़ाई करते हैं।with conn: 负责提交(或回滚)但 不会 关闭 — 请用 conn.close() 或 contextlib.closing()。在 SQLAlchemy 中,with Session(engine) 和 with engine.begin() 会替你收尾。SELECT … WHERE with the same filter first, and check rowcount afterwards. There is no undo after commit.SELECT … WHERE चलाइए, और बाद में rowcount जाँचिए। Commit के बाद कोई undo नहीं।SELECT … WHERE,事后检查 rowcount。提交之后就无法撤销。PRAGMA foreign_keys = ON after every connect (with SQLAlchemy, in a "connect" event listener). It is off by default for backwards compatibility.PRAGMA foreign_keys = ON (SQLAlchemy में "connect" event listener में)। पुरानी अनुकूलता के लिए यह default रूप से बंद है।PRAGMA foreign_keys = ON(在 SQLAlchemy 中放进 "connect" 事件监听器)。为了向后兼容,它默认是关闭的。executemany(). Committing per row was hundreds of times slower in the run above.executemany() लीजिए। ऊपर के run में हर row पर commit सैकड़ों गुना धीमा था।executemany()。在上面的运行中,每行提交一次要慢上几百倍。selectinload() or joinedload() to fetch the children up front.selectinload() या joinedload() लीजिए।selectinload() 或 joinedload() 预先取回子对象。Rule 2 said the context managers close connections — not true for sqlite3 (Section 06). Rule 6 said executemany() is "10–100× faster because it batches the round-trips". SQLite runs inside your process, so there are no network round trips; the run above shows the real cost is committing per row, while executemany() versus a loop in one transaction is a small gain.
नियम 2 ने कहा कि context managers connections बंद करते हैं — sqlite3 के लिए सच नहीं (Section 06)। नियम 6 ने कहा कि executemany() "round-trips को batch करने से 10–100× तेज़" है। SQLite आपके process के अंदर चलता है, इसलिए कोई network round trips नहीं; ऊपर का run दिखाता है कि असली लागत हर row पर commit है, जबकि एक transaction में loop बनाम executemany() का फ़ायदा छोटा है।
规则 2 说上下文管理器会关闭连接 — 对 sqlite3 并不成立(第 06 节)。规则 6 说 executemany()"因为批量减少往返而快 10–100 倍"。SQLite 运行在你的进程内部,根本没有网络往返;上面的运行表明真正的开销在于每行都提交,而在同一事务中 executemany() 相比循环只快一点点。
Quick Quiz — 8 Questionsछोटा Quiz — 8 सवाल小测验 — 8 道题
A database is organised storage you query with SQL. Send values with placeholders, never f-strings. Group related changes in a transaction and check that each step did what you expected. Turn on foreign keys in SQLite, commit once for bulk work, and remember that the sqlite3 with commits but does not close. When the app grows, SQLAlchemy Core and ORM give you composable queries, Python objects and many databases — just watch for N+1 queries.
Database व्यवस्थित storage है जिससे आप SQL में सवाल पूछते हैं। Values placeholders से भेजिए, f-strings से कभी नहीं। जुड़े बदलाव एक transaction में रखिए और जाँचिए कि हर step ने वही किया जो आप चाहते थे। SQLite में foreign keys चालू कीजिए, bulk काम में एक बार commit कीजिए, और याद रखिए कि sqlite3 का with commit करता है पर बंद नहीं करता। App बढ़ने पर SQLAlchemy Core और ORM जोड़ने लायक queries, Python objects और कई databases देते हैं — बस N+1 queries पर नज़र रखिए।
数据库是用 SQL 查询的有序存储。用占位符传值,绝不要用 f-string。把相关修改放进一个事务,并检查每一步是否达到预期。在 SQLite 中打开外键,批量操作只提交一次,并记住 sqlite3 的 with 会提交但不会关闭连接。应用变大后,SQLAlchemy Core 和 ORM 带来可组合的查询、Python 对象和多数据库支持 — 只是要留意 N+1 查询。