繁体   English   中英

在PHP和MySQL中使用联结表对类别进行分类,包括和排除

[英]Using junction tables in PHP and MySQL to categorize and include and exclude categories

我正在尝试使用手动分配的类别来分析推文。 一切都存储在MySQL数据库中。 我可以添加和删除推文,类别以及它们之间的关系,而不会出现任何问题。

使用OR逻辑包括类别的工作符合预期。 如果我想找到归类为“委内瑞拉”或“ Maduro”的推文,则将这两个术语发送到名为$include的数组中,并将$include_logic设置为"or" 返回归类到任一类别的推文。 大!

当我尝试使用AND逻辑(即按所有包含的术语分类的推文,例如委内瑞拉马杜罗)时,或者当我尝试排除类别时,问题就开始了。

这是代码:

function filter_tweets($db, $user_id, $from_utc, $to_utc, $include = null, $include_logic = null, $exclude = null) {

    $include_sql = '';
    if (isset($include)) {
        $include_sql = 'AND (';
        $logic_op = '';
        foreach ($include as $cat) {
            $include_sql .= "{$logic_op}cats.name = '$cat' ";
            $logic_op = ($include_logic != 'and') ? 'OR ' : 'AND '; # AND doesn't work here
        }
        $include_sql .= ')';
    }
    $exclude_sql = ''; # Nothing I've tried with this works.

    $sql = "
        SELECT DISTINCT tweets.id FROM tweets
            LEFT OUTER JOIN tweets_cats ON tweets.id = tweets_cats.tweet_id
            LEFT OUTER JOIN cats ON tweets_cats.cat_id = cats.id 
        WHERE tweets.user_id = $user_id
            AND created_at
                BETWEEN '{$from_utc->format('Y-m-d H:i:s')}'
                    AND '{$to_utc->format('Y-m-d H:i:s')}'
            $include_sql
            $exclude_sql
        ORDER BY tweets.created_at ASC;";

    return db_fetch_all($db, $sql);   
}

db_fetch_all()在哪里

function db_fetch_all($con, $sql) {

    if ($result = mysqli_query($con, $sql)) {
        $rows = mysqli_fetch_all($result);
        mysqli_free_result($result);
        return $rows;
    }
    die("Failed: " . mysqli_error($con)); 
}

tweets_catstweetscats表之间的连接表。

阅读了联接和联结表后,我了解了为什么在上述两种情况下我的代码不起作用。 它一次只能查看一个推文和相应的类别。 因此,要求它忽略分类为“ X”的推文是没有意义的,因为当遇到相同的分类为“ Y”的推文时,它不会将其忽略。

我不明白的是如何修改代码以使其起作用。 我还没有发现有人尝试做类似事情的例子。 也许我不是在寻找正确的术语。 如果有人可以向我提供一个很好的资源来使用MySQL中的联结表(类似于我使用它们的方式),我将不胜感激。


编辑 :这是该函数使用上述示例创建的工作SQL,其中包括VP Twitter帐户上的“委内瑞拉”或“ Maduro”,日期范围设置为本月到目前为止的推文(EST转换为UTC)。

 SELECT DISTINCT tweets.id FROM tweets LEFT OUTER JOIN tweets_cats ON tweets.id = tweets_cats.tweet_id LEFT OUTER JOIN cats ON tweets_cats.cat_id = cats.id WHERE tweets.user_id = 818910970567344128 AND created_at BETWEEN '2019-02-01 05:00:00' AND '2019-03-01 05:00:00' AND (cats.name = 'Venezuela' OR cats.name = 'Maduro' ) ORDER BY tweets.created_at ASC; 


更新 :这里的工作SQL遵循包含类别的AND逻辑。 非常感谢@Strawberry的建议!

 SELECT tweets.id FROM tweets LEFT OUTER JOIN tweets_cats ON tweets.id = tweets_cats.tweet_id LEFT OUTER JOIN cats ON tweets_cats.cat_id = cats.id WHERE tweets.user_id = 818910970567344128 AND created_at BETWEEN '2019-02-01 05:00:00' AND '2019-03-01 05:00:00' AND cats.name IN ('Venezuela', 'Maduro') GROUP BY tweets.id HAVING COUNT(*) = 2 ORDER BY tweets.created_at ASC; 

