MySQL返回列包含任何但仅包含一组关键字的所有行

Posted

技术标签:

【中文标题】MySQL返回列包含任何但仅包含一组关键字的所有行【英文标题】:MySQL return all rows where a column contains any but only keywords from a set 【发布时间】:2014-04-03 20:13:45 【问题描述】:

有没有办法选择其中一列仅包含但任意数量的预定义值的行?

我一直在使用它,但它会返回我的列中至少包含一个值的任何行(这正是它应该做的,我知道)。

但我正在寻找一种仅选择关键字列中只有我的关键字的行的方法。

SELECT * 
FROM 
    `products`.`product` 
WHERE 
    keywords LIKE '%chocolate%' 
AND keyword LIKE '%vanilla%';

示例关键字:chocolate, sugar, milk, oats

使用上面的关键字,我希望返回前两个结果,而不是最后两个:

Product1: chocolate, sugar 

Product2: chocolate 

Product3: chocolate, sugar, milk, oats, bran 

Product4: chocolate, sugar, salt

我的列包含适用于该产品行的所有关键字的逗号分隔列表。

【问题讨论】:

【参考方案1】:

由于您将列表存储为包含逗号分隔列表的字符串,而不是作为一个集合,因此 mysql 对此无能为力。当它被插入数据库时​​,MySQL 将其视为单个字符串。当从数据库中检索到它时,MySQL 将其视为单个字符串。当我们在查询中引用它时,MySQL 将其视为单个字符串。


如果“列表”存储为标准关系集,产品的每个关键字存储为表中的单独行,则返回您指定的结果集几乎是微不足道的。

例如,如果我们有这个表:

CREATE TABLE product_keyword 
product_id      BIGINT UNSIGNED COMMENT 'FK ref products.id'
keyword         VARCHAR(20)

将与特定产品关联的每个关键字作为单独的行:

product_id keyword
---------- ---------
         1 chocolate
         1 sugar
         2 chocolate
         3 bran
         3 chocolate
         3 milk
         3 oats
         3 sugar
         4 chocolate
         4 salt
         4 sugar

然后查找product 中除'chocolate''vanilla' 以外的关键字的所有行

SELECT p.id
  FROM product p
  JOIN product_keyword k
 WHERE k.product_id = p.id
    ON k.keyword NOT IN ('chocolate','vanilla')
 GROUP BY p.id

--或--

SELECT p.id
  FROM product p
  LEFT
  JOIN ( SELECT j.id
           FROM product_keyword j
          WHERE j.keyword NOT IN ('chocolate','vanilla')
         GROUP BY j.id
       ) k
    ON k.id = p.id 
 WHERE k.id IS NULL

要获取至少包含关键字“巧克力”和“香草”之一但没有关联其他关键字的产品,它与上述查询相同,但有一个额外的连接:

SELECT p.id
  FROM product p
  JOIN ( SELECT g.id
           FROM product_keyword g
          WHERE g.keyword IN ('chocolate','vanilla')
         GROUP BY g.id
       ) h
    ON h.id = p.id 
  LEFT
  JOIN ( SELECT j.id
           FROM product_keyword j
          WHERE j.keyword NOT IN ('chocolate','vanilla')
         GROUP BY j.id
       ) k
    ON k.id = p.id 
 WHERE k.id IS NULL

我们可以解开这些查询,它们并不难。查询h 返回包含至少一个关键字的product_id 列表,查询k 返回包含除指定关键字之外的其他关键字的product_id 列表。那里的“技巧”(如果你想这样称呼它)是反连接模式......进行外连接以匹配行,并包括没有匹配的行,以及 WHERE 子句中的谓词消除匹配的行,留下产品中没有匹配的行集。


但是将集合存储为单个字符列中的“逗号分隔列表”,我们失去了关系代数的所有优点;没有任何简单的方法可以将关键字列表作为“集合”处理。

将整个列表存储为单个字符串,我们有一些可怕的 SQL 来获得指定的结果。

进行您指定的检查的一种方法是创建一组所有可能的“匹配”,然后检查这些。这适用于几个关键字。例如,要获取仅包含关键字 'vanilla' 和/或 'chocolate' 的产品列表(即,至少包含其中一个关键字并且没有任何其他关键字):

SELECT p.id
  FROM product 
 WHERE keyword_list = 'chocolate'
    OR keyword_list = 'vanilla'
    OR keyword_list = 'chocolate,vanilla'
    OR keyword_list = 'vanilla,chocolate'

