紫色喵 發表於 2026-2-3 22:26:00

一种多选项的高效存取(存储、查询)解决方案

<p><strong>本文主要内容</strong></p>
<ul>
<li>对于多选项的值,<span style="color: rgba(224, 62, 45, 1)">如何保存</span>?本文提供了一种非常规的方案。</li>
<li>对于记录在数据库中的多选项的值,如何<span style="color: rgba(224, 62, 45, 1)">快速查询</span>那些记录是包含了某个(某些)选项?本文使用了“与位运算”解决查询问题。</li>
<li><strong>源码地址:https://github.com/HackyleShawe/JavaBackEndDemos/tree/master/BusinessCommonSolution/multi-options-storage-query-demo</strong></li>
</ul>
<p><strong>文章前置知识:</strong>SpringBoot、JDBCTemplate、位运算(与运算)、jQuery</p>
<p><strong>内容导览&nbsp;</strong></p>
<div class="mce-toc">
<ul>
<li>背景</li>
<li>设计思想
<ul>
<li>编码:将多选项转换为数字
<ul>
<li>例1</li>
<li>例2</li>
</ul>
</li>
<li>查询原理</li>
</ul>
</li>
<li>项目启动
<ul>
<li>新增数据演示</li>
<li>查询数据演示</li>
</ul>
</li>
<li>查询示例
<ul>
<li>构造数据</li>
<li>插入到数据库</li>
<li>查询选择了"1-编程"这个选项的记录</li>
<li>查询选择了"2-听音乐唱歌,5-看电影"这些选项的记录</li>
</ul>
</li>
</ul>
</div>
<h1 id="mcetoc_1h68ttla31tr7" class="md-end-block md-heading md-focus"><span class="md-plain">背景</span></h1>
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-plain">在项目开发中,如何保存多选项的值呢?</span></strong></span><span class="md-plain md-expand">例如下图中的职业发展和兴趣爱好</span></p>
<p><img src="https://res.hackyle.com/blog/2023/07/f0d775b5795840f4999f5cbc88ee5ab0.png"></p>
<ul class="ul-list" data-mark="-">
<li class="md-list-item md-focus-container">
<p class="md-end-block md-p md-focus"><span class="md-plain md-expand">最容易想到的就是,选择了哪些选项,就把该选项值存储起来。</span></p>
</li>
<li class="md-list-item md-focus-container">
<p class="md-end-block md-p md-focus"><span class="md-plain md-expand">在数据库层面设置一个VARCHAR,例如选择了"收入无上线、培训与发展、职业价值感",前端就传递"收入无上线、培训与发展、职业价值感",数据库就保存为"收入无上线、培训与发展、职业价值感"。</span></p>
</li>
</ul>
<p class="md-end-block md-p md-focus"><span class="md-pair-s "><strong><span class="md-plain">可是这样存在一个问题</span></strong></span><span class="md-plain">,我要查询那些人选择了其中的某个、某些选项,就很难实现。例如,查询那些人的兴趣爱好是"编程、篮球",查询兴趣爱好是"看书、写作"的人数有多少。</span></p>
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-plain">为了解决这种多选项的高效查询问题</span></strong></span><span class="md-plain">,本人设计了一种方法,可以实现</span><span class="md-pair-s "><strong><span class="md-plain">快速、高效地查询</span></strong></span><span class="md-plain md-expand">多选项中有某个、某些选项的所有记录。</span></p>
<h1 id="mcetoc_1h68ttla31tr8" class="md-end-block md-heading"><span class="md-plain">设计思想</span></h1>
<p class="md-end-block md-p"><span class="md-pair-s"><strong><span class="md-plain">核心思路:</span></strong></span></p>
<ol class="ol-list">
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-plain">编码</span></strong></span><span class="md-plain">:将每个选项与特定的比特位上对应起来,选中为1,未选中为0</span></p>
</li>
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-pair-s"><strong><span class="md-plain">存储</span></strong></span><span class="md-plain">:将比特位转换为Long型(64bit,最大支持64个多选项),数据库表字段为BIGINT</span></p>
</li>
<li class="md-list-item md-focus-container">
<p class="md-end-block md-p md-focus"><span class="md-pair-s"><strong><span class="md-plain">查询</span></strong></span><span class="md-plain md-expand">:与(&amp;)位运算</span></p>
</li>
</ol>
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-plain">主要流程</span></strong></span></p>
<ul class="ul-list" data-mark="-">
<li class="md-list-item">
<p class="md-end-block md-p"><span style="background-color: rgba(255, 255, 255, 1)"><span class="md-plain">编码:将多选项进行</span><span class="md-pair-s "><strong><span class="md-plain">编号</span></strong></span><span class="md-plain">:</span></span></p>
<ul class="ul-list" data-mark="-">
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-plain">例如:对职业发展的多选项进行编号为:1-收入无上限,2-培训与发展,3-职业价值感,4-行业稳定性,5-社交与人脉,6-塑造个人品牌,7-团队综合素质,8-终身学习;</span></p>
</li>
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-plain">对兴趣爱好的多选项进行编号为:1-编程,2-听音乐唱歌,3-篮球,4-玩游戏,5-看电影,6-享美食,7-健身,8-旅游,9-看书,10-写作。</span></p>
</li>
</ul>
</li>
<li class="md-list-item">
<p class="md-end-block md-p"><span style="background-color: rgba(255, 255, 255, 1)"><span class="md-plain">存储</span></span></p>
<ul class="ul-list" data-mark="-">
<li class="md-list-item">
<p class="md-end-block md-p"><span style="background-color: rgba(236, 202, 250, 1)"><span class="md-plain">前端:</span><span class="md-pair-s "><strong><span class="md-plain">选择了哪些选项,就传递哪些选项的编号</span></strong></span></span><span class="md-plain"><span style="background-color: rgba(236, 202, 250, 1)">。</span>例如:选择了"编程、篮球",则传递的编号串为:"1,3"</span></p>
</li>
<li class="md-list-item">
<p class="md-end-block md-p"><span style="background-color: rgba(191, 237, 210, 1)"><span class="md-plain">后端:将</span><span class="md-pair-s "><strong><span class="md-plain">编号串转换为二进制串</span></strong></span><span class="md-plain">,再</span><span class="md-pair-s "><strong><span class="md-plain">转换为数字</span></strong></span><span class="md-plain">,落库</span></span></p>
</li>
</ul>
</li>
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-plain">查询:前端还是只传选项编号;后端将其转换为数字;在数据库层面使用</span><span class="md-pair-s "><strong><span class="md-plain">位运算中的<span style="background-color: rgba(251, 238, 184, 1)">与运算</span></span></strong></span><span class="md-plain"><span style="background-color: rgba(251, 238, 184, 1)">,匹配包含了参数选项的记录</span>。</span></p>
</li>
</ul>
<h2 id="mcetoc_1h68ttla31tr9" class="md-end-block md-heading md-focus"><span class="md-plain md-expand">编码:将多选项转换为数字</span></h2>
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-plain">现以兴趣爱好为例,为其多选项定义编号:</span></strong></span><span class="md-plain">1-编程,2-听音乐唱歌,3-篮球,4-玩游戏,5-看电影,6-享美食,7-健身,8-旅游,9-看书,10-写作</span></p>
<p class="md-end-block md-p"><span class="md-pair-s"><strong><span class="md-plain">根据二进制位下标与十进制数的互转:</span></strong></span></p>
<p><span class="md-plain md-expand"><img src="https://res.hackyle.com/blog/2023/07/e97841b3e50f445bbef0e652e238570a.png"></span></p>
<h3 id="mcetoc_1h68ttla31tra" class="md-end-block md-heading md-focus"><span class="md-plain md-expand">例1</span></h3>
<blockquote>
<p>用户A勾选的兴趣爱好为:"1-编程,2-听音乐唱歌,5-看电影,6-享美食"<br>前端传递的串为:1,2,5,6<br>转换为二进制串:0011 0011<br>转换规则:在有选项编号出现的位下标的位置上填充1,其他位置填充0<br>转换为十进制后落库:51</p>
</blockquote>
<p><span class="md-plain md-expand"><img src="https://res.hackyle.com/blog/2023/07/baf08e7a01df45a48d72cb2a13de8496.png"></span></p>
<h3 id="mcetoc_1h68ttla31trb" class="md-end-block md-heading md-focus"><span class="md-plain md-expand">例2</span></h3>
<blockquote>
<p><span class="md-plain md-expand">用户B勾选的兴趣爱好为:"3-篮球,4-玩游戏,6-享美食,7-健身,10-写作"<br>前端传递的串为:3,4,6,7,10<br>转换为二进制串:0010 0110 1100<br>转换为十进制后落库:620</span></p>
</blockquote>
<p><span class="md-plain md-expand"><img src="https://res.hackyle.com/blog/2023/07/03d6e1abf7254392a4376722a1d9a554.png"></span></p>
<h2 id="mcetoc_1h68ttla31trc" class="md-end-block md-heading"><span class="md-plain">查询原理</span></h2>
<p class="md-end-block md-p"><span class="md-plain">查询的核心思想:与(&amp;)位运算</span></p>
<ul class="ul-list" data-mark="-">
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-plain"><span style="color: rgba(224, 62, 45, 1)"><strong>X &amp; Y = X</strong></span>,则说明X的所有值为"1"的二进制位、在Y中对应的二进制位也为"1"。</span></p>
</li>
<li class="md-list-item md-focus-container">
<p class="md-end-block md-p md-focus"><span class="md-plain">也就是说,<span style="color: rgba(241, 196, 15, 1)">Y为"1"的二进制位包含了X的所有为"1"的二进制位</span>。从而实现了,查询条件X,在Y中存在(包含)</span></p>
</li>
</ul>
<p><span class="md-plain md-expand"><img src="https://res.hackyle.com/blog/2023/07/4fcddc458e614fb1889177d12fd8fd42.png"></span></p>
<p><span class="md-plain md-expand"><img src="https://res.hackyle.com/blog/2023/07/9c7e8bcfa5b44620933ff1d796c7dfb0.png"></span></p>
<h1 id="mcetoc_1h68ttla31trd" class="md-end-block md-heading"><span class="md-plain">项目启动</span></h1>
<p class="md-end-block md-p"><span class="md-plain">Step 1:在application.yml中修改数据库连接参数</span></p>
<p class="md-end-block md-p"><span class="md-plain">Step 2:执行resources/sql.sql下的SQL文件,初始化数据</span></p>
<p class="md-end-block md-p"><span class="md-plain">Step 3:从启动类App.java启动</span></p>
<p class="md-end-block md-p md-focus"><span class="md-plain">Step 4:启动成功后进入前端页面:</span><span class="md-link md-pair-s md-expand" spellcheck="false">http://localhost:9898/person.html</span></p>
<h2 id="mcetoc_1h68ttla31tre" class="md-end-block md-heading"><span class="md-plain">新增数据演示</span></h2>
<p class="md-end-block md-p md-focus"><span class="md-pair-s "><strong><span class="md-plain">必要数据,多选项进行勾选:</span></strong></span></p>
<p><span class="md-plain md-expand"><img src="https://res.hackyle.com/blog/2023/07/0c1bbd09e9b64b588a2d24300c4c0bc7.png"></span></p>
<p><span class="md-plain md-expand"><strong><span class="md-plain">去数据库查看刚刚新增的记录:</span></strong></span></p>
<p><span class="md-plain md-expand"><img src="https://res.hackyle.com/blog/2023/07/29eeeb3be74a4e78ada71752f5f470bd.png"></span></p>
<h2 id="mcetoc_1h68ttla31trf" class="md-end-block md-heading"><span class="md-plain">查询数据演示</span></h2>
<p class="md-end-block md-p md-focus md-expand"><span class="md-pair-s md-expand"><strong><span class="md-plain">选择查询条件,点击“Query”进行查询:</span></strong></span></p>
<p><span class="md-plain md-expand"><img src="https://res.hackyle.com/blog/2023/07/23525cb83ce746f0ac51c2403a8ee35c.png"></span></p>
<p><span class="md-plain md-expand"><strong><span class="md-plain">查看运行日志,显示执行的SQL:</span></strong></span></p>
<p><span class="md-plain md-expand"><img src="https://res.hackyle.com/blog/2023/07/aa446ed6b9b946ef99d12a1032fc2a9b.png"></span></p>
<h1 id="mcetoc_1h68ttla31trg" class="md-end-block md-heading"><span class="md-plain md-expand">查询示例</span></h1>
<p class="md-end-block md-p"><span class="md-plain">以多选项”兴趣爱好“为例,展示查询的工作原理</span></p>
<h2 id="mcetoc_1h68ttla31trh" class="md-end-block md-heading"><span class="md-plain">构造数据</span></h2>
<p>AA选择了"1-编程,2-听音乐唱歌,5-看电影,6-享美食,7-健身,8-旅游"<br>前端传递的编号串:1,2,5,6,7,8<br>转换为二进制串:0000 1111 0011<br>转换为数字:243</p>
<p>BB选择了"3-篮球,4-玩游戏,8-旅游,9-看书,10-写作"<br>前端传递的编号串:3,4,8,9,10<br>转换为二进制串:0011 1000 1100<br>转换为数字:908</p>
<p>CC选择了"2-听音乐唱歌,3-篮球,5-看电影,6-享美食,7-健身,9-看书,10-写作"<br>前端传递的编号串:2,3,5,6,7,9,10<br>转换为二进制串:0011 0111 0110<br>转换为数字:886</p>
<p>DD选择了"1-编程,3-篮球,4-玩游戏,6-享美食,7-健身,8-旅游,10-写作"<br>前端传递的编号串:1,3,4,6,7,8,10<br>转换为二进制串:0010 0110 1101<br>转换为数字:749</p>
<p>EE选择了"2-听音乐唱歌,4-玩游戏,5-看电影,7-健身,8-旅游,10-写作"<br>前端传递的编号串:2,4,5,7,8,10<br>转换为二进制串:0010 1101 1010<br>转换为数字:730</p>
<p>FF选择了"1-编程,2-听音乐唱歌,3-篮球,4-玩游戏,5-看电影,7-健身"<br>前端传递的编号串:1,2,3,4,5,7<br>转换为二进制串:0000 0101 1111<br>转换为数字:95</p>
<p>GG选择了"1-编程,3-篮球,4-玩游戏,6-享美食,8-旅游,10-写作"<br>前端传递的编号串:1,3,4,6,8,10<br>转换为二进制串:0010 1010 1101<br>转换为数字:685</p>
<h2 id="mcetoc_1h68ttla31tri" class="md-end-block md-heading md-focus"><span class="md-plain">插入到数据库</span></h2>
<pre class="line-numbers language-sql highlighter-hljs"><code>DROP TABLE IF EXISTS person;
CREATE TABLE person (
    id BIGINT AUTO_INCREMENT,
    name VARCHAR(50) DEFAULT NULL COMMENT '姓名',
    gender INT DEFAULT NULL COMMENT '性别,0-女,1-男',
    address VARCHAR(128) DEFAULT NULL COMMENT '地址',
    careers BIGINT DEFAULT NULL COMMENT '职业发展多选项',
    -- 兴趣爱好多选项。可选项:1-编程,2-听音乐唱歌,3-篮球,4-玩游戏,5-看电影,6-享美食,7-健身,8-旅游,9-看书,10-写作。
    -- 例如,全选:"11 1111 1111",保存为十进制=1023,全不选:"00 0000 0000",保存为十进制=0,只选择听音乐唱歌:"0000 0010",保存为十进制=2
    -- LONG最大支持64位,最多支持64个多选项的任意选择
    interests BIGINT DEFAULT NULL COMMENT '兴趣爱好多选项',
    create_time DATETIME DEFAULT NULL,
    update_time DATETIME DEFAULT NULL,
    deleted INT DEFAULT 0 COMMENT '是否删除:0-否,1-是',
    PRIMARY KEY(id)
);</code></pre>
<pre class="line-numbers language-sql highlighter-hljs"><code>-- 将上文中构造的数据,以SQL的形式插入到数据库中,只以多选项"兴趣爱好"为例
INSERT INTO person(name, gender, address, careers, interests, create_time, update_time, deleted)
VALUES ('AA', 1, 'SH CN', 1, 243, '2022-12-12','2023-12-12', 0),
       ('BB', 1, 'SH CN', 1, 908, '2022-12-12','2023-12-12', 0),
       ('CC', 1, 'SH CN', 1, 886, '2022-12-12','2023-12-12', 0),
       ('DD', 1, 'SH CN', 1, 749, '2022-12-12','2023-12-12', 0),
       ('EE', 1, 'SH CN', 1, 730, '2022-12-12','2023-12-12', 0),
       ('FF', 1, 'SH CN', 1, 95, '2022-12-12','2023-12-12', 0),
       ('GG', 1, 'SH CN', 1, 685, '2022-12-12','2023-12-12', 0);</code></pre>
