Contents

首先, 我先要说明一下, 这里的“多维度”可能并不太准确.  这里说的并不是数据仓库里的维度, 而只是数据的属性.

举个例子可能就比较好明白了.

比如, 如果你上京东, 或者苏宁一些网上商场买电脑, 上面一堆的过滤条件 (比如: CPU, 内存, 硬盘等) , 其实就是电脑的属性, 也就是我这里说的数据 (电脑) 的维度。

我最近做的一个系统模块, 其实就是关于数据维度匹配和使用的.  这个模块的大概原理是这样的, 根据一些输入的数据维度值, 在数据库内寻找出维度值和输入的维度值不冲突的数据.

假设数据库现在想保存的数据是眼镜的资料, 那么这些数据的维度有: 材料, 颜色, 设计 (半框, 全框, 等), 面向人群 (青, 中, 老), 面向性别(男, 女).  我现在要做的模块就是当一个人来了, 我把他的相对应的维度 (性别, 喜欢的颜色, 设计, 材料, 等), 输入到数据库中查找匹配的数据 (眼镜), 系统应该要拿出和这个人要求不冲突的.

这个模块要怎么设计呢?  数据库的表要怎么设计呢?  这里假设系统用的是JPA Entity和Oracle DB.

首先应该有一个表是存储了眼镜的信息的 (UUID, 名称, 厂家, 等).  一般来说, 刚才的那些关于眼镜的属性也就是数据维度, 自然的想法, 当然也是放在同一张表里面, 也就是用表里面不同的列, 来存放不同的维度, 比如:

表 GLASSES的列有:

| UUID | 名称 | 厂家 | 材料 | 颜色 | 设计 | 面向人群 | 面向性别 |

那么, 假设一个喜欢红色半框设计眼镜的男人来找眼镜, 根据输入来查寻数据的SQL就会是类似:

SELECT * FROM GLASSES
WHERE 设计 = '半框' AND 颜色 = '红' AND 面向性别 = '男';

但是, 其实要找出和这个人要求不冲突的眼镜, 情况并不是这样子的.

比如说, 很可能GLASSES表里有些眼镜, 它的某些属性列为空, 假设有一款眼镜并不指定面向性别.  你可能会说SQL就要变成这个样子:

SELECT * FROM GLASSES
WHERE (设计 = '半框' OR 设计 IS NULL)
AND (颜色 = '红' OR 颜色 IS NULL)
AND (面向性别 = '男' OR 面向性别 IS NULL);

但是, 也有可能这个人对设计没什么偏好.  如果是这样的话, 那可能你就要动态生成SQL, 也就是这个人如果哪个条件没有要求, 哪个条件就不加到SQL里面, 比如对设计没偏好:

SELECT * FROM GLASSES
WHERE (颜色 = '红' OR 颜色 IS NULL)
AND (面向性别 = '男' OR 面向性别 IS NULL);

到这里, 问题就比较清楚了.  如果从写代码和DB设计来考虑, 属性做为表的列来设计的话, 我个人觉得有几种不好之处:

  • 如果以后要添加或者删除属性, 表结构要不断改变, 代码也要不断改变来生成各种组合的动态SQL.

  • 从SQL的特性 (条件不确定, 维度组合多) 来看, 并且如果维度或者说列多的话, 为每一个维度创建index也不太可行, 查询效率也不高.

所以, 自然的, 我把数据维度的值, 设计成存储在子表里, 结构示例如下:

表GLASSES_ATTRIBUTES:

| UUID | GLASSES_UUID | 维度类别 | 维度值 |

这样的话, 上面的SQL就转换成:

