准备好的陈述是否是正常查询的浪费? (PHP)

时间:2009-12-04 21:37:51

标签: php mysql database prepared-statement

如今,“准备好的语句”似乎是任何人建议向数据库发送查询的唯一方式。我甚至看到了为存储过程使用预准备语句的建议。但是,对于额外的查询准备语句需要 - 以及它们持续的短时间 - 我被说服它们仅对一行INSERT / UPDATE查询有用。

我希望有人可以在此纠正我,但这似乎是整个“桌子都是邪恶的”CSS事情的重复。如果用于布局,表格只是邪恶的 - 而不是表格数据。对表格数据使用DIV是违反WC3的风格。

同样明智的,SQL(或者从AR生成的)似乎对80%的查询更有用,在大多数站点上都是单个SELECT,不再重复那个页面加载(我说话)关于PHP这样的脚本语言)。为什么我要让我过度征税的数据库准备一个声明,它只能在删除之前运行一次?

MySQL的:

  

准备好的声明特定于   它创建的会话。   如果你没有终止会话   解除以前准备的   声明,服务器解除分配它   自动。

因此,在脚本结束时,PHP将自动关闭连接,您将丢失准备好的语句,只是为了让您的脚本在下次加载时重新创建它。

我错过了什么或者这只是一种降低性能的方法吗?

:UPDATE:

我突然意识到我正在为每个脚本假设新的连接。我认为如果使用持久连接,那么这些问题就会消失。这是对的吗?

:UPDATE2:

似乎即使持久连接是解决方案 - 对于大多数网络来说它们都是not a very good option - 特别是如果你使用交易。所以我回到原点只有下面的基准测试......

:UPDATE3:

大多数人只是重复短语“准备好的语句防止SQL注入”,但这并不能完全解释问题。为每个数据库库提供的“转义”方法也可以防止SQL注入。但它不止于此:

  

以正常方式发送查询时,   客户端(脚本)转换数据   到字符串然后传递给   数据库服务器。然后DB服务器使用   将CPU转换回来的电源   正确的二进制数据类型。该   然后数据库引擎解析   声明并查找语法错误。

     

使用准备好的陈述时......   数据以原生二进制形式发送,   这节省了转换CPU使用率,   并使数据传输更多   高效。显然,这也将   如果客户端减少带宽使用   不与DB服务器位于同一位置。

     

...变量类型是预定义的,   因此MySQL考虑到了   这些人物,他们不需要   被逃脱。

     

http://www.webdesignforums.net/showthread.php?t=18762

感谢OIS最终解决了这个问题。

8 个答案:

答案 0 :(得分:6)

与CSS表辩论不同,准备好的陈述有明显的安全隐患。

如果您使用预准备语句作为将用户提供的数据放入查询的唯一方法,那么它们在SQL注入时绝对是防弹的。

答案 1 :(得分:1)

当您在数据库上执行sql语句时,sql解析器需要事先对其进行分析,这与准备过程完全相同。

因此,将执行sql语句直接与准备和执行进行比较没有任何缺点,但有一些优点:

  • 首先,正如longneck已经说过的那样,将用户输入传递到预准备语句会自动转义输入。就好像数据库已经为值准备了过滤器,并且只允许那些适合的值。

  • 其次,如果彻底使用预处理语句,并且遇到需要多次执行它的情况,则不需要重写代码来准备和执行,但只需执行它。 / p>

  • 第三:如果正确完成,代码将变得更具可读性:


$sql = 'SELECT u.id, u.user, u.email, sum(r.points)
        FROM users u
        LEFT JOIN reputation r on (u.id=r.user_id)
        LEFT JOIN badge b on (u.id=b.user_id and badge=:badge)
        WHERE group=:group';

$params = array(
    ':group' => $group, 
    ':badge' => $_GET['badge']
);

$stmt = $pdo->prepare($sql);
$result = $stmt->execute($params);

而不是


$sql = 'SELECT u.id, u.user, u.email, sum(r.points)
        FROM users u
        LEFT JOIN reputation r on (u.id=r.user_id)
        LEFT JOIN badge b on (u.id=b.user_id and badge="'.mysql_real_escape_string($_GET['badge']).'")
        WHERE group="'.mysql_real_escape_string($group).'"';

$result = mysql_query($sql);

想象一下你必须改变sql语句,哪个代码是你最喜欢的? ; - )

答案 2 :(得分:1)

准备好的陈述在几种情况下派上用场:

  • 查询数据与不受信任的用户数据的良好分离。
  • 多次执行相同查询时性能提升
  • 传输二进制数据时性能提高,因为预准备语句可以使用二进制协议,而传统查询最终会进行编码等。

在正常情况下(不重复,没有二进制数据)会导致性能下降,因为您现在必须来回两次。第一个用于“准备”查询,第二个用于传输令牌以及要插入的数据。大多数人都愿意为安全利益做出这种牺牲。

关于持久连接: MySQL拥有市场上最快的连接建立时间之一。它对于大多数设置来说基本上是免费的,所以你不会使用持久连接看到太多的变化。

答案 3 :(得分:1)

