Проблема: Написание синтаксического анализатора MySQL для разделения JOIN и выполнения их как отдельных запросов (динамическая денормализация запроса) - PullRequest
2 голосов
/ 16 февраля 2011

Я пытаюсь найти сценарий, чтобы принять запрос MySQL и превратить его в отдельные запросы, то есть динамически денормализовать запрос.

В качестве теста я построил простую систему статей, которая имеет 4 таблицы:

  • статьи
    • article_id
    • article_format_id
    • article_title
    • article_body
    • article_date
  • article_categories
    • article_id
    • category_id
  • категории
    • category_id
    • category_title
  • форматы
    • format_id
    • format_title

Статья может относиться к нескольким категориям, но иметь только один формат. Я чувствую, что это хороший пример реальной ситуации.

На странице категории, на которой перечислены все статьи (в том числе и с помощью format_title), это можно легко сделать с помощью следующего запроса:

SELECT articles.*, formats.format_title 
FROM articles 
INNER JOIN formats ON articles.article_format_id = formats.format_id 
INNER JOIN article_categories ON articles.article_id = article_categories.article_id 
WHERE article_categories.category_id = 2 
ORDER BY articles.article_date DESC

Однако скрипт, который я пытаюсь создать, получит этот запрос, проанализирует его и выполнит запросы по отдельности.

Таким образом, в этом примере страницы категории скрипт эффективно запустит это (работает динамически):

// Select article_categories
$sql = "SELECT * FROM article_categories WHERE category_id = 2";
$query = mysql_query($sql);
while ($row_article_categories = mysql_fetch_array($query, MYSQL_ASSOC)) {

    // Select articles
    $sql2 = "SELECT * FROM articles WHERE article_id = " . $row_article_categories['article_id'];
    $query2 = mysql_query($sql2);
    while ($row_articles = mysql_fetch_array($query2, MYSQL_ASSOC)) {

        // Select formats
        $sql3 = "SELECT * FROM formats WHERE format_id = " . $row_articles['article_format_id'];
        $query3 = mysql_query($sql3);
        $row_formats = mysql_fetch_array($query3, MYSQL_ASSOC);

        // Merge articles and formats
        $row_articles = array_merge($row_articles, $row_formats);

        // Add to array
        $out[] = $row_articles;
    }
}

// Sort articles by date
foreach ($out as $key => $row) {
    $arr[$key] = $row['article_date'];
}

array_multisort($arr, SORT_DESC, $out);

// Output articles - this would not be part of the script obviously it should just return the $out array
foreach ($out as $row) {
    echo '<p><a href="article.php?id='.$row['article_id'].'">'.$row['article_title'].'</a> <i>('.$row['format_title'].')</i><br />'.$row['article_body'].'<br /><span class="date">'.date("F jS Y", strtotime($row['article_date'])).'</span></p>';
}

Сложность этого заключается в разработке правильных запросов в правильном порядке, так как вы можете размещать имена столбцов для SELECT и JOIN в любом порядке в запросе (это то, что MySQL и другие базы данных SQL переводят так хорошо) и разработка информационная логика в PHP.

В настоящее время я выполняю синтаксический анализ запроса с использованием SQL_Parser , который хорошо работает при разбиении запроса на многомерный массив, но решение упомянутых выше проблем является головной болью.

Любая помощь или предложения будут высоко оценены.

Ответы [ 4 ]

13 голосов
/ 22 февраля 2011

Из того, что я понял, вы пытаетесь поместить слой между сторонним приложением форума, который вы не можете изменить (возможно, обфусцированный код?), И MySQL. Этот слой будет перехватывать запросы, переписывать их, чтобы они выполнялись индивидуально, и генерировать PHP-код для их выполнения в базе данных и возврата совокупного результата. Это очень плохая идея.

Кажется странным, что вы подразумеваете невозможность добавления кода и одновременно предлагаете создать код для добавления. Надеюсь, вы не планируете использовать что-то вроде funcall для внедрения кода. Это очень плохая идея.

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

Мы примем некоторые ограничения:

  • Вы используете MySQL 5.0 или выше.
  • Запросы не могут быть изменены.
  • Таблицы базы данных не могут быть изменены.
  • У вас уже есть соответствующих индексов для таблиц, на которые ссылаются проблемные запросы.
  • У вас есть тройная проверка медленные запросы (и запуск EXPLAIN), попадающие в вашу БД, и вы пытались настроить индексы, которые помогли бы им работать быстрее.
  • Загрузка внутренних соединений в установку MySQL недопустима.

Три возможных решения:

  1. Вы можете легко справиться с этой проблемой, вкладывая деньги в свою текущую базу данных, обновляя оборудование, на котором она работает, до чего-то большего с большим количеством ядер, большей (насколько вы можете себе позволить) оперативной памятью и более быстрыми дисками. Если у вас есть деньги Продукты Fusion-io очень рекомендуются для такого рода вещей. Это, вероятно, самый простой из трех вариантов, которые я предложу
  2. Настройте вторую основную базу данных MySQL и соедините ее с первой. Убедитесь, что у вас есть возможность принудительно изменять идентификатор AUTO_INCREMENT (одна БД использует четные идентификаторы, а другая - нечетные). Это не масштабируется вечно, но предлагает вам некоторую передышку по цене оборудования и места в стойке. Опять же, усилить оборудование. Возможно, вы уже сделали это, но если нет, то стоит подумать.
  3. Используйте что-то вроде dbShards . Вам все еще нужно использовать больше оборудования, но у вас есть дополнительное преимущество, заключающееся в возможности масштабирования более двух машин, и вы можете со временем покупать более дешевое оборудование.
3 голосов
/ 21 февраля 2011

Чтобы повысить производительность базы данных, вы обычно ищите способы:

  • Сократить количество вызовов базы данных
  • Сделать каждый вызов базы данных максимально эффективным (с помощью хорошего дизайна)
  • Уменьшите объем передаваемых данных

... а вы делаете прямо противоположное?Умышленно?
По каким причинам?

Извините, вы делаете это совершенно неправильно, и каждая проблема, с которой вы столкнетесь на этом пути, будет являться следствием того первого решения о внедрении механизма базы данных внедвижка базы данных.Вы будете вынуждены обходить обходные пути вплоть до даты доставки.(если вы туда доберетесь).

Кроме того, речь идет о форуме?Я имею в виду давай!Даже на самых популярных форумах, посвященных веб-масштабам, мы говорим о том, что меньше, чем 100 т / с в среднем?Вы можете сделать это на своем ноутбуке!

Мой совет - забыть обо всем этом и реализовать вещи самым простым способом.Затем кэшируйте агрегаты (последние, популярные, статистические данные и т. Д.) На уровне приложений.Все остальное на форуме уже является поиском первичного ключа.

0 голосов
/ 27 февраля 2011

Я согласен, что это звучит как неправильный выбор, но я могу вспомнить некоторые ситуации, когда разделение запроса может быть полезным.

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

<?php
/**
 * That's a weird problem, but an interesting challenge!
 * @link /4963980/problema-napisanie-sintaksicheskogo-analizatora-razdeleniya-vypolneniya-otdelnyh-dinamicheskaya-denormalizatsiya
 */

// Taken from the given example:
$sql = "SELECT articles.*, formats.format_title 
FROM articles 
INNER JOIN formats ON articles.article_format_id = formats.format_id 
INNER JOIN article_categories ON articles.article_id = article_categories.article_id 
WHERE article_categories.category_id = 2 
ORDER BY articles.article_date DESC";

// Parse query
// (Limited to the clauses that are present in the example...)
// Edit: Made WHERE optional
if(!preg_match('/^\s*'.
    'SELECT\s+(?P<select_rows>.*[^\s])'. 
    '\s+FROM\s+(?P<from>.*[^\s])'.
    '(?:\s+WHERE\s+(?P<where>.*[^\s]))?'.
    '(?:\s+ORDER\s+BY\s+(?P<order_by>.*[^\s]))?'.
    '(?:\s+(?P<desc>DESC))?'.
    '(.*)$/is',$sql,$query)
) {
    trigger_error('Error parsing SQL!',E_USER_ERROR);
    return false;
}

## Dump matches
#foreach($query as $key => $value) if(!is_int($key)) echo "\"$key\" => \"$value\"<br/>\n";

/* We get the following matches:
"select_rows" => "articles.*, formats.format_title"
"from" => "articles INNER JOIN formats ON articles.article_format_id = formats.format_id INNER JOIN article_categories ON articles.article_id = article_categories.article_id"
"where" => "article_categories.category_id = 2"
"order_by" => "articles.article_date"
"desc" => "DESC"
/**/