<h2 id="mcetoc_1h68ttla31trj" class="md-end-block md-heading"><span class="md-plain md-expand">查询选择了"1-编程"这个选项的记录</span></h2>
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-plain">目标:</span></strong></span><span class="md-plain">在多选项中查询选择了"1-编程"这个选项的记录</span></p>
<p class="md-end-block md-p md-focus"><span class="md-pair-s "><strong><span class="md-plain">用户的"兴趣爱好"多选项(二进制形式)</span></strong></span></p>
<blockquote>
<p><span class="md-plain md-expand">AA: 0000 1111 0011<br>BB: 0011 1000 1100<br>CC: 0011 0111 0110<br>DD: 0010 0110 1101<br>EE: &nbsp;0010 1101 1010<br>FF: &nbsp;0000 0101 1111<br>GG: 0010 1010 1101</span></p>
</blockquote>
<p class="md-end-block md-p md-focus"><span class="md-pair-s"><strong><span class="md-plain">将"1-编程"进行转换</span></strong></span></p>
<ul class="ul-list" data-mark="-">
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-plain">转换为二进制:0000 0000 0001</span></p>
</li>
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-plain">转换为十进制:1</span></p>
</li>
</ul>
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-plain">查询原理:</span></strong></span><span class="md-plain md-expand">将查询条件"0000 0000 0001"与AA~GG的二进制位进行与运算后,仍然为查询条件的记录,则是选择了"1-编程"这个选项的记录</span></p>
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-plain">查询过程</span></strong></span></p>
<ul>
<li><span class="md-plain md-expand">将查询条件与AA的二进制位进行与运算:</span></li>
</ul>
<pre class="line-numbers language-properties highlighter-hljs"><code>   0000 0000 0001
&amp;0000 1111 0011
#--------------------------
   0000 0000 0001与运算结果仍为查询条件,说明这条记录包含了"1-编程"这个选项</code></pre>