答案与安全性和抽象性有关。其他人都已经提到了安全性,但真正的好处是你的输入完全是从查询本身中抽象出来的。这允许在使用抽象层时实现真正的数据库不可知性,而内联输入通常是依赖于数据库的过程。如果你关心任何可移植性,准备好的陈述是要走的路。

在现实世界中,我很少编写DML查询。我的所有INSERTS / UPDATES都是由抽象层自动构建的,只需传递一个输入数组即可执行。对于所有意图和目的,准备查询然后执行它们确实没有“性能损失”(保存初始PREPARE中的​​连接延迟)。但是当使用UDS(Unix域套接字)连接时,您不会注意到(甚至能够进行基准测试)差异。它通常只有几微秒。

鉴于安全性和抽象性上升,我几乎不称它为浪费。

答案 4 :(得分:1)

性能优势并非来自较少的解析 - 它来自于只需要一次而不是重复地计算访问路径。当您发出数千个查询时,这会有很大帮助。

鉴于mysql的非常简单的优化器/规划器,这可能不是一个问题,而是一个更成熟的数据库和更复杂的优化器。

但是,如果您有一个了解数据偏差的复杂优化器,那么这种性能优势实际上会变得有害。在这种情况下,通常可以更好地使用不同的文字值为同一查询获取不同的访问路径,而不是重用预先存在的路径。

答案 5 :(得分:0)

当使用像SELECT x,y,z FROM foo WHERE c='mary had a little lamb'这样的sql查询时,服务器必须解析包含数据的sql语句+你必须清理“mary have ...”部分(对每个参数调用mysql_real_escape()或类似)。 使用预准备语句,服务器也必须解析语句,但没有数据,只返回语句的标识符(一个微小的数据包)。然后您发送实际数据而不先清理它。我没有看到这里的开销,但我承认我从未测试过它。你呢? ;-)

编辑:使用预处理语句可以消除将每个参数(输入/输出)转换为字符串的需要。如果你的php版本使用mysqlnd(而不是“旧的”libmysql客户端库),可能更是如此。还没有测试过它的性能方面。

答案 6 :(得分:0)

我似乎没有找到使用持久连接的任何好处 - 或者为该主管准备好的语句。看看这些数字 - 对于6000个选择语句(在页面请求中永远不会发生!),你几乎无法区分它们。我的大多数页面使用的查询少于10个。

  

更新我刚刚将测试修改为   包括4k SELECT和4k INSERT   声明!自己动手让我   知道是否有任何设计错误。

如果我的MySQL服务器没有在与Apache相同的机器上运行,那么差异可能更大。

Persistent: TRUE
Prepare: TRUE
2.3399310112 seconds

Persistent: FALSE
Prepare: TRUE
2.3265211582184 seconds

Persistent: TRUE
Prepare: FALSE
2.3666892051697 seconds

Persistent: FALSE
Prepare: FALSE
2.3496441841125 seconds

这是我的测试代码:

$hostname = 'localhost';
$username = 'root';
$password = '';
$dbname = 'db_name';

$persistent = FALSE;
$prepare = FALSE;

try 
{

    // Force PDO to use exceptions for all errors
    $attrs = array(PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION);

    if($persistent) 
    { 
        // Make the connection persistent
        $attrs[PDO::ATTR_PERSISTENT] = TRUE;
    }

    $db = new PDO("mysql:host=$hostname;dbname=$dbname", $username, $password, $attrs);

    // What type of connection?
    print 'Persistent: '.($db->getAttribute(PDO::ATTR_PERSISTENT) ? 'TRUE' : 'FALSE').'<br />';
    print 'Prepare: '.($prepare ? 'TRUE' : 'FALSE').'<br />';

    //Clean table from last run
    $db->exec('TRUNCATE TABLE `pdo_insert`');

}
catch(PDOException $e)
{
    echo $e->getMessage();
}

$start = microtime(TRUE);

$name = 'Jack';
$body = 'This is the text "body"';

if( $prepare ) {

    // Select
    $select = $db->prepare('SELECT * FROM pdo_insert WHERE id = :id');
    $select->bindParam(':id', $x);

    // Insert
    $insert = $db->prepare('INSERT INTO pdo_insert (`name`, `body`, `author_id`) 
    VALUES (:name, :body, :author_id)');
    $insert->bindParam(':name', $name);
    $insert->bindParam(':body', $body);
    $insert->bindParam(':author_id', $x);


    $run = 0;
    for($x=0;$x<4000;++$x) 
    {
        if( $insert->execute() && $select->execute() ) 
        {
            $run++;
        }
    }

}
else
{

    $run = 0;
    for($x=0;$x<4000;++$x) {

        // Insert
        if( $db->query('INSERT INTO pdo_insert (`name`, `body`, `author_id`) 
        VALUES ('.$db->quote($name).', '. $db->quote($body).', '. $db->quote($x).')') 

        AND

        // Select
        $db->query('SELECT * FROM pdo_insert WHERE id = '. $db->quote($x)) )
        {
            $run++;
        }

    }

}





print (microtime(true) - $start).' seconds and '.($run * 2).' queries';

答案 7 :(得分:0)

卡西是对的。如果你没有准备/编译它,那么dbms在任何情况下都必须能够运行它。

此外,优点是您可以检查准备结果,如果准备失败,您的算法可以分支处理异常,而不会浪费数据库资源来运行失败的查询。