Использование соединительных таблиц в PHP и MySQL для категоризации, включения и исключения категорий - PullRequest
0 голосов
/ 10 февраля 2019

Я пытаюсь анализировать твиты, используя назначенные вручную категории.Все хранится в базе данных MySQL.Я могу добавлять и удалять твиты, категории и отношения между ними без каких-либо проблем.

Включение категорий с использованием логики ИЛИ работает, как и ожидалось.Если я хочу найти твиты, классифицированные как «Венесуэла» или «Мадуро», я отправляю эти два термина в массив под названием $include с $include_logic, установленным на "or".Твиты, отнесенные к любой категории, возвращаются.Отлично!

Проблемы начинаются, когда я пытаюсь использовать логику AND (т. Е. Твиты, отнесенные к all включенным терминам, например, как Венесуэла и Maduro) или когдаЯ пытаюсь исключить категории.

Вот код:

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_cats - это таблица соединений между tweets и cats таблиц.

После прочтения таблиц соединений и соединений я понимаю, почему мой код не работает в двух упомянутых случаях.Он может просматривать только один твит и соответствующую категорию за раз.Поэтому просьба опустить твит, классифицированный как «X», является спорным, потому что он не пропустит его, когда встречается тот же твит, и классифицируется как «Y».

Что я не понимаю, так это как изменитькод, чтобы он работал.Я не нашел примеров людей, пытающихся сделать что-то подобное.Возможно, я не ищу правильные термины.Буду признателен, если кто-нибудь подскажет мне хороший ресурс для работы с соединительными таблицами в MySQL, аналогичный тому, как я их использую.


Редактировать : Вот рабочий SQLсозданная функцией с использованием вышеупомянутого примера, включая «Венесуэла» ИЛИ «Мадуро» в учетной записи VP в Твиттере с диапазоном дат, установленным на твиты в этом месяце (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, исключающий категории.Я понял, что логика И / ИЛИ, которая применяется к включенным категориям, также относится к исключенным.В этом примере используется логика ИЛИ.Синтаксис, по сути, 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('Y-m-d H:i:s')}'
                    AND '{$to_utc  ->format('Y-m-d 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('Y-m-d H:i:s')}'
                AND '{$to_utc  ->format('Y-m-d H:i:s')}'
          $include_sql
        $group_sql
        $exclude_sql";
}

1 Ответ

0 голосов
/ 10 февраля 2019

Один из способов сделать это - несколько раз соединить вашу таблицу 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'

Обратите внимание, что для простоты выше я предполагаю, что ваша таблица соединений содержит имена категорий.Если вы настаиваете на использовании числовых идентификаторов категорий, но при поиске категорий по имени, я бы порекомендовал создать представление, объединяющее таблицы категорий и соединений, используя числовой идентификатор категории и используя это представление вместо фактической таблицы соединений в вашем запросе.Это избавляет вас от необходимости включать в запрос целую кучу ненужного стандартного кода только для поиска числовых идентификаторов категорий.

Для запросов на исключение можно использовать 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

(При использовании этого метода вам необходимо включить условие tweet_cats_foo.name = 'foo' в *Предложение 1016 * вместо предложения WHERE.

Конечно, вы также можете комбинировать их.Например, чтобы найти твиты в категории foo, но не в 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.Другой способ найти пересечения категорий, , как предлагает Strawberry в комментариях выше , состоит в том, чтобы выполнить одно объединение с таблицей соединений, сгруппировать результаты по идентификатору твита и использовать для подсчета предложение 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 наивная база данных может закончитьсяПотратив немало усилий, сначала найдите все твиты, которые соответствуют любой категорий, и только потом примените предложение HAVING, чтобы отфильтровать все, кроме тех, которые соответствуют всем категориям.Простым тестовым примером для этого может быть пересечение нескольких очень больших категорий с одной очень маленькой, что, как я ожидаю, будет более эффективным при использовании метода множественного объединения.Но, конечно, всегда нужно проверять такие вещи, а не полагаться только на интуицию.

...