<ul>
<li><span class="md-plain md-expand">将查询条件与BB的二进制位进行与运算:</span></li>
</ul>
<pre class="line-numbers language-properties highlighter-hljs"><code>   0000 0000 0001
&amp;0011 1000 1100
#----------------------------
   0000 0000 0000   与运算结果不为查询条件,说明这条记录不包含了"1-编程"这个选项</code></pre>
<ul class="ul-list" data-mark="-">
<li class="md-list-item md-focus-container">
<p class="md-end-block md-p md-focus"><span class="md-plain md-expand">其他记录运算同理</span></p>
</li>
<li class="md-list-item md-focus-container">
<p class="md-end-block md-p md-focus"><span class="md-plain md-expand">最终发现只有AA、DD、FF、GG的运算结果符合条件,这四个记录就是满足"选择了1-编程这个选项的所有记录"</span></p>
</li>
</ul>
<p><span class="md-plain md-expand"><strong><span class="md-plain">SQL实现</span></strong></span></p>
<pre class="line-numbers language-sql highlighter-hljs"><code>select * FROM person WHERE interests &amp; 1 = 1;</code></pre>
<p><span class="md-plain md-expand"><strong><img src="https://res.hackyle.com/blog/2023/07/986da085eab5476693f1dc689150aeed.png"></strong></span></p>
<h2 id="mcetoc_1h68ttla31trk" class="md-end-block md-heading"><span class="md-plain">查询选择了"2-听音乐唱歌,5-看电影"这些选项的记录</span></h2>
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-plain">目标:</span></strong></span><span class="md-plain">在多选项中查询选择了"2-听音乐唱歌,5-看电影"这些选项的记录</span></p>
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-plain">用户的"兴趣爱好"多选项(二进制形式)</span></strong></span></p>
<blockquote>
<p><span class="md-plain md-expand"><strong>AA: 0000 1111 0011<br>BB: 0011 1000 1100<br>CC: 0011 0111 0110<br>DD: 0010 0110 1101<br>EE: &nbsp;0010 1101 1010<br>FF: &nbsp;0000 0101 1111<br>GG: 0010 1010 1101</strong></span></p>
</blockquote>
<p class="md-end-block md-p"><span class="md-pair-s md-expand"><strong><span class="md-plain">将"2-听音乐唱歌,5-看电影"进行转换</span></strong></span></p>
<ul class="ul-list" data-mark="-">
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-plain">转换为二进制:0000 0001 0010</span></p>
</li>
<li class="md-list-item">
<p class="md-end-block md-p"><span class="md-plain">转换为十进制:18</span></p>
</li>
</ul>
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-plain">查询原理:</span></strong></span><span class="md-plain">将查询条件"0000 0001 0010"与AA~GG的二进制位进行与运算后,仍然为查询条件的记录,则是选择了"2-听音乐唱歌,5-看电影"这个选项的记录</span></p>
<p class="md-end-block md-p"><span class="md-pair-s "><strong><span class="md-plain">查询过程</span></strong></span></p>
<ul class="ul-list" data-mark="-">
<li class="md-list-item md-focus-container">
<p class="md-end-block md-p md-focus"><span class="md-plain">将查询条件与AA的二进制位进行与运算:</span></p>
</li>
</ul>
<pre class="line-numbers language-properties highlighter-hljs"><code>0000 0001 0010
&amp; 0000 1111 0011
#--------------------------
   0000 0001 0010与运算结果仍为查询条件,说明这条记录包含了"2-听音乐唱歌,5-看电影"这些选项</code></pre>
