工作原理
规范化通过规则消除冗余。第一范式要求原子值;第二范式消除对复合键的部分依赖;第三范式消除传递依赖,使非键属性只依赖键。更高范式处理更细微的异常。回报是单一事实来源:修改客户名称只更新一行,任何报表都不会给出互相矛盾的答案。
反规范化则有意逆转部分工作。可以把客户名称复制到订单上,把评论数存在帖子中,或把父子行扁平化为一个文档。每一份副本都是对别处拥有的事实的缓存,每次写入都必须更新它,或者安排核对任务。
具体权衡
一张订单表连接客户表和订单明细表,渲染页面可能需要三次连接。读流量很高时,覆盖索引或汇总表可以消除这些连接。但如果客户地址变化,订单历史往往应当保留旧地址,因此这种复制在语义上是正确的,而不是妥协。要区分可以安全复制的不可变快照与需要传播的可变属性。
何时选择
在正确性和写放大重要的交易路径上使用规范化:余额、库存、权限。在读路径稳定,或可以从源数据重建物化视图、搜索索引时使用反规范化。先测量;大多数小型系统并不需要任何极端做法。
权衡与陷阱
规范化模型减少更新异常,但增加连接成本和查询复杂度。反规范化模型读取快,却成倍增加写入成本,并可能在某条路径忘记副本时产生漂移。触发器、事务性发件箱或定期重建可以维护副本,但每一种都增加运维面。最危险的情况是可变事实被复制却没有归属者:最终两个界面显示不同的值,没有人知道哪个正确。
代码示例
const DEFAULT_PORTS = new Map([
["http:", "80"],
["https:", "443"],
]);
export function canonicalizeUrl(input, options = {}) {
if (typeof input !== "string" || input.trim() === "") {
throw new TypeError("input must be a non-empty URL string");
}
const {
stripFragment = true,
sortQuery = true,
dropQuery = [],
keepTrailingSlash = true,
} = options;
if (!Array.isArray(dropQuery)) throw new TypeError("dropQuery must be an array");
const dropped = new Set(dropQuery.map((key) => String(key)));
let url;
try {
url = new URL(input.trim());
} catch {
throw new TypeError("invalid URL: " + input);
}
if (url.protocol !== "http:" && url.protocol !== "https:") {
throw new TypeError("only http and https URLs are supported");
}
if (stripFragment) url.hash = "";
if (DEFAULT_PORTS.get(url.protocol) === url.port) url.port = "";
if (url.pathname === "") url.pathname = "/";
if (!keepTrailingSlash && url.pathname.length > 1 && url.pathname.endsWith("/")) {
url.pathname = url.pathname.slice(0, -1);
}
if (sortQuery) {
const pairs = [...url.searchParams.entries()].filter(([key]) => !dropped.has(key));
pairs.sort((left, right) => {
if (left[0] !== right[0]) return left[0] < right[0] ? -1 : 1;
if (left[1] === right[1]) return 0;
return left[1] < right[1] ? -1 : 1;
});
url.search = "";
for (const [key, value] of pairs) url.searchParams.append(key, value);
}
return url.toString();
}
const urlKeyCache = new Map();
// Memoize canonical results so one logical URL maps to one cache key.
export function urlCacheKey(input, options = {}) {
const fingerprint = [input, options.sortQuery ?? true, options.stripFragment ?? true, options.keepTrailingSlash ?? true, (options.dropQuery ?? []).join(',')].join('|');
let key = urlKeyCache.get(fingerprint);
if (key === undefined) {
key = canonicalizeUrl(input, options);
urlKeyCache.set(fingerprint, key);
}
return key;
}
import csv
import io
def detect_dialect(sample: str) -> dict[str, object]:
dialect = csv.Sniffer().sniff(sample, delimiters=",;\t|")
return {
"delimiter": dialect.delimiter,
"quotechar": dialect.quotechar,
"doublequote": dialect.doublequote,
"skipinitialspace": dialect.skipinitialspace,
}
def read_rows(text: str) -> list[dict[str, str]]:
dialect = csv.Sniffer().sniff(text, delimiters=",;\t|")
reader = csv.DictReader(io.StringIO(text), dialect=dialect)
return [dict(row) for row in reader]
if __name__ == "__main__":
sample = "name;role;city\nAda;engineer;London\nGrace;admiral;New York\n"
print(detect_dialect(sample))
print(read_rows(sample))