{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "id": "gp10-14-0",
   "source": [
    "# 实验 14：连接顺序枚举（DPsize / DPsub / DPccp）\n",
    "\n",
    "## 为什么做这个实验\n",
    "\n",
    "一条涉及 n 张表的连接查询，执行的先后顺序（连接顺序，join order）可以有非常多种。\n",
    "n = 6 时丛生树（bushy tree）形状的计划就有 6! × C(5) = 720 × 42 = 30 240 种（C(5) = 42 是第 5 个卡塔兰数）。\n",
    "靠\"试一遍\"是不现实的，优化器必须用**动态规划**：把\"哪几张表连成一个中间结果\"当作子问题，\n",
    "用一张 plan table 记住每个子问题的最优计划，再一层层拼上去。\n",
    "\n",
    "但\"拼\"的时候要枚举哪些 (S1, S2) 对，大有讲究。枚举得太粗（先枚举再过滤）会白算一大堆\n",
    "笛卡尔积、重叠、不连通的无效对；枚举得太细又要额外的簿记开销。本实验对比三种经典做法：\n",
    "\n",
    "| 算法 | 枚举方式 | 是否利用连接图 |\n",
    "| --- | --- | --- |\n",
    "| **DPsize** | 按结果集大小 s = 2…n 分层枚举 | 否，先枚举后过滤 |\n",
    "| **DPsub** | 对每个子集枚举它的所有非空真子集 | 否，但不会枚举重叠对 |\n",
    "| **DPccp** | 只枚举连通互补对（csg-cmp pair） | 是，一个无效对都不枚举 |\n",
    "\n",
    "出处：Moerkotte & Neumann，《Analysis of Two Existing and One New Dynamic Programming\n",
    "Algorithm for the Enumerations of Join Orders》，VLDB 2006。\n",
    "\n",
    "读本 notebook 时请先记住一句：**DPsize / DPsub 是\"先枚举后过滤\"，DPccp 是\"只枚举有用的\"。**\n",
    "\n",
    "## 计数口径（务必先看清，否则数字对不上）\n",
    "\n",
    "下面所有枚举量都是**直接运行本 notebook 的三个枚举器**得到的。该实现有一个统一约定：\n",
    "**三个算法都对对称对做了去重，每个 (S₁, S₂) 连接对只计一次**（S₁、S₂ 互换视为同一个对）。\n",
    "若按有序对计数，每个数字都会翻倍。\n",
    "\n",
    "另一个容易踩的坑：教科书上说\"DPsize 与 DPsub 的枚举量只由 n 决定、与图形状无关\"，\n",
    "那是对**原始定义**而言（原始定义的 DP 表里放着全部 2ⁿ 个子集）。\n",
    "**本仓库的实现只在 DP 表里放连通的子问题**，所以连 DPsize 也随图形状变化。\n",
    "本 notebook 末尾的对比表会给出实测值，讲课与做题请以那些数字为准，不要拿教材公式去核对。\n",
    "\n",
    "## 运行前准备\n",
    "\n",
    "- Python 3.10 及以上（大量用到位运算）。\n",
    "- 依赖：`numpy`、`matplotlib`（画枚举耗时曲线）；可选 `jupyter`。\n",
    "- 缺包时安装：\n",
    "\n",
    "  ```bash\n",
    "  python -m pip install numpy matplotlib\n",
    "  ```\n",
    "\n",
    "  （本机已配置环境可用：`python -m pip install numpy matplotlib`）\n",
    "\n",
    "- 打开方式：在仓库根目录执行 `jupyter notebook notebooks/PlanEnumeration.ipynb`。\n",
    "  第一个代码 cell 会把仓库根目录加进 `sys.path`，这样 `from system...` 无论从哪打开都能用。\n",
    "- 仓库相关代码：`system/query_optimization/` 下的 `join_graph.py`（四种图形状的工厂类）、\n",
    "  `problems.py`（用位序列表示关系子集的 `Problem`）、`plan_table.py`、`cost_function.py`（`C_Out`）、\n",
    "  `cardinality_table.py`。"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import sys, pathlib\n",
    "\n",
    "# Put the repo root (the directory containing `system/`) on sys.path so\n",
    "# `from system...` works whether this notebook is opened from its own\n",
    "# folder or from the repo root.\n",
    "for _p in (pathlib.Path.cwd(), *pathlib.Path.cwd().parents):\n",
    "    if (_p / \"system\").is_dir():\n",
    "        if str(_p) not in sys.path:\n",
    "            sys.path.insert(0, str(_p))\n",
    "        break"
   ],
   "id": "609e765a-5d18-472c-be7a-0a15112c72af"
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-28T11:33:23.712902Z",
     "start_time": "2025-01-28T11:33:11.795222Z"
    }
   },
   "cell_type": "code",
   "source": [
    "from typing import Type, Iterator\n",
    "\n",
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "from abc import ABC, abstractmethod"
   ],
   "id": "67238dab2029ec5d",
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Matplotlib is building the font cache; this may take a moment.\n"
     ]
    }
   ],
   "execution_count": 1
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "id": "gp10-14-3",
   "source": [
    "# 实验设定：枚举器的统一接口\n",
    "\n",
    "三种算法实现同一个抽象类 `PlanEnumerator`，都只有一个方法 `enumerate(...)`，返回三元组：\n",
    "\n",
    "| 返回值 | 含义 |\n",
    "| --- | --- |\n",
    "| 第 1 项 | 最优计划的代价（按给定代价函数） |\n",
    "| 第 2 项 | 最优计划本身（嵌套元组形式的连接树） |\n",
    "| 第 3 项 | **枚举过程中产生的 (S₁, S₂) 对数量** ← 本实验的观测对象 |\n",
    "\n",
    "第三个返回值正是我们比较三种算法的抓手：`print_results(results)` 会把它打印出来。\n",
    "\n",
    "搜索空间限定为**不含笛卡尔积的丛生树**（bushy plans without cross products）。\n",
    "所谓丛生树，就是连接的左右两支都可以是中间结果（而不要求右孩子必须是基表）；\n",
    "所谓不含笛卡尔积，就是每个连接都必须对应连接图里的一条边。"
   ]
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-28T11:33:23.731475Z",
     "start_time": "2025-01-28T11:33:23.720984Z"
    }
   },
   "cell_type": "code",
   "source": [
    "from system.interfaces.cost_functions import CostFunction\n",
    "from system.query_optimization.cardinality_table import CardinalityTable\n",
    "from system.query_optimization.join_graph import JoinGraph\n",
    "\n",
    "\n",
    "class PlanEnumerator(ABC):\n",
    "    \"\"\"\n",
    "    Abstract enumerator class to enumerate the join plans for a given join graph.\n",
    "    \"\"\"\n",
    "\n",
    "    @abstractmethod\n",
    "    def enumerate(\n",
    "        self,\n",
    "        join_graph: JoinGraph,\n",
    "        cardinality_table: CardinalityTable,\n",
    "        cost_function: CostFunction,\n",
    "        print_info: bool = False,\n",
    "    ) -> tuple[int, tuple, int]:\n",
    "        \"\"\"\n",
    "        Computes and returns the best join order for the given problem sizes.\n",
    "        # TODO: join order vs join plan\n",
    "        :param join_graph: The join graph.\n",
    "        :param cardinality_table: The cardinality table to be used.\n",
    "        :param cost_function: A cost function determining the cost of different operations.\n",
    "        :param print_info: A flag indicating whether additional info should be printed during enumeration.\n",
    "        :return: A tuple, consisting of the costs of the best plan, the best plan as a tuple and the number of pairs that were enumerated.\n",
    "        \"\"\""
   ],
   "id": "7939c96240e01f87",
   "outputs": [],
   "execution_count": 2
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "id": "gp10-14-5",
   "source": [
    "## 公共基础：plan table 与动态规划\n",
    "\n",
    "三个算法共用一张 **plan table**（计划表），它记录\"每个子问题到目前为止找到的最优计划及其代价\"。\n",
    "有了它，n 张表的查询不必枚举 n! 棵树，只需枚举所有子集的划分——这就是**动态规划**（dynamic programming）。\n",
    "\n",
    "动态规划能成立靠的是**最优子结构**：大问题 S 的最优解，一定由它的某个划分 (S₁, S₂) 的**最优解**拼成。\n",
    "因此两件事必须同时满足：\n",
    "\n",
    "1. 每个子问题只算一次，结果存进 plan table 反复查用；\n",
    "2. 生成 (S₁, S₂) 时，S₁ 与 S₂ 的最优计划**必须已经算好**。后面会看到，DPccp 的枚举顺序天生保证了这一点，\n",
    "   而 DPsize / DPsub 必须靠\"按大小/按编号分层\"来间接保证。\n",
    "\n",
    "本仓库的代价函数是 `C_Out`（`system/query_optimization/cost_function.py`），定义极简：\n",
    "\n",
    "    C_out(S₁ ⋈ S₂) = |S₁ ∪ S₂| 的估计基数 + C_out(S₁) + C_out(S₂)\n",
    "    C_out(单表 R)   = |R| 的估计基数\n",
    "\n",
    "它只统计\"流过的行数\"，是最朴素也最好手算的代价模型。真实系统还要再乘代价常数，\n",
    "例如 PostgreSQL 的默认值 `seq_page_cost = 1.0`、`random_page_cost = 4.0`（官方文档 19.7 Query Planning）。\n",
    "\n",
    "注意：本仓库的 plan table **只收录连通的子问题**（不连通的子集根本不是合法的连接中间结果）。\n",
    "这一条取舍是后面\"DPsize 也受图形状影响\"的直接原因。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "id": "gp10-14-6",
   "source": [
    "## DPsize：按结果集大小分层枚举\n",
    "\n",
    "思路：按结果集大小 s = 2, 3, …, n 分层。对每一层，枚举所有满足 |S₁| + |S₂| = s 的组合，\n",
    "并检查一下：\n",
    "\n",
    "- S₁ 与 S₂ 是否**不相交**（`s1.intersects(s2)` 为假）；\n",
    "- S₁ 与 S₂ 在连接图里是否**有边相连**（`join_graph.are_connected(s1, s2)`）。\n",
    "\n",
    "只有两条都满足，才拿去更新 plan table；否则就白枚举了一次。\n",
    "\n",
    "为什么会有那么多无效对？因为 DPsize **完全不看连接图**去生成候选，它只是机械地按大小配对：\n",
    "\n",
    "- 重叠对：S₁ ∩ S₂ ≠ ∅，例如 S₁ = {R0, R1}、S₂ = {R1, R2}；\n",
    "- 笛卡尔积对：S₁ 与 S₂ 之间没有连接谓词；\n",
    "- 不连通对：S₁ 或 S₂ 自身在连接图上不连通（本仓库的 plan table 里根本没有它）。\n",
    "\n",
    "代码里的对称去重：当 |S₁| = |S₂| 时，内层从下标 `i + 1` 开始枚举，\n",
    "这样 (S₁, S₂) 与 (S₂, S₁) 只会出现一次。**这就是\"每个对只计一次\"这条口径的来源。**"
   ]
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-28T11:33:23.792258Z",
     "start_time": "2025-01-28T11:33:23.786703Z"
    }
   },
   "cell_type": "code",
   "source": [
    "from system.query_optimization.problems import Problem\n",
    "from system.query_optimization.plan_table import SizeBasedPlanTable\n",
    "\n",
    "\n",
    "class DP_Size(PlanEnumerator):\n",
    "    \"\"\"\n",
    "    DPsize uses a size-based problem enumeration, i.e. it enumerates problems of increasing sizes. During the enumeration, it does\n",
    "    not consider the actual join graph, which means that Cartesian Products, as well as disconnected join pairs will be enumerated. In other words,\n",
    "    we also enumerate cross products in the algorithm (but then discard them).\n",
    "    \"\"\"\n",
    "\n",
    "    def enumerate(\n",
    "        self,\n",
    "        join_graph: JoinGraph,\n",
    "        cardinality_table: CardinalityTable,\n",
    "        cost_function: CostFunction,\n",
    "        print_info: bool = False,\n",
    "    ) -> tuple[int, tuple, int]:\n",
    "        \"\"\"\n",
    "        Computes and returns the best join order for the given problem sizes.\n",
    "        :param join_graph: The join graph.\n",
    "        :param cardinality_table: The cardinality table to be used.\n",
    "        :param cost_function: A cost function determining the cost of different operations.\n",
    "        :param print_info: A flag indicating whether additional info should be printed during enumeration.\n",
    "        :return: A tuple consisting of: the costs of the best plan, the best plan as a tuple and the number of pairs that were enumerated.\n",
    "        \"\"\"\n",
    "\n",
    "        # Initialize plan table with the join graph\n",
    "        # A size plan table stores entries according to their problem size, which\n",
    "        # allows iterating through existing problems based on their size\n",
    "        plan_table: SizeBasedPlanTable = SizeBasedPlanTable(\n",
    "            join_graph, cost_function, cardinality_table\n",
    "        )\n",
    "\n",
    "        # Count number of pairs that were enumerated\n",
    "        enumerated_pairs: int = 0\n",
    "\n",
    "        # Count number of valid pairs\n",
    "        valid_pairs: int = 0\n",
    "\n",
    "        # Go through each pair generated by the enumerator below\n",
    "        # It still needs to make sure that both problems are actually\n",
    "        # connected and that they are disjoint.\n",
    "        for s1, s2 in self.__generate_pairs_size_based(join_graph, plan_table):\n",
    "            # Both s1 and s2 will already be in the plan table\n",
    "            enumerated_pairs += 1\n",
    "\n",
    "            # Check whether s1 and s2 are disjoint. Pairs which are not disjoint\n",
    "            # are no valid pairs. They also need to be connected in the join graph.\n",
    "            # this condition could be removed to also enumerate disconnected problems, i.e. cross products\n",
    "            if not s1.intersects(s2) and join_graph.are_connected(s1, s2):\n",
    "                valid_pairs += 1\n",
    "\n",
    "                # Update the table with the newly generated pair\n",
    "                plan_table.update_table(s1, s2, cardinality_table, cost_function)\n",
    "                if print_info:\n",
    "                    print(f\"Enumerated valid pair {s1.as_set()}, {s2.as_set()}.\")\n",
    "            else:\n",
    "                if print_info:\n",
    "                    print(f\"Enumerated invalid pair {s1.as_set()}, {s2.as_set()}.\")\n",
    "\n",
    "        problem_with_all_relations: Problem = Problem.get_problem_with_all_relations(\n",
    "            len(join_graph)\n",
    "        )\n",
    "\n",
    "        if print_info:\n",
    "            print(\n",
    "                f\"{enumerated_pairs} pairs were enumerated, out of which {valid_pairs} were valid!\"\n",
    "            )\n",
    "\n",
    "        return (\n",
    "            plan_table.get_costs_for_problem(problem_with_all_relations),\n",
    "            plan_table.get_plan_for_problem(problem_with_all_relations),\n",
    "            enumerated_pairs,\n",
    "        )\n",
    "\n",
    "    def __generate_pairs_size_based(\n",
    "        self, join_graph: JoinGraph, plan_table: SizeBasedPlanTable\n",
    "    ) -> Iterator[tuple[Problem, Problem]]:\n",
    "        \"\"\"\n",
    "        Enumerate through the search space by utilizing the size of problems.\n",
    "        :param join_graph: The join graph.\n",
    "        :param plan_table: The underlying plan table.\n",
    "        \"\"\"\n",
    "        num_nodes: int = len(join_graph.adjacency_matrix)\n",
    "        # Enumerate all possible problem sizes starting from 2\n",
    "        for main_size in range(2, num_nodes + 1):\n",
    "            # Enumerate all sizes that are smaller than size_1 // 2\n",
    "            # to avoid duplicate work.\n",
    "            for s1_size in range(1, (main_size // 2) + 1):\n",
    "                s2_size: int = main_size - s1_size\n",
    "\n",
    "                # If both sizes are equal, you need to evaluate symmetric pairs only once\n",
    "                if s1_size == s2_size:\n",
    "                    # Enumerate all problems of size s1_size\n",
    "                    for i, s1 in enumerate(plan_table.entries_with_size(s1_size)):\n",
    "                        # Evaluated entries can be addressed by an index\n",
    "                        # Enumerate all problems of size s2_size, and start with index i + 1\n",
    "                        # to avoid having to re-enumerate symmetric pairs\n",
    "                        for s2 in plan_table.entries_with_size(s2_size, i + 1):\n",
    "                            # New pair was generated\n",
    "                            yield s1, s2\n",
    "                else:\n",
    "                    # Enumerate all problems of size s1_size\n",
    "                    for s1 in plan_table.entries_with_size(s1_size):\n",
    "                        # Enumerate all problems of size s2_size\n",
    "                        for s2 in plan_table.entries_with_size(s2_size):\n",
    "                            # New pair was generated\n",
    "                            yield s1, s2"
   ],
   "id": "198fb6bf64c2afd",
   "outputs": [],
   "execution_count": 3
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "id": "gp10-14-8",
   "source": [
    "## 看一遍 DPsize 的枚举过程\n",
    "\n",
    "下面构造一个 **4 节点环形查询**（`CycleQueryFactory.construct_join_graph(4)`）：\n",
    "\n",
    "    R0 —— R1\n",
    "    |      |\n",
    "    R2 —— R3\n",
    "\n",
    "即边集 {R0–R1, R0–R2, R1–R3, R2–R3}，首尾相连成环。\n",
    "\n",
    "基数表用 `randomize_cardinalities(cardinality_table, 4, 42)` 随机生成（种子 42），\n",
    "**它不代表任何真实数据**，只是让代价有数可算；本实验关心的是枚举量，不是这个代价具体是多少。\n",
    "\n",
    "`print_info=True` 会把每个枚举出的对打印出来，并标注 valid / invalid。\n",
    "请重点观察：**valid 的行数远少于总行数**——这些 invalid 就是 DPsize 白做的功。"
   ]
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-28T11:33:23.803465Z",
     "start_time": "2025-01-28T11:33:23.800394Z"
    }
   },
   "cell_type": "code",
   "source": [
    "from system.query_optimization.cost_function import C_Out\n",
    "from system.query_optimization.utils import randomize_cardinalities, print_results\n",
    "from system.query_optimization.join_graph import CycleQueryFactory\n",
    "\n",
    "# Construct a cycle query with 4 nodes. Therefore, this query has the following structure:\n",
    "# 0 -- 1\n",
    "# |    |\n",
    "# 2 -- 3\n",
    "cycle_query = CycleQueryFactory.construct_join_graph(4)\n",
    "\n",
    "# Define a Cardinality Table, i.e. a table estimating the sizes of problems.\n",
    "# For this demonstration, we just randomize them, i.e. they do not represent realistic data by any means\n",
    "cardinality_table: CardinalityTable = CardinalityTable()\n",
    "randomize_cardinalities(cardinality_table, 4, 42)\n",
    "\n",
    "# Start the enumeration, use C_out as the underlying cost function\n",
    "results = DP_Size().enumerate(\n",
    "    join_graph=cycle_query,\n",
    "    cardinality_table=cardinality_table,\n",
    "    cost_function=C_Out(),\n",
    "    print_info=True,\n",
    ")\n",
    "print_results(results)"
   ],
   "id": "dc04e402e9d6458",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Enumerated valid pair {0}, {1}.\n",
      "Enumerated invalid pair {0}, {2}.\n",
      "Enumerated valid pair {0}, {3}.\n",
      "Enumerated valid pair {1}, {2}.\n",
      "Enumerated invalid pair {1}, {3}.\n",
      "Enumerated valid pair {2}, {3}.\n",
      "Enumerated invalid pair {0}, {0, 1}.\n",
      "Enumerated invalid pair {0}, {0, 3}.\n",
      "Enumerated valid pair {0}, {1, 2}.\n",
      "Enumerated valid pair {0}, {2, 3}.\n",
      "Enumerated invalid pair {1}, {0, 1}.\n",
      "Enumerated valid pair {1}, {0, 3}.\n",
      "Enumerated invalid pair {1}, {1, 2}.\n",
      "Enumerated valid pair {1}, {2, 3}.\n",
      "Enumerated valid pair {2}, {0, 1}.\n",
      "Enumerated valid pair {2}, {0, 3}.\n",
      "Enumerated invalid pair {2}, {1, 2}.\n",
      "Enumerated invalid pair {2}, {2, 3}.\n",
      "Enumerated valid pair {3}, {0, 1}.\n",
      "Enumerated invalid pair {3}, {0, 3}.\n",
      "Enumerated valid pair {3}, {1, 2}.\n",
      "Enumerated invalid pair {3}, {2, 3}.\n",
      "Enumerated invalid pair {0}, {0, 1, 2}.\n",
      "Enumerated invalid pair {0}, {0, 2, 3}.\n",
      "Enumerated invalid pair {0}, {0, 1, 3}.\n",
      "Enumerated valid pair {0}, {1, 2, 3}.\n",
      "Enumerated invalid pair {1}, {0, 1, 2}.\n",
      "Enumerated valid pair {1}, {0, 2, 3}.\n",
      "Enumerated invalid pair {1}, {0, 1, 3}.\n",
      "Enumerated invalid pair {1}, {1, 2, 3}.\n",
      "Enumerated invalid pair {2}, {0, 1, 2}.\n",
      "Enumerated invalid pair {2}, {0, 2, 3}.\n",
      "Enumerated valid pair {2}, {0, 1, 3}.\n",
      "Enumerated invalid pair {2}, {1, 2, 3}.\n",
      "Enumerated valid pair {3}, {0, 1, 2}.\n",
      "Enumerated invalid pair {3}, {0, 2, 3}.\n",
      "Enumerated invalid pair {3}, {0, 1, 3}.\n",
      "Enumerated invalid pair {3}, {1, 2, 3}.\n",
      "Enumerated invalid pair {0, 1}, {0, 3}.\n",
      "Enumerated invalid pair {0, 1}, {1, 2}.\n",
      "Enumerated valid pair {0, 1}, {2, 3}.\n",
      "Enumerated valid pair {0, 3}, {1, 2}.\n",
      "Enumerated invalid pair {0, 3}, {2, 3}.\n",
      "Enumerated invalid pair {1, 2}, {2, 3}.\n",
      "44 pairs were enumerated, out of which 18 were valid!\n",
      "The best join plan is (3⋈((1⋈0)⋈2)) with costs 2011! Overall, we needed to enumerate 44 pairs.\n"
     ]
    }
   ],
   "execution_count": 4
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "id": "gp10-14-10",
   "source": [
    "## 观察：大部分枚举是白做的\n",
    "\n",
    "从上面的输出可以看到，枚举出的对里绝大多数是 invalid（重叠、笛卡尔积、或不连通）。\n",
    "原因上一节已经分析过：DPsize 按大小机械配对，连接图只在\"验证\"阶段起作用，没有参与\"生成\"。\n",
    "\n",
    "一个自然的想法：如果直接枚举**子集**而不是按大小配对，能不能少一些无效对？这就是下一个算法 DPsub。\n",
    "\n",
    "衡量标准不是\"少枚举几个\"这么简单，而是三个条件能不能同时满足：\n",
    "\n",
    "1. 所有合法（不相交、有边相连）的对都要枚举到，不能漏；\n",
    "2. 一个无效对都不要枚举；\n",
    "3. 生成 (S₁, S₂) 时，S₁ 和 S₂ 的最优计划**必须已经算好**（动态规划的前提）。\n",
    "\n",
    "DPsub 解决了第 1 条、改善了第 2 条，但仍然做不到第 2 条。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "id": "gp10-14-11",
   "source": [
    "## DPsub：直接枚举子集划分\n",
    "\n",
    "思路：对每个子集 S，枚举它的所有非空真子集作为左孩子 S₁，右孩子就是 S₂ = S − S₁。\n",
    "\n",
    "与 DPsize 相比的进步：**不会再枚举出重叠对**，因为 S₁ ⊆ S 且 S₂ = S − S₁，两者必然不相交。\n",
    "\n",
    "仍然存在的问题：S₁ 或 S₂ **自身可能不连通**。例如环形查询里 S₁ = {R0, R2} 是连通的，\n",
    "但如果连接图是链形 R0–R1–R2，那么 {R0, R2} 就不连通，它根本不在 plan table 里，\n",
    "这次枚举就作废了。代码用 `if s1 in plan_table and s2 in plan_table` 来判定。\n",
    "\n",
    "实现细节（也是对称去重的来源）：代码并没有枚举 S 的全部真子集，而是用\n",
    "`stop_problem = main_problem.get_most_significant_bit()` 把枚举停在一半处，\n",
    "这样 (S₁, S₂) 与 (S₂, S₁) 只产生一次。所以本仓库 DPsub 的计数同样是\"每个无序对只计一次\"。\n",
    "\n",
    "另外一个与本仓库实现密切相关的点：只有 `join_graph.is_connected(main_problem)` 为真时才会继续枚举。\n",
    "图形状不同，连通子集的个数就不同——**这就是 DPsub 的计数为什么会随图形状变化**。"
   ]
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-28T11:33:23.819072Z",
     "start_time": "2025-01-28T11:33:23.815435Z"
    }
   },
   "cell_type": "code",
   "source": [
    "from system.query_optimization.plan_table import StandardPlanTable\n",
    "from system.interfaces.query_optimization.planning import PlanTable\n",
    "\n",
    "\n",
    "class DP_Sub(PlanEnumerator):\n",
    "    \"\"\"\n",
    "    DPsub generates all possible pairs for each connected problem. It still generates Cartesian Products, that need to be filtered out.\n",
    "    \"\"\"\n",
    "\n",
    "    def enumerate(\n",
    "        self,\n",
    "        join_graph: JoinGraph,\n",
    "        cardinality_table: CardinalityTable,\n",
    "        cost_function: CostFunction,\n",
    "        print_info: bool = False,\n",
    "    ) -> tuple[int, tuple, int]:\n",
    "        \"\"\"\n",
    "        Computes and returns the best join order for the given problem sizes.\n",
    "        :param join_graph: The join graph.\n",
    "        :param cardinality_table: The cardinality table to be used.\n",
    "        :param cost_function: A cost function determining the cost of different operations.\n",
    "        :param print_info: A flag indicating whether additional info should be printed during enumeration.\n",
    "        :return: A tuple, consisting of the costs of the best plan, the best plan as a tuple and the number of pairs that were enumerated.\n",
    "        \"\"\"\n",
    "        # Initialize plan table with the join graph\n",
    "        plan_table: PlanTable = StandardPlanTable(\n",
    "            join_graph, cost_function, cardinality_table\n",
    "        )\n",
    "\n",
    "        # Count number of pairs that were enumerated\n",
    "        enumerated_pairs: int = 0\n",
    "        # Count number of valid pairs\n",
    "        valid_pairs: int = 0\n",
    "\n",
    "        # Go through each generated pair and make sure that both problems s1 and s2\n",
    "        # are connected\n",
    "        for s1, s2 in self.__generate_pairs_sub_based(join_graph):\n",
    "            enumerated_pairs += 1\n",
    "            # s1 | s2 is guaranteed to be connected and disjoint as this point\n",
    "            # We still need to check whether s1 and s2 themselves are connected\n",
    "            # s1 and s2 are connected if they are contained in the plan table\n",
    "            # meaning they were enumerated as a valid problem at an earlier point\n",
    "            if s1 in plan_table and s2 in plan_table:\n",
    "                valid_pairs += 1\n",
    "                # Update the table with the newly generated pair\n",
    "                plan_table.update_table(s1, s2, cardinality_table, cost_function)\n",
    "                if print_info:\n",
    "                    print(f\"Enumerated valid pair {s1.as_set()}, {s2.as_set()}\")\n",
    "            else:\n",
    "                if print_info:\n",
    "                    print(f\"Enumerated invalid pair {s1.as_set()}, {s2.as_set()}\")\n",
    "\n",
    "        problem_with_all_relations: Problem = Problem.get_problem_with_all_relations(\n",
    "            len(join_graph)\n",
    "        )\n",
    "\n",
    "        if print_info:\n",
    "            print(\n",
    "                f\"{enumerated_pairs} pairs were enumerated, out of which {valid_pairs} were valid\"\n",
    "            )\n",
    "\n",
    "        return (\n",
    "            plan_table.get_costs_for_problem(problem_with_all_relations),\n",
    "            plan_table.get_plan_for_problem(problem_with_all_relations),\n",
    "            enumerated_pairs,\n",
    "        )\n",
    "\n",
    "    def __generate_pairs_sub_based(\n",
    "        self, join_graph: JoinGraph\n",
    "    ) -> Iterator[tuple[Problem, Problem]]:\n",
    "        \"\"\"\n",
    "        Generate the required pairs by enumerating all possible problems.\n",
    "        :param join_graph: The join graph.\n",
    "        \"\"\"\n",
    "        num_nodes: int = len(join_graph.adjacency_matrix)\n",
    "        # Enumerate all problems by increasing the underlying problem number\n",
    "        for i in range(1, 1 << num_nodes):\n",
    "            # Find the best plan for this problem\n",
    "            main_problem: Problem = Problem(i)\n",
    "\n",
    "            # Singletons do not have to be considered\n",
    "            if len(main_problem) == 1:\n",
    "                continue\n",
    "\n",
    "            # The problem also needs to be connected, otherwise\n",
    "            # it would not be a valid problem\n",
    "            if not join_graph.is_connected(main_problem):\n",
    "                continue\n",
    "\n",
    "            # Again, there is no need to enumerate symmetric pair twice, which can be achieved\n",
    "            # by stopping the enumeration after the first half is done.\n",
    "            # Setting the stop problem exactly to the singleton pointing to the\n",
    "            # position of the MSB stops the enumeration exactly at this point\n",
    "            stop_problem: Problem = main_problem.get_most_significant_bit()\n",
    "            # Start the enumeration with the smallest singleton of the main problem, stop when the stop problem has been reached\n",
    "            for s1 in Problem.enumerate_all_problems(\n",
    "                main_problem, stop_problem=stop_problem\n",
    "            ):\n",
    "                # Generate the corresponding partner subset\n",
    "                # Given that s1 | s2 == main_problem, we know s1 and s2 are connected via at least one join edge\n",
    "                s2: Problem = main_problem - s1\n",
    "                # New pair was generated\n",
    "                yield s1, s2"
   ],
   "id": "95944d4ffe7155bd",
   "outputs": [],
   "execution_count": 5
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "id": "gp10-14-13",
   "source": [
    "## 再看一遍：DPsub 的枚举过程\n",
    "\n",
    "同样的 4 节点环形查询、同样的随机基数表。请把这次输出的 invalid 行数与 DPsize 那次对比：\n",
    "\n",
    "- 无效对的数量明显下降（重叠对已经消失）；\n",
    "- 但仍然不为零（S₁ 或 S₂ 自身不连通的情况还在）。\n",
    "\n",
    "也就是说 DPsub 只是\"少浪费一点\"，没有根治问题。"
   ]
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-28T11:33:23.828278Z",
     "start_time": "2025-01-28T11:33:23.826246Z"
    }
   },
   "cell_type": "code",
   "source": [
    "results = DP_Sub().enumerate(\n",
    "    join_graph=cycle_query,\n",
    "    cardinality_table=cardinality_table,\n",
    "    cost_function=C_Out(),\n",
    "    print_info=True,\n",
    ")\n",
    "print_results(results)"
   ],
   "id": "c20c1a7bc7729480",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Enumerated valid pair {0}, {1}\n",
      "Enumerated valid pair {1}, {2}\n",
      "Enumerated valid pair {0}, {1, 2}\n",
      "Enumerated invalid pair {1}, {0, 2}\n",
      "Enumerated valid pair {0, 1}, {2}\n",
      "Enumerated valid pair {0}, {3}\n",
      "Enumerated invalid pair {0}, {1, 3}\n",
      "Enumerated valid pair {1}, {0, 3}\n",
      "Enumerated valid pair {0, 1}, {3}\n",
      "Enumerated valid pair {2}, {3}\n",
      "Enumerated valid pair {0}, {2, 3}\n",
      "Enumerated valid pair {2}, {0, 3}\n",
      "Enumerated invalid pair {0, 2}, {3}\n",
      "Enumerated valid pair {1}, {2, 3}\n",
      "Enumerated invalid pair {2}, {1, 3}\n",
      "Enumerated valid pair {1, 2}, {3}\n",
      "Enumerated valid pair {0}, {1, 2, 3}\n",
      "Enumerated valid pair {1}, {0, 2, 3}\n",
      "Enumerated valid pair {0, 1}, {2, 3}\n",
      "Enumerated valid pair {2}, {0, 1, 3}\n",
      "Enumerated invalid pair {0, 2}, {1, 3}\n",
      "Enumerated valid pair {1, 2}, {0, 3}\n",
      "Enumerated valid pair {0, 1, 2}, {3}\n",
      "23 pairs were enumerated, out of which 18 were valid\n",
      "The best join plan is (3⋈((1⋈0)⋈2)) with costs 2011! Overall, we needed to enumerate 23 pairs.\n"
     ]
    }
   ],
   "execution_count": 6
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "id": "gp10-14-15",
   "source": [
    "## 问题的根源：连接图只被用来\"验证\"，没被用来\"生成\"\n",
    "\n",
    "DPsize 与 DPsub 的共同毛病：**先按集合论的方式把候选生成出来，再拿连接图去判断合不合法**。\n",
    "生成阶段不看图，就一定会生成大量图里根本不存在的组合。\n",
    "\n",
    "正确的做法是反过来——**让连接图直接驱动生成**。这就是 DPccp 的出发点，也是本 notebook 后半段的核心。\n",
    "\n",
    "思路分两步：\n",
    "\n",
    "1. 先解决\"怎么不重复、不遗漏地枚举所有**连通子图**（csg）\"；\n",
    "2. 再解决\"对每个 csg，怎么枚举它的**互补连通子图**（cmp），并保证动态规划的顺序正确\"。\n",
    "\n",
    "第一步就是下一节的内容。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "id": "gp10-14-16",
   "source": [
    "## 第一步：不重复地枚举所有连通子图（csg）\n",
    "\n",
    "朴素做法会从每个单点出发做 BFS/DFS 扩展，但那样同一个集合会被很多条路径重复生成\n",
    "（例如 {R0, R1, R2} 可以由 R0→R1→R2 生成，也可以由 R1→R0→R2 生成）。\n",
    "\n",
    "解决办法是给每个节点定一个**编号**（这里就用关系的 id），然后约定一条规则：\n",
    "\n",
    "> 扩展集合 S 时，只允许加入编号**大于** S 中最小编号的邻居。\n",
    "\n",
    "配套一个**排除集 X**（exclude set）：X 始终包含\"编号小于等于当前集合最小编号\"的关系，\n",
    "扩展时直接从邻居里把 X 减掉即可。\n",
    "\n",
    "举个例子（链形 R0–R1–R2–R3）：\n",
    "\n",
    "- 从 {R2} 出发，可以扩展到 {R2, R3}（R3 编号更大），不能扩展到 {R1, R2}（R1 编号更小）；\n",
    "- {R1, R2} 会由从 {R1} 出发的那条路径生成，所以不会漏；\n",
    "- 于是每个连通子集**恰好**被生成一次。\n",
    "\n",
    "代码里还有一处细节：递归调用子集合 S′ 时，会把\"当前层用过的邻居\"也并进 X′\n",
    "（`exclude | neighbors`），这样更深一层的扩展也不会绕回已经处理过的分支。\n",
    "文字描述对应代码里的 `generate_recursively(join_graph, start | neighbor_problem, exclude | neighbors)`。\n",
    "\n",
    "这样一来，递归**不再需要显式的终止条件**：当没有可加的邻居时，迭代自然结束。"
   ]
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-28T11:33:23.841100Z",
     "start_time": "2025-01-28T11:33:23.838652Z"
    }
   },
   "cell_type": "code",
   "source": [
    "def generate_connected_problems(join_graph: JoinGraph) -> Iterator[Problem]:\n",
    "    \"\"\"\n",
    "    Generate all connected problems of the join graph, according to the description given above.\n",
    "    :param join_graph: The join graph.\n",
    "    \"\"\"\n",
    "    problem_with_all_relations: Problem = Problem.get_problem_with_all_relations(\n",
    "        len(join_graph)\n",
    "    )\n",
    "\n",
    "    # Top-level exclude set, always ignore neighbors whose order is <= than the current singleton. See the cell above for more details.\n",
    "    exclude: Problem = problem_with_all_relations\n",
    "\n",
    "    # Traverse all singleton sets (sets containing only one relation) as starting point. Go from the highest bit to the lowest bit.\n",
    "    for singleton in reversed(problem_with_all_relations):\n",
    "        yield singleton\n",
    "\n",
    "        # Recursively extend the singleton with all its neighbors, as long as the order of the neighbor is not lower/equal than the singleton\n",
    "        yield from generate_recursively(join_graph, singleton, exclude)\n",
    "\n",
    "        # Current problem can now be considered\n",
    "        exclude -= singleton\n",
    "\n",
    "\n",
    "def generate_recursively(\n",
    "    join_graph: JoinGraph,\n",
    "    start: Problem,\n",
    "    exclude: Problem,\n",
    ") -> Iterator[Problem]:\n",
    "    \"\"\"\n",
    "    Recursively extends the given start problem with its neighbors. Ignores relations contained in the exclude problem.\n",
    "    :param join_graph: The join graph.\n",
    "    :param start: The start problem.\n",
    "    :param exclude: The exclude problem.\n",
    "    \"\"\"\n",
    "    # Get direct neighbors of the problem, ignore those from the exclude set\n",
    "    neighbors: Problem = join_graph.get_neighbors(start) - exclude\n",
    "\n",
    "    # Get all problems contained in the neighbor set\n",
    "    neighbor_problems: list[Problem] = Problem.get_all_problems(neighbors)\n",
    "\n",
    "    # Combine all neighbor problems with the start problem\n",
    "    for neighbor_problem in neighbor_problems:\n",
    "        yield start | neighbor_problem\n",
    "\n",
    "    for neighbor_problem in neighbor_problems:\n",
    "        # Proceed recursively, extend the exclude set with the current neighbors\n",
    "        yield from generate_recursively(\n",
    "            join_graph,\n",
    "            start | neighbor_problem,\n",
    "            exclude | neighbors,\n",
    "        )"
   ],
   "id": "d6d427407d5a3523",
   "outputs": [],
   "execution_count": 7
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "id": "gp10-14-18",
   "source": [
    "## 验证：一个重复都没有\n",
    "\n",
    "跑一下 `generate_connected_problems(cycle_query)`，把 4 节点环形查询的所有连通子集打印出来。\n",
    "\n",
    "请核对两件事：\n",
    "\n",
    "1. **没有重复**：同一个集合只出现一次；\n",
    "2. **没有不连通的集合**：例如环形图里 {R0, R3} 没有直接边，它不会出现在输出里。\n",
    "\n",
    "还可以自己数一下总数，与\"环形 n = 4 的连通子集个数\"对上。"
   ]
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-28T11:33:23.851674Z",
     "start_time": "2025-01-28T11:33:23.849862Z"
    }
   },
   "cell_type": "code",
   "source": [
    "for problem in generate_connected_problems(cycle_query):\n",
    "    print(f\"{problem}\")"
   ],
   "id": "bab1690c440b9d09",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "{3}\n",
      "{2}\n",
      "{2, 3}\n",
      "{1}\n",
      "{1, 2}\n",
      "{1, 2, 3}\n",
      "{0}\n",
      "{0, 1}\n",
      "{0, 3}\n",
      "{0, 1, 3}\n",
      "{0, 1, 2}\n",
      "{0, 2, 3}\n",
      "{0, 1, 2, 3}\n"
     ]
    }
   ],
   "execution_count": 8
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "id": "gp10-14-20",
   "source": [
    "## 第二步：从 csg 到 ccp（连通互补对）\n",
    "\n",
    "现在我们能不重复地枚举连通子图了，但动态规划要的是**成对**的东西。目标是枚举所有\n",
    "**(S₁, S₂)**，同时满足三条：\n",
    "\n",
    "1. **完整性**：所有合法（不相交、有边相连）的对都要枚举到，不能漏；\n",
    "2. **无浪费**：一个无效对都不要枚举；\n",
    "3. **顺序正确**：生成 (S₁, S₂) 时，S₁ 与 S₂ 的最优计划必须**已经算好**——这是动态规划的前提。\n",
    "\n",
    "术语：\n",
    "\n",
    "| 缩写 | 全称 | 含义 |\n",
    "| --- | --- | --- |\n",
    "| csg | connected subgraph | 在连接图上连通的关系子集 |\n",
    "| cmp | complement（这里指 complementing subgraph） | 与某个 csg 不相交、且与之有边相连的连通子集 |\n",
    "| ccp | connected complementing pair | 一对 (S₁, S₂)，两者都连通、互不相交、且之间有边 |\n",
    "\n",
    "只有 ccp 才对应一个合法的、非笛卡尔积的连接。\n",
    "\n",
    "难点在第 3 条：如果拿到一个 csg 就立刻枚举它所有的 cmp，那么生成某个 (S₁, S₂) 时，\n",
    "S₁ 的最优计划可能还没算出来。\n",
    "\n",
    "解决办法仍然是\"编号\"这个抓手。回顾 csg 的生成规则：扩展时只加编号更大的邻居，\n",
    "于是**集合被生成的顺序本身就带有编号信息**。利用这一点：枚举 S₁ 的 cmp 时，\n",
    "排除掉任何含有\"编号小于等于 S₁ 中最小编号\"的关系的集合，就能保证顺序正确。\n",
    "代码里对应两行：\n",
    "\n",
    "- `always_exclude_set = csg | csg.get_smaller_or_equal_relations()`：cmp 里绝不允许出现编号更小的关系；\n",
    "- 从 csg 的直接邻居出发，复用 `generate_recursively` 逐层扩展 cmp。"
   ]
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-28T11:33:23.863235Z",
     "start_time": "2025-01-28T11:33:23.861002Z"
    }
   },
   "cell_type": "code",
   "source": [
    "def generate_ccp_pairs(join_graph: JoinGraph) -> Iterator[tuple[Problem, Problem]]:\n",
    "    \"\"\"\n",
    "    Enumerate all ccp pairs of the given join graph.\n",
    "    :param join_graph: The join graph.\n",
    "    \"\"\"\n",
    "    for csg in generate_connected_problems(join_graph):\n",
    "        # We do not want the csg_problem to be contained in any cmp problem at any problem\n",
    "        # We also always want to ignore cmp problems that contain relations whose number is smaller than the smallest in the csg\n",
    "        always_exclude_set: Problem = csg | csg.get_smaller_or_equal_relations()\n",
    "\n",
    "        # Start the enumeration of the cmp problems with the direct neighbors of the csg problem\n",
    "        csg_neighbors: Problem = join_graph.get_neighbors(csg) - always_exclude_set\n",
    "\n",
    "        # Define an additional top-level ignore set, we still want to ignore all problems with smaller/equal numbers than the current problem\n",
    "        # This time, however, we only enumerate the direct neighbors of the csg, thus we need to gradually reduce the neighbor set, instead of the whole problem.\n",
    "        exclude_set: Problem = csg_neighbors\n",
    "\n",
    "        # Again traverse all singleton problems in reversed order\n",
    "        for singleton in reversed(csg_neighbors):\n",
    "            yield csg, singleton\n",
    "\n",
    "            # The exclude_set is gradually reduced, we still need to make sure that we never include a relation from the always_exclude_set\n",
    "            # Make use of the already defined recursive function, as the required behavior here is exactly the same.\n",
    "            for cmp_problem in generate_recursively(\n",
    "                join_graph,\n",
    "                singleton,\n",
    "                exclude_set | always_exclude_set,\n",
    "            ):\n",
    "                yield csg, cmp_problem\n",
    "\n",
    "            # Current problem can now be considered\n",
    "            exclude_set -= singleton"
   ],
   "id": "a23d28b93a981f5a",
   "outputs": [],
   "execution_count": 9
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "id": "gp10-14-22",
   "source": [
    "## 试跑一下 ccp 枚举\n",
    "\n",
    "跑 `generate_ccp_pairs(cycle_query)`，把 4 节点环形查询的所有 ccp 打印出来。\n",
    "\n",
    "对照上一节的三条要求检查输出：\n",
    "\n",
    "- 每一对 (S₁, S₂) 两侧都连通、不相交、之间有边——**没有无效对**；\n",
    "- (S₁, S₂) 与 (S₂, S₁) 只出现一次——**没有对称重复**；\n",
    "- 观察输出的先后顺序：S₁ 从编号大的单点开始，S₂ 逐步长大——**子问题先于父问题**。\n",
    "\n",
    "这个数就是 DPccp 在该图上的枚举量（环形 n = 4 时为 18）。"
   ]
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-28T11:33:23.876349Z",
     "start_time": "2025-01-28T11:33:23.874369Z"
    }
   },
   "cell_type": "code",
   "source": [
    "for ccp_pair in generate_ccp_pairs(cycle_query):\n",
    "    print(f\"Enumerated ccp pair ({ccp_pair[0].as_set()}, {ccp_pair[1].as_set()})\")"
   ],
   "id": "6ba1cb1f486e0507",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Enumerated ccp pair ({2}, {3})\n",
      "Enumerated ccp pair ({1}, {2})\n",
      "Enumerated ccp pair ({1}, {2, 3})\n",
      "Enumerated ccp pair ({1, 2}, {3})\n",
      "Enumerated ccp pair ({0}, {3})\n",
      "Enumerated ccp pair ({0}, {2, 3})\n",
      "Enumerated ccp pair ({0}, {1})\n",
      "Enumerated ccp pair ({0}, {1, 2})\n",
      "Enumerated ccp pair ({0}, {1, 2, 3})\n",
      "Enumerated ccp pair ({0, 1}, {3})\n",
      "Enumerated ccp pair ({0, 1}, {2})\n",
      "Enumerated ccp pair ({0, 1}, {2, 3})\n",
      "Enumerated ccp pair ({0, 3}, {2})\n",
      "Enumerated ccp pair ({0, 3}, {1})\n",
      "Enumerated ccp pair ({0, 3}, {1, 2})\n",
      "Enumerated ccp pair ({0, 1, 3}, {2})\n",
      "Enumerated ccp pair ({0, 1, 2}, {3})\n",
      "Enumerated ccp pair ({0, 2, 3}, {1})\n"
     ]
    }
   ],
   "execution_count": 10
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "id": "gp10-14-24",
   "source": [
    "## ccp 枚举到位了\n",
    "\n",
    "上面的函数已经同时满足三条要求：不漏、不浪费、顺序正确。\n",
    "剩下的工作只是把它接到 plan table 上——这就是 DPccp。\n",
    "\n",
    "对比一下三种算法的\"过滤\"发生在哪里：\n",
    "\n",
    "| 算法 | 无效对 | 生成时是否参考连接图 |\n",
    "| --- | --- | --- |\n",
    "| DPsize | 重叠对 + 笛卡尔积对 + 不连通对，数量最多 | 否 |\n",
    "| DPsub | 只剩不连通对 | 否 |\n",
    "| DPccp | **零** | 是 |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "id": "gp10-14-25",
   "source": [
    "## DPccp：只枚举有用的\n",
    "\n",
    "DPccp 的 `enumerate` 极其干净：直接用上一节的 `generate_ccp_pairs` 迭代，\n",
    "**不做任何有效性检查**（`if s1 in plan_table` 之类的判断全部省掉），\n",
    "因为生成器已经保证产出的每一个对都是合法 ccp，而且子问题都已算好。\n",
    "\n",
    "这就是\"把判断从运行时挪到生成时\"的价值：省下的不只是那几次 `if`，\n",
    "更重要的是枚举总量本身变小了。"
   ]
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-28T11:33:23.890389Z",
     "start_time": "2025-01-28T11:33:23.887986Z"
    }
   },
   "cell_type": "code",
   "source": [
    "class DP_CCP(PlanEnumerator):\n",
    "    \"\"\"\n",
    "    Uses the join graph to enumerate all ccp pairs, without enumerating any invalid pair.\n",
    "    \"\"\"\n",
    "\n",
    "    def enumerate(\n",
    "        self,\n",
    "        join_graph: JoinGraph,\n",
    "        cardinality_table: CardinalityTable,\n",
    "        cost_function: CostFunction,\n",
    "        print_info: bool = False,\n",
    "    ) -> tuple[int, tuple, int]:\n",
    "        \"\"\"\n",
    "        Computes and returns the best join order for the given problem sizes.\n",
    "        :param join_graph: The join graph.\n",
    "        :param cardinality_table: The cardinality table to be used.\n",
    "        :param cost_function: A cost function determining the cost of different operations.\n",
    "        :param print_info: A flag indicating whether additional info should be printed during enumeration.\n",
    "        :return: A tuple, consisting of the costs of the best plan, the best plan as a tuple and the number of pairs that were enumerated.\n",
    "        \"\"\"\n",
    "        # Initialize plan table with the join graph\n",
    "        plan_table: PlanTable = StandardPlanTable(\n",
    "            join_graph, cost_function, cardinality_table\n",
    "        )\n",
    "\n",
    "        # Count number of pairs that were enumerated\n",
    "        enumerated_pairs: int = 0\n",
    "\n",
    "        # Use previously defined generator to iterator through each ccp\n",
    "        # Note that no checks are required here anymore, as the ccp is guaranteed to be valid\n",
    "        for s1, s2 in generate_ccp_pairs(join_graph):\n",
    "            enumerated_pairs += 1\n",
    "\n",
    "            if print_info:\n",
    "                print(f\"Enumerated valid pair {s1.as_set()}, {s2.as_set()}\")\n",
    "\n",
    "            # Update the table with the newly generated pair\n",
    "            plan_table.update_table(s1, s2, cardinality_table, cost_function)\n",
    "\n",
    "        problem_with_all_relations: Problem = Problem.get_problem_with_all_relations(\n",
    "            len(join_graph)\n",
    "        )\n",
    "\n",
    "        if print_info:\n",
    "            print(f\"Enumerated {enumerated_pairs} valid pairs\")\n",
    "\n",
    "        return (\n",
    "            plan_table.get_costs_for_problem(problem_with_all_relations),\n",
    "            plan_table.get_plan_for_problem(problem_with_all_relations),\n",
    "            enumerated_pairs,\n",
    "        )"
   ],
   "id": "bb1fcefde9f230b8",
   "outputs": [],
   "execution_count": 11
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "id": "gp10-14-27",
   "source": [
    "## 最后跑一遍同一个例子\n",
    "\n",
    "还是那个 4 节点环形查询，还是那张随机基数表。\n",
    "\n",
    "请把这次输出与 DPsize、DPsub 的两次输出放在一起看：\n",
    "\n",
    "- 得到的**最优计划相同**、**代价相同**——三种算法只是枚举顺序不同，最终答案是同一个\n",
    "  （这也说明\"省枚举量\"换来的是速度，不是不同的计划）；\n",
    "- 枚举出的对的数量就是合法 ccp 的数量（环形 n = 4 时为 18）；\n",
    "- **一行 invalid 都没有**。"
   ]
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-28T11:33:23.897875Z",
     "start_time": "2025-01-28T11:33:23.895880Z"
    }
   },
   "cell_type": "code",
   "source": [
    "results = DP_CCP().enumerate(\n",
    "    join_graph=cycle_query,\n",
    "    cardinality_table=cardinality_table,\n",
    "    cost_function=C_Out(),\n",
    "    print_info=True,\n",
    ")\n",
    "print_results(results)"
   ],
   "id": "48dd1eba230125de",
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Enumerated valid pair {2}, {3}\n",
      "Enumerated valid pair {1}, {2}\n",
      "Enumerated valid pair {1}, {2, 3}\n",
      "Enumerated valid pair {1, 2}, {3}\n",
      "Enumerated valid pair {0}, {3}\n",
      "Enumerated valid pair {0}, {2, 3}\n",
      "Enumerated valid pair {0}, {1}\n",
      "Enumerated valid pair {0}, {1, 2}\n",
      "Enumerated valid pair {0}, {1, 2, 3}\n",
      "Enumerated valid pair {0, 1}, {3}\n",
      "Enumerated valid pair {0, 1}, {2}\n",
      "Enumerated valid pair {0, 1}, {2, 3}\n",
      "Enumerated valid pair {0, 3}, {2}\n",
      "Enumerated valid pair {0, 3}, {1}\n",
      "Enumerated valid pair {0, 3}, {1, 2}\n",
      "Enumerated valid pair {0, 1, 3}, {2}\n",
      "Enumerated valid pair {0, 1, 2}, {3}\n",
      "Enumerated valid pair {0, 2, 3}, {1}\n",
      "Enumerated 18 valid pairs\n",
      "The best join plan is (3⋈((1⋈0)⋈2)) with costs 2011! Overall, we needed to enumerate 18 pairs.\n"
     ]
    }
   ],
   "execution_count": 12
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "id": "gp10-14-29",
   "source": [
    "## 小结：同样的答案，少做了多少功\n",
    "\n",
    "三种算法在 4 节点环形查询上给出了同一个最优计划与同一个代价，差别只在枚举了多少个对。\n",
    "DPsize 白枚举了绝大多数，DPsub 少一些，DPccp 一个都不浪费。\n",
    "\n",
    "但要回答\"到底省了多少\"，必须上规模、换图形状。下一节做这个基准测试。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "id": "gp10-14-30",
   "source": [
    "## 在四种图形状上比较三种算法\n",
    "\n",
    "下面这个基准用四种典型的连接图形状（n 从 5 到 9）：\n",
    "\n",
    "- **链形 chain**：R0–R1–R2–…，多表主外键串联，最常见的形状；\n",
    "- **星形 star**：中心一张事实表、其余是维度表，数据仓库的典型形状；\n",
    "- **环形 cycle**：链形再把首尾连起来；\n",
    "- **团形 clique**：任意两表之间都有连接谓词，连接图最密，也最不现实。\n",
    "\n",
    "**请注意这张图画的是\"枚举耗时\"（秒），不是枚举量。** 耗时里包含了 Python 解释器开销，\n",
    "所以它是\"趋势\"而不是\"精确计数\"。\n",
    "\n",
    "**要看枚举量，请读 `enumerate()` 的第三个返回值**：把 `randomize_cardinalities_and_enumerate_join_graph(...)`\n",
    "返回的第三个元素取出来即可（本 notebook 的单元测试里就是这么断言的）。\n",
    "\n",
    "### 枚举量实测对照表（n = 6，仓库真实输出）\n",
    "\n",
    "| 图形状 | DPsize | DPsub | DPccp |\n",
    "| --- | --- | --- | --- |\n",
    "| 链形 | 150 | 99 | **35** |\n",
    "| 星形 | 350 | 211 | **80** |\n",
    "| 环形 | 261 | 187 | **75** |\n",
    "| 团形 | 1171 | 301 | **301** |\n",
    "\n",
    "其他规模（只列 DPccp，顺序为 链形 / 星形 / 环形 / 团形）：\n",
    "\n",
    "| n | 链形 | 星形 | 环形 | 团形 |\n",
    "| --- | --- | --- | --- | --- |\n",
    "| 4 | 10 | 12 | 18 | 25 |\n",
    "| 5 | 20 | 32 | 40 | 90 |\n",
    "| 7 | 56 | 192 | 126 | 966 |\n",
    "| 8 | 84 | 448 | 196 | 3025 |\n",
    "\n",
    "**计数口径：三个算法都做对称去重，每个 (S₁, S₂) 对只计一次；若按有序对计数，每个数字翻倍。**\n",
    "\n",
    "从表里能读出三条规律：\n",
    "\n",
    "1. **DPccp 永远不比另两个多**——四种形状、多个规模，无一例外；\n",
    "2. **图越稀疏，DPccp 的优势越大**：链形 n = 6 时 35 对 vs DPsub 的 99 对，省掉约 65%；\n",
    "3. **团形是最坏情况**：团形 n = 6 时 DPccp 与 DPsub 都是 301，DPccp 一点都没省。\n",
    "   原因是团形里任意子集都连通、任意两个不相交子集之间都有边，ccp 的条件自动成立，\n",
    "   剪枝无从下手。好在真实查询几乎不会是团形。\n",
    "\n",
    "### 两条通项公式（可代入上表验证）\n",
    "\n",
    "- 链形：DPccp = C(n+1, 3)。链形里连通子集一定是连续区间，一个 ccp 就是两个相邻区间，\n",
    "  确定它等价于从 n+1 个\"间隙\"里选 3 个边界。n = 4…8 依次得 10、20、35、56、84。\n",
    "- 团形：DPccp = (3ⁿ − 2^(n+1) + 1) / 2，恰好等于本仓库团形下的 DPsub。\n",
    "  n = 6 得 301，n = 7 得 966，n = 8 得 3025。\n",
    "\n",
    "### 一个常见误区\n",
    "\n",
    "教科书（Moerkotte & Neumann, VLDB 2006）给出 DPsize / DPsub 的枚举量公式时，\n",
    "是按\"DP 表里放着全部 2ⁿ 个子集、且按有序对计数\"推的，因此**只由 n 决定、与图形状无关**；\n",
    "其中 DPsub 的闭式是 3ⁿ − 2^(n+1) + 1。\n",
    "\n",
    "**那不是本仓库的实测值。** 本仓库的 DP 表只收录连通子问题，还做了对称去重，\n",
    "所以连 DPsize 都随图形状变化——上面的表里，n = 6 时 DPsize 这一列四种形状各不相同\n",
    "（链形 150、星形 350、环形 261、团形 1171）。\n",
    "\n",
    "结论：**公式刻画的是算法的思想，实测值反映的是这一份具体实现的取舍。**\n",
    "讲课和做题都不要拿教材公式去核对实测值，也不要反过来。"
   ]
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-28T11:41:36.299128Z",
     "start_time": "2025-01-28T11:41:36.295018Z"
    }
   },
   "cell_type": "code",
   "source": "from system.query_optimization.utils import (\n    randomize_cardinalities_and_enumerate_join_graph,\n)\nfrom system.query_optimization.join_graph import JoinGraphFactory\nimport time\n\n# Number of join-graph nodes (relations) the benchmark below runs for. The\n# enumeration cost grows exponentially in the number of relations -- for the\n# clique query in particular DP_Size/DP_Sub explode -- so the largest value\n# dominates the total run time. We cap it at 9 to keep CI fast (~2s instead of\n# ~90s). For the lecture, uncomment the range(5, 15) line to show the full\n# exponential blow-up in the plots.\nnum_nodes: list[int] = list(range(5, 10))\n# num_nodes: list[int] = list(range(5, 15))\n\n\ndef evaluate_algorithms_on_graphs(graph_factory: Type[JoinGraphFactory]):\n    \"\"\"\n    Evaluate all enumeration algorithms on different graph sizes.\n    :param graph_factory: A static class type used to define methods to create certain graphs.\n    \"\"\"\n    # Store number of enumerated pairs and the required time\n    result_dict: dict[str, list[int]] = {\n        algorithm: [] for algorithm in [\"DP_CCP\", \"DP_Sub\", \"DP_Size\"]\n    }\n    method_dict: dict[str, PlanEnumerator] = {\n        \"DP_CCP\": DP_CCP(),\n        \"DP_Sub\": DP_Sub(),\n        \"DP_Size\": DP_Size(),\n    }\n\n    # Do an experiment for each node size\n    for num in num_nodes:\n        # Perform the experiment for each algorithm and store the result in the dictionary from above\n        for algorithm, algorithm_result_lists in result_dict.items():\n            start_time = time.time()\n            _, _, _ = randomize_cardinalities_and_enumerate_join_graph(\n                graph_factory.construct_join_graph(num), method_dict[algorithm]\n            )\n            end_time = time.time()\n            algorithm_result_lists.append(end_time - start_time)\n\n    # Plot data\n    x = np.arange(len(num_nodes))\n    fig, ax = plt.subplots(layout=\"constrained\", figsize=(4, 3))\n    for shape, shape_list in result_dict.items():\n        ax.plot(x, shape_list, label=shape)\n\n    ax.set_ylabel(\"Required Enumeration time in s\")\n    ax.set_xlabel(\"# of Nodes\")\n    ax.set_xticks(x, num_nodes)\n    fig.legend(bbox_to_anchor=(0.2, 0.99), loc=\"upper left\")",
   "id": "174eb80f3eed454a",
   "outputs": [],
   "execution_count": null
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "id": "gp10-14-32",
   "source": [
    "### 链形查询\n",
    "\n",
    "预期：链形是最稀疏的形状，DPccp 在这里优势最大（n = 6 时 35 对，而 DPsub 要 99 对）。\n",
    "三条曲线的间距应当比较明显，且 DPccp 最低。"
   ]
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-28T11:33:54.749519Z",
     "start_time": "2025-01-28T11:33:53.378014Z"
    }
   },
   "cell_type": "code",
   "source": [
    "from system.query_optimization.join_graph import ChainQueryFactory\n",
    "\n",
    "evaluate_algorithms_on_graphs(ChainQueryFactory)"
   ],
   "id": "e328a7020549284e",
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Figure size 400x300 with 1 Axes>"
      ],
      "image/png": "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pzm/du3aIpdTkPIcrFy50ly3e/fuYivz1KlT5rqff/7ZsvL27dtnq1atmu3vv/+21apVyzZ58uQil5VfeUOGDLH169fPZgV5lde/f3/b4MGDbVahnk0BocmDTzJ169bFoEGD3Na1zIuvv/4al156qelVVKxYEa1atcJ7772H4iI1NRWffPIJ7rzzzkIFKy0INGEtWLAAW7duNft//vmneTq98sorLSkvPT3dmO5yP6XRpGVlL5Xs3LkThw4dMr0oBzRTtGvXDsuXL4c/whTB/O6ULl262L6z06ZNM58rn96tCuJ722234fHHHzeWjuJg4cKFpg24+OKLMWzYMBw/ftyy9/a///3P9BbZ42aZ/H6ez5ToKhKbAsAPfcaMGZg/f74x/bDx6NKlS3a6AnezY8cOU079+vXxww8/mC/ZQw89hI8++gjFAb9gJ0+eNDZjq3jqqadw6623mi47zYUUVI6nUMitgFG+O3ToYMaFOJZA4aGgsrGnecJKKDSkUqVKOY5z33HOn6BpkGM4AwYMsDyQ5LfffosSJUqYh4jJkycbM2n58uUtKYsmydDQUPNbLA769OmDmTNnmocylr1o0SLzMMbvrrs5cuQI4uPj8fLLL5tyf/zxR1x//fW44YYbTLluwbI+kx8TFxdnK1mypO3999+35P5hYWG2Dh065Dj24IMP2tq3b28rDnr16mW7+uqrLS2D5qPq1aub9fr1620zZ860lS1b1jZjxgzLyty2bZuta9euxoRAk1qbNm2M2a5hw4aWmiiWLVtmjh04cCDHdTfffLPtlltucXt5njSjpaam2q655hpbq1atjFnL6jLj4+Nt//zzj2358uXGLFu7dm3b4cOH3V7eqlWrbJUqVbLt378/+5jVZrTcbN++3TIzId8Xjw0YMCDHdfxb3nrrrTZ3oJ5NIaBpgN3Nbdu2WXJ/eiw1btw4x7FGjRpZarpzsHv3bvz8889mMN1KaIpw9G7oxUTzxCOPPIJx48ZZViY93viUxic45u5YuXKl8aSiadRKKleubNaHDx/OcZz7jnP+AD/LW265xXyH2MMojvD4dLS46KKL0L59e3zwwQem58G1u1myZIl5+q9Zs6Ypgwvf54gRI4xjQnFQt25d02uzot3hffmerGx3JDaFgI0VPTcoClZATzR68zjDsQ267FrN9OnTjb22b9++lpZDD6bc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"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "execution_count": 20
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "id": "gp10-14-34",
   "source": [
    "### 星形查询\n",
    "\n",
    "预期：星形比链形密一些（中心表与所有维度表相连），DPccp 与 DPsize/DPsub 的差距会缩小，\n",
    "但 DPccp 仍然不劣于另两者。"
   ]
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-28T11:41:48.184868Z",
     "start_time": "2025-01-28T11:41:41.308763Z"
    }
   },
   "cell_type": "code",
   "source": [
    "from system.query_optimization.join_graph import StarQueryFactory\n",
    "\n",
    "evaluate_algorithms_on_graphs(StarQueryFactory)"
   ],
   "id": "c3bef8f1d79764ee",
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Figure size 400x300 with 1 Axes>"
      ],
      "image/png": "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"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "execution_count": 26
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "id": "gp10-14-36",
   "source": [
    "### 环形查询\n",
    "\n",
    "预期：环形只比链形多一条边，规律与链形接近，DPccp 依然领先。"
   ]
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-28T11:35:18.768488Z",
     "start_time": "2025-01-28T11:35:16.218551Z"
    }
   },
   "cell_type": "code",
   "source": "evaluate_algorithms_on_graphs(CycleQueryFactory)",
   "id": "ff579d7f8741fd01",
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Figure size 400x300 with 1 Axes>"
      ],
      "image/png": "iVBORw0KGgoAAAANSUhEUgAAAZsAAAE3CAYAAAB8RuxtAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAABIsElEQVR4nO2dB3hUVfrG30lPCL0TmiAIiBTpiF0BRcSOiIK46z6WdRVBQF1FWBWVv8JaVgRFwIauorsq2FiRIoI0RWmCktB7gJCe3P/znsmESUhIZjI3094fz+XWuedMyfnuV873OSzLsiCEEELYSISdNxdCCCGIhI0QQgjbkbARQghhOxI2QgghbEfCRgghhO1I2AghhLAdCRshhBC2E4UwIz8/H7t370bVqlXhcDj83R0hhAhqOFXz+PHjaNSoESIiStdfwk7YUNA0adLE390QQoiQYseOHWjcuHGp58NO2FCjcX0w1apV83d3hBAiqDl27Jh5gHeNraURdsLGZTqjoJGwEUII31CWW0IBAkIIIWxHwkYIIYTthJ0ZLZDIy8tDTk6Ov7sh3IiJiTltRI0QwjskbPwUKrh3716kpqb6uyuiGBQ0Z5xxhhE6QgjfIWHjB1yCpl69ekhISNB8nwCbg7Vnzx40bdpU34sQPkTCxg+mM5egqV27tr+7I4pRt25dI3Byc3MRHR3t7+4IETLIOF3JuHw01GhE4OEyn/GhQAjhO6TZ+AmZaAITfS8i7Fj7jnPd6nIgsZ5tzUizEUKIcGbJ88B/7gF2r7W1GQkbIYQIV47vAw5vo04PNOlha1MSNqLc3H777cbMxIXO8/r16+Pyyy/HzJkzTSSXi+bNmxdeV6VKFZx77rn497//7VGupUcffRRt2rRBXFwcGjRogMsuuwzz5s0zYeMutm7dihEjRpjkf7GxsSZkeciQIVi1alXhNa5+cKlevTrOO+88/O9///PhpyJEEJPyvXPdoD0QX8PWpiRshEf079/fhAZv374dCxYswMUXX4z7778fV111lYngcjFx4kRz3dq1a9GtWzcMHjwY339f8MM+DYzU6927N+bMmYOHH34Ya9asweLFi83rx4wZg6NHj5rrKFC6dOmCLVu24LXXXsOGDRvw8ccfGwE1atSoIvd88803TV+WLVuGOnXqmL7+/vvvNnw6QgQZyQV/k01729+WFWYcPXqUj8Zm7Q8yMjKsDRs2mHWwMXz4cGvQoEGnHF+4cKH5TGfMmGH2mzVrZk2ZMqXwfE5OjpWQkGCNGzeuzDbuvvtuq0qVKtauXbtOOXf8+HFzr/z8fOvss8+2unTpYuXl5Z1y3ZEjRwq32a+PP/64cJ/35bFp06aF3PcjhMf8q7dlja9mWb+c/Buxa0z1STQan0Zr1LBXBQtVOB5m5PgnzDY+OtIn0VeXXHIJOnbsaMxcf/7zn085HxUVZcxu2dnZp70PTXFz587F0KFDTSGm4iQmJpo1taVff/0V7777bompZU73W4yPjzfrsvoiRMiTcQTY96tzu5n9mo3HwubZZ581NnmaNchNN92Ejz76yNjV58+fbwYdUX4oaNo9/qVf2t4wsR8SYnwT/U7z1c8//3zKcQ7qzz//vDF/USidjoMHD+LIkSPmXqfjt99+K2zTE9LT0/H3v/8dkZGRuPDCCz16rRAhR8oKPu4CtVvZGvLstc9m2rRphZUuv/76a7PQdn/FFVfgoYce8uhetMUPHDjQPMXyCfuTTz4p8zWLFi0yDmc6hM8880zMmjXL07cgbNLQ3LWksWPHGk2Ek1f5gPLMM89gwIABZd6jvG15AoMG2BcWd+KD0RtvvIEOHTp4dA8hQo7kZc51s16V0lyUN3m9XMLms88+M5pN3759jbbTo4dnoXMnTpwwmtAdd9yB6667rszr//jjDzNg3XXXXXjnnXewcOFCY7Zp2LAh+vXrh2CEpixqGP5q21ds3LjRRIO54IMHo9c4yDNqrTzmOqaKoQls06ZNp72udevWZs3rOnfuXOZ9p0yZYqLZGI3GNoQQOBkc0Oy8wBQ2NWvWNCWVKXC++OILPPnkk4VPm56m+KA2xMUTrYoDGs0ypG3btli6dKkZTIJV2HAQ9pUpy18wlHj9+vUYOXJk4TFGfVHz9AT6X26++Wa89dZbGD9+/Cl+m7S0NBMK3alTJ7Rr1878DmjOLe63Ke5DpInX074IEdJknwD2rKs0f41XZjRqILfccouZX3Ho0KFCYUGnrd1/0MuXLzdPqO5QyPB4aWRlZZl5G+6L8B5+ntRud+3aZcKSn376aQwaNMiEEw8bNqzC93/qqafMgwy1ZIY/M6SZPhrO5aEWQ4FDAc1wZoY9n3/++cZXyFBm+oz4evZHCHEadv4I5OcC1ZsANZqiMvD4kZpaBE1m1G6ee+65wgghzmO45557YCcc5GiScYf7FCAZGRmFkUbuTJo0CRMmTLC1X+EEtVmaLRlhRi2XZtAXX3wRw4cP90nRsVq1auGHH34wPh5qzcnJyaadc845B5MnTzamMNK9e3cz14bC5c477zTBBewX5+hMnTrVB+9UiHCYX9Or0pp0MP4ZAQCfVjkp75prrjmtrZ4zxjnZzwWfaunHYaRRScKGT+JcXFAw8cmZ0VHVqlVDZZOZmWl8TzQH0iQkAgt9PyIsmHUVsH0JcNVUoOuICt2KYyofAssaU4PKWUDb+759+4oc4z7fYEmChjBqjYsQQggAudlOM1olBgcEXbqaXr16mQg0dxh6zeMiOKDZtbRlyZIl/u6eEKHP7rVAbiaQUAeo06rSmvWrZkNnL5MpuqD5Yt26dcZuz7K8NJfREU1HMWHI88svv2xyZDFcmlFQH3zwAT7//HM/vgvhCfx+SyMpKalS+yIEwn1+jcMRHsKGDl4mcnTx4IMPmjWdzZysyaCDlJSUwvO0o1OwMMT2n//8p8n2+/rrrwdt2HM4ohBkIcJrfk2FhQ3TkOzfv79IanlCjaS8XHTRRaedDV5SdgC+hmHWQgghPCQ/D9ixolLn13gtbDjngSas4uniXelKVLtdCCEClH2/AFnHgNhqQP32gS1smIKEcyyYqobzGlSzXQghgsyE1qQHEOG7dFW2CBs6eFevXu1xxl0hhBCBEhxQuSY0r0KfmZOKs7WFEEIEEZYFJC/3S3CAV8KG6eIZesxU/8yNprxjQggRBBz8DUg/CETFAY3KzpbudzOaKxHmpZdeWuS4AgREoLB9+3YTJs+oRWaIFkLgpAmtcTcgKibwhc23335rT09EwMPgkNmzZ5ttBolw8i2LkLE4Gc+5EnEyUSsTaBIWTzvrrLPMBN0bb7yxzDaY4+4f//iHmazLCb0seEbTLedgKZuzEL6YX1P5/hqvhI3K6YY3/fv3N+n9qcEyLx2zQN9///348MMP8d///tcIITJx4kSTjZmmVVfdGWYIYFbm08EsEStWrMBLL71khAxNtQyz51oIUQFSlge+sGGdkPbt25sn15LqzLujcrteOO1y0v3TdnSCx+kqmNSUCVEJhQdLdPfs2dOYVTkJl5VTCTUSXsfllVdewdtvv41PP/20TGFDgcXsEFdeeWWhltSlS5cyM4SzWBpLC1DDcsFKnix7wbo7zFzAfuhhSYQlqSnA0R1ARJTTjBaowoZ2b9aSqVevntnmH3tJM//ls/ECCpqni1akrDQe2Q3EVKnwbS655BJT12bevHmFwsYdajvR0dEm60RZUDixbASL9FFgVQSWpqYAoob0wgsvYODAgSb/Xu3atSt0XyGC1oTWsJNP/uZtEzb8A3XVbue2EMXhvKuStF4KGJrRWOuCQqkspk+fjqFDhxqBQAHWp08f3HDDDTjvPM9DNf/617/i+uuvN9uvvvqqMfm98cYbJppSiLAi2X/zazwSNs2aNStxW/jIlEUNw19t+whXNKKLsWPH4u9//7spRsbyAay8ySJ3ZXHBBReYEs+s1klfDUtK0KzGaquPPfaYR31yLz1B7apr167YuHGjh+9MiBAg2X/za4KyeFpIwgHaT2qtL+EgznBjdxMW/ScUNCzd7UlaI5rczj//fLNQaLE8NAMOuB0TE1OiGTcnJ8en70eIkCFtP3DoNw42QNMefutGUBVPE4EJ6wqtX7++0GRF6tSpY5zy9MFUNH8efS65ublGSyI06bL8hHtyWIZMF4fakQu+nmmW2rZtW6G+CBG0/pr6ZwPxNf3WDWk2wiOysrJMsIh76POkSZNw1VVXYdiwYRW+P0tIcN4OTV7022zYsAGPPPKIqXvkqm9O3w+L6NFMxn5Q46E2VBxGn7Vq1coImClTpuDIkSMmY7kQYUWyf+fXuJCwER5B4cJs3/SB1KxZ0zjxX3zxRVPwzjWpsyKwEB4njlLAUFtp1KiREWSPP/544TUMOBgxYoQxs/E8fTrUWopDPxEXJo+llsWwampcQoQVKYEhbBzW6aqXlUJqaqqZxLdt2zZjm+dMcs5loG0+0Ev7cpJh9erVTXSU60m5MqEpiBF99G/ExcVVevvi9Oj7ESFFRirwbHOG8ACjtgBV6/ttTPVYs2F4K/Oj8ebMQcVZ4hQ2nGPBEs5z5sypaN+FEEL4AlOV0wJqtbRF0HiCx3YP5qhilBGdsu5PfpzxvXjxYl/3T4QYjE4rbVmyZIm/uydEaJHs//k1Xms2P/74I1577bVTjtN8RsexEKeD/pPSCHQTrBBBR7L/59d4LWyYG6ukujVbtmwpzDIgRGnQUS+EqASy04HdawJGs/HYjHb11VebCXauSXScQ0FfDcNP3edZCCGE8CM7fwTyc4FqSUCNpsEnbBh2mpaWZpJyZmRkmCy6fFpl0sSnnnrKnl4KIYTwfn5NBSdW+8WMxii0r7/+GkuXLjWRaRQ8TDPvquAphBAiAEgJjPk1FZ7UyWy8XIQQQgQYudnAjh8DJjjAa2HDiDSWh96/fz/y8/OLnGPdECGEEH5kzzogNwNIqA3UaY2gFDZPP/20SR3PuvLFs/lWNOGiEOWB1TsfeOABswghTjO/pmmvgPDXeBUgwDxUM2fONCnlFy1aZDQc18LsvyJ04WRePlBwYeJLPmxcfvnl5vfgruFSGLiuq1KlivHp/fvf/y5XG8yH9vDDD6Nly5Zm0jDD6RmE8p///KeIZv2Xv/zFlvcoREiQHDjza7wWNky26E3VRBEa9O/f36T3Z6qiBQsWmGzM999/v0mWyTT+Lhgez+vWrl2Lbt26YfDgwaYYWlncddddJvXRSy+9hE2bNpnEn6zUeejQocJrKIASEnxX+E2IkCI/D0j5IaCCA7wSNiNHjjSp24VvYB7U9Jx0vyxe5GA1k3pZo4az/amxMDsztQ4KnlmzZhVex1B4Xte6dWvze4mPj8enn35a5v2ZmZn3ZPojakhdunTBfffdV6Q0AI9PnTrVbLNNlxblvjzxxBOF17/++uumzAA1JZav/te//uXx+xYiaNj3K5B1FIipCjQ4B4GCxz6b0aNHm/K+NHOwqFXxOiJ8KhXlJyM3Az3e9U/1vBW3rECCD0pDs74MSw3wu//zn/98ynmWI+DvJDs7u8x7UUDNnz8f1113nRFYZUGNidqWC5p2b7vttkLt+5133jHlCVj/pnPnzkbTYvJYmvdYFkGIkJ1f07QHEBGJoBU2f/vb34x/huYTFrdSUIAg1Bg476o4FDCcCMz04xRKZTF9+nQMHTrU/LYowBheTzNaaaZbakxcCEte3HvvvSaIhb4kMn78eNM+hRdh6QAWZGN+PwkbEZKkBNb8mkIsD0lMTLQ+++wzy1e8/PLLVrNmzazY2Fire/fu1ooVK057/ZQpU6zWrVtbcXFxVuPGja0HHnjAysjIKHd7R48epe3IrP0B+7phw4bCPufn51snsk/4ZWHbnjB8+HBr0KBBJZ676aabrHbt2pltfp8xMTFWlSpVrMjISKt69erWM888U+52srOzrcWLF5vXXH755ZbD4bAmTpxYeJ735+/AndTUVKtNmzbWbbfdVngsLS3NfNfx8fGmL66Fv7V69eqV6/sRIqjIz7es51pa1vhqlpW8vFKaLO+Y6rFmw9o1NKH5gvfff9+ULJg2bRp69Ohh7PCs1Lh582aTDqc47777LsaNG2ein3r37m2Sf7oipIJ1fg/77gtTlr9hdCK1BhcsqsfvhqUDiofIlwVNbqzCyYU595588kkTcMDtmJiYU65naWia01i4iZqRC2a3IDNmzDC/L3ciIwPHvCCEzzi0FThxAIiKAxp1RiDhcYAAHa80TTBEtaJQQNB+zhK/9P9Q6DDKiMKkJBjNRHPKLbfcYpzEffv2NfXqV65cWeG+CO9hyPv69euLJGJl+WXmzKMPpqKmVv42GOnGKpqlBa2w/U8++aRIjSUKOZaN/v33301f3Bd3wShEyM2vSeoKRMUikPBYs2G9edrG+YfMAb94gADLQ5cH2vJZN55zKtzDqpljbfnyghjxYlCbefvtt41w6d69uxlE6EymQ7g0srKyzOKipPIIovzws2TdImoT+/btM6HJkyZNMqHPw4YNq/D9L7roIvMA0bVrV+O3oX+F0Wn0EZZUcvbNN9800WUff/yxEWqumkqugmwTJkwwfkbm9GMgAfu/atUqHDlyxGjVQoRs8s0Aw2Nhc8011/ik4YMHD5oBi0LLHe5zfkVJUKPh6+g0Ztgun3Y5L4ODUWlwIOSAI3wDhUvDhg1NhFnNmjWNE58PIHS282GhotCMOnv2bPOdUnumZkJBxoiykvjuu+/M74ilL9yh9k0tnNFx1JYnT55sTHuMQjvnnHOUfUCE+GTO3gg0HHTc+KPh3bt3m7kaNI316tWr8PiYMWPMALJiBWtnF4VhrTfffLOx4dMGv3XrVjOhkKa4xx57rNyaTZMmTUx0VElPynZDU9Aff/xhzDjuJh8RGOj7EUFLagow9RwgIgoYlwLEVKmUZjmm0nJQ1pjqddbnikKbPp20NMW4w33a+UuCAoUmM9dcDj6hnjhxwqQuefTRR0t8suYkRC5CCBEWWk3DjpUmaDwhorwRaDRfEZpOuF/aUl4YVcTZ4QsXLiw8xvxa3HfXdNyhWaW4QHFFFflJQRMe4vKllLQsWbLE390TIviDA5oFngmt3JrNlClTCmdzc9tXEznpoKWtn85gOvwZ+kxNhdFphA5nmtrodyEDBw40EWycCe4yo1Hb4XGFsgYH69atK/Ucv2shhJekBF7yTY+FjftMa86d8BWcG3HgwAHj/GUUUadOnYwD2hU0kJKSUkSTYWkDCjqud+3aZRIyUtCoHHXwwLBjIYSPSTsAHNxCNzzQtCcCEY8DBKhBMJtv8UmXzMrLY4wMCmTK68yy2wHNsHFXmhUROGRkZJiM1goQEEHFhv8AHwwD6p0N3FN2dnV/jKkex6qWJpsY8VXS7G5RFNe8JF9MihW+x5UsVGZZEVQkB+78Go+j0TiXgtCMxZTtdOi6oDazePFik4xRnB4OYjVq1DAltQnngCiZaWDAABWadfmdcB6REEFDcggJGwYGuDQbppVxf/KjRkOzEI+LsnGFdrsEjggc6CNs2rSpHgBE8JB5FNi7PnSEDf0MhGlDWLeEIdDCOziQcRY+fVw5OTn+7o5wgw9OvsiEIESlkcIJ8BZQqwVQteQ5ioGAx7YC1rIRvoHaoXwDQohQnl/jQo9wQggRzKQE9vwaFxI2QggRrGSnA7sKMu1LsxFCCGELu1YB+TlA1UZAjWYIZCRshBAiFEKeHYEdQenVZILU1FRTwIyhu5yb4I4vCmgJIYQIjfk1XgubTz/9FEOHDjX13ZmawH0+ArclbIQQohLIzQZ2rAyK4ACvzGijRo3CHXfcYYQNNRyW13Uthw8ftqeXQgghirLnJyA3A4ivBdQ9CyEnbJhtmTXdmdJDCCFEAMyvcQS2v8YrYcMa8atWrbKnN0IIITycXxP4/hqvfDYDBgzAQw89hA0bNpiyzK4sxi6uvvpqX/ZPCCFEcfLzTpaBDhJh43E9m9PljWKAgOrZCCGEzexdD0zrA8QkAmOTgciogB9TPe5h8VBnIYQQfgp5btLDr4LGEzSpUwghgo3k4JlfUyFh891332HgwIGmnjwX+mmWLFni+94JIYQoCj0fhcIm8OfXeC1s3n77bVx22WUm9Jkh0Fzi4+Nx6aWX4t1337Wnl0IIIZwc2gac2A9ExgJJ5yJY8DhAoG3btvjLX/6CkSNHFjn+wgsvYMaMGdi4cSMCGQUICCGCmtWzgU//5tRqRswPmjHVY83m999/Nya04tCU5qrmKYQQwiZSgivk2Wth06RJEyxcuPCU49988405J4QQwkaSg6MyZ3GivMmNRj/NunXr0Lu3880uW7YMs2bNwj//+U87+iiEEIKk7gBSUwBHJNC4O0Ja2Nx9991o0KABnn/+eXzwwQeFfpz3338fgwYNsqOPQggh3E1oDTsCsYkIJryaDXTttdeaRQghRCWSHHzza1xoUqcQQgQLycE3v8YjzaZWrVrYsmUL6tSpg5o1axYpmFYc1bQRQggbSDsAHNzs3G7aEyEpbKZMmYKqVasWbp9O2AghhLDRX1OvHZBQCyEpbIYPH164ffvtt9vZHyGEECE0v8Zrn01kZCT2799/yvFDhw6Zc0IIIWwgOTjn13gtbErLbpOVlYWYmBhf9EkIIYQ7mUedNWxI0+AUNuUOfX7xxRfNmv6a119/HYmJJ2O8WTBt8eLFaNOmjccdeOWVVzB58mTs3bsXHTt2xEsvvYTu3UufrJSamopHH30U8+bNM8EIzZo1w9SpU3HllVd63LYQQgQFO1YCVj5Q8wygWkOEtLBhYIBLs5k2bVoRkxk1mubNm5vjnsCJoA8++KB5XY8ePYzQ6NevHzZv3ox69eqdcn12djYuv/xyc+7DDz9EUlISkpOTUaNGDY/aFUKIoCI5eEOePRY2riSbF198sdEqGAJdUZgp+s4778SIESPMPoXO559/jpkzZ2LcuHGnXM/j1Ga+//57REdHm2MUckIIEdIkB+9kTq99Nt9++61PBA21lNWrV5vaOIWdiYgw+8uXF0RdFOO///0vevXqhXvvvRf169dH+/bt8fTTTxszXmnQl8QU2O6LEEIEDTkZwK7Vzu1mvRCseJWuZufOnWbgT0lJMUKjuLZSHg4ePGiEBIWGO9zftGlTqeUN/ve//2Ho0KGYP38+tm7dinvuuQc5OTkYP358ia+ZNGkSJkyYUO73JoQQAcXOVUB+DlC1odNnEy7ChuUFWLumRYsWRihQu9i+fbvx5Zx7rr1V4/Lz842/Zvr06cZn1KVLF+zatcsEGJQmbB5++GHjF3JBzUalEIQQQTm/xuEIHzMaB+/Ro0dj/fr1iIuLw0cffYQdO3bgwgsvxI033lju+zD1DQXGvn37ihznPrNKl0TDhg3RunXrIsEJzDjNSLbiGpaL2NhYUz3OfRFCiKAhObjn13gtbFj2ediwYWY7KioKGRkZJgx64sSJePbZZ8t9H0awUTNxL8RGzYX79MuUxHnnnWdMZ7zOBXO2UQhpjo8QIuTIy3GGPQfx/BqvhU2VKlUKtQgO8tu2bSvih/EEmrdmzJiB2bNnGyHGWjknTpwojE6jUKMm5YLnGY12//33GyHDyDUGCDBgQAghQo49PwE56UB8TaCu5/MYg9pn07NnTyxdutSYrziRkpU7aVJjODTPecLgwYNx4MABPP7448YU1qlTJ3zxxReFQQMMQGCEmgv6Wr788kuMHDkSHTp0MPNsKHjGjh3r6dsQQojgCXlu2pvhughmHFZp+WdKgRFhaWlpZrCnFkJhw3kvrVq1MpFonNEfyDBAoHr16jh69Kj8N0KIwObdm4EtC4C+TwG9/4pgHlM90mwYqsywZwoal0nN06wBQgghygF90ynfB/38Ghce6WWMAuvbty+OHDliX4+EEEIA+zc4E3BGVwEadESw47ERkPNqaEoTQghRCfNrmvYAIr2afx/cwubJJ58082w+++wz7NmzR6lghBDCDpJDY36NC4/FpSuVP7MIuJeHZpwB90+Xp0wIIUQ5YNyWeyRaOAobJuIUQghhI4d/B9L2AZExQFIXhKWwYVoaIYQQNpJcoNUkdQWi4xAKeDVLaMmSJbj11lvRu3dvkwiTvPXWW2aypxBCiAqSHPz1ayosbJh4k9U04+PjsWbNGlMvhnBCD1PHCCGE8FVwQC+ECl5Fo3EiJ3OauaplupJkUvgIIYSoAEd3AanJgCMCaNIDYStsNm/ejAsuuOCU40xXkJqa6qt+CSFEeM+vadgRiK2KsBU2rDXDNP/Fob+GBdWEEEL4woR2HkIJj4XNnXfeaTItr1ixwsyr2b17N9555x0z0ZMlAIQQQlSA7QXCpmno+Gu8Cn0eN26cKV526aWXIj093ZjUWA2Twua+++6zp5dCCBEOJC8HDm4GIqJCKhLNqxIDLlhAjeY0lhto166dqdYZDKjEgBAiYJkzCPh9EdDldmDgPxEM2FJiwB2WYaaQEUII4SOt5vdFTq2mz4MINTwWNpmZmXjppZdM2pr9+/cbk5o7Cn8WQggv+O4Z57rTUKBmYBehrBRh86c//QlfffUVbrjhBnTv3r1IMk4hhBBekPLDSa3m/FEIRTwWNiwtMH/+fDOJUwghhA9YFNpajVehz0lJSahaNXQmGgkhhP+1mm9DWqvxStg8//zzGDt2LJKTk+3pkRBChKVWc0vIajVemdG6du1qggSYLSAhIaFIfjRy+PBhX/ZPCCFCl5QVYaHVeCVshgwZYsoKMMNz/fr1FSAghBAVjkC7BajZHKGMx8Lm+++/x/Lly9GxY0d7eiSEEOGi1Wz7X1hoNV75bNq0aYOMjAx7eiOEEOGm1XQcEvJajVfC5plnnsGoUaOwaNEiHDp0yKQqcF+EEEKUQUp4aTVemdH69+9v1kzE6Q5TrNF/k5eX57veCSFEqGs1tc5AOOCxsGGaGiGEEF6yY2XYaTVeCZsLL7zQnp4IIUQ4zavpGD5ajVfCZvHixac9X1LJaCGEECjQahYCjsiw0mq8EjYXXXTRKcfc59rIZyOEEGVlCxgSVlqNV9FoR44cKbKwzMAXX3yBbt26mWzQQgghSmDHj25azWiEGx4LG1Zkc1/q1KmDyy+/HM8++yzGjBnjVSdeeeUVNG/eHHFxcejRowdWrlxZrtfNnTvXaFXXXHONV+0KIUTlZwsYEnZajVfCpjSYumbz5s0ev+7999/Hgw8+iPHjx5vCa8xM0K9fP6MxnY7t27dj9OjROP/88yvQayGEqCStZus3YavVeOWz+fnnn0+ZX7Nnzx4z2bNTp04ed+CFF17AnXfeiREjRpj9adOm4fPPP8fMmTMxbty4El9Dv9DQoUMxYcIELFmyBKmpqR63K4QQlcZ34RmBViFhQ4FC0xWFjDs9e/Y0AsITsrOzsXr1ajz88MOFxyIiInDZZZeZ/GulMXHiRNSrV89UDaWwOR1ZWVlmcaEsB0KISmXnqpNazQXhFYFWIWHzxx9/FNmncKhbt67xt3jKwYMHjZZCE5w73N+0aVOJr1m6dCneeOMNrFu3rlxtTJo0yWhAQgjh/3k1LRCueCxsmjXzX3Gf48eP47bbbsOMGTNMYEJ5oNZEn5C7ZtOkSRMbeymEEO5azddhr9V4JWzIwoULzUInfn5+fpFznpjSKDAiIyOxb9++Ise536BBg1Ou37ZtmwkMGDhwYOExV/tRUVEmQKFly5ZFXhMbG2sWIYTwn1Zzc1hrNV5Fo9Ek1bdvXyNsaAYrPu/GE2JiYtClSxdzL3fhwf1evXqVWN5g/fr1xoTmWq6++mpcfPHFZlsaixAiYNi5+qRWc354azVeaTaMFps1a5YxZ/kCmriGDx9uyk13794dU6dOxYkTJwqj04YNG4akpCTje6FfqH379kVeX6NGDbMuflwIIQIjAu1moHZRi0s44rGwYQRZ7969fdaBwYMH48CBA3j88cexd+9eE+3GjASuoIGUlBQThCCEEEGl1fz2lbQaNxxW8RjmMhg7diwSExPx2GOPIRhhgAAzHxw9ehTVqlXzd3eEEKHIOzc6hU2nocA1/0IoU94x1WPNJjMzE9OnT8c333yDDh06IDo6+pRJmkIIEbZIq/FdBgFXpoBffvml1OzPQggR1r6aDoPlq3FDlTqFEMJX7HLTai4IzxxopeFTz3tZyTOFECKkWfSscy2txnthk5CQYKLGXAwYMMAk4HSfiNmwYcPy3k4IIUJQq/kScERIq6mIsGFggHvgGstDZ2RkFLnGw8A2IYQIHaTVVJ4ZTQECQoiwZNcaN63mIX/3JiDRbEkhhKgo30mr8ZmwodbirrkU3xdCiLDVarZ8Ia3GV6HP9Me0bt26UMCkpaWhc+fOhalk5K8RQoS1VnPOTdJqfCFs3nzzzfJeKoQQ4YG0Gt8LG2ZmFkII4cZ3z53Uauqc6e/eBDQKEBBCCG/YvRbYskBaTTmRsBFCiIrMqznnRmk15UDCRgghPEVajcdI2AghhNe+Gmo1rfzdm6BAwkYIITxh9zpg83xpNXZEoz344IPlvqGKpwkhwmNejbQanwubtWvXFtlfs2YNcnNzcdZZZ5n9LVu2IDIyEl26dPGocSGECCqk1dgrbNwLplFzqVq1KmbPno2aNWuaY0eOHMGIESNw/vnne98TIYQIFq2m/Q3SajzEYXmYZyYpKQlfffUVzj777CLHWSK6b9++2L17NwKZY8eOoXr16jh69CiqVavm7+4IIYKFPT8Br13g1GruWQHUbe3vHgXVmBrhzY3di6i54LHjx4973lMhhAimeTXUaiRoPMZjYXPttdcak9m8efOwc+dOs3z00Uf405/+hOuuu87zHgghRDBoNZs/pzFIvhq7c6O5mDZtGkaPHo1bbrkFOTk5zptERRlhM3nyZG/7IYQQQZAtQFpNpflsXJw4cQLbtm0z2y1btkSVKlUQDMhnI4TwyldDrebelRI2leWzcbFnzx6ztGrVygga1bMRQoR2tgBpNRXBY2Fz6NAhXHrppaaQ2pVXXmkEDqEZbdSoURXqjBBCBAx8gP5uMrDpswJfzRh/9yi8hM3IkSMRHR2NlJQUJCQkFB4fPHgwvvjiC1/3Twgh/CNovn4M+PZJ5/4lf5dWU9kBApxj8+WXX6Jx48ZFjtOclpycXNH+CCGEf8nPAz5/EFg9y7nf72mg173+7lX4CRsGBrhrNC4OHz6M2NhYX/VLCCEqn7wc4OO7gF8+dJrOrn4ROHeYv3sVnmY0pqSZM2dO4b7D4UB+fj6ee+45XHzxxb7unxBCVA45GcD7tzoFTUQUcMNMCRp/ajYUKgwQWLVqFbKzszFmzBj8+uuvRrNZtmyZL/smhBCVQ9Zx4L0hwPYlQFQccNNbQOu+/u5VeGs27du3N1me+/Tpg0GDBhmzGjMHMDM059sIIURQkX4YmDPIKWhiqgK3fiRB429hw4wB1Gr279+PRx99FB988AHmz5+PJ598Eg0bNvS6E6+88gqaN2+OuLg49OjRAytXriz12hkzZhhTHjNOc7nssstOe70QQpTK8b3ArAHArtVAfE1g+H+B5n383auQxCNhw5Dnn3/+2acdeP/9901xtvHjx5s6OR07dkS/fv2MQCuJRYsWYciQIabswfLly9GkSROTbXrXrl0+7ZcQIsQ5kgzM7A/s3wAkNgBunw8knevvXoUsHqer4TwbRp0988wzPukANZlu3brh5ZdfNvsMNqAAue+++zBu3LgyX5+Xl2c0HL5+2LBTnXlZWVlmcU+twPsrXY0QYczB35yms2O7gBpNgWH/AWq18HevQjpdjccBAqzQOXPmTHzzzTemMmfxnGielIVmgMHq1avx8MMPFx6LiIgwpjFqLeUhPT3dmPdq1apV4vlJkyZhwoQJ5e6TECLE2fMz8Na1QPpBoM5ZwLBPgGqN/N2rkMdjYcMiaeee61Q1GSjgDsOgPeHgwYNGM6lfv36R49zftGlTue4xduxYNGrUyAiokqAgo5muuGYjhAhDUlYA79wIZB0FGnYEbp0HVKnj716FBR4LG/cS0f6Gpry5c+caPw6DC0qCJj9NNhVCYNv/gLlDgZx0oGkv4Jb3gbjq/u5V2OCxsPElderUQWRkJPbt21fkOPcbNGhw2tf+3//9nxE2NOd16NDB5p4KIYKajZ8CH94B5GUDLS8FBr8NxJyaCUX4WdhwHs2sWbOM86esapys4FleYmJijN9n4cKFuOaaawoDBLj/17/+9bQTS5966imTo61r167lbk8IEYasew/4z72AlQe0vRq4/nUgStaOgBQ2jDRw+WO47UvoTxk+fLgRGt27d8fUqVPNRFGWniaMMEtKSjKOfvLss8/i8ccfx7vvvmvm5uzdu9ccT0xMNIsQQhSycgYwf7Rzu9NQYOCLQKRfDTphS7k+9TfffLPEbV/A0gQHDhwwAoSCo1OnTqZUgStogKUMGKHm4tVXXzVRbDfccEOR+3CezhNPPOHTvgkhgpglzwMLJzq3e9wF9JvEcFd/9yps8bosdLCistBChDgc0r55Alg21bnPomcXP8JwWX/3LCSxbZ7NGWeccdoQ599//93TWwohhG/IzwfmjwJWzXTuX/4P4Ly/+btXwhth88ADDxTZ54RKJuGk6euhhx7yZd+EEMKzWjSf3A2s/7ezFs1VU4CuTt+vCEJhc//995eaTJNlB4QQotLJyQQ+HAFsnu+sRXPta8A5Rf26wr/4zFt2xRVX4KOPPvLV7YQQonxkpQHv3ugUNJGxwOB3JGgCEJ/FAH744Yel5icTQghbyDjiTD+z80cgJhEY8h5wxgX+7pXwhbDp3LlzkQABBrMxZJnhy//61788vZ0QQnhH2n5nQs19vwBxNZx5zhp38XevhK+EjWumvwvOgalbty4uuugitGnTxtPbCSGE56TucJYIOLwNqFLPmbm5/tn+7pU4DZpnI4QILg5uLahFsxOozlo0nwC1VZLeX9g2z4Y3Li8azIUQPiMnA1g9G1j8HJB+CKjdyiloqjf2d89EOfBY2NSoUaPMujVUlngNa9UIIUSFo804SfP7l4ATBeXiG3Rw+mgS6/q7d8IuYcPcaCzXfPvtt6NXr17mGKtqzp492yTLZHJMIYSoMJnHgB9nAN+/DGQcdh6j2ez8kc6kmsrcHNrCZs6cOab085AhQwqPXX311TjnnHMwffp0U8hMCCEqFM684jXgh1eBzFTnsZpnABeMBjoMBiKj/d1DURkBAgkJCfjpp5/QqlWrIsdZIpoZm9PT0xHIKEBAiAAl/TCw/BVg5XQgq8A3XKc1cP5ooP31Kg0QbgECTZo0wYwZM0wBM3def/11c04IITwi7QCw/CVg5etAzgnnsXrtgAseAtoNAiIi/d1D4QM8FjZTpkzB9ddfjwULFqBHjx7m2MqVK/Hbb78pXY0Qovwc3wsse9Hp/M/NOOn4v3AMcNYA1Z6pRI5mHUW1mGplBn9V+jybHTt2mCJmmzZtMvtt27bFXXfdFRSajcxoQviZozuBpVOBNXOAvCznsaQuzrozrfup7ozNcMjfeXwn1h5Yi7X712LtvrXYdnQbFly3AI2rNg4cMxqhUHn66ae9eakQIlw5sh1YOgVY+w6Qn+M81qSnU5NpeYmEjE3k5Odg06FNRrCsO7AOa/atwaHMQ6dct/HQRq+ETXnxStgsWbIEr732mimU9u9//xtJSUl46623TGG1Pn36+L6XQojg5dA2YMkLwE/vAVbB3Lvm5zuFDNcSMj7lWPYx/LT/J6fWsn8tfjn4CzLzMotcE+WIQv24MxGZfQb27muIw4cbodWA3rATj4UN/TK33XYbhg4dijVr1iAry6kGU4WitjN//nw7+imECDYObAYW/x/wy4eAle88Rg2G5rJmzjl6ouImsV1pu5xay/51WLN/DbalboOFot6RxOhqaBTXBlZGc+zcWx/7D9bDEetkCHlMVAS27k9D8zpVEDDC5sknn8S0adMwbNgwzJ07t/D4eeedZ84JIcKcfb8CiycDv37C4dB5rFU/pybTuKu/exfU5ObnYvPhzYVaC5cDGQdOua5RlcaoF9MGuSeaIWV3Pew6UBV73MqXRUc60KlJDfRqURs9W9bGuU1rIi7a3qg/j4XN5s2bccEFp9aLoIMoNbVgApYQIvzYvc4pZDZ9dvJYm6uckzEbdfZnz4KWtOw0/HTgp0LN5eeDPyPDFbnnZhJrVeMs1I46C5lpTfHHzrrYvDESm92uiYxw4Jyk6ujVsjZ6t6yNLs1qIiGmcuctedxagwYNsHXr1lPS0ixduhQtWrTwZd+EEIFOfr6zcNmS54Hfviw46ADOvsY5GbNBez93MDjIt/KxP30/dhzfYZYNhzYY4bLlyJZTTGJVo6vi7NodUCOiFdKPNcWWlJpYuaEgqq8AusHOblQNvVvWMdpL1+Y1UTXOv5kXPBY2d955J+6//37MnDnTxGTv3r3b5EYbPXo0HnvsMXt6KYQIDLKOAztXOQXMjhXOdeZR5zlHBND+BqcmU/csf/c04MjOyzb+FZdAYfix+3Z2fnaJr0tKTEL72h1RzdEKx440xoaUeHyzPg0nJ604BU2bBlWN5kLh0uOM2qieEFhpfTwWNkzCmZ+fj0svvdSkpqFJLTY21gib++67z55eCiEqH45mh38/KVh2rAT2bzjp7HcRFQ+0vw44f1TY15Wh2cslQIove0/sPUVLKW4Oa5jYEE2qNkHTqmegitUShw8l4adkCx+vPoq8fL6W0Xxp5voz6yUawUIB0+OMWqidGBuaxdOys7ONOS0tLQ3t2rVDYmIiMjIyEB8fj0BGkzqFOE29mN1rTwoWLukHT72OmZebdD+51G8fNskxOVxyjopLgKQcSymipRzJOnLa18dHxRthwqVBQhISIuohIrcucrJq4uixROxOzcKOIxnYuv84cvKKDs3NaycYwdKTAqZFbdSrFodAwNZJnSQmJsYIGcLwZ2aCZr60vXv3entLIURlz+R3Fyx7fwbyc4teExkDNOx0UrA07g5Ua4hQJy8/DynHU7Dp8CazbD+6HTvSnEKluIO+OLXiapnJkYwIqxbVADFWXVg5dZB+ojoOpEZjV0omvjucjiPpBRNbjbbDEgoFZRQKSKoRX+jQp4BpVCOwH+TLotzChgLliSeewNdff20EzZgxY3DNNdeY+jaPPvooIiMjMXLkSHt7K4TwjtxspzBxFy7Hd596XWIDN62lB9CwY8jXjcnMzcTW1K2FgoULHfOlCZUIRwQaVmmIpMTGqBnTEAmOenDk1kF2Zg0cPVYdew9Z2LI1A8uOuzvt804RJqRGQjQa14xHk5oJznWtBLNNExm3Q4lyC5vHH3/cZA247LLL8P333+PGG2/EiBEj8MMPPxithvsUOEKIAOD4PmBngVDhQvOYKw+ZC0ck0OAcp1BxCZjqTUJ6Rn9qZio2Hdlk5qpsPLzRrP84+gfyXJkN3IiLjEPTxJZoEN8SCUhCXnYtnDhRHYdTE7FrVza2HMuEcaMUQl9WUTNalZhIIzQa10xAk1rxznVN57pxrXhU83OEWEAKG6alYeE0Fkr75Zdf0KFDB+Tm5praNnZmChVCFINuVkaAHdsNHNvlNIeZ9S7g2E5nDrLUlFNfF1+rQLB0c6459yXGvhnj/vat7D6xu4i2svHQJuxLL9nMH42qiM1vAmQlIfNEAxw7VhfHs+rggNtEyJMCpaDWDoDYqIgiGolr26WtUHPR+OihsNm5cye6dOlittu3b28i0Gg20wcphI/JPnFScJh1cYGyC8h2RiSVjsNZE8bdJFarRUhpLfn5Fo5m5GDf8RP45cBv2HBwI34/tgW70rfhUPYfyEHJhRzzs2sjL7Mh8jMbIS+rkVlbuVWdn1kxaiZEmyivBtXiCjUTd4FSNzFWY6CvhU1eXp7x1RS+MCrKRKAJITwgJ9MpLNw1EbN201JcpZDLIr4mUK0xUD0JqJZUsOZ+Y+dkyrjqCBQtIys3HxnZecjIyUM614XbuaUc53Yu0rNzcDw7Hem5J5CWfQLpuenIzD2BY3m7kOHYAUfsbkTE7oUj4lQzmGVFIj+rnlOoZDqFSmRuI9RJqG4ESJ06MWZdOzHGCA2ua1eJRR2eS4xBzSoxiI5UTZ1KFzb8wdx+++1GoyGZmZmmhk2VKkXV8Hnz5vmsc0IE7Kx5ahYsXcxJjpkF66yjxfa5HAMyjpwULiWFEpdEbDU3AcJ1Y6Bao6LbFTSBUTPIzM1DZk4+MnOcgzzXrn33bdc5l9Aw5wpea85lOwVEek4O0ikc8k4YB3tWXjqy8tNhObLhiMgyCwrWjojsk9uRruPZxa7JcQuBLVhKGLwc+XGIt5qgRlRz1I9riWZVW+HMGi1Qv2oVp2ApECZVY6OkiQS6sBk+fHiR/VtvvdVnnXjllVcwefJkEzbdsWNHvPTSS+jevftp/UfMVrB9+3a0atUKzz77LK688kqf9UeEqIDIywZyM51LoVBwCQZ3weF2vFBwuAuQ4ycTTHrTlah45CQ2Qk6VhshKaIjMhAbIiGuA9PgGSI9rgOOxDZAVmYCcvHwz1yKXS34+stPykXvMQs72XGTlbTcz0rPycpCVl42cvBxk5+cgOzcHOfnZyMx1LjzHY7yOM9Sz83LMtTlWDvIY5uzIg8ORBzhc2871yeOuJbfI/inXUTBEZ8ERm1M0cosKmA++PgciEBuZgLjIeMRHJqBhlSS0rdUGneqfjbPrtkXjxMYSIgGO15M6fcX7779vMkgzkzTLTE+dOtUIEyb8rFev3inXMxKOWQsmTZqEq666Cu+++64RNix3QF9SWYTjpE5+xYyaybcsMws5v2DfbBfs5/FYvvMaLvxV5OfnmWwRVn4urDznmrPH84vtW/l5bgtvUrBt5cIyDRdcZ7nO55rB/+R+Dhy5WXBQGORlw5GbabbNks/tHDjyshDJaKr8LKfQ4GvyM53X52fDYeXQGF+wnQtHfhYcyEUEr0MOHFaece3mOejidZhA1HwHA1Id5phr7bym4Dwjhh2OosccQC6cx7IdkUhHDDIQg3RHNDIQhQwrBhmOKGQiCllwrjMdUUhDDNIcMTiBaORwUHTklzCAnxzYiw/2RQf3YjP4A5AIRCI+KgEJXKITkBhdBVViqjjX0VXMMbOOcq5LOua+Hxsp30igUt4x1e/ChgKmW7duePnll80+BzdWAmXqG6bGKc7gwYNx4sQJfPbZycyyPXv2RKdOnYzAslPYfPn9e/j455ecKSfcPjVXCgr+KRRNR3Fyu3iaCu6fen2xfW46XEdc/zv3T1538jwHQlfCJMvhfqbgOnPMvWcFryng5BDmvNa9G87eurbdzhW7p/u17p+A67qi7bvadZh+uJJxWAUDv3Pf7ZwGm1JwIBLRiHBEIdIskYiKiDHpT6IiohETQd9DNKK5HRmN2MhoxETFmHVcVIxZYiJjzHnXdUWWwtfGlHqOM+NdwoEL25RwCA+O2Z1BwBcw5c3q1avx8MMPFx6LiIgwc3mY3LMkePzBBx8scqxfv3745BPWzih5MqqrwJvrg/GWPYe3Ylk8TSiBiOsPW3/gLrOLo/BfhBn4OCSbfUckIvjPEQmHg+sIM0DzaTwiIpL/O49FRBWeM0sEt6MK1hzQeSwSUWbbeZxrHnct0QXXxEa6DejFBnW+prRB3pwrZYB3Lby/EIGOX4XNwYMHTZRb/fr1ixzn/qZNm0p8Df06JV1fWpocmtsmTJjgk/52PLMPbjrGfhUMYmZc53/ObT6RO5/meLbg/4JrilxvBj7nOQ6EzrVTSHCINBe6rjHpwjkoOsxg5hocuTgiIp3bZu0oWPN4weAZ4RxMORhRiDsH0EjzOuexggE04uS1XJi919kmr3Xb56DmOl9wfeG7K3iKPdnnk/vux05+JifP8bh5HwWDfFkL35t5DU495loLIQILvwqbyoBak7smRM2GZjpv6NzmYrMIIYQIImFTp04dk+Jm3759RY5zn0XaSoLHPbmeodqucG0hhBD+wa8zljhJlFkJFi5cWHiMAQLc79WrV4mv4XH36wmTg5Z2vRBCCP/jdzMaTVycw9O1a1czt4ahz4w2Y5JPwrDopKQk43shrBJ64YUX4vnnn8eAAQMwd+5crFq1CtOnT/fzOxFCCBGwwoahzAcOHDBZpenkZwjzF198URgEkJKSYpzbLnr37m3m1vz973/HI488YiZ1MhKtPHNshBBC+Ae/z7OpbMJxUqcQQvh7TFWWOSGEELYjYSOEEMJ2JGyEEEKEfoBAZeNyUVUkbY0QQggUGUvLcv+HnbA5ftyZ28zbLAJCCCFKHlsZKFAaYReNxkmju3fvRtWqVb3KoeVKd7Njx45KiWZTe2ov0NtUe+HdnmVZRtA0atSoyDQVhLtmww+jcePGFb4Pv5TKDJ1We2ov0NtUe+HbXvXTaDQuFCAghBDCdiRshBBC2I6EjYcwg/T48eMrLZO02lN7gd6m2lN75SHsAgSEEEJUPtJshBBC2I6EjRBCCNuRsBFCCGE7EjZCCCFsR8KmHDzxxBMm24D70qZNG1vb3LVrF2699VbUrl0b8fHxOOecc0xFUrto3rz5Ke+Ry7333mtLe3l5eXjsscdwxhlnmPfXsmVL/OMf/ygzv1JF4CznBx54AM2aNTNtshDfjz/+6JN7L168GAMHDjSzqPm5saCfO3xfLBDYsGFD0/Zll12G3377zbb25s2bh759+5rfD8+vW7fO67bKai8nJwdjx441v9EqVaqYa1hhl5k67GrT9XfJv0O2WbNmTfOZrlixwrb23LnrrrvMNawsbFd7t99++yl/j/3797etPbJx40ZcffXVZpImP9du3bqZApa+QMKmnJx99tnYs2dP4bJ06VLb2jpy5AjOO+88REdHY8GCBdiwYYMpg80/KLvgoOv+/r7++mtz/MYbb7SlvWeffRavvvoqXn75ZfMD5/5zzz2Hl156CXbx5z//2byvt956C+vXrzeDMQcoCvaKwlLmHTt2xCuvvFLieb63F198EdOmTTMDIv+Q+/Xrh8zMTFva4/k+ffqYz9UXnK699PR0rFmzxjw8cE1Bt3nzZjNo2dUmad26tfn98Lvk3yMfmPidsvKvHe25+Pjjj/HDDz+YQbsinChHexQu7n+X7733nm3tbdu2zfxmKMAXLVqEn3/+2XyncXFx8AkMfRanZ/z48VbHjh0rrb2xY8daffr0sfzJ/fffb7Vs2dLKz8+35f4DBgyw7rjjjiLHrrvuOmvo0KG2tJeenm5FRkZan332WZHj5557rvXoo4/6tC3+WX388ceF+/wMGzRoYE2ePLnwWGpqqhUbG2u99957Pm/PnT/++MOcX7t2bYXbKU97LlauXGmuS05OrrQ2jx49aq775ptvbGtv586dVlJSkvXLL79YzZo1s6ZMmVLhtkprb/jw4dagQYMsOyipvcGDB1u33nqrZRfSbMoJTR58kmnRogWGDh3qM9WyJP773/+ia9euRquoV68eOnfujBkzZqCyyM7Oxttvv4077rjDq2Sl5YEmrIULF2LLli1m/6effjJPp1dccYUt7eXm5hrTXfGnNJq07NRSyR9//IG9e/caLcoFzRQ9evTA8uXLEYqwRDB/OzVq1Ki03+z06dPN58qnd7uS+N5222146KGHjKWjMli0aJEZA8466yzcfffdOHTokG3v7fPPPzfaIjVutsnf5+lMiZ4iYVMO+KHPmjULX3zxhTH9cPA4//zzC8sV+Jrff//dtNOqVSt8+eWX5kf2t7/9DbNnz0ZlwB9YamqqsRnbxbhx43DzzTcblZ3mQgpU+lMoyO2AWb579epl/EL0JVDwUKBysKd5wk4oaEj9+vWLHOe+61woQdMgfThDhgyxPZHkZ599hsTERPMQMWXKFGMmrVOnji1t0SQZFRVl/hYrg/79+2POnDnmoYxtf/fdd+ZhjL9dX7N//36kpaXhmWeeMe1+9dVXuPbaa3HdddeZdn2CbTpTCHPkyBGrWrVq1uuvv27L/aOjo61evXoVOXbfffdZPXv2tCqDvn37WldddZWtbdB81LhxY7P++eefrTlz5li1atWyZs2aZVubW7dutS644AJjQqBJrVu3bsZs16ZNG1tNFMuWLTPHdu/eXeS6G2+80brpppt83p4/zWjZ2dnWwIEDrc6dOxuzlt1tpqWlWb/99pu1fPlyY5Zt3ry5tW/fPp+3t2rVKqt+/frWrl27Co/ZbUYrzrZt22wzE/J98diQIUOKXMfv8uabb7Z8gTQbL6BpgOrm1q1bbbk/I5batWtX5Fjbtm1tNd25SE5OxjfffGOc6XZCU4RLu2EUE80TI0eOxKRJk2xrkxFvfErjExxrd6xcudJEUtE0aicNGjQw63379hU5zn3XuVCAn+VNN91kfkPUMCojPT4DLc4880z07NkTb7zxhtE8uPY1S5YsMU//TZs2NW1w4fscNWqUCUyoDFq0aGG0NjvGHd6X78nOcUfCxgs4WDFyg0LBDhiJxmged+jbYMiu3bz55pvGXjtgwABb22EEU/FCS5GRkcZ2XBkDFL87Rv3RTDlo0CBb22N4N4UKzSHuBasYlUbTXigJGvo2+bDCkGt/wN9PVlaWz+/LhyFGZzGE3LXQh8uHJv6GKoOdO3can40d405MTIwJc7Zz3Am74mneMHr0aBOfzg+d9n5mSOXASJu0HfAJnw70p59+2vwB8wmczk8udv+hUtgMHz7cPOXYCT/Pp556yjwp0tm6du1avPDCCyYowS44KNCCQGcrnw45UNBnNGLECJ88gLg/cdKvxwGpVq1a5j3SH/Xkk08aPxyFD0NKOVhdc801trR3+PBh80TqmuviGkQo9LzRpk7XHge/G264wYQ904dCn4LLF8XzHMh8/R4pzPj7YXg12z948KAJ6WUYu7fh+mV9psUFKH2N/Cz5e/J1e7Vq1cKECRNw/fXXmzb4cDtmzBijxdGBb8f749/D4MGDccEFF+Diiy82PupPP/3UBCn4BJ8Y40IchgQ2bNjQiomJMWGP3Kf9304+/fRTq3379iY8lj6F6dOnW3bz5ZdfGrvt5s2bbW/r2LFjJry6adOmVlxcnNWiRQsTgpyVlWVbm++//75ph98jQ5HvvfdeE4LsC7799lvz2RVfGL7qCn9+7LHHjN2f3+mll15aoc+5rPbefPPNEs8zjN/X7bn8QiUtfJ0d7zEjI8O69tprrUaNGpnvk3+fV199tQm5tqO9kqioz+bb07THUH36TuvWrWt8uGzrzjvvtPbu3Wvr+3vjjTesM8880/xNcrrHJ598YvkKlRgQQghhO/LZCCGEsB0JGyGEELYjYSOEEMJ2JGyEEELYjoSNEEII25GwEUIIYTsSNkIIIWxHwkYIIYTtSNgIUUls2rTJJIxkOvxOnTr5pQ9MGlmRUsZCeIuEjRDFYFlh5vNiGV0mmGTiTl9kvmVOPd6Lecrck3KWVHeedUWK1xiyq5CdEJWBhI0QxWBBNVZ7pGBgcklXosKK4qrxzoSup8uKTM2HxbKYlVqIUEHCRohifP/996bMA2HJaNd2WRmzJ06ciMaNGyM2NtaYyZg11wW1ktWrV5truP3EE0+Uei+Wj2am37Jq+3z00UcmYzbbo3ns+eefL3Ke9VeYXZulr5lp+p133jnlHqzIytpFdevWNfVnLrnkElOi2wW3mQGYlU55vkuXLli1alWZn4cQp+CzlJ5CBDHJyclW9erVzcIsu8x6y21mFGaWZm7ffffdpb7+hRdeMNVbWXl006ZN1pgxY8x9tmzZYs7v2bPHOvvss61Ro0aZ7ePHj5d4H2bgHTRokDVv3jzThx07dpjjrKro/ufKypERERHWxIkTTfZoZnmOj483axdXXHGFydzLKpa8vnfv3uYa90zFl112manG+OOPP5q+sn+1a9e2Dh06ZM6zz7feequ1ceNGc/6DDz6w1q1b54NPXIQbEjZCWJaVk5NjUuX/9NNPRkhwzTISiYmJ1nfffWfOHThwoNTXM9X9U089VeQYy07fc889hfsc+MtK8e8SNoRlwFnquCRhc8stt1iXX355kdc+9NBDVrt27cw2BRCvd0+5T4HBYy5hs2TJEiMgMzMzi9ynZcuW1muvvWa2q1atamupbhE+yIwmBKsIRkUZUxQjxlixsEOHDqYAWP369U0xKZ5j6dySYNVNFikrbm7j/saNG73uE/02s2fPLvEePFZSe6yUyeJlPM/3RLOXCxaKY0lzdxMZC2rRf5SYmFi4sKgW/UvkwQcfNGY2mvYYtOA6LoSnqFKnEIDxfbCmPKPP6H/hoJubm2sWbtOp/+uvv1ZqnyjkWJXx4YcfNlFqvoaChlUuS6rE6BJK9C3dcsst+Pzzz7FgwQITUTd37lxce+21Pu+PCG2k2QgBYP78+aZELh3zb7/9ttlu3769mZPCbZ4vDTrOWeJ52bJlRY5zv127dhXqF7UJluZlhJw7bdu2LbG91q1bm5Ll1GIoKBmU4IIh1wwIcHHuueca7Y0aEMsNuy/uWhzvyVLlX331Fa677jpTOlwIj/G3HU+IQIGOewYDsOQw/Rh00O/evbtcr6UfhP6PuXPnmgCBsWPHFgkQ8MZn4+K2224zfXH/c129enWRAAH6VYoHCPTv39/q3Lmz9cMPP5gAgT59+hQJEGCpah5jv1gSnH6pZcuWWY888ogJGGBpYpbOZjnh7du3W0uXLjX+HAY/COEpEjZCFMBIMg6+ZPHixaYWe3nJy8uznnjiCSspKckIGQ7gCxYsKHKNt8KGQoBRccWfDT/88EMTEMD2mjZtak2ePPkU4TlgwAAjQHl+zpw5ppa9ezTasWPHrPvuu88EOPA+TZo0sYYOHWqlpKRYWVlZ1s0332yOsX1e89e//tUIYyE8xcH/PNeHhBBCiPIjn40QQgjbkbARQghhOxI2QgghbEfCRgghhO1I2AghhLAdCRshhBC2I2EjhBDCdiRshBBC2I6EjRBCCNuRsBFCCGE7EjZCCCFgN/8PAjEDfbxx/mYAAAAASUVORK5CYII="
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "execution_count": 22
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "id": "gp10-14-38",
   "source": [
    "### 团形查询\n",
    "\n",
    "预期：这是最坏情况。团形下任意子集都连通、任意两个不相交子集之间都有边，\n",
    "DPccp 的剪枝完全用不上，因此它与 DPsub 的枚举量相同（n = 6 时都是 301），\n",
    "而 DPsize 会明显更差（1171）。\n",
    "\n",
    "图上若看到 DP_Size 在团形上\"爆炸\"，那是正常的：它的枚举量随 n 增长最快。\n",
    "这也解释了为什么代码注释里把 `num_nodes` 上限设成 9——再往上跑会很慢。"
   ]
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-28T11:43:25.251192Z",
     "start_time": "2025-01-28T11:41:58.071375Z"
    }
   },
   "cell_type": "code",
   "source": [
    "from system.query_optimization.join_graph import CliqueQueryFactory\n",
    "\n",
    "evaluate_algorithms_on_graphs(CliqueQueryFactory)"
   ],
   "id": "1932b7c12856d81b",
   "outputs": [
    {
     "data": {
      "text/plain": [
       "<Figure size 400x300 with 1 Axes>"
      ],
      "image/png": "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"
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "execution_count": 27
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "id": "gp10-14-40",
   "source": [
    "# 单元测试\n",
    "\n",
    "枚举器这种\"看起来对、跑起来差一个\"的代码，一定要用测试钉住。这里有两个测试：\n",
    "\n",
    "1. `test_plan_enumerators`：链形 n = 5，手工给定基数表，断言三种算法给出**同一个**最优计划\n",
    "   `(((3, 2), 1), 0), 4`（即 `((((R3 ⋈ R2) ⋈ R1) ⋈ R0) ⋈ R4)`）与**同一个**最优代价 10770，\n",
    "   同时断言各自的枚举量：`DP_Size = 73`、`DP_Sub = 42`、`DP_CCP = 20`。\n",
    "   这三个数字与本 notebook 的计数口径一致（对称去重、只收录连通子问题）。\n",
    "   注意最优计划并不是\"按 R0→R1→R2 的自然顺序\"，而是先连小而便宜的 R3 ⋈ R2——这正是优化器的价值。\n",
    "2. `test_csg_enumeration` 与 `test_ccp_helper_enumeration`：把链形 n = 5 的全部 csg 与全部 ccp\n",
    "   **逐个列举**出来做精确比对，确保\"不重复、不遗漏\"这条性质没有被改坏。\n",
    "\n",
    "跑完应看到 `OK`。"
   ]
  },
  {
   "metadata": {
    "ExecuteTime": {
     "end_time": "2025-01-28T11:33:34.898797Z",
     "start_time": "2025-01-28T11:33:34.884787Z"
    }
   },
   "cell_type": "code",
   "source": [
    "import unittest\n",
    "\n",
    "\n",
    "class EnumeratorTests(unittest.TestCase):\n",
    "    def test_plan_enumerators(self):\n",
    "        join_graph: JoinGraph = ChainQueryFactory.construct_join_graph(5)\n",
    "        cardinality_table: CardinalityTable = CardinalityTable()\n",
    "        # Set cardinalities for important problems\n",
    "        # Cardinalities for Size 1\n",
    "        cardinality_table.update_cardinality_estimation(Problem(0b1, 5), 5000)\n",
    "        cardinality_table.update_cardinality_estimation(Problem(0b10, 5), 4000)\n",
    "        cardinality_table.update_cardinality_estimation(Problem(0b100, 5), 500)\n",
    "        cardinality_table.update_cardinality_estimation(Problem(0b1000, 5), 100)\n",
    "        cardinality_table.update_cardinality_estimation(Problem(0b10000, 5), 1000)\n",
    "\n",
    "        # Cardinalities for Size 2\n",
    "        cardinality_table.update_cardinality_estimation(Problem(0b11, 5), 100)\n",
    "        cardinality_table.update_cardinality_estimation(Problem(0b110, 5), 500)\n",
    "        cardinality_table.update_cardinality_estimation(Problem(0b1100, 5), 100)\n",
    "        cardinality_table.update_cardinality_estimation(Problem(0b11000, 5), 100)\n",
    "\n",
    "        # Cardinalities for Size 3\n",
    "        cardinality_table.update_cardinality_estimation(Problem(0b111, 5), 500)\n",
    "        cardinality_table.update_cardinality_estimation(Problem(0b1110, 5), 50)\n",
    "        cardinality_table.update_cardinality_estimation(Problem(0b11100, 5), 100)\n",
    "\n",
    "        # Cardinalities for Size 4\n",
    "        cardinality_table.update_cardinality_estimation(Problem(0b1111, 5), 10)\n",
    "        cardinality_table.update_cardinality_estimation(Problem(0b11110, 5), 100)\n",
    "\n",
    "        # Cardinalities for Size 5\n",
    "        cardinality_table.update_cardinality_estimation(Problem(0b11111, 5), 10)\n",
    "\n",
    "        best_plan: tuple = (\n",
    "            (((3, 2), 1), 0),\n",
    "            4,\n",
    "        )\n",
    "        best_costs: int = 10770\n",
    "        algorithm_pairs: dict[str, int] = {\n",
    "            \"DP_Sub\": 42,\n",
    "            \"DP_Size\": 73,\n",
    "            \"DP_CCP\": 20,\n",
    "        }\n",
    "\n",
    "        def _test_plan_costs(enumeration_algorithm: PlanEnumerator):\n",
    "            costs, plan, enumerated_pairs = enumeration_algorithm.enumerate(\n",
    "                join_graph, cardinality_table, C_Out(), False\n",
    "            )\n",
    "            self.assertEqual(plan, best_plan)\n",
    "            self.assertEqual(costs, best_costs)\n",
    "            self.assertEqual(\n",
    "                enumerated_pairs,\n",
    "                algorithm_pairs[enumeration_algorithm.__class__.__name__],\n",
    "            )\n",
    "\n",
    "        _test_plan_costs(DP_Size())\n",
    "        _test_plan_costs(DP_Sub())\n",
    "        _test_plan_costs(DP_CCP())\n",
    "\n",
    "    def test_csg_enumeration(self):\n",
    "        chain_query: JoinGraph = ChainQueryFactory.construct_join_graph(5)\n",
    "        correct_csgs: list[Problem] = [\n",
    "            Problem(0b10000, 5),\n",
    "            Problem(0b1000, 5),\n",
    "            Problem(0b11000, 5),\n",
    "            Problem(0b100, 5),\n",
    "            Problem(0b1100, 5),\n",
    "            Problem(0b11100, 5),\n",
    "            Problem(0b10, 5),\n",
    "            Problem(0b110, 5),\n",
    "            Problem(0b1110, 5),\n",
    "            Problem(0b11110, 5),\n",
    "            Problem(0b1, 5),\n",
    "            Problem(0b11, 5),\n",
    "            Problem(0b111, 5),\n",
    "            Problem(0b1111, 5),\n",
    "            Problem(0b11111, 5),\n",
    "        ]\n",
    "\n",
    "        csgs_without_duplicates: list[Problem] = list(\n",
    "            generate_connected_problems(chain_query)\n",
    "        )\n",
    "\n",
    "        self.assertListEqual(csgs_without_duplicates, correct_csgs)\n",
    "\n",
    "    def test_ccp_helper_enumeration(self):\n",
    "        chain_query: JoinGraph = ChainQueryFactory.construct_join_graph(5)\n",
    "\n",
    "        correct_ccps: list[tuple[Problem, Problem]] = [\n",
    "            (Problem(0b1000, 5), Problem(0b10000, 5)),\n",
    "            (Problem(0b100, 5), Problem(0b1000, 5)),\n",
    "            (Problem(0b100, 5), Problem(0b11000, 5)),\n",
    "            (Problem(0b1100, 5), Problem(0b10000, 5)),\n",
    "            (Problem(0b10, 5), Problem(0b100, 5)),\n",
    "            (Problem(0b10, 5), Problem(0b1100, 5)),\n",
    "            (Problem(0b10, 5), Problem(0b11100, 5)),\n",
    "            (Problem(0b110, 5), Problem(0b1000, 5)),\n",
    "            (Problem(0b110, 5), Problem(0b11000, 5)),\n",
    "            (Problem(0b1110, 5), Problem(0b10000, 5)),\n",
    "            (Problem(0b1, 5), Problem(0b10, 5)),\n",
    "            (Problem(0b1, 5), Problem(0b110, 5)),\n",
    "            (Problem(0b1, 5), Problem(0b1110, 5)),\n",
    "            (Problem(0b1, 5), Problem(0b11110, 5)),\n",
    "            (Problem(0b11, 5), Problem(0b100, 5)),\n",
    "            (Problem(0b11, 5), Problem(0b1100, 5)),\n",
    "            (Problem(0b11, 5), Problem(0b11100, 5)),\n",
    "            (Problem(0b111, 5), Problem(0b1000, 5)),\n",
    "            (Problem(0b111, 5), Problem(0b11000, 5)),\n",
    "            (Problem(0b1111, 5), Problem(0b10000, 5)),\n",
    "        ]\n",
    "\n",
    "        generated_ccps = list(generate_ccp_pairs(chain_query))\n",
    "        self.assertListEqual(generated_ccps, correct_ccps)\n",
    "\n",
    "\n",
    "unittest.main(\n",
    "    argv=[\"ignored\", \"-v\"], defaultTest=\"EnumeratorTests\", verbosity=2, exit=False\n",
    ")"
   ],
   "id": "7dac7573e90db19",
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "test_ccp_helper_enumeration (__main__.EnumeratorTests.test_ccp_helper_enumeration) ... ok\n",
      "test_csg_enumeration (__main__.EnumeratorTests.test_csg_enumeration) ... ok\n",
      "test_plan_enumerators (__main__.EnumeratorTests.test_plan_enumerators) ... ok\n",
      "\n",
      "----------------------------------------------------------------------\n",
      "Ran 3 tests in 0.002s\n",
      "\n",
      "OK\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "<unittest.main.TestProgram at 0x10d93a750>"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "execution_count": 18
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "id": "gp10-14-90",
   "source": [
    "## 真实系统的兜底：到多少张表就不穷举了\n",
    "\n",
    "枚举量随 n 指数增长，任何穷举方案都有撑不住的一天。真实系统的做法是设阈值，超过就换成随机搜索：\n",
    "\n",
    "| 系统 / 参数 | 默认值 | 含义 |\n",
    "| --- | --- | --- |\n",
    "| PostgreSQL `geqo_threshold` | 12 | 参与连接的关系数 ≥ 12 时不再穷举，改用 GEQO |\n",
    "| PostgreSQL `join_collapse_limit` | 8 | 超过 8 个表项就不强行把 JOIN 重排成最优顺序 |\n",
    "| PostgreSQL `from_collapse_limit` | 8 | 同理，针对 FROM 里的子查询 |\n",
    "| PostgreSQL `geqo_effort` | 5 | GEQO 搜索的用力程度，越大搜索越久、计划通常越好 |\n",
    "\n",
    "（出处：PostgreSQL 官方文档 19.7 Query Planning。不同大版本默认值可能调整，使用前请核对所用版本。）\n",
    "\n",
    "换来的是**优化时间可控**——不会因为表多而把 planning 时间拖到比执行还久；\n",
    "牺牲的是**不再保证找到代价最小的计划**，只保证找到一个\"足够好\"的计划。\n",
    "\n",
    "这正是本实验的现实落点：DPccp 把可穷举的边界往上推了一截，但推不到无穷远。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "id": "gp10-14-91",
   "source": [
    "## 一个必须知道的旁注：枚举得再完整，也可能选错\n",
    "\n",
    "动态规划比较的是**估计代价**，而估计代价依赖基数估计（cardinality estimation）。\n",
    "基数估计一旦有系统性偏差，误差会沿连接的层数**指数放大**。\n",
    "\n",
    "模型：每张基表 10 000 行，过滤选择率 0.3，每次连接真实选择率 0.02；\n",
    "但优化器每层都系统性高估 f = 1.6 倍。实测得到的\"估计 / 真实\"倍数：\n",
    "\n",
    "| 连接层数 | 1 | 2 | 3 | 4 | 5 | 6 |\n",
    "| --- | --- | --- | --- | --- | --- | --- |\n",
    "| 估计 / 真实 | 1.00× | 1.60× | 2.56× | 4.10× | 6.55× | 10.49× |\n",
    "\n",
    "即第 k 层的误差约为 f^(k−1)（1.6⁰ = 1.0，1.6¹ = 1.6，1.6² = 2.56，1.6³ = 4.096，1.6⁴ = 6.5536，1.6⁵ = 10.4858）。\n",
    "与 Ioannidis & Christodoulakis（SIGMOD 1991）\"多路自然连接的基数估计误差随路数指数增长\"的结论一致。\n",
    "\n",
    "含义：**枚举算法的正确性和代价估计的准确性是两件事。** DPccp 保证\"该比的都比了\"，\n",
    "但不保证\"比出来的赢家是真的赢家\"。这也是为什么真实系统要投入大量精力做多列统计、采样与反馈校正。"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "id": "gp10-14-92",
   "source": [
    "## 思考题与详解\n",
    "\n",
    "**1. n = 6 且不考虑连接图约束（任意两表都能连）时，左深树、右深树、丛生树各有多少个计划？写出计算过程。**\n",
    "\n",
    "<details><summary>答案要点</summary>\n",
    "\n",
    "- 左深树：**n!** = 6! = 720\n",
    "- 右深树：**n!** = 720（与左深树对称，只是左右镜像）\n",
    "- 丛生树：**n! · C(n−1)** = 6! · C(5) = 720 × 42 = **30 240**\n",
    "\n",
    "其中 C(5) 是第 5 个卡塔兰数（Catalan number）：C(k) = 1/(k+1) · C(2k, k)，\n",
    "C(5) = 1/6 · C(10, 5) = 1/6 × 252 = 42。卡塔兰数在这里数的是\"n 个叶子能组成多少种不同的二叉树形状\"。\n",
    "\n",
    "出处：MPI-INF 查询优化课程讲义 / Moerkotte《Building a Query Compiler》。\n",
    "\n",
    "</details>\n",
    "\n",
    "**2. 有人说\"DPsize 与 DPsub 的枚举量只由 n 决定、与图形状无关\"。这句话对教科书的原始定义成立吗？对本仓库的实现成立吗？请用实测数字佐证。**\n",
    "\n",
    "<details><summary>答案要点</summary>\n",
    "\n",
    "**对教科书原始定义：成立。** 因为按原始定义，两种算法枚举时都不看连接图，\n",
    "只按子集的大小或组成机械枚举：\n",
    "\n",
    "- DPsize(n) = Σ_{s=2}^{n} Σ_{a=1}^{s−1} C(n,a) · C(n, s−a)\n",
    "- DPsub(n) = Σ_{k=1}^{n} C(n,k) · (2^k − 2) = 3ⁿ − 2^(n+1) + 1\n",
    "\n",
    "两式里只有 n，没有图，所以计数与图形状无关。\n",
    "\n",
    "**对本仓库实现：不成立。** 差别在三点：\n",
    "\n",
    "| | 教科书原始定义 | 本仓库实现 |\n",
    "| --- | --- | --- |\n",
    "| DP 表里放什么 | 全部 2ⁿ 个子集 | 只放**连通**的子问题 |\n",
    "| 对称对 | 按有序对计两次 | **去重**，每对只计一次 |\n",
    "| 结果 | 只由 n 决定 | **受图形状影响** |\n",
    "\n",
    "实测（n = 6）：链形 150 / 99 / 35，星形 350 / 211 / 80，环形 261 / 187 / 75，团形 1171 / 301 / 301。\n",
    "**DPsize 这一列四种形状各不相同**，与教科书公式算出的数（且不是同一口径）当然对不上。\n",
    "\n",
    "这个差异本身就是一堂课：公式刻画算法的**思想**，实测值反映**这一份实现的取舍**。\n",
    "不要拿教材公式去核对实测值。\n",
    "\n",
    "</details>\n",
    "\n",
    "**3. 链形的 DPccp 计数：n = 4 时 10、n = 5 时 20、n = 6 时 35、n = 7 时 56、n = 8 时 84。请找规律、推导通项并代入验证。**\n",
    "\n",
    "<details><summary>答案要点</summary>\n",
    "\n",
    "链形图中，连通子集一定是**连续区间**。一个 ccp 要求两侧都连通、不相交、之间有边，\n",
    "因此两侧必然是两个**相邻**的区间：S₁ = [i..j]、S₂ = [j+1..k]。\n",
    "\n",
    "确定这样一对区间，等价于从 n 个关系之间的 **n+1 个间隙位置**中选出 3 个边界\n",
    "（i 的左端、j 与 j+1 的分界、k 的右端）：\n",
    "\n",
    "    DPccp(chain, n) = C(n+1, 3)\n",
    "\n",
    "代入验证：\n",
    "\n",
    "- n = 4 → C(5,3) = 10 ✓\n",
    "- n = 5 → C(6,3) = 20 ✓\n",
    "- n = 6 → C(7,3) = 35 ✓\n",
    "- n = 7 → C(8,3) = 56 ✓\n",
    "- n = 8 → C(9,3) = 84 ✓\n",
    "\n",
    "（若按有序对计数，上式要乘 2。本仓库做对称去重，所以用不带 2 的版本。）\n",
    "\n",
    "</details>\n",
    "\n",
    "**4. 团形 n = 6 时 DPccp = 301，恰好等于本仓库的 DPsub。n = 7、n = 8 时是否仍然相等？这是巧合吗？**\n",
    "\n",
    "<details><summary>答案要点</summary>\n",
    "\n",
    "仍然相等：n = 7 时两者都是 966，n = 8 时都是 3025。**不是巧合。**\n",
    "\n",
    "团形中任意子集都连通，且任意两个非空不相交子集之间一定有边，所以 ccp 的三个条件\n",
    "（都连通、不相交、有边）自动满足。于是 DPccp 的\"跳过不连通子集\"这一步**省不掉任何东西**，\n",
    "退化成\"枚举所有无序的不重叠非空子集对\"：\n",
    "\n",
    "    DPccp(clique, n) = (3ⁿ − 2^(n+1) + 1) / 2\n",
    "\n",
    "代入：n = 6 → (729 − 128 + 1)/2 = 301 ✓；n = 7 → (2187 − 256 + 1)/2 = 966 ✓；\n",
    "n = 8 → (6561 − 512 + 1)/2 = 3025 ✓。\n",
    "\n",
    "而本仓库的 DPsub 因为做了对称去重，在团形下恰好也是同一个数，所以两者相等。\n",
    "\n",
    "**这正好说明团形是 DPccp 的最坏情况**：它全部的优势（利用连接图剪枝）在这里一点都用不上。\n",
    "幸运的是真实查询几乎不会是团形。\n",
    "\n",
    "</details>\n",
    "\n",
    "**5. 为什么 DPccp 必须保证\"生成 ccp (S₁, S₂) 时 S₁ 与 S₂ 的最优计划已经算出\"？顺序错了会怎样？**\n",
    "\n",
    "<details><summary>答案要点</summary>\n",
    "\n",
    "动态规划的前提是**最优子结构**：大问题的最优解由子问题的最优解拼成。\n",
    "\n",
    "生成 (S₁, S₂) 时若 S₁ 的最优计划还没算出来（plan table 里没有，或只有次优解），\n",
    "那么用 `C_out(S₁ ⋈ S₂) = |S₁ ∪ S₂| + C_out(S₁) + C_out(S₂)` 算出的代价就是错的，\n",
    "而且这个错误会沿着连接树**向上传递**，最终可能选出一个明显更差的计划。\n",
    "更隐蔽的是：这类错误不会报错，只会得到一个\"次优但看起来合理\"的计划。\n",
    "\n",
    "DPccp 靠两件事天然保证顺序正确：\n",
    "① csg 的扩展只允许加入编号更大的邻居，于是集合按编号单调地\"长大\"；\n",
    "② 枚举 cmp 时用 `always_exclude_set` 排掉编号更小的关系。\n",
    "结果就是子问题一定先于父问题被枚举到，不需要额外的排序或拓扑检查。\n",
    "\n",
    "</details>\n",
    "\n",
    "**6. 本仓库的 DPsize 也随图形状变化（链形 150、星形 350、环形 261、团形 1171），可 DPsize 明明\"不看连接图\"。为什么？**\n",
    "\n",
    "<details><summary>答案要点</summary>\n",
    "\n",
    "因为\"不看连接图\"指的是**生成 (S₁, S₂) 的方式**不看图（按大小机械配对），\n",
    "但**配对所用的候选池**来自 plan table，而本仓库的 plan table **只收录连通的子问题**。\n",
    "\n",
    "图形状不同 ⇒ 连通子集的个数不同 ⇒ `plan_table.entries_with_size(k)` 每次能取出的条目数不同\n",
    "⇒ 按大小配对产生的组合数就不同。团形里所有 2ⁿ 个子集都连通，候选池最大，DPsize 的枚举量也就最大（1171）；\n",
    "链形里连通子集少得多（只有连续区间），候选池小，枚举量也就小（150）。\n",
    "\n",
    "再叠加\"对称去重\"（|S₁| = |S₂| 时内层从 i+1 开始）与\"跳过重叠对\"，\n",
    "最终数字既不等于教科书公式，也确实随图形状变化。\n",
    "\n",
    "检验办法：把 plan table 改成\"收录全部子集\"再数一次，计数就会回到只由 n 决定的版本。\n",
    "\n",
    "</details>\n",
    "\n",
    "**7. 基数估计误差为什么会指数放大？用\"每层高估 f 倍\"的模型写出第 k 层的误差表达式；若每层高估 1.2 倍，6 层之后误差是多少？**\n",
    "\n",
    "<details><summary>答案要点</summary>\n",
    "\n",
    "因为每一层的估计结果会成为上一层的**输入**：若每层都高估 f 倍，这个 f 会沿着连接树连乘。\n",
    "\n",
    "    误差(k) = f^(k−1)\n",
    "\n",
    "（\"第 1 层 = 单表过滤后\"，还没有经过任何连接，误差为 f⁰ = 1。）\n",
    "\n",
    "f = 1.2、k = 6：**1.2⁵ ≈ 2.49 倍**。即使每层只高估 20%，6 张表的连接也已经把中间结果估大了近 1.5 倍。\n",
    "\n",
    "含义：优化器在表多时容易\"选崩\"，不是因为枚举算法不够好，而是因为**输入不准**。\n",
    "这也解释了为什么真实系统要在多列统计、采样、反馈校正上投入大量工程。\n",
    "\n",
    "出处：Ioannidis & Christodoulakis（SIGMOD 1991）证明 N 路自然连接的基数估计误差随 N 指数增长。\n",
    "\n",
    "</details>\n",
    "\n",
    "**8. PostgreSQL 在多少张表以上放弃穷举？用了什么替代方法？换来的是什么、牺牲的是什么？**\n",
    "\n",
    "<details><summary>答案要点</summary>\n",
    "\n",
    "`geqo_threshold` 默认 **12**：参与连接的关系数达到 12 就不再穷举，改用 **GEQO**\n",
    "（Genetic Query Optimizer，遗传算法）做随机搜索（PostgreSQL 官方文档 19.7 Query Planning）。\n",
    "相关默认值还有 `join_collapse_limit` = 8、`from_collapse_limit` = 8、`geqo_effort` = 5。\n",
    "\n",
    "- **换来的是**：优化时间可控。不会因为表多而把 planning 时间拖到比执行还久。\n",
    "- **牺牲的是**：不再保证找到代价最小的计划，只保证找到一个\"足够好\"的计划。\n",
    "  `geqo_effort` 越大，搜索越久、计划通常越好，这是在\"优化时间\"与\"计划质量\"之间做的二次权衡。\n",
    "\n",
    "这也说明 DPccp 的现实意义：它把\"还能穷举\"的边界往上推了一截，但推不到无穷远，\n",
    "所以阈值 + 随机搜索是所有真实优化器都必须有的兜底。\n",
    "\n",
    "</details>"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 2
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython2",
   "version": "2.7.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}