← 中文资源

简体中文版本

规范化与反规范化

规范化消除重复事实以保护完整性;反规范化有选择地复制事实以加速读取,平衡点取决于写入规则和查询形态。

阅读英文原文

工作原理

规范化通过规则消除冗余。第一范式要求原子值;第二范式消除对复合键的部分依赖;第三范式消除传递依赖,使非键属性只依赖键。更高范式处理更细微的异常。回报是单一事实来源:修改客户名称只更新一行,任何报表都不会给出互相矛盾的答案。

反规范化则有意逆转部分工作。可以把客户名称复制到订单上,把评论数存在帖子中,或把父子行扁平化为一个文档。每一份副本都是对别处拥有的事实的缓存,每次写入都必须更新它,或者安排核对任务。

具体权衡

一张订单表连接客户表和订单明细表,渲染页面可能需要三次连接。读流量很高时,覆盖索引或汇总表可以消除这些连接。但如果客户地址变化,订单历史往往应当保留旧地址,因此这种复制在语义上是正确的,而不是妥协。要区分可以安全复制的不可变快照与需要传播的可变属性。

何时选择

在正确性和写放大重要的交易路径上使用规范化:余额、库存、权限。在读路径稳定,或可以从源数据重建物化视图、搜索索引时使用反规范化。先测量;大多数小型系统并不需要任何极端做法。

权衡与陷阱

规范化模型减少更新异常,但增加连接成本和查询复杂度。反规范化模型读取快,却成倍增加写入成本,并可能在某条路径忘记副本时产生漂移。触发器、事务性发件箱或定期重建可以维护副本,但每一种都增加运维面。最危险的情况是可变事实被复制却没有归属者:最终两个界面显示不同的值,没有人知道哪个正确。

代码示例

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))