<ul>
<li><span class="md-plain md-expand">将查询条件与BB的二进制位进行与运算:</span></li>
</ul>
<pre class="line-numbers language-properties highlighter-hljs"><code>0000 0001 0010
&amp; 0011 1000 1100
#----------------------------
0000 0000 0000   与运算结果不为查询条件,说明这条记录不包含了"2-听音乐唱歌,5-看电影"这些选项</code></pre>
<ul class="ul-list" data-mark="-">
<li class="md-list-item md-focus-container">
<p class="md-end-block md-p md-focus"><span class="md-plain md-expand">其他记录运算同理</span></p>
</li>
<li class="md-list-item md-focus-container">
<p class="md-end-block md-p md-focus"><span class="md-plain md-expand">最终发现只有AA、CC、EE、FF的运算结果符合条件,这四个记录就是满足"选择了2-听音乐唱歌,5-看电影这些选项的所有记录"</span></p>
</li>
</ul>
<p><span class="md-plain md-expand"><strong><span class="md-plain">SQL实现</span></strong></span></p>
<pre class="line-numbers language-sql highlighter-hljs"><code>select * FROM person WHERE interests &amp; 18 = 18;</code></pre>
<p><img src="https://res.hackyle.com/blog/2023/07/2ef948cfe3034769b7b4df09e5daabdb.png"></p>
<p>&nbsp;</p><br><br>
来源:https://www.cnblogs.com/hackyle/p/19530712
頁: [1]
查看完整版本: 一种多选项的高效存取(存储、查询)解决方案