但是将其扩展到三个、四个或五个关键字很快就会变得笨拙(除非保证关键字以特定顺序出现。而且很难检查四个关键字中的三个。

另一种(丑陋的)方法是将keyword_list 转换为一个集合,以便我们可以使用我的答案中的第一个查询。但是执行转换的 SQL 受到可以从关键字列表中提取的任意最大关键字数量的限制。

使用一些简单的 SQL 字符串函数从逗号分隔列表中提取第 n 个元素相当容易,例如,从逗号分隔列表中提取前五个元素:

SET @l := 'chocolate,sugar,bran,oats'
SELECT NULLIF(SUBSTRING_INDEX(CONCAT(@l,','),',',1),'')                         AS kw1
     , NULLIF(SUBSTRING_INDEX(SUBSTRING_INDEX(CONCAT(@l,','),',',2),',',-1),'') AS kw2
     , NULLIF(SUBSTRING_INDEX(SUBSTRING_INDEX(CONCAT(@l,','),',',3),',',-1),'') AS kw3
     , NULLIF(SUBSTRING_INDEX(SUBSTRING_INDEX(CONCAT(@l,','),',',4),',',-1),'') AS kw4
     , NULLIF(SUBSTRING_INDEX(SUBSTRING_INDEX(CONCAT(@l,','),',',5),',',-1),'') AS kw5

但它们仍然在同一行。如果我们想对这些进行检查,我们需要做一些比较,我们需要检查每一个,看看它是否在指定的列表中。

如果我们可以在一行上将这些关键字转换为一组行,每行上都有一个关键字,那么我们可以使用与我的答案中的第一个类似的查询。举个例子:

SELECT t.product_id
     , NULLIF(CASE n.i
       WHEN 1 THEN SUBSTRING_INDEX(CONCAT(t.l,','),',',1)
       WHEN 2 THEN SUBSTRING_INDEX(SUBSTRING_INDEX(CONCAT(t.l,','),',',2),',',-1)
       WHEN 3 THEN SUBSTRING_INDEX(SUBSTRING_INDEX(CONCAT(t.l,','),',',3),',',-1)
       WHEN 4 THEN SUBSTRING_INDEX(SUBSTRING_INDEX(CONCAT(t.l,','),',',4),',',-1)
       WHEN 5 THEN SUBSTRING_INDEX(SUBSTRING_INDEX(CONCAT(t.l,','),',',5),',',-1)
       END,'') AS kw
  FROM ( SELECT 4 AS product_id,'fee,fi,fo,fum' AS l  
          UNION ALL 
         SELECT 5, 'coffee,sugar,milk'
        ) t
 CROSS
  JOIN ( SELECT 1 AS i
         UNION ALL SELECT 2
         UNION ALL SELECT 3
         UNION ALL SELECT 4
         UNION ALL SELECT 5
       ) n
HAVING kw IS NOT NULL
ORDER BY t.product_id, n.i

这为我们提供了单独的行,但前 5 个关键字中的每一个都被限制为一行。很容易看出如何扩展(n 返回 6,7,8,...)并将 CASE 中的 WHEN 条件扩展为处理 6,7,8...

但是会有一些任意的限制。 (我使用了一个别名为t 的内联视图来返回两个“示例”行,作为演示。该内联视图可以替换为对包含product_id 和keyword_list 列的表的引用。)

因此,该查询为我们提供了一个行集,就像我在上面作为示例给出的 product_keyword 表中返回的那样。

在示例查询中,对product_keyword 表的引用可以替换为此查询。但这是一大堆丑陋的 SQL,而且它的效率极低,在运行查询时创建和填充临时 MyISAM 表。

【讨论】:

这太完美了!我可以轻松地重新创建具有正确结构的表来完成这项工作。谢谢!我在创建表格时不确定如何存储关键字,但我应该能够正确创建它们以使其发挥最佳效果。 @loopifnil:为了清楚起见,我指的不是 MySQL "SET" 数据类型。我所说的“集合”只是指表中的“一组行”,每一行代表产品的一个关键字。这与包含字符串的单行相反。 (并不是说"SET" 数据类型没有一些性能优势,它确实有,但它仅限于有效值的静态列表;并且在处理方面,它具有字符串中逗号分隔列表的缺点作为行。【参考方案2】:

您可能想在keywords 上为您的桌子设置一个fulltext index。这允许您搜索关键字列并指定要包含或不包含的关键字。这是一个设置索引的命令:

ALTER TABLE products ADD FULLTEXT index_products_keywords (keywords);

完成后,您可以使用MATCH AGAINST 短语并指定关键字。您可以像WHERE MATCH(keywords) AGAINST ('chocolate') 一样使用它来搜索术语巧克力。或者您可以使用BOOLEAN MODE 来“关闭”某些关键字。

SELECT * FROM products 
WHERE MATCH(keywords) AGAINST ('+chocolate -bran' IN BOOLEAN MODE);

Here's a small tutorial about fulltext indexes

【讨论】:

有没有办法关闭除用户输入的少数关键字之外的所有关键字?我的关键字数据库很大。 我不认为它是这样工作的。如果您以某种方式关闭了所有关键字,然后搜索chocolate,则只会显示包含单个关键字chocolate 的行。 这就是我想要发生的事情。我想让用户提供一个关键字列表,然后只返回仅包含提供的列表中的关键字的产品,但包含任意数量的关键字。 在布尔模式下,如果你像+chocolate +sugar +milk +oats这样搜索,那么它只会找到包含每个单词的关键字:巧克力、糖、牛奶和燕麦。用户的搜索越具体,结果就越窄。 如果用户搜索“巧克力”。我只想退回只有一个关键字的产品,那就是巧克力。如果他们要寻找巧克力、糖和牛奶。我希望它返回仅包含巧克力、糖和牛奶的结果 -> 或任何关键字组合。我正在尽力解释这个问题,很抱歉造成混淆!

以上是关于MySQL返回列包含任何但仅包含一组关键字的所有行的主要内容,如果未能解决你的问题,请参考以下文章

TSQL - 选择包含列表中所有项目的行

如何选择包含特定单词的 postgreSQL 行

MYSQL查询--聚合函数查询

从不同的相关记录组中选择两列之一中包含重复值的所有行

为每个 ID 乘以一组日期

Mysql语法复习总结