但是,这超出了我的SQL理解。 我很高兴它有效。 我只是希望我能理解。


更新2 :这是不包含类别的有效SQL。 我意识到适用于包含类别的AND / OR逻辑也适用于排除类别。 本示例使用OR逻辑。 语法本质上是Q1 NOT IN(Q2),其中Q2被排除在外,基本上与用于包含的查询相同。

 SELECT id FROM tweets WHERE user_id = 818910970567344128 AND created_at BETWEEN '2019-02-01 05:00:00' AND '2019-03-01 05:00:00' AND id NOT IN ( SELECT tweets.id FROM tweets LEFT OUTER JOIN tweets_cats ON tweets.id = tweets_cats.tweet_id LEFT OUTER JOIN cats ON tweets_cats.cat_id = cats.id WHERE tweets.user_id = 818910970567344128 AND created_at BETWEEN '2019-02-01 05:00:00' AND '2019-03-01 05:00:00' AND cats.name IN ('Venezuela','Maduro') ) ORDER BY created_at ASC; 


更新3 :这是工作代码。

 function filter_tweets($db, $user_id, $from_utc, $to_utc, $include = null, $include_logic = null, $exclude = null, $exclude_logic = null) { if (isset($exclude)) { $exclude_sql = " AND tweets.id NOT IN (\\n" . include_tweets($user_id, $from_utc, $to_utc, $exclude, $exclude_logic) . "\\n)"; } else { $exclude_sql = ''; } if (isset($include)) { $sql = include_tweets($user_id, $from_utc, $to_utc, $include, $include_logic, $exclude_sql); } else { $sql = " SELECT id FROM tweets WHERE user_id = $user_id AND created_at BETWEEN '{$from_utc->format('Ymd H:i:s')}' AND '{$to_utc ->format('Ymd H:i:s')}' $exclude_sql"; } $sql .= "\\nORDER BY tweets.created_at ASC;"; return db_fetch_all($db, $sql); } 

它依赖于此附加功能来生成SQL:

 function include_tweets($user_id, $from_utc, $to_utc, $include, $logic, $exclude_sql = '') { $group_sql = ''; $include_sql = 'AND cats.name IN ('; $comma = ''; foreach ($include as $cat) { $include_sql .= "$comma'$cat'"; $comma = ','; } $include_sql .= ')'; if ($logic == 'and') $group_sql = 'GROUP BY tweets.id HAVING COUNT(*) = ' . count($include); return " SELECT tweets.id FROM tweets LEFT OUTER JOIN tweets_cats ON tweets.id = tweets_cats.tweet_id LEFT OUTER JOIN cats ON tweets_cats.cat_id = cats.id WHERE tweets.user_id = $user_id AND created_at BETWEEN '{$from_utc->format('Ymd H:i:s')}' AND '{$to_utc ->format('Ymd H:i:s')}' $include_sql $group_sql $exclude_sql"; } 

一种方法是将您的tweets表与联结表多次连接,例如:

SELECT tweets.*
FROM tweets
  JOIN tweet_cats AS tweet_cats_foo
    ON tweet_cats_foo.tweet_id = tweets.id
  JOIN tweet_cats AS tweet_cats_bar
    ON tweet_cats_bar.tweet_id = tweets.id
WHERE
  tweet_cats_foo.name = 'foo' AND tweet_cats_bar.name = 'bar'

或者,等效地,像这样:

SELECT tweets.*
FROM tweets
  JOIN tweet_cats AS tweet_cats_foo
    ON tweet_cats_foo.tweet_id = tweets.id
    AND tweet_cats_foo.name = 'foo'
  JOIN tweet_cats AS tweet_cats_bar
    ON tweet_cats_bar.tweet_id = tweets.id
    AND tweet_cats_bar.name = 'bar'

