帮助优化MySQL表(~500,000条记录)和PHP代码

时间:2010-12-24 05:26:21

标签: sql mysql database normalization

我有一个MySQL表,可以从各种游戏服务器(Urban Terror)收集玩家数据。收集数据的机器人每天24小时运行,目前该表最多可达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和服务器)作为4字节整数编写,并使用MySQL函数NTOA()等进行编码和解码,我听说这种方式更快,而不是varchar(15)。 / p>

guid是md5sum,32 char hex。日期存储为unix时间戳。

我在名称,ip和guid上有一个唯一的键,以避免重复同一个播放器。

我的钥匙设置正确吗?我存储数据的方式是否有效?

以下是查询此表的代码。您搜索名称,IP或guid,它会抓取查询结果并交叉引用与第一个查询的结果中的name,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 个答案:

答案 0 :(得分:2)

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. 查询在where子句中使用3个字段,OR在每个字段上使用条件。 MySQL只能使用一个索引进行查询。因此,必须为此查询选择名称或ip或guid的索引。即使存在复合索引(name,ip,guid),也不能在此方案中使用它,因为条件是OR。执行此类查询的更好方法是使用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。这允许使用每个字段上的索引来使查询有效。如果同一行在多个条件上匹配,则它具有获得重复结果的缺点。为了避免这种情况,您可以使用UNION DISTINCT而不是UNION,但由于mysql必须对输出进行去重复操作,因此会更昂贵。为了使这个建议起作用,下面讨论的问题也需要解决。 (guid上没有索引,需要构建)。

  2. 条件使用LIKE '%query%'作为名称和guid,即开头的通配符(%)。这意味着即使存在索引也无法使用。当您在字符串末尾使用=或%作为“query%”时,可以使用索引。当在字符串索引的开头使用%时,将不使用索引。 (参考:http://dev.mysql.com/doc/refman/5.1/en/mysql-indexes.html)。一种可能的出路是最后只使用通配符或在这些字段上使用全文索引。

  3. ip上的条件为INET_NTOA(ip) LIKE "%query%"。在字段上使用函数时,不能使用该字段上的任何索引。 MySQL目前不支持功能索引。如果需要支持这样的查询,您可能必须将此字段也存储为varchar,并将其视为与name和guid类似。

  4. 由于上述问题,查询将始终执行全表扫描,并且不会使用任何索引。使用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注入攻击。使用带有绑定变量的预准备语句。

答案 1 :(得分:0)

由于您的表是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获取相关信息:

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索引中最左边的字段
  • guid字段中没有索引,因此guid找到相关的查询会很慢。

答案 2 :(得分:0)

回到原始结构,我将摆脱(name,ip,guid)上的复合索引,并在名称上创建一个非唯一索引,并在ip上创建另一个非唯一索引。

我不知道如何处理guid。如果你想要防止重复的玩家记录,既没有单独的名称,也没有名称与ip足以保证唯一性,也许附加一个autoincrementing-integer-converted-to-string而不是guid会更好。

正如其他人所说,“包含子字符串”,即%foo%搜索无法充分利用索引;由于子字符串可能出现在任何/每个索引值中,因此必须扫描整个索引。另一方面,“开始 - 使用”子字符串搜索,即foo%能够利用索引。