我有一个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).");
?>
任何帮助都表示赞赏!
谢谢。
答案 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%" )
上述查询的一些问题:
查询在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上没有索引,需要构建)。
条件使用LIKE '%query%'
作为名称和guid,即开头的通配符(%)。这意味着即使存在索引也无法使用。当您在字符串末尾使用=或%作为“query%”时,可以使用索引。当在字符串索引的开头使用%时,将不使用索引。 (参考:http://dev.mysql.com/doc/refman/5.1/en/mysql-indexes.html)。一种可能的出路是最后只使用通配符或在这些字段上使用全文索引。
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注入攻击。使用带有绑定变量的预准备语句。
答案 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%能够利用索引。