请注意,为简单起见,我在上面假设您的联结表直接包含类别名称。 如果您坚持使用数字类别ID,但要按名称搜索类别,则建议创建一个视图,该视图使用数字类别ID将类别和联结表连接在一起,并使用该视图代替查询中的实际联结表。 这使您不必为了查找数字类别ID而在查询中包括一堆不必要的样板代码。

对于排除查询,可以使用LEFT JOIN并检查联结表中是否没有匹配的记录(在这种情况下,该表中的所有列均为NULL ),如下所示:

SELECT tweets.*
FROM tweets
  LEFT JOIN tweet_cats AS tweet_cats_foo
    ON tweet_cats_foo.tweet_id = tweets.id
    AND tweet_cats_foo.name = 'foo'
WHERE
  tweet_cats_foo.tweet_id IS NULL  -- could use any non-null column here

(使用此方法,您确实需要在LEFT JOIN子句而不是WHERE子句中包含tweet_cats_foo.name = 'foo'条件。)

当然,您也可以将它们结合起来。 例如,要在foo类别中找到tweets,但在bar找不到,则可以执行以下操作:

SELECT tweets.*
FROM tweets
  JOIN tweet_cats AS tweet_cats_foo
    ON tweet_cats_foo.tweet_id = tweets.id
    AND tweet_cats_foo.name = 'foo'
  LEFT JOIN tweet_cats AS tweet_cats_bar
    ON tweet_cats_bar.tweet_id = tweets.id
    AND tweet_cats_bar.name = 'bar'
WHERE
  tweet_cats_bar.tweet_id IS NULL

或再次等效地:

SELECT tweets.*
FROM tweets
  LEFT JOIN tweet_cats AS tweet_cats_foo
    ON tweet_cats_foo.tweet_id = tweets.id
    AND tweet_cats_foo.name = 'foo'
  LEFT JOIN tweet_cats AS tweet_cats_bar
    ON tweet_cats_bar.tweet_id = tweets.id
    AND tweet_cats_bar.name = 'bar'
WHERE
  tweet_cats_foo.tweet_id IS NOT NULL
  AND tweet_cats_bar.tweet_id IS NULL

PS。 如草莓在上面的评论中所建议的,找到类别相交的另一种方法是对联结表进行单个联接,按推特ID将结果分组,并使用HAVING子句计算为每个分类找到了多少个匹配类别鸣叫:

SELECT tweets.*
FROM tweets
  JOIN tweet_cats ON tweet_cats.tweet_id = tweets.id
WHERE
   tweet_cats.name IN ('foo', 'bar')
GROUP BY tweets.id
HAVING COUNT(DISTINCT tweet_cats.name) = 2

也可以通过使用第二个(左)连接将这种方法推广到处理排除,例如:

SELECT tweets.*
FROM tweets
  JOIN tweet_cats AS tweet_cats_wanted
    ON tweet_cats_wanted.tweet_id = tweets.id
    AND tweet_cats_wanted.name IN ('foo', 'bar')
  LEFT JOIN tweet_cats AS tweet_cats_unwanted
    ON tweet_cats_unwanted.tweet_id = tweets.id
    AND tweet_cats_unwanted.name IN ('baz', 'blorgh', 'xyzzy')
WHERE
  tweet_cats_unwanted.tweet_id IS NULL
GROUP BY tweets.id
HAVING COUNT(DISTINCT tweet_cats_wanted.name) = 2

我还没有对这两种方法进行基准测试,以查看哪种方法更有效,因此我强烈建议您在决定使用哪种方法之前先这样做。 原则上,我希望数据库引擎可以很容易地优化多重联接方法,因为它显然可以映射到联接的交集,而对于GROUP BY ... HAVING方法,天真的数据库可能最终会浪费很多首先要找到与任何类别匹配的所有tweet,然后才应用HAVING子句过滤掉所有与所有类别匹配的tweet。 一个简单的测试用例可能是几个非常大的类别与一个很小的类别的交集,我希望使用多联接方法会更有效率。 但是,当然,应该总是测试这样的事情,而不是仅仅依靠直觉。

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM