Помощь в оптимизации таблицы MySQL (~ 500 000 записей) и кода PHP - PullRequest
0 голосов
/ 24 декабря 2010

У меня есть таблица MySQL, которая собирает данные игроков с различных игровых серверов (Urban Terror).Бот, который собирает данные, работает 24/7, и в настоящее время в таблице содержится до 475 000 записей.Из-за этого запросы к этой таблице из PHP стали довольно медленными.Интересно, что я могу сделать на стороне базы данных, чтобы сделать ее как можно более оптимальной, тогда я могу сосредоточиться на приложении для запроса базы данных.Таблица выглядит следующим образом:

CREATE TABLE IF NOT EXISTS `people` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(40) NOT NULL,
  `ip` int(4) unsigned NOT NULL,
  `guid` varchar(32) NOT NULL,
  `server` int(4) unsigned NOT NULL,
  `date` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `Person` (`name`,`ip`,`guid`),
  KEY `server` (`server`),
  KEY `date` (`date`),
  KEY `PlayerName` (`name`)
) ENGINE=MyISAM  DEFAULT CHARSET=latin1 COMMENT='People that Play on Servers' AUTO_INCREMENT=475843 ;

Я описываю IPv4 (ip и server) как 4-байтовые целые числа и использую функции MySQL NTOA () и т. Д. Для кодирования и декодирования, я слышал, что этопуть быстрее, чем varchar (15).

Гид - гекс md5sum, 32 символа.Дата хранится в виде метки времени Unix.

У меня есть уникальный ключ для имени, ip и guid, чтобы избежать дублирования одного и того же игрока.

Правильно ли настроены мои ключи?Эффективен ли способ хранения данных?

Вот код для запроса этой таблицы.Вы ищете имя, ip или guid, и он захватывает результаты запроса и сопоставляет другие записи, которые соответствуют имени, ip или guid из результатов первого запроса, и делает это для каждого поля.Это сложно объяснить.Но в основном, если я ищу одного игрока по имени, я увижу каждое другое имя, которое он использовал, каждый IP, который он использовал, и каждый GUID, который он использовал.

<form action="<?php echo $_SERVER['PHP_SELF']; ?>" method="post">
Search: <input type="text" name="query" id="query" /><input type="submit" name="btnSubmit" value="Submit" />
</form>

<?php if (!empty($_POST['query'])) { ?>

<table cellspacing="1" id="1up_people" class="tablesorter" width="300">
<thead>
<tr>
    <th>ID</th>
    <th>Player Name</th>
    <th>Player IP</th>
    <th>Player GUID</th>
    <th>Server</th>
    <th>Date</th>
</tr>
</thead>
<tbody>
<?php

function super_unique($array)
{
  $result = array_map("unserialize", array_unique(array_map("serialize", $array)));

  foreach ($result as $key => $value)
  {
    if ( is_array($value) )
    {
      $result[$key] = super_unique($value);
    }
  }

  return $result;
}

    if (!empty($_POST['query'])) {
        $query = trim($_POST['query']);
        $count = 0;
        $people = array();
        $link = mysql_connect('localhost', 'mysqluser', 'yea right!');
                if (!$link) {
                        die('Could not connect: ' . mysql_error());
                }
                mysql_select_db("1up");
                $sql = "SELECT id, name, INET_NTOA(ip) AS ip, guid, INET_NTOA(server) AS server, date FROM 1up_people WHERE (name LIKE \"%$query%\" OR INET_NTOA(ip) LIKE \"%$query%\" OR guid LIKE \"%$query%\")";
        $result = mysql_query($sql, $link);
        if (!$result) {
            die(mysql_error());
        }
        // Now take the initial results and parse each column into its own array
        while ($row = mysql_fetch_array($result, MYSQL_NUM)) {
            $name = htmlspecialchars($row[1]);
            $people[] = array(
                'id' => $row[0],
                'name' => $name,
                'ip' => $row[2],
                'guid' => $row[3],
                'server' => $row[4],
                'date' => $row[5]
            );
        }
        // now for each name, ip, guid in results, find additonal records
        $people2 = array();
        foreach ($people AS $person) {
            $ip = $person['ip'];
            $sql = "SELECT id, name, INET_NTOA(ip) AS ip, guid, INET_NTOA(server) AS server, date FROM 1up_people WHERE (ip = \"$ip\")";
            $result = mysql_query($sql, $link);
            while ($row = mysql_fetch_array($result, MYSQL_NUM)) {
                $name = htmlspecialchars($row[1]);
                $people2[] = array(
                    'id' => $row[0],
                    'name' => $name,
                    'ip' => $row[2],
                    'guid' => $row[3],
                    'server' => $row[4],
                    'date' => $row[5]
                );
            }
        }

                $people3 = array();
                foreach ($people AS $person) {
                        $guid = $person['guid'];
                        $sql = "SELECT id, name, INET_NTOA(ip) AS ip, guid, INET_NTOA(server) AS server, date FROM 1up_people WHERE (guid = \"$guid\")";
                        $result = mysql_query($sql, $link);
                        while ($row = mysql_fetch_array($result, MYSQL_NUM)) {
                                $name = htmlspecialchars($row[1]);
                                $people3[] = array(
                                        'id' => $row[0],
                                        'name' => $name,
                                        'ip' => $row[2],
                    'guid' => $row[3],
                    'server' => $row[4],
                    'date' => $row[5]
                                );
                        }
                }


                $people4 = array();
                foreach ($people AS $person) {
                        $name = $person['name'];
                        $sql = "SELECT id, name, INET_NTOA(ip) AS ip, guid, INET_NTOA(server) AS server, date FROM 1up_people WHERE (name = \"$name\")";
                        $result = mysql_query($sql, $link);
                        while ($row = mysql_fetch_array($result, MYSQL_NUM)) {
                                $name = htmlspecialchars($row[1]);
                                $people4[] = array(
                                        'id' => $row[0],
                                        'name' => $name,
                                        'ip' => $row[2],
                    'guid' => $row[3],
                    'server' => $row[4],
                    'date' => $row[5]
                                );
                        }
                }


        // Combine people and people2 into just people
        $people = array_merge($people, $people2);
        $people = array_merge($people, $people3);
        $people = array_merge($people, $people4);

        $people = super_unique($people);        

        foreach ($people AS $person) {
            $date = ($person['date']) ? date("M d, Y", $person['date']) : 'Before 8/1/10';
            echo "<tr>\n";
            echo "<td>".$person['id']."</td>";
            echo "<td>".$person['name']."</td>";
            echo "<td>".$person['ip']."</td>";
            echo "<td>".$person['guid']."</td>";
            echo "<td>".$person['server']."</td>";
            echo "<td>".$date."</td>";
            echo "</tr>\n";
            $count++;
        }
        // Find Total Records
        //$result = mysql_query("SELECT id FROM 1up_people", $link);
        //$total  = mysql_num_rows($result);
        mysql_close($link);
    }
?>
</tbody>
</table>
<p>
<?php 
    echo $count." Records Found for \"".$_POST['query']."\" out of $total";
?>
</p>

<?php 
} 
$time_stop = microtime(true);
print("Done (ran for ".round($time_stop-$time_start)." seconds).");

?>

Любая помощь приветствуется!

Спасибо.

Ответы [ 3 ]

2 голосов
/ 24 декабря 2010
SELECT id,
       name,
       Inet_ntoa(ip)     AS ip,
       guid,
       Inet_ntoa(server) AS server,
       DATE
FROM   1up_people
WHERE  ( name LIKE "%$query%"
          OR Inet_ntoa(ip) LIKE "%$query%"
          OR guid LIKE "%$query%" ) 