SELECT * FROM GLASSES G
WHERE NOT EXISTS (
    SELECT 1
      FROM GLASSES_ATTRIBUTES GA
    WHERE G.UUID = GA.GLASSES_UUID
              AND (
                    (维度类别 = '颜色‘ AND 维度值 <> '红色')
                     OR
                    (维度类别 = '面向性别‘ AND 维度值 <> '男')
                    )
);

这条SQL也是需要动态生成的, 也就是最里面的OR的部份.  如果一款眼镜, 它没有定义特定的维度, 那它在子表里面就没有记录, 也满足要求.  如果是那个人没有什么特殊要求, 也就不需要生成特定的OR的部份.

这种实现方法, 个人认为相对来说好处有:

  • 虽然SQL也要动态生成, 但是变化的部份从表的列名, 转换成数据值, 逻辑会相当简单, 减少一些Hardcode的成份.

  • 数据库表GLASSES_ATTRIBUTES可以创建一个维度类别+维度值的复合index就可以了

不过, 老实说, 我也不敢肯定后面一种SQL的查询效率会高点, 因为也会有比较多OR的条件, 并且还要用上NOT EXISTS.  我曾经测试过在GLASSESE里有6W条记录, GLASSES_ATTRIBUTES上有37W条记录的环境里, 用3个维度(也就是3个OR组合), 找出1W条左右记录, 大概0.01秒.  用6个维度, 找出1K条左右记录, 大概0.1秒;  用11个维度, 找出15条记录, 也大概0.2秒左右.  所以, 看起来, 好像还可以.

其实, 我在这里把数据的维度值用行记录来存储, 而不是列值, 还因为实际的系统需求, 还有更多复杂的要求.  再举一个纯属搞笑的例子, 但实际原理是一样的.

比如一个女的要征婚, 她列下了一些要求, 也就是”如果男的xxx, 就要准备yyy之类的.   xxx就是数据 (男) 的维度, 比如年龄30以上, 或者帅, 等.  yyy就是需求, 比如有房, 有车,  年薪,  爱宠物什么的.

这里呢, 会有3个表:

  • 主表 - 这里的每一条记录, 代表一份要求

  • 维度子表 (CONDITIONS) - 存的是归到这份要求, 男的情况是什么, 如年龄30以上, 不帅

  • 需求子表 (REQUIREMENTS) - 存的是归到这份要求, 男的要准备什么, 女的才会嫁他, 如车30W以上, 房要50W以上等

系统的行为, 就是当把一个男的所有情况输入进去, 就会找到所有女方的需求是什么, 都组合在一起, 作为总的要求.  这种情况下, 系统就复杂了.  比如主表有三条记录A, B, C.  维度和需求分别是:

CONDITIONS:

| A | 年龄 | 30以上 |
| B | 相貌 | 不帅 |
| C | 相貌 | 不帅 |
| C | 宠物 | 不讨厌 |

REQUIREMENTS:
| A | 车 | 20W以上 |
| B | 房 | 50W以上 |
| C | 房 | 40W以上 |
| C | 车 | 有 |

看得明白吗?  假设一个男的30以上, 那他就要有20W以上的车才行了.  如果他又不帅, 但是讨厌宠物的话, 就要加上50W以上的房了.  既然需求有组合的情况, 不知道你们有没有想到一些问题, 就是需求是否会有冲突.

假设男的30以下, 不帅, 不讨厌宠物, 那查询出来的记录就是B和C, 那这里会同时对房有要求, 但这种情况, 不算有冲突, 因为C的Codition是包含了B的, 所以只要有车和40W的房就行了.  但是如果男的30以上, 不帅, 不讨厌宠物, 那就A, B, C都是满足条件的, 但是A和C对车都有要求, 选哪个呢?  这时就要解决冲突了, 其中一种做法, 就是在不同的Condition上, 加上比重, 比如年龄看重点, 宠物看轻点, 那就以A的为准, 要20W以上的车了.

好玩吧?  所以, 把数据维度设成行记录来存储, 还可以把不同维度加上其它一些设置, 如比重等, 配置起来会比较方便.

不知道大家有什么意见?  欢迎拍砖.

Contents