// Will only support WHERE conditions separated by AND that are to be
// tested on a single individual table.
if(@$query['where']) // Edit: Made WHERE optional
    $where_conditions = preg_split('/\s+AND\s+/is',$query['where']);

// Retrieve individual table information & data
$tables = array();
$from_conditions = array();
$from_tables = preg_split('/\s+INNER\s+JOIN\s+/is',$query['from']);

foreach($from_tables as $from_table) {

    if(!preg_match('/^(?P<table_name>[^\s]*)'.
        '(?P<on_clause>\s+ON\s+(?P<table_a>.*)\.(?P<column_a>.*)\s*'.
        '=\s*(?P<table_b>.*)\.(?P<column_b>.*))?$/im',$from_table,$matches)
    ) {
        trigger_error("Error parsing SQL! Unexpected format in FROM clause: $from_table", E_USER_ERROR);
        return false;
    }
    ## Dump matches
    #foreach($matches as $key => $value) if(!is_int($key)) echo "\"$key\" => \"$value\"<br/>\n";

    // Remember on_clause for later jointure
    // We do assume each INNER JOIN's ON clause compares left table to
    // right table. Forget about parsing more complex conditions in the
    // ON clause...
    if(@$matches['on_clause'])
        $from_conditions[$matches['table_name']] = array(
            'column_a' => $matches['column_a'],
            'column_b' => $matches['column_b']
        );

    // Match applicable WHERE conditions
    $where = array();
    if(@$query['where']) // Edit: Made WHERE optional
    foreach($where_conditions as $where_condition)
        if(preg_match("/^$matches[table_name]\.(.*)$/",$where_condition,$matched))
            $where[] = $matched[1];
    $where_clause = empty($where) ? null : implode(' AND ',$where);

    // We simply ignore $query[select_rows] and use '*' everywhere...
    $query = "SELECT * FROM $matches[table_name]".($where_clause? " WHERE $where_clause" : '');
    echo "$query<br/>\n";

    // Retrieve table's data
    // Fetching the entire table data right away avoids multiplying MySQL
    // queries exponentially...
    $table = array();
    if($results = mysql_query($table))
        while($row = mysql_fetch_array($results, MYSQL_ASSOC))
            $table[] = $row;

    // Sort table if applicable
    if(preg_match("/^$matches[table_name]\.(.*)$/",$query['order_by'],$matched)) {
        $sort_key = $matched[1];

        // @todo Do your bubble sort here!

        if(@$query['desc']) array_reverse($table);
    }

    $tables[$matches['table_name']] = $table;
}

// From here, all data is fetched.
// All left to do is the actual jointure.

/**
 * Equijoin/Theta-join.
 * Joins relation $R and $S where $a from $R compares to $b from $S.
 * @param array $R A relation (set of tuples).
 * @param array $S A relation (set of tuples).
 * @param string $a Attribute from $R to compare.
 * @param string $b Attribute from $S to compare.
 * @return array A relation resulting from the equijoin/theta-join.
 */
function equijoin($R,$S,$a,$b) {
    $T = array();
    if(empty($R) or empty($S)) return $T;
    foreach($R as $tupleR) foreach($S as $tupleS)
        if($tupleR[$a] == @$tupleS[$b])
            $T[] = array_merge($tupleR,$tupleS);
    return $T;
}

$jointure = array_shift($tables);
if(!empty($tables)) foreach($tables as $table_name => $table)
    $jointure = equijoin($jointure, $table,
        $from_conditions[$table_name]['column_a'],
        $from_conditions[$table_name]['column_b']);

return $jointure;

?>

Спокойной ночи и удачи!

0 голосов
/ 21 февраля 2011

Вместо того, чтобы переписывать SQL, я думаю, вы должны создать денормализованную таблицу статей и изменять ее при каждой вставке / удалении / обновлении статьи. Это будет НАМНОГО проще и дешевле.

Создайте и заполните его:

create table articles_denormalized
...

insert into articles_denormalized 
    SELECT articles.*, formats.format_title 
    FROM articles 
    INNER JOIN formats ON articles.article_format_id = formats.format_id 
    INNER JOIN article_categories ON articles.article_id = article_categories.article_id 

Теперь введите соответствующую статью вставки / обновления / удаления для нее, и у вас будет денормализованная таблица, всегда готовая для запроса.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...