Некоторые проблемы с указанным выше запросом:

  1. В запросе используются 3 поля в выражениях where и условие OR для каждого поля.MySQL может использовать только один индекс для запроса.Таким образом, для этого запроса он должен выбрать индекс по имени, ip или guid.Даже если есть составной индекс (name, ip, guid), его нельзя использовать в этом сценарии, так как условия имеют ИЛИ-ed.Лучший способ сделать такие запросы - использовать UNION.Например,

     SELECT <fields> FROM table1 WHERE field1='val1' /*will use index on field1*/
     UNION
     SELECT <fields> FROM table1 WHERE field2='val2' /*will use index on field2*/
     ...
     SELECT <fields> FROM table1 WHERE fieldn='valn' /*will use index on fieldn*/.
    

    В приведенном выше запросе вы делаете выбор в каждом поле отдельно, а затем СОЕДИНЯЕТЕ его.Это позволяет использовать индексы в каждом из этих полей, чтобы сделать запрос эффективным.Недостатком является получение дублированных результатов, если одна и та же строка соответствует более чем одному условию.Чтобы избежать этого, вы можете использовать UNION DISTINCT вместо UNION, но это будет стоить дороже, поскольку mysql должен дедуплицировать вывод.Для того чтобы это предложение работало, необходимо обсудить и вопросы, которые обсуждаются ниже.(На guid нет индекса, и его необходимо построить).

  2. Условия используют LIKE '%query%' для имени и guid, т.е. подстановочный знак (%) в начале.Это означает, что индекс нельзя использовать, даже если он существует.Индекс можно использовать, когда вы используете = или% в конце строки как «запрос%».Когда% используется в начале строки, индекс не будет использоваться.(Ссылка: http://dev.mysql.com/doc/refman/5.1/en/mysql-indexes.html). Возможным выходом является использование только символа подстановки в конце или полнотекстовое индексирование в этих полях.

  3. Условие для ip имеет вид INET_NTOA(ip) LIKE "%query%". Когда функция используется в поле, любой индекс в этом поле не может быть использован. MySQL не поддерживает функциональный индекс на данный момент. Если такой запрос необходимо поддерживать, вам может потребоваться сохранить это поле также как varchar иотноситесь к нему аналогично name и guid.

Из-за вышеуказанных проблем запрос всегда выполняет полное сканирование таблицы и не использует никаких индексов. Использование UNION (как предложено в 1) приведет кне дают никаких улучшений 2 и 3 не исправлены, и на самом деле это может снизить производительность, так как это может быть 3 сканирования таблицы вместо 1. Вы можете попробовать создать полнотекстовый индекс для (name, guid, ip_string) и сделатьВаш запрос как MATCH(name, guid, ip_string) AGAINST ("$query")

Из просмотра кода я вижу, что после получения результатов этого запроса последующие запросы запускаются на основе результатов этого запроса. Я не уверен, чтоТребуется шляпа, так как я думаю, что она не найдет никаких новых записей.При поиске f LIKE "%q%" и использовании результатов поиска, например, f='r1', условие LIKE должно уже охватить все вхождения 'r1', а последующие запросы будут возвращать только дублированные результаты.По моему мнению, дополнительные запросы могут быть пропущены, но, возможно, я что-то упустил.

В примечании к стороне не интерполируйте строки запроса в операторе SQL как name LIKE "%$query%".Это небезопасно и может быть использовано для атаки SQL-инъекцией.Используйте подготовленные операторы со связанными переменными.

0 голосов
/ 24 декабря 2010

Возвращаясь к исходной структуре, я избавился бы от составного индекса (name, ip, guid) и создал бы неуникальный индекс по имени и еще один неуникальный индекс по ip.

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

Как уже отмечали другие, «содержит подстроку», то есть поиск% foo% не может в полной мере использовать индекс; поскольку подстрока может встречаться в любом / каждом индексированном значении, весь индекс должен быть отсканирован. С другой стороны, поиск по подстроке «начинается с», т. Е. Foo% может использовать индекс.

0 голосов
/ 24 декабря 2010

Поскольку ваша таблица MyISAM, создайте индексы FULLTEXT, которые будут работать лучше, чем LIKE '%%'

, чтобы избежать всех запросов в цикле, вставьте основной запрос во временную таблицу, которую вы будете использовать позже длясвязанные с запросом записи:

Пример

Вместо основного SELECT сначала вставьте строки:

CREATE TEMPORARY TABLE IF NOT EXISTS `tmp_people` (
  `id` bigint(20) unsigned NOT NULL,
  `name` varchar(40) NOT NULL,
  `ip` int(4) unsigned NOT NULL,
  `guid` varchar(32) NOT NULL,
  `server` int(4) unsigned NOT NULL,
  `date` int(11) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `server` (`server`),
  KEY `date` (`date`),
  KEY `PlayerName` (`name`)
);

TRUNCATE TABLE tmp_people;

INSERT tmp_people
SELECT id, name, ip AS ip, guid, server AS server, date
FROM up_people
WHERE (name LIKE \"%$query%\" OR INET_NTOA(ip) LIKE \"%$query%\" OR guid LIKE \"%$query%\")

Затем запросите результаты:

SELECT id, name, INET_NTOA(ip) AS ip, guid, INET_NTOA(server) AS server, date FROM tmp_people;

Наконец, вместо того, чтобы зацикливаться на отдельных записях, запросите все связанные записи в том же самом выборе:

Чтобы получить связанный по ip:

SELECT up.id, up.name, INET_NTOA(up.ip) AS ip, up.guid, INET_NTOA(up.server) AS server, up.date FROM up_people up JOIN tmp_people tmp ON up.ip = tmp.ip

, чтобы получить связанный по guid:1019 *

SELECT up.id, up.name, INET_NTOA(up.ip) AS ip, up.guid, INET_NTOA(up.server) AS server, up.date FROM up_people up JOIN tmp_people tmp ON up.guid = tmp.guid;

для получения связанного по имени:

SELECT up.id, up.name, INET_NTOA(up.ip) AS ip, up.guid, INET_NTOA(up.server) AS server, up.date FROM up_people up JOIN tmp_people tmp ON up.name = tmp.name

Примечания:

  • вам не нужен индекс PlayerName, так как name поле - самое левое поле в Person Index
  • В поле guid нет индекса, поэтому поиск, связанный с guid, будет медленным.
...