请不要让 NULL 出现在表设计中

博客指出程序员设计表时常用NULL处理不确定值,但建议给字段设定默认值。以SQL SERVER为例,展示了含NULL值查询语句执行计划性能不佳,去掉NULL、给字段设默认值可优化执行计划,避免OR操作,提升表设计质量。

一般程序员在设计表的时候,都喜欢对于不确认的值采用 NULL 的方式来处理,无论 ORACLE  SQL SERVER , MYSQL Postgresql, 都是这样(当然如果你用MONGODB 这样的问题就不存在了), 问题是当初设计虽然不知道一个确认的默认值,但我一般都建议,一定要给一个值。

我听到的最多的回复是,为什么,我不知道值是多少,我怎么确认一个默认值。

那我们就来看看下面这个例子。语句写的没有任何问题,并且索引也建立了,那我们就来看看这个语句的执行计划

在执行计划中,虽然我们没有看到脑仁疼的 table scan ,这并不是因为查询语句还好,而是因为高版本的 SQL SERVER 2016 已经优化了,TABLE SCAN 基本你看不到了,这里的主键扫描其实就已经相等于 table scan了,性能必然好不到哪里去。

在得知语句的逻辑,看似我们已经山穷水尽了,但其实问题并不是没法解决。我们只要这样干一下,我们的查询的优化器就会这样走。

不用说,上面的查询计划,已经比最初的执行计划要好的多了,基本上已经INDEX SEEK了,在最大方向上的变化是可以接受的了。

但我们的语句怎么改写后,会变成上面那样我们来看一下。

有人在看完上面的语句,已经提出异议,你这样改,如果 endtime 和 timing 有NULL 值,结果就不对了。

好了 到此我们的文字才到了 关键点, NULL 值。NULL 不是值,他什么都不是, 字段里面有  TRUE  FALSE  ,真假, 而NULL 属于什么,不知道,不晓得,不搭嘎,NULL 值在数据库处理中,大多是要多费一道手,或者浪费存储空间的(具体的原理这里不讲了,有兴趣 BAIDU 一下即可)

那我们怎么改的,让执行计划变得比原来完美的多,我们什么都没有做,就是去掉了 NULL , 我们在数据表层面,将数据字段给予默认值,例如这个我们可以添加 时间为 1800-01-01 作为默认时间(具体根据逻辑进行设定),而既然有了默认的时间,那语句自然就已经抛弃掉了 OR 这个脑仁疼的操作。

所以,没有添加额外的索引,没有对语句进行大的变动,仅仅我们添加了两个默认值,就可以化解,OR 的操作,而且我们程序中很多的 OR 的操作均源于此,所以我极力的建议,不要让表中的字段存在 NULL ,或者最低的要求是 你的查询的 字段不要有 NULL 的情况,那么其实你的表设计已经比以前进步多了。

<think>我们已知类 TYPE_DATA 的定义如下(不使用C++11,因此不使用std::string,使用C风格的字符串): class TYPE_DATA { public: tag_t body; Cstring TYPE; // 假设Cstring是char*类型 Cstring subTYPE; Cstring size; // 例如"30.5x50.25" }; 目标:根据size字符串拆分成两个double(长和宽),然后根据这两个double的值进行分组。 步骤: 1. 解析size字符串:将形如"30.5x50.25"的字符串拆分成两个double值(注意:可能包含空格,如"30.5 x 50.25")。 2. 分组:将解析后的长和宽作为分组依据,将TYPE_DATA对象分组。由于是浮点数,需要考虑浮点数的相等比较(使用容差)。 不使用C++11,因此不能使用std::istringstream(因为问题中使用了,但用户要求不使用C++11,实际上istringstream是C++98就有的,但为了确保兼容性,我们也可以使用C函数解析),但用户给出的拆分代码使用了istringstream,所以我们可以使用C函数(如strtod)来解析。 设计两个部分: a. 解析函数:输入为const char*,输出两个double值,并标记解析是否成功。 b. 分组结构:使用一个map,但map的key需要是两个double(自定义结构),并自定义比较函数(容差比较)。 但由于不使用C++11,我们可能不能用自定义结构作为map的key并传入函数对象(因为C++98中map需要严格可比较的类型,我们可以定义一个结构体并重载operator<,但C++98中需要在结构体内重载,或者使用函数对象作为比较器)。 具体步骤: 1. 解析字符串: 处理步骤: - 跳过开头的空格。 - 解析第一个浮点数(使用strtod)。 - 然后跳过中间的空格和非数字字符(我们期望是'x',但可能有空格),直到遇到分隔符(我们这里假设分隔符是'x',但可能前后有空格)。 - 解析第二个浮点数。 2. 分组: 我们定义一个结构体DimensionKey,包含两个double(length和width),并为这个结构体定义比较规则(使用容差)。 但是,由于浮点数的精度问题,我们不能直接比较两个浮点数是否相等,所以在分组时,我们使用一个自定义的比较函数(或函数对象)来定义容差。 在C++98中,我们可以这样定义: struct DimensionKey { double length; double width; }; // 自定义比较函数对象,用于map struct DimensionKeyCompare { bool operator()(const DimensionKey& a, const DimensionKey& b) const { // 使用容差比较,先比较长度,再比较宽度 const double epsilon = 0.001; if (fabs(a.length - b.length) > epsilon) { return a.length < b.length; } if (fabs(a.width - b.width) > epsilon) { return a.width < b.width; } return false; // 相等 } }; 然后使用map<DimensionKey, std::vector<TYPE_DATA*>, DimensionKeyCompare>进行分组。 注意:由于不使用C++11,我们无法使用统一的初始化列表,因此需要逐个添加元素。 但是,用户的数据可能是多个TYPE_DATA对象,我们假设有一个容器存放这些对象(比如std::vector<TYPE_DATA>)。 解析函数设计: bool parseSize(const char* str, double& length, double& width) { if (str == NULL) return false; // 跳过前导空格 while (isspace(*str)) str++; char* end; length = strtod(str, &end); if (end == str) { // 转换失败 return false; } // 此时end指向转换后的下一个字符,跳过数字后面的空格 str = end; while (isspace(*str)) str++; // 检查分隔符,我们期望是'x',但也可能是其他字符?这里我们只处理一个非数字字符(可能是任意非数字,跳过) // 实际中,我们期望是'x',但可能有空格,所以我们已经跳过了空格,现在应该是一个字符(可能是'x') if (*str == 'x') { str++; } else { // 如果不是'x',则尝试跳过所有非数字字符(除了小数点,负号)?但这里简单处理:跳过所有非数字(除了.和-)并不合适,我们只跳过空格和单个分隔符 // 如果遇到其他字符,我们跳过这个字符,但只跳一个(因为可能后面就是数字) str++; // 跳过第一个非空格非数字的字符(例如空格后的一个字符),然后继续 } // 跳过可能存在的空格 while (isspace(*str)) str++; width = strtod(str, &end); if (end == str) { // 转换失败 return false; } // 检查是否还有非空字符?如果要求整个字符串都是有效的,可以检查,但这里我们只要求成功解析两个数 return true; } 但是,上面的解析函数可能不够健壮,比如对于"30.5xx50.25"这样的字符串,它会跳过第一个'x',然后第二个'x'会被当作分隔符?这样就会出错。因此,我们更精确的做法是:第一个数字后,我们允许一个分隔符(可以是任意非数字字符,但通常我们指定为'x')并且跳过它,然后解析第二个数字。 改进:我们允许第一个数字后出现一个分隔符(可以是'x',也可以是其他非数字字符,但只有一个非数字字符),但这样也不够严谨。因此,我们可以指定分隔符为'x',并且允许前后有空格。 另一种解析方法:使用sscanf,它可以忽略空格,例如: const char* input = "30.5 x 50.25"; double l, w; if (sscanf(input, "%lf %*[xX] %lf", &l, &w) == 2) { // 成功 } 但是,sscanf的格式字符串中,%*[xX]表示跳过任意多个连续的'x'或'X',但实际中可能只有一个'x'。所以我们可以这样写:"%lf%*[^0-9]%lf",但这可能跳过非数字,但第二个数字可能是负数?所以这样并不安全。 考虑到简单性和兼容性,我们使用sscanf: if (sscanf(str, "%lf %*[xX] %lf", &length, &width) == 2) return true; // 或者尝试另一种格式:允许没有分隔符(但用空格分隔)?但题目要求是'x'分隔,所以我们可以尝试: if (sscanf(str, "%lf x %lf", &length, &width) == 2) return true; // 因为sscanf会跳过空格,所以上面的格式可以匹配"30.5x50.25", "30.5 x 50.25", "30.5X50.25"等。 但是,如果输入是"30.5,50.25",那么就会失败。因此,我们需要根据实际数据格式确定。题目中是'x',所以我们用这个。 因此,解析函数可以这样写: bool parseSize(const char* str, double& length, double& width) { // 尝试两种分隔符:x和X int count = sscanf(str, "%lf x %lf", &length, &width); if (count == 2) return true; count = sscanf(str, "%lfX%lf", &length, &width); if (count == 2) return true; // 再尝试没有空格的情况 count = sscanf(str, "%lf%*c%lf", &length, &width); // %*c跳过任意一个字符 if (count == 2) return true; return false; } 但是,这样可能会跳过非'x'的字符(比如逗号),不符合要求。所以还是按照第一种方法:明确使用'x'(小写均可)作为分隔符,且允许前后有空格。 所以,我们统一使用一个sscanf: if (sscanf(str, "%lf %*[xX] %lf", &length, &width) == 2) // 注意:%*[xX]会跳过连续的x或X,并且前面的空格和后面的空格会自动跳过(因为格式字符串中的空格匹配任意空白) 但是,%*[xX]会匹配一个或多个x或X,所以对于"30.5xx50.25"也是可以的。但题目中只有一个'x',所以这样也可以。 因此,解析函数: bool parseSize(const char* str, double& length, double& width) { // 使用sscanf,跳过空格,读取一个double,然后跳过任意空格,再跳过任意多个连续的x或X,再跳过任意空格,再读取一个double // 注意:%*[xX]会匹配一个或多个x或X,所以如果有多个x,也会被跳过 if (sscanf(str, "%lf %*[xX] %lf", &length, &width) == 2) { return true; } // 如果上述失败,可能是因为没有x,那么尝试只跳过任意一个非数字分隔符(但题目要求有x,所以以上应该足够) // 再尝试一种情况:没有空格,且只有一个x(或X) if (sscanf(str, "%lf%*[xX]%lf", &length, &width) == 2) { return true; } return false; } 但是,第二种情况可能没有空格也能被第一种匹配吗?第一种格式字符串中的空格可以匹配0个或多个空格,所以第一种格式可以匹配"30.5x50.25"(没有空格)吗?实际上,第一个%lf会吃掉数字,然后遇到空格:格式字符串中的空格表示跳过0个或多个空白(包括没有空白),然后%*[xX]会匹配x(注意此时x不是空白),然后后面的空格再匹配0个或多个空白,然后%lf。所以实际上第一种可以匹配没有空格的情况吗?我们来分析: "%lf %*[xX] %lf" - %lf: 读取第一个double,遇到非数字(这里应该是x)停止,所以第一个double读取了30.5,然后下一个字符是x。 - 然后格式字符串中的空格:匹配0个或多个空白,这里没有空白,所以跳过0个。 - 然后%*[xX]:匹配一个或多个在集合[xX]中的字符,这里匹配了x(一个字符)。 - 然后空格:匹配0个或多个空白,后面是50.25,所以接下来是数字,所以空格匹配0个空白。 - %lf:读取第二个double。 所以,实际上第一种格式可以匹配"30.5x50.25"。 因此,我们只需要一个sscanf: if (sscanf(str, "%lf %*[xX] %lf", &length, &width) == 2) { return true; } 但是,注意:如果输入是"30.5X50.25",也可以匹配。 因此,解析函数如下: bool parseSize(const char* str, double& length, double& width) { if (str == NULL) return false; return (sscanf(str, "%lf %*[xX] %lf", &length, &width) == 2); } 这个解析函数可以处理: "30.5x50.25" "30.5 x 50.25" "30.5X50.25" "30.5 x 50.25" "30.5xx50.25" // 连续的x也会被跳过(因为%*[xX]会匹配多个x) 但是,如果输入是"30.5 * 50.25",那么%*[xX]不会匹配'*',所以失败?不符合要求。但题目要求是'x',所以这样设计是符合的。 分组部分: 步骤: 1. 定义一个结构体DimensionKey,包含两个double:length和width。 2. 定义一个比较函数对象(仿函数)DimensionKeyCompare,用于比较两个DimensionKey(使用容差)。 3. 使用std::map<DimensionKey, std::vector<TYPE_DATA*>, DimensionKeyCompare>进行分组。 注意:由于不使用C++11,我们使用指针来避免拷贝对象,并且使用std::vector<TYPE_DATA*>来存储同一组内的对象指针。 代码结构: #include <map> #include <vector> #include <cmath> // fabs #include <cstdio> // sscanf #include <cctype> // isspace struct DimensionKey { double length; double width; }; struct DimensionKeyCompare { bool operator()(const DimensionKey& a, const DimensionKey& b) const { const double epsilon = 0.0001; double diff_length = a.length - b.length; if (fabs(diff_length) > epsilon) { return diff_length < 0; } double diff_width = a.width - b.width; if (fabs(diff_width) > epsilon) { return diff_width < 0; } return false; // 相等 } }; typedef std::map<DimensionKey, std::vector<TYPE_DATA*>, DimensionKeyCompare> DimensionGroupMap; void groupBySize(const std::vector<TYPE_DATA>& dataVec, DimensionGroupMap& result) { for (std::vector<TYPE_DATA>::const_iterator it = dataVec.begin(); it != dataVec.end(); ++it) { double length, width; if (!parseSize(it->size, length, width)) { // 解析失败,跳过 continue; } // 创建键 DimensionKey key; key.length = length; key.width = width; // 在map中查找或插入 // 注意:由于map使用自定义比较器,所以即使浮点数有微小差异也能正确分组 result[key].push_back(const_cast<TYPE_DATA*>(&(*it))); } } 注意:这里我们将原始数据对象的地址存入分组映射中,因此要确保原始数据在分组映射使用期间一直有效。 使用示例: std::vector<TYPE_DATA> dataSet; // 填充dataSet... DimensionGroupMap groupMap; groupBySize(dataSet, groupMap); // 遍历分组 for (DimensionGroupMap::iterator it = groupMap.begin(); it != groupMap.end(); ++it) { double length = it->first.length; double width = it->first.width; const std::vector<TYPE_DATA*>& items = it->second; // 输出分组信息 } 注意:这里我们使用const_cast是因为迭代器指向的是const对象,但是我们存储在map中的是TYPE_DATA*(非const)。如果TYPE_DATA对象在分组后不会被修改,那么应该使用const指针存储,但这里为了通用性,使用非const指针。如果数据是const的,我们可以修改为存储const指针: typedef std::map<DimensionKey, std::vector<const TYPE_DATA*>, DimensionKeyCompare> DimensionGroupMap; // 在groupBySize中 result[key].push_back(&(*it)); // 无需const_cast,因为it指向const对象,但取地址是const TYPE_DATA*,可以存入const TYPE_DATA*的vector 因此,改进: typedef std::map<DimensionKey, std::vector<const TYPE_DATA*>, DimensionKeyCompare> DimensionGroupMap; void groupBySize(const std::vector<TYPE_DATA>& dataVec, DimensionGroupMap& result) { for (std::vector<TYPE_DATA>::const_iterator it = dataVec.begin(); it != dataVec.end(); ++it) { double length, width; if (!parseSize(it->size, length, width)) { continue; } DimensionKey key = {length, width}; result[key].push_back(&(*it)); } } 这样更安全。 完整设计方案: 步骤1:定义解析函数parseSize。 步骤2:定义DimensionKey和比较仿函数DimensionKeyCompare。 步骤3:定义分组映射类型。 步骤4:遍历数据,解析每个对象的size,然后根据解析结果将对象地址放入分组映射中。 注意:浮点数的容差值可以根据实际情况调整。 下面是一个完整的代码示例(不包括main函数): </think>### 基于 C++98 的尺寸分组方案设计 #### 核心设计思路 根据 TYPE_DATA 类的需求,使用 C 标准库函数将 `size` 字符串拆分为两个 double 值,并通过自定义比较函数实现浮点数分组。方案分为三个核心模块: ```mermaid graph TD A[数据输入] --> B[尺寸解析器] B --> C[分组管理器] C --> D[分组结果] ``` #### 完整实现方案 ##### 1. 尺寸解析器(DimensionParser) ```cpp #include <cstdlib> // strtod #include <cstring> // strchr, isspace class DimensionParser { public: struct Dimensions { double length; double width; bool isValid; }; // 支持空白字符的解析方法(C++98兼容) static Dimensions parse(const char* sizeStr) { Dimensions dim = {0.0, 0.0, false}; if (!sizeStr) return dim; // 创建可修改的副本 char* input = strdup(sizeStr); if (!input) return dim; // 预处理:移除所有空白字符 char* p = input; char* q = input; while (*q) { if (!isspace(*q)) *p++ = *q; q++; } *p = '\0'; // 查找分隔符'x' char* sepPos = strchr(input, 'x'); if (!sepPos) { free(input); return dim; } // 提取长度部分 char* endPtr; dim.length = strtod(input, &endPtr); if (endPtr != sepPos) { free(input); return dim; } // 提取宽度部分 dim.width = strtod(sepPos + 1, &endPtr); if (*endPtr != '\0') { free(input); return dim; } free(input); dim.isValid = true; return dim; } }; ``` ##### 2. 分组管理器(DimensionGrouper) ```cpp #include <map> #include <vector> #include <cmath> // fabs struct DimensionKey { double length; double width; }; // 自定义比较函数(C++98兼容) struct DimensionComparator { static const double EPSILON; bool operator()(const DimensionKey& a, const DimensionKey& b) const { if (fabs(a.length - b.length) > EPSILON) return a.length < b.length; return a.width < b.width; } }; const double DimensionComparator::EPSILON = 0.001; class DimensionGrouper { public: typedef std::map<DimensionKey, std::vector<TYPE_DATA*>, DimensionComparator> GroupMap; static GroupMap groupItems(std::vector<TYPE_DATA*>& items) { GroupMap groups; for (std::vector<TYPE_DATA*>::iterator it = items.begin(); it != items.end(); ++it) { if (!(*it)->size) continue; DimensionParser::Dimensions dim = DimensionParser::parse((*it)->size); if (!dim.isValid) continue; DimensionKey key = {dim.length, dim.width}; groups[key].push_back(*it); } return groups; } }; ``` ##### 3. TYPE_DATA 集成示例 ```cpp // 类定义(示例) class TYPE_DATA { public: tag_t body; char* TYPE; // C风格字符串 char* subTYPE; char* size; // 格式 "长x宽" }; // 使用示例 int main() { std::vector<TYPE_DATA*> dataSet; // 初始化数据... // 执行分组 DimensionGrouper::GroupMap groups = DimensionGrouper::groupItems(dataSet); // 遍历分组结果 for (DimensionGrouper::GroupMap::iterator it = groups.begin(); it != groups.end(); ++it) { printf("尺寸分组: %.2fx%.2f | 成员数量: %d\n", it->first.length, it->first.width, static_cast<int>(it->second.size())); // 处理组内每个项目 for (std::vector<TYPE_DATA*>::iterator item = it->second.begin(); item != it->second.end(); ++item) { // 业务逻辑... } } return 0; } ``` #### 关键技术解析 1. **字符串预处理技术** 使用 $O(n)$ 时间复杂度的原地算法移除空白字符: $$ \text{时间复杂度}: O(n) \quad \text{空间复杂度}: O(1) $$ 核心逻辑: ```c char* p = input; char* q = input; while (*q) { if (!isspace(*q)) *p++ = *q; q++; } *p = '\0'; ``` 2. **浮点数精确分组** 采用相对容差比较策略: $$ |a - b| \leq \varepsilon \quad \text{视为相等} $$ 其中 $\varepsilon = 0.001$ 为预设阈值 3. **内存安全管理** - 使用 `strdup` 创建字符串副本[^1] - 显式调用 `free` 释放内存 - 空指针安全检查 ```c char* input = strdup(sizeStr); if (!input) return dim; // 分配失败检查 // ... 业务逻辑 ... free(input); // 显式释放内存 ``` #### 错误处理扩展方案 ```cpp enum ParseError { SUCCESS, NULL_INPUT, MEM_ALLOC_FAIL, MISSING_DELIMITER, INVALID_NUMBER }; DimensionParser::Dimensions parseWithError( const char* sizeStr, ParseError* errCode = NULL) { if (errCode) *errCode = SUCCESS; if (!sizeStr) { if (errCode) *errCode = NULL_INPUT; return Dimensions{0.0, 0.0, false}; } char* input = strdup(sizeStr); if (!input) { if (errCode) *errCode = MEM_ALLOC_FAIL; return Dimensions{0.0, 0.0, false}; } // ... 解析逻辑 ... if (!sepPos) { free(input); if (errCode) *errCode = MISSING_DELIMITER; return Dimensions{0.0, 0.0, false}; } if (endPtr != sepPos || *endPtr != '\0') { free(input); if (errCode) *errCode = INVALID_NUMBER; return Dimensions{0.0, 0.0, false}; } free(input); return Dimensions{dim.length, dim.width, true}; } ```
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值