<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>EXPLAIN EXTENDED</title>
	<atom:link href="http://explainextended.com/feed/" rel="self" type="application/rss+xml" />
	<link>http://explainextended.com</link>
	<description>How to create fast database queries</description>
	<lastBuildDate>Tue, 09 Mar 2010 18:43:36 +0000</lastBuildDate>
	<generator>http://wordpress.org/?v=2.9.2</generator>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
			<item>
		<title>Aggregates and LEFT JOIN</title>
		<link>http://explainextended.com/2010/03/05/aggregates-and-left-join/</link>
		<comments>http://explainextended.com/2010/03/05/aggregates-and-left-join/#comments</comments>
		<pubDate>Fri, 05 Mar 2010 20:00:09 +0000</pubDate>
		<dc:creator>Quassnoi</dc:creator>
				<category><![CDATA[MySQL]]></category>

		<guid isPermaLink="false">http://explainextended.com/?p=4548</guid>
		<description><![CDATA[From Stack Overflow:
I have a table product with products and table sale with all sale operations that were done on these products.
I would like to get 10 most often sold products today and what I did is this:

SELECT  p.*, COUNT(s.id) AS sumsell
FROM    product p
LEFT JOIN
       [...]]]></description>
			<content:encoded><![CDATA[<p>From <a href="http://stackoverflow.com/questions/2388122/how-to-increase-last-day-count-query-performance"><strong>Stack Overflow</strong></a>:</p>
<blockquote><p>I have a table <code>product</code> with products and table <code>sale</code> with all sale operations that were done on these products.</p>
<p>I would like to get <strong>10</strong> most often sold products today and what I did is this:</p>
<pre class="brush: sql">
SELECT  p.*, COUNT(s.id) AS sumsell
FROM    product p
LEFT JOIN
        sale s
ON      s.product_id = p.id
        AND s.dt &gt;= &#039;2010-01-01&#039;
        AND s.dt &lt; &#039;2010-01-02&#039;
GROUP BY
        p.id
ORDER BY
        sumsell DESC
LIMIT 10
</pre>
<p>, but performance of it is very slow.</p>
<p>What can I do to increase performance of this particular query?
</p></blockquote>
<p>The query involves a <code>LEFT JOIN</code> which in <strong>MySQL</strong> world means that <code>products</code> will be made leading in the query. Each record of <code>product</code> will be taken and checked against <code>sale</code> table to find out the number of matching records. If no matching records are found, <strong>0</strong> is returned.</p>
<p>Let&#8217;s create the sample tables:<br />
<span id="more-4548"></span><br />
<a href="#" onclick="xcollapse('X9754');return false;"><strong>Table creation details</strong></a><br />
</p>
<div id="X9754" style="display: none; ">
<pre class="brush: sql">
CREATE TABLE filler (
        id INT NOT NULL PRIMARY KEY AUTO_INCREMENT
) ENGINE=Memory;

DELIMITER $$

CREATE TABLE product (
        id INT NOT NULL PRIMARY KEY,
        name VARCHAR(30) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE sale (
        id INT NOT NULL PRIMARY KEY,
        product_id INT NOT NULL,
        amount FLOAT NOT NULL,
        dt DATETIME NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE PROCEDURE prc_filler(cnt INT)
BEGIN
        DECLARE _cnt INT;
        SET _cnt = 1;
        WHILE _cnt &lt;= cnt DO
                INSERT
                INTO    filler
                SELECT  _cnt;
                SET _cnt = _cnt + 1;
        END WHILE;
END
$$

DELIMITER ;

START TRANSACTION;
CALL prc_filler(500000);
COMMIT;

INSERT
INTO    product
SELECT  id, CONCAT(&#039;Product &#039;, id)
FROM    filler;

INSERT
INTO    sale (id, product_id, amount, dt)
SELECT  id,
        FLOOR(RAND(20100305) * 500000) + 1,
        RAND(20100305 &lt;&lt; 1) * 100 + 1,
        &#039;2010-03-06&#039; - INTERVAL id MINUTE
FROM    filler;

CREATE INDEX ix_sale_product_dt ON sale (product_id, dt);
CREATE INDEX ix_sale_dt_product ON sale (dt, product_id);
</pre>
</div>
<p>The table contains <strong>500,000</strong> products and <strong>500,000</strong> random sales (<strong>1,440</strong> sales per day).</p>
<p>Now, let&#8217;s run the query similar to the author&#8217;s one. I adjusted the period so that fewer than <strong>10</strong> actual sales were made during the period and <code>LEFT JOIN</code> records can be seen in the table:</p>
<pre class="brush: sql">
SELECT  p.*, COUNT(s.id) AS sumsell
FROM    product p
LEFT JOIN
        sale s
ON      s.product_id = p.id
        AND s.dt &gt;= &#039;2010-01-01&#039;
        AND s.dt &lt; &#039;2010-01-01 00:07:00&#039;
GROUP BY
        p.id
ORDER BY
        sumsell DESC, p.id
LIMIT 10
</pre>
<p><a href="#" onclick="xcollapse('X3182');return false;"><strong>View query details</strong></a><br />
</p>
<div id="X3182" style="display: none; ">
<div class="terminal">
<table class="terminal">
<tr>
<th>id</th>
<th>name</th>
<th>sumsell</th>
</tr>
<tr>
<td class="integer">50630</td>
<td class="varchar">Product 50630</td>
<td class="bigint">1</td>
</tr>
<tr>
<td class="integer">143395</td>
<td class="varchar">Product 143395</td>
<td class="bigint">1</td>
</tr>
<tr>
<td class="integer">222114</td>
<td class="varchar">Product 222114</td>
<td class="bigint">1</td>
</tr>
<tr>
<td class="integer">322966</td>
<td class="varchar">Product 322966</td>
<td class="bigint">1</td>
</tr>
<tr>
<td class="integer">340133</td>
<td class="varchar">Product 340133</td>
<td class="bigint">1</td>
</tr>
<tr>
<td class="integer">345789</td>
<td class="varchar">Product 345789</td>
<td class="bigint">1</td>
</tr>
<tr>
<td class="integer">462937</td>
<td class="varchar">Product 462937</td>
<td class="bigint">1</td>
</tr>
<tr>
<td class="integer">1</td>
<td class="varchar">Product 1</td>
<td class="bigint">0</td>
</tr>
<tr>
<td class="integer">2</td>
<td class="varchar">Product 2</td>
<td class="bigint">0</td>
</tr>
<tr>
<td class="integer">3</td>
<td class="varchar">Product 3</td>
<td class="bigint">0</td>
</tr>
<tr class="statusbar">
<td colspan="100">10 rows fetched in 0.0009s (3.0312s)</td>
</tr>
</table>
</div>
<div class="terminal">
<table class="terminal">
<tr>
<th>id</th>
<th>select_type</th>
<th>table</th>
<th>type</th>
<th>possible_keys</th>
<th>key</th>
<th>key_len</th>
<th>ref</th>
<th>rows</th>
<th>filtered</th>
<th>Extra</th>
</tr>
<tr>
<td class="bigint">1</td>
<td class="varchar">SIMPLE</td>
<td class="varchar">p</td>
<td class="varchar">index</td>
<td class="varchar"></td>
<td class="varchar">PRIMARY</td>
<td class="varchar">4</td>
<td class="varchar"></td>
<td class="bigint">10</td>
<td class="double">5007270.00</td>
<td class="varchar">Using temporary; Using filesort</td>
</tr>
<tr>
<td class="bigint">1</td>
<td class="varchar">SIMPLE</td>
<td class="varchar">s</td>
<td class="varchar">ref</td>
<td class="varchar">ix_sale_product_dt,ix_sale_dt_product</td>
<td class="varchar">ix_sale_product_dt</td>
<td class="varchar">4</td>
<td class="varchar">20100305_left.p.id</td>
<td class="bigint">1</td>
<td class="double">100.00</td>
<td class="varchar">Using index</td>
</tr>
</table>
</div>
<pre>
select `20100305_left`.`p`.`id` AS `id`,`20100305_left`.`p`.`name` AS `name`,count(`20100305_left`.`s`.`id`) AS `sumsell` from `20100305_left`.`product` `p` left join `20100305_left`.`sale` `s` on(((`20100305_left`.`s`.`product_id` = `20100305_left`.`p`.`id`) and (`20100305_left`.`s`.`dt` &gt;= &#39;2010-01-01&#39;) and (`20100305_left`.`s`.`dt` &lt; &#39;2010-01-01 00:07:00&#39;))) where 1 group by `20100305_left`.`p`.`id` order by count(`20100305_left`.`s`.`id`) desc,`20100305_left`.`p`.`id` limit 10
</pre>
</div>
<p>The query runs for <strong>3 seconds</strong>.</p>
<p>We see that, first, <code>product</code> is made leading, and, second, only a part of the index on <code>sale (product, dt)</code> is used: each sale is only filtered on product, not on date.</p>
<p>Since there were only <strong>7</strong> sales during the period we have chosen, it would be a wise decision to make <code>sale</code> leading in the join so that it could be filtered on date and the resulting recordset was then joined to <code>product</code>. This would result in at most <strong>7</strong> <code>PRIMARY KEY</code> seeks instead of <strong>500,000</strong> range scans and would be much more efficient.</p>
<p>However, this is only possible with the <code>INNER JOIN</code>, and if there are less then <strong>10</strong> products sold within the time period, we will not see the rest.</p>
<p>To work around this, we need to emulate the <code>LEFT JOIN</code>:</p>
<ol>
<li>
<p>Find the products sold within the time period, using an <code>INNER JOIN</code> of <code>product</code> with the resultset containg aggregated sales.</p>
</li>
<li>
<p>Find the products <strong>not</strong> sold within the time period, using <code>NOT EXISTS</code> predicate.</p>
</li>
<li>
<p>Concatenate the two resultsets using <code>UNION ALL</code>.</p>
</li>
</ol>
<p>The step <strong>2</strong> implies that <code>product</code> is leading again, so normally it would not be much of improvement. But in our case, we don&#8217;t need the whole recordset, we only need the top <strong>10</strong> sales.</p>
<p>So we can just order and limit the recordsets retrieved on steps <strong>1</strong> and <strong>2</strong> to ten records each, concatenate them, then order and limit the resulting recordset again to ten records.</p>
<p>The second resultset will contain a hardcoded <strong>0</strong> in the <code>sumsell</code>, so we just need to order it on <code>product.id</code>. Since <code>product</code> is an <strong>InnoDB</strong> table and <code>product.id</code> is a clustered <code>PRIMARY KEY</code>, this is not a problem.</p>
<p>Here&#8217;s the query:</p>
<pre class="brush: sql">
SELECT  p.*, sumsell
FROM    (
        SELECT  *
        FROM    (
                SELECT  product_id, sumsell
                FROM    (
                        SELECT  product_id, COUNT(*) AS sumsell
                        FROM    sale si
                        WHERE   dt &gt;= &#039;2010-01-01&#039;
                                AND dt &lt; &#039;2010-01-01 00:07:00&#039;
                        GROUP BY
                                product_id
                        ) si
                ORDER BY
                        sumsell DESC, product_id
                LIMIT 10
                ) q1
        UNION ALL
        SELECT  *
        FROM    (
                SELECT  p.id, 0
                FROM    product p
                WHERE   NOT EXISTS
                        (
                        SELECT  NULL
                        FROM    sale si
                        WHERE   product_id = p.id
                                AND dt &gt;= &#039;2010-01-01&#039;
                                AND dt &lt; &#039;2010-01-01 00:07:00&#039;
                        )
                ORDER BY
                        p.id
                LIMIT 10
                ) q2
        ORDER BY
                sumsell DESC, product_id
        LIMIT 10
        ) q
JOIN    product p
ON      p.id = q.product_id
</pre>
<p><a href="#" onclick="xcollapse('X1041');return false;"><strong>View query details</strong></a><br />
</p>
<div id="X1041" style="display: none; ">
<div class="terminal">
<table class="terminal">
<tr>
<th>id</th>
<th>name</th>
<th>sumsell</th>
</tr>
<tr>
<td class="integer">50630</td>
<td class="varchar">Product 50630</td>
<td class="bigint">1</td>
</tr>
<tr>
<td class="integer">143395</td>
<td class="varchar">Product 143395</td>
<td class="bigint">1</td>
</tr>
<tr>
<td class="integer">222114</td>
<td class="varchar">Product 222114</td>
<td class="bigint">1</td>
</tr>
<tr>
<td class="integer">322966</td>
<td class="varchar">Product 322966</td>
<td class="bigint">1</td>
</tr>
<tr>
<td class="integer">340133</td>
<td class="varchar">Product 340133</td>
<td class="bigint">1</td>
</tr>
<tr>
<td class="integer">345789</td>
<td class="varchar">Product 345789</td>
<td class="bigint">1</td>
</tr>
<tr>
<td class="integer">462937</td>
<td class="varchar">Product 462937</td>
<td class="bigint">1</td>
</tr>
<tr>
<td class="integer">1</td>
<td class="varchar">Product 1</td>
<td class="bigint">0</td>
</tr>
<tr>
<td class="integer">2</td>
<td class="varchar">Product 2</td>
<td class="bigint">0</td>
</tr>
<tr>
<td class="integer">3</td>
<td class="varchar">Product 3</td>
<td class="bigint">0</td>
</tr>
<tr class="statusbar">
<td colspan="100">10 rows fetched in 0.0012s (0.0064s)</td>
</tr>
</table>
</div>
<div class="terminal">
<table class="terminal">
<tr>
<th>id</th>
<th>select_type</th>
<th>table</th>
<th>type</th>
<th>possible_keys</th>
<th>key</th>
<th>key_len</th>
<th>ref</th>
<th>rows</th>
<th>filtered</th>
<th>Extra</th>
</tr>
<tr>
<td class="bigint">1</td>
<td class="varchar">PRIMARY</td>
<td class="varchar">&lt;derived2&gt;</td>
<td class="varchar">ALL</td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="bigint">10</td>
<td class="double">100.00</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="bigint">1</td>
<td class="varchar">PRIMARY</td>
<td class="varchar">p</td>
<td class="varchar">eq_ref</td>
<td class="varchar">PRIMARY</td>
<td class="varchar">PRIMARY</td>
<td class="varchar">4</td>
<td class="varchar">q.product_id</td>
<td class="bigint">1</td>
<td class="double">100.00</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="bigint">2</td>
<td class="varchar">DERIVED</td>
<td class="varchar">&lt;derived3&gt;</td>
<td class="varchar">ALL</td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="bigint">7</td>
<td class="double">100.00</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="bigint">3</td>
<td class="varchar">DERIVED</td>
<td class="varchar">&lt;derived4&gt;</td>
<td class="varchar">ALL</td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="bigint">7</td>
<td class="double">100.00</td>
<td class="varchar">Using filesort</td>
</tr>
<tr>
<td class="bigint">4</td>
<td class="varchar">DERIVED</td>
<td class="varchar">si</td>
<td class="varchar">range</td>
<td class="varchar">ix_sale_dt_product</td>
<td class="varchar">ix_sale_dt_product</td>
<td class="varchar">8</td>
<td class="varchar"></td>
<td class="bigint">6</td>
<td class="double">100.00</td>
<td class="varchar">Using where; Using index; Using temporary; Using filesort</td>
</tr>
<tr>
<td class="bigint">5</td>
<td class="varchar">UNION</td>
<td class="varchar">&lt;derived6&gt;</td>
<td class="varchar">ALL</td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="bigint">10</td>
<td class="double">100.00</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="bigint">6</td>
<td class="varchar">DERIVED</td>
<td class="varchar">p</td>
<td class="varchar">index</td>
<td class="varchar"></td>
<td class="varchar">PRIMARY</td>
<td class="varchar">4</td>
<td class="varchar"></td>
<td class="bigint">10</td>
<td class="double">5007270.00</td>
<td class="varchar">Using where; Using index</td>
</tr>
<tr>
<td class="bigint">7</td>
<td class="varchar">DEPENDENT SUBQUERY</td>
<td class="varchar">si</td>
<td class="varchar">ref</td>
<td class="varchar">ix_sale_product_dt,ix_sale_dt_product</td>
<td class="varchar">ix_sale_product_dt</td>
<td class="varchar">4</td>
<td class="varchar">20100305_left.p.id</td>
<td class="bigint">1</td>
<td class="double">100.00</td>
<td class="varchar">Using where; Using index</td>
</tr>
<tr>
<td class="bigint"></td>
<td class="varchar">UNION RESULT</td>
<td class="varchar">&lt;union2,5&gt;</td>
<td class="varchar">ALL</td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="bigint"></td>
<td class="double"></td>
<td class="varchar">Using filesort</td>
</tr>
</table>
</div>
<pre>
Field or reference &#39;20100305_left.p.id&#39; of SELECT #7 was resolved in SELECT #6
select `20100305_left`.`p`.`id` AS `id`,`20100305_left`.`p`.`name` AS `name`,`q`.`sumsell` AS `sumsell` from (select `q1`.`product_id` AS `product_id`,`q1`.`sumsell` AS `sumsell` from (select `si`.`product_id` AS `product_id`,`si`.`sumsell` AS `sumsell` from (select `20100305_left`.`si`.`product_id` AS `product_id`,count(0) AS `sumsell` from `20100305_left`.`sale` `si` where ((`20100305_left`.`si`.`dt` &gt;= &#39;2010-01-01&#39;) and (`20100305_left`.`si`.`dt` &lt; &#39;2010-01-01 00:07:00&#39;)) group by `20100305_left`.`si`.`product_id`) `si` order by `si`.`sumsell` desc,`si`.`product_id` limit 10) `q1` union all select `q2`.`id` AS `id`,`q2`.`0` AS `0` from (select `20100305_left`.`p`.`id` AS `id`,0 AS `0` from `20100305_left`.`product` `p` where (not(exists(select NULL AS `NULL` from `20100305_left`.`sale` `si` where ((`20100305_left`.`si`.`product_id` = `20100305_left`.`p`.`id`) and (`20100305_left`.`si`.`dt` &gt;= &#39;2010-01-01&#39;) and (`20100305_left`.`si`.`dt` &lt; &#39;2010-01-01 00:07:00&#39;))))) order by `20100305_left`.`p`.`id` limit 10) `q2` order by `sumsell` desc,`product_id` limit 10) `q` join `20100305_left`.`product` `p` where (`20100305_left`.`p`.`id` = `q`.`product_id`)
</pre>
</div>
<p>This query completes in less than <strong>7 ms</strong> (which is comparable to the time measurement error).</p>
]]></content:encoded>
			<wfw:commentRss>http://explainextended.com/2010/03/05/aggregates-and-left-join/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>SQL Server: deleting with self-referential FOREIGN KEY</title>
		<link>http://explainextended.com/2010/03/03/sql-server-deleting-with-self-referential-foreign-key/</link>
		<comments>http://explainextended.com/2010/03/03/sql-server-deleting-with-self-referential-foreign-key/#comments</comments>
		<pubDate>Wed, 03 Mar 2010 20:00:45 +0000</pubDate>
		<dc:creator>Quassnoi</dc:creator>
				<category><![CDATA[SQL Server]]></category>

		<guid isPermaLink="false">http://explainextended.com/?p=4539</guid>
		<description><![CDATA[From Stack Overflow:

I have an SQL Server table defined as below:

TestComposite

id (PK)
siteUrl (PK)
name
parentId


1
site1
Item1
NULL


2
site1
Item2
NULL


3
site1
Folder1
NULL


4
site1
Folder1.Item1
3


5
site1
Folder1.Item2
3


6
site1
Folder1.Folder1
3


7
site1
Folder1.Folder1.Item1
6


Items and folders are stored inside the same table
If an item is inside a folder, the parentID column is the id of the folder.
I would like to be able to DELETE CASCADE items/folders when I delete a folder.
I tried to define a constraint [...]]]></description>
			<content:encoded><![CDATA[<p>From <a href="http://stackoverflow.com/questions/2371351/composite-primary-keys-and-foreign-key-constraint-error"><strong>Stack Overflow</strong></a>:</p>
<blockquote>
<p>I have an <strong>SQL Server</strong> table defined as below:</p>
<table class="excel">
<caption>TestComposite</caption>
<tr>
<th>id (<em>PK</em>)</th>
<th>siteUrl (<em>PK</em>)</th>
<th>name</th>
<th>parentId</th>
</tr>
<tr>
<td>1</td>
<td>site1</td>
<td>Item1</td>
<td>NULL</td>
</tr>
<tr>
<td>2</td>
<td>site1</td>
<td>Item2</td>
<td>NULL</td>
</tr>
<tr>
<td>3</td>
<td>site1</td>
<td>Folder1</td>
<td>NULL</td>
</tr>
<tr>
<td>4</td>
<td>site1</td>
<td>Folder1.Item1</td>
<td>3</td>
</tr>
<tr>
<td>5</td>
<td>site1</td>
<td>Folder1.Item2</td>
<td>3</td>
</tr>
<tr>
<td>6</td>
<td>site1</td>
<td>Folder1.Folder1</td>
<td>3</td>
</tr>
<tr>
<td>7</td>
<td>site1</td>
<td>Folder1.Folder1.Item1</td>
<td>6</td>
</tr>
</table>
<p>Items and folders are stored inside the same table</p>
<p>If an item is inside a folder, the <code>parentID</code> column is the <code>id</code> of the folder.</p>
<p>I would like to be able to <code>DELETE CASCADE</code> items/folders when I delete a folder.</p>
<p>I tried to define a constraint similar to:</p>
<pre class="brush: sql">
ALTER TABLE [TestComposite]
ADD CONSTRAINT fk_parentid
FOREIGN KEY (ParentID, SiteUrl)
REFERENCES [TestComposite] (ID, SiteUrl) ON DELETE CASCADE
</pre>
<p>, but it gives me this error:</p>
<pre>
Introducing FOREIGN KEY constraint 'fk_parentid' on table 'TestComposite' may cause cycles or multiple cascade paths. Specify ON DELETE NO ACTION or ON UPDATE NO ACTION, or modify other FOREIGN KEY constraints.
</pre>
</blockquote>
<p><strong>SQL Server</strong> does support chained <code>CASCADE</code> updates, but does not allow one table to participate more that once in a chain (i. e. does not allow <em>loops</em>).</p>
<p><strong>SQL Server</strong>, unlike most other engines, optimizes cascading <strong>DML</strong> operations to be set-based which requires building a cycle-free <strong>DML</strong> order (which you can observe in the execution plan). With the loops, that would not be possible.</p>
<p>However, it is possible to define such a constraint without cascading operations, and with a little effort it is possible to delete a whole tree branch at once.</p>
<p>Let&#8217;s create a sample table:<br />
<span id="more-4539"></span><br />
<a href="#" onclick="xcollapse('X7255');return false;"><strong>Table creation details</strong></a><br />
</p>
<div id="X7255" style="display: none; ">
<pre class="brush: sql">
CREATE SCHEMA [20100303_cascade]
CREATE TABLE TestComposite (
        id INT NOT NULL,
        siteUrl NVARCHAR(255) NOT NULL,
        name NVARCHAR(MAX) NOT NULL,
        parentId INT NULL,
        PRIMARY KEY (id, siteUrl)
);
GO
BEGIN TRANSACTION
DECLARE @cnt INT
SET @cnt = 0
WHILE @cnt &lt; 50000
BEGIN
        INSERT
        INTO    [20100303_cascade].TestComposite (id, siteUrl, name, parentID)
        VALUES  (
                @cnt / 10 + 1,
                &#039;site&#039; + CAST((@cnt % 10 + 1) AS NVARCHAR(255)),
                &#039;name &#039; + CAST(@cnt / 10 + 1 AS NVARCHAR(MAX)),
                CASE WHEN @cnt &lt; 50 THEN NULL ELSE @cnt / 50 END
                )
        SET @cnt = @cnt + 1
END
COMMIT
GO
</pre>
</div>
<p>This table contains <strong>50,000</strong> records forming a hierarchy.</p>
<p>If we try to delete an entry that has some children, we&#8217;ll fail:</p>
<pre class="brush: sql">
DELETE
FROM    [20100303_cascade].TestComposite
WHERE   id = 42
        AND siteUrl = &#039;site1&#039;
</pre>
<pre>
Msg 547, Level 16, State 0, Line 1
The DELETE statement conflicted with the SAME TABLE REFERENCE constraint "fk_TestComposite_self". The conflict occurred in database "test", table "20100303_cascade.TestComposite".
The statement has been terminated.
</pre>
<p>To delete an item and all of its children, we should build a hierarchical query to retrieve the whole branch, and delete the branch all at once.</p>
<p>To do it, we just semi-join the table to the results of the recursive <strong>CTE</strong>:</p>
<pre class="brush: sql">
WITH    q AS
        (
        SELECT  id, siteUrl
        FROM    [20100303_cascade].TestComposite
        WHERE   id = 42
                AND siteUrl = &#039;site1&#039;
        UNION ALL
        SELECT  tc.id, tc.siteUrl
        FROM    q
        JOIN    [20100303_cascade].TestComposite tc
        ON      tc.parentID = q.id
                AND tc.siteUrl = q.siteUrl
        )
DELETE
FROM    [20100303_cascade].TestComposite
OUTPUT  DELETED.*
WHERE   EXISTS
        (
        SELECT  id, siteUrl
        INTERSECT
        SELECT  id, siteUrl
        FROM    q
        )
</pre>
<p><a href="#" onclick="xcollapse('X4752');return false;"><strong>View query results</strong></a><br />
</p>
<div id="X4752" style="display: none; ">
<div class="terminal">
<table class="terminal">
<tr>
<th>id</th>
<th>siteUrl</th>
<th>name</th>
<th>parentId</th>
</tr>
<tr>
<td class="int">42</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 42</td>
<td class="int">8</td>
</tr>
<tr>
<td class="int">211</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 211</td>
<td class="int">42</td>
</tr>
<tr>
<td class="int">212</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 212</td>
<td class="int">42</td>
</tr>
<tr>
<td class="int">213</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 213</td>
<td class="int">42</td>
</tr>
<tr>
<td class="int">214</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 214</td>
<td class="int">42</td>
</tr>
<tr>
<td class="int">215</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 215</td>
<td class="int">42</td>
</tr>
<tr>
<td class="int">1056</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 1056</td>
<td class="int">211</td>
</tr>
<tr>
<td class="int">1057</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 1057</td>
<td class="int">211</td>
</tr>
<tr>
<td class="int">1058</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 1058</td>
<td class="int">211</td>
</tr>
<tr>
<td class="int">1059</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 1059</td>
<td class="int">211</td>
</tr>
<tr>
<td class="int">1060</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 1060</td>
<td class="int">211</td>
</tr>
<tr>
<td class="int">1061</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 1061</td>
<td class="int">212</td>
</tr>
<tr>
<td class="int">1062</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 1062</td>
<td class="int">212</td>
</tr>
<tr>
<td class="int">1063</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 1063</td>
<td class="int">212</td>
</tr>
<tr>
<td class="int">1064</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 1064</td>
<td class="int">212</td>
</tr>
<tr>
<td class="int">1065</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 1065</td>
<td class="int">212</td>
</tr>
<tr>
<td class="int">1066</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 1066</td>
<td class="int">213</td>
</tr>
<tr>
<td class="int">1067</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 1067</td>
<td class="int">213</td>
</tr>
<tr>
<td class="int">1068</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 1068</td>
<td class="int">213</td>
</tr>
<tr>
<td class="int">1069</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 1069</td>
<td class="int">213</td>
</tr>
<tr>
<td class="int">1070</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 1070</td>
<td class="int">213</td>
</tr>
<tr>
<td class="int">1071</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 1071</td>
<td class="int">214</td>
</tr>
<tr>
<td class="int">1072</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 1072</td>
<td class="int">214</td>
</tr>
<tr>
<td class="int">1073</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 1073</td>
<td class="int">214</td>
</tr>
<tr>
<td class="int">1074</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 1074</td>
<td class="int">214</td>
</tr>
<tr>
<td class="int">1075</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 1075</td>
<td class="int">214</td>
</tr>
<tr>
<td class="int">1076</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 1076</td>
<td class="int">215</td>
</tr>
<tr>
<td class="int">1077</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 1077</td>
<td class="int">215</td>
</tr>
<tr>
<td class="int">1078</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 1078</td>
<td class="int">215</td>
</tr>
<tr>
<td class="int">1079</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 1079</td>
<td class="int">215</td>
</tr>
<tr>
<td class="int">1080</td>
<td class="nvarchar">site1</td>
<td class="ntext">name 1080</td>
<td class="int">215</td>
</tr>
<tr class="statusbar">
<td colspan="100">31 rows fetched in 0.0038s (0.0234s)</td>
</tr>
</table>
</div>
<pre>
SQL Server parse and compile time:    CPU time = 0 ms, elapsed time = 1 ms.
Table 'TestComposite'. Scan count 62, logical reads 498, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 5, logical reads 256, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
SQL Server Execution Times:   CPU time = 16 ms,  elapsed time = 4 ms.
</pre>
<pre>
  |--Sequence
       |--Table Spool
       |    |--Clustered Index Delete(OBJECT:([test].[20100303_cascade].[TestComposite].[PK__TestComposite__78E9C54B]), OBJECT:([test].[20100303_cascade].[TestComposite].[ix_TestComposite_parent_site]))
       |         |--Top(ROWCOUNT est 0)
       |              |--Sort(DISTINCT ORDER BY:([test].[20100303_cascade].[TestComposite].[id] ASC, [test].[20100303_cascade].[TestComposite].[siteUrl] ASC))
       |                   |--Nested Loops(Inner Join, OUTER REFERENCES:([Recr1010], [Recr1011]))
       |                        |--Index Spool(WITH STACK)
       |                        |    |--Concatenation
       |                        |         |--Compute Scalar(DEFINE:([Expr1019]=(0)))
       |                        |         |    |--Clustered Index Seek(OBJECT:([test].[20100303_cascade].[TestComposite].[PK__TestComposite__78E9C54B]), SEEK:([test].[20100303_cascade].[TestComposite].[id]=(42) AND [test].[20100303_cascade].[TestComposite].[siteUrl]=N'site1') ORDERED FORWARD)
       |                        |         |--Assert(WHERE:(CASE WHEN [Expr1021]&gt;(100) THEN (0) ELSE NULL END))
       |                        |              |--Nested Loops(Inner Join, OUTER REFERENCES:([Expr1021], [Recr1006], [Recr1007]))
       |                        |                   |--Compute Scalar(DEFINE:([Expr1021]=[Expr1020]+(1)))
       |                        |                   |    |--Table Spool(WITH STACK)
       |                        |                   |--Index Seek(OBJECT:([test].[20100303_cascade].[TestComposite].[ix_TestComposite_parent_site] AS [tc]), SEEK:([tc].[parentId]=[Recr1006] AND [tc].[siteUrl]=[Recr1007]) ORDERED FORWARD)
       |                        |--Clustered Index Seek(OBJECT:([test].[20100303_cascade].[TestComposite].[PK__TestComposite__78E9C54B]), SEEK:([test].[20100303_cascade].[TestComposite].[id]=[Recr1010] AND [test].[20100303_cascade].[TestComposite].[siteUrl]=[Recr1011]) ORDERED FORWARD)
       |--Table Spool
       |--Assert(WHERE:(CASE WHEN NOT [Expr1018] IS NULL THEN (0) ELSE NULL END))
            |--Nested Loops(Left Semi Join, OUTER REFERENCES:([test].[20100303_cascade].[TestComposite].[id], [test].[20100303_cascade].[TestComposite].[siteUrl], [Expr1023]) WITH UNORDERED PREFETCH, DEFINE:([Expr1018] = [PROBE VALUE]))
                 |--Table Spool
                 |--Index Seek(OBJECT:([test].[20100303_cascade].[TestComposite].[ix_TestComposite_parent_site]), SEEK:([test].[20100303_cascade].[TestComposite].[parentId]=[test].[20100303_cascade].[TestComposite].[id] AND [test].[20100303_cascade].[TestComposite].[siteUrl]=[test].[20100303_cascade].[TestComposite].[siteUrl]) ORDERED FORWARD)
</pre>
</div>
<p>We see that the whole branch of <strong>31</strong> records was deleted all at once, without violating the <code>FOREIGN KEY</code>.</p>
<p>Unlike some other systems, we don&#8217;t have to worry about the order the records are deleted, since all <strong>SQL Server</strong> referential constraints are deferred till the end of the query.</p>
]]></content:encoded>
			<wfw:commentRss>http://explainextended.com/2010/03/03/sql-server-deleting-with-self-referential-foreign-key/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>PostgreSQL: using recursive functions in nested sets</title>
		<link>http://explainextended.com/2010/03/02/postgresql-using-recursive-functions-in-nested-sets/</link>
		<comments>http://explainextended.com/2010/03/02/postgresql-using-recursive-functions-in-nested-sets/#comments</comments>
		<pubDate>Tue, 02 Mar 2010 20:00:45 +0000</pubDate>
		<dc:creator>Quassnoi</dc:creator>
				<category><![CDATA[PostgreSQL]]></category>

		<guid isPermaLink="false">http://explainextended.com/?p=4528</guid>
		<description><![CDATA[In the previous article, I discussed a way to improve nested sets model in PostgreSQL.
The approach shown in the article used an analytical function to filter all immediate children of a node in a recursive CTE.
This allowed us to filter a node&#8217;s children on the level more efficiently than R-Tree or B-Tree approaches do (since [...]]]></description>
			<content:encoded><![CDATA[<p>In the previous article, I discussed <a href="/2010/03/01/postgresql-nested-sets-and-r-tree/">a way to improve nested sets model in <strong>PostgreSQL</strong></a>.</p>
<p>The approach shown in the article used an analytical function to filter all immediate children of a node in a recursive <strong>CTE</strong>.</p>
<p>This allowed us to filter a node&#8217;s children on the level more efficiently than <strong>R-Tree</strong> or <strong>B-Tree</strong> approaches do (since they rely on <code>COUNT(*)</code>).</p>
<p>That solution was pure <strong>SQL</strong> and it was quite fast, but not optimal.</p>
<p>The drawback of that solution is that it still needs to fetch all children of a node to apply the analytic function to them. This can take much time for the top of the hierarchy. And since the top of the hierarchy is what is what usually shown at the start page, it would be very nice to improve this query yet a little more.</p>
<p>We can do it by creating and using a simple recursive <strong>SQL</strong> function. This function does not even require <strong>PL/pgSQL</strong> to be enabled.</p>
<p>Let&#8217;s create a sample table:<br />
<span id="more-4528"></span><br />
<a href="#" onclick="xcollapse('X9881');return false;"><strong>Table creation details</strong></a><br />
</p>
<div id="X9881" style="display: none; ">
<pre class="brush: sql">
CREATE TABLE t_hierarchy (
        id INT NOT NULL,
        parent INT NOT NULL,
        lft INT NOT NULL,
        rgt INT NOT NULL,
        data VARCHAR(100) NOT NULL,
        stuffing VARCHAR(100) NOT NULL
);

INSERT
INTO    t_hierarchy
WITH RECURSIVE
        ini AS
        (
        SELECT  8 AS level, 5 AS children
        ),
        range AS
        (
        SELECT  level, children,
                (
                SELECT  SUM(POW(children, n)::INTEGER * ((n &lt; level)::INTEGER + 1))
                FROM    generate_series(level, 0, -1) n
                ) width
        FROM    ini
        ),
        q AS
        (
        SELECT  s AS id, 0 AS parent, level, children,
                1 + width * (s - 1) AS lft,
                1 + width * s - 1 AS rgt,
                width / children AS width
        FROM    (
                SELECT  r.*, generate_series(1, children) s
                FROM    range r
                ) q2
        UNION ALL
        SELECT  id * children + position, id, level - 1, children,
                1 + lft + width * (position - 1),
                1 + lft + width * position - 1,
                width / children
        FROM    (
                SELECT  generate_series(1, children) AS position, q.*
                FROM    q
                ) q2
        WHERE   level &gt; 0
        )
SELECT  id, parent, lft, rgt, &#039;Value &#039; || id, RPAD(&#039;&#039;, 100, &#039;*&#039;)
FROM    q;

ALTER TABLE t_hierarchy ADD CONSTRAINT pk_hierarchy_id PRIMARY KEY (id);
CREATE UNIQUE INDEX ux_hierarchy_lft ON t_hierarchy (lft);
CREATE UNIQUE INDEX ux_hierarchy_rgt ON t_hierarchy (rgt);
CREATE INDEX ix_hierarchy_parent ON t_hierarchy (parent);
CREATE INDEX ix_hierarchy_sets ON t_hierarchy USING GIST(POLYGON(BOX(POINT(-1, lft), POINT(1, rgt))));
</pre>
</div>
<p>If we run the query introduced in the previous article to fetch all children up to level <strong>2</strong> from a really top node, we get the following results:</p>
<pre class="brush: sql">
WITH    RECURSIVE
        q AS
        (
        SELECT  id, lft, rgt, 1 AS lvl
        FROM    t_hierarchy
        WHERE   id = 1
        UNION ALL
        SELECT  *
        FROM    (
                SELECT  DISTINCT ON (MAX(hc.rgt) OVER (PARTITION BY q.id ORDER BY hc.lft)) hc.id, hc.lft, hc.rgt, lvl + 1
                FROM    q
                JOIN    t_hierarchy hc
                ON      hc.lft &gt; q.lft
                        AND hc.lft &lt; q.rgt
                WHERE   lvl &lt;= 2
                ORDER BY
                        MAX(hc.rgt) OVER (PARTITION BY q.id ORDER BY hc.lft), hc.lft
                ) q2
        )
SELECT  *
FROM    q
</pre>
<p><a href="#" onclick="xcollapse('X3758');return false;"><strong>View query details</strong></a><br />
</p>
<div id="X3758" style="display: none; ">
<div class="terminal">
<table class="terminal">
<tr>
<th>id</th>
<th>lft</th>
<th>rgt</th>
<th>lvl</th>
</tr>
<tr>
<td class="int4">1</td>
<td class="int4">1</td>
<td class="int4">585937</td>
<td class="int4">1</td>
</tr>
<tr>
<td class="int4">6</td>
<td class="int4">2</td>
<td class="int4">117188</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">7</td>
<td class="int4">117189</td>
<td class="int4">234375</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">8</td>
<td class="int4">234376</td>
<td class="int4">351562</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">9</td>
<td class="int4">351563</td>
<td class="int4">468749</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">10</td>
<td class="int4">468750</td>
<td class="int4">585936</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">31</td>
<td class="int4">3</td>
<td class="int4">23439</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">32</td>
<td class="int4">23440</td>
<td class="int4">46876</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">33</td>
<td class="int4">46877</td>
<td class="int4">70313</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">34</td>
<td class="int4">70314</td>
<td class="int4">93750</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">35</td>
<td class="int4">93751</td>
<td class="int4">117187</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">36</td>
<td class="int4">117190</td>
<td class="int4">140626</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">37</td>
<td class="int4">140627</td>
<td class="int4">164063</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">38</td>
<td class="int4">164064</td>
<td class="int4">187500</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">39</td>
<td class="int4">187501</td>
<td class="int4">210937</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">40</td>
<td class="int4">210938</td>
<td class="int4">234374</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">41</td>
<td class="int4">234377</td>
<td class="int4">257813</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">42</td>
<td class="int4">257814</td>
<td class="int4">281250</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">43</td>
<td class="int4">281251</td>
<td class="int4">304687</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">44</td>
<td class="int4">304688</td>
<td class="int4">328124</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">45</td>
<td class="int4">328125</td>
<td class="int4">351561</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">46</td>
<td class="int4">351564</td>
<td class="int4">375000</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">47</td>
<td class="int4">375001</td>
<td class="int4">398437</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">48</td>
<td class="int4">398438</td>
<td class="int4">421874</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">49</td>
<td class="int4">421875</td>
<td class="int4">445311</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">50</td>
<td class="int4">445312</td>
<td class="int4">468748</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">51</td>
<td class="int4">468751</td>
<td class="int4">492187</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">52</td>
<td class="int4">492188</td>
<td class="int4">515624</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">53</td>
<td class="int4">515625</td>
<td class="int4">539061</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">54</td>
<td class="int4">539062</td>
<td class="int4">562498</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">55</td>
<td class="int4">562499</td>
<td class="int4">585935</td>
<td class="int4">3</td>
</tr>
<tr class="statusbar">
<td colspan="100">31 rows fetched in 0.0169s (14.7499s)</td>
</tr>
</table>
</div>
<pre>
CTE Scan on q  (cost=3923687.62..4086447.04 rows=8137971 width=16)
  CTE q
    -&gt;  Recursive Union  (cost=0.00..3923687.62 rows=8137971 width=16)
          -&gt;  Index Scan using pk_hierarchy_id on t_hierarchy  (cost=0.00..8.54 rows=1 width=12)
                Index Cond: (id = 1)
          -&gt;  Subquery Scan q2  (cost=363885.01..376091.97 rows=813797 width=16)
                -&gt;  Unique  (cost=363885.01..367954.00 rows=813797 width=20)
                      -&gt;  Sort  (cost=363885.01..365919.50 rows=813797 width=20)
                            Sort Key: (max(hc.rgt) OVER (?)), hc.lft
                            -&gt;  WindowAgg  (cost=265682.87..283993.30 rows=813797 width=20)
                                  -&gt;  Sort  (cost=265682.87..267717.36 rows=813797 width=20)
                                        Sort Key: q.id, hc.lft
                                        -&gt;  Nested Loop  (cost=5335.66..185791.16 rows=813797 width=20)
                                              -&gt;  WorkTable Scan on q  (cost=0.00..0.22 rows=3 width=16)
                                                    Filter: (lvl &lt;= 2)
                                              -&gt;  Bitmap Heap Scan on t_hierarchy hc  (cost=5335.66..57861.32 rows=271266 width=12)
                                                    Recheck Cond: ((hc.lft &gt; q.lft) AND (hc.lft &lt; q.rgt))
                                                    -&gt;  Bitmap Index Scan on ux_hierarchy_lft  (cost=0.00..5267.84 rows=271266 width=0)
                                                          Index Cond: ((hc.lft &gt; q.lft) AND (hc.lft &lt; q.rgt))
</pre>
</div>
<p>This runs for almost <strong>15 seconds</strong>: too much.</p>
<p>This can be improved by exploiting these two properties of the nested sets model:</p>
<ol>
<li>
<p>The first immediate child of a node is the node holding the first <code>lft</code> next to the node&#8217;s <code>lft</code></p>
</li>
<li>
<p>The next sibling of a node is the node holding the first <code>lft</code> next to the node&#8217;s <code>rgt</code></p>
</li>
</ol>
<p>If we recursively traverse through the nodes, we can find the first child as well as all of its siblings. This is enough to build a hierarchy, and level filter can be implemented merely by limiting the recursion depth.</p>
<p>However, recursive <strong>CTE</strong>&#8217;s only allow one recursion level. We cannot nest the <code>WITH</code> clause.</p>
<p>To work around that, we can use <strong>PostgreSQL</strong>&#8217;s ability to run set-returning functions recursively. We will use the function-based recursion to iterate the parent-child axis, and the <strong>CTE</strong>-based recursion to iterate siblings axis.</p>
<p>We need to create a function that would take a node&#8217;s id on input and return a set of its children on output, with the function recursively applied to each of the children. To find a set of children, we will implement a recursive <strong>CTE</strong> that finds the first child in the anchor part and the next sibling in the recursive part.</p>
<p>Here&#8217;s the function:</p>
<pre class="brush: sql">
CREATE OR REPLACE FUNCTION fn_get_children(id INT, level INT)
RETURNS SETOF INT[] AS
$$
        WITH    RECURSIVE q AS
                (
                SELECT  (hc).id, (hc).lft, (hc).rgt, prgt
                FROM    (
                        SELECT  (
                                SELECT  hc
                                FROM    t_hierarchy hc
                                WHERE   hc.lft &gt; hp.lft
                                        AND hc.lft &lt; hp.rgt
                                ORDER BY
                                        hc.lft
                                LIMIT 1
                                ) hc,
                                rgt AS prgt
                        FROM    t_hierarchy hp
                        WHERE   hp.id = $1
                        ) q2
                UNION ALL
                SELECT  (hc).id, (hc).lft, (hc).rgt, prgt
                FROM    (
                        SELECT  (
                                SELECT  hc
                                FROM    t_hierarchy hc
                                WHERE   hc.lft &gt; q.rgt
                                        AND hc.lft &lt; q.prgt
                                ORDER BY
                                        hc.lft
                                LIMIT 1
                                ) hc,
                                prgt
                        FROM    q
                        WHERE   q.lft IS NOT NULL
                        ) q2
                )
        SELECT  CASE which
                WHEN 1 THEN ARRAY[q.id, $2]
                ELSE fn_get_children(q.id, $2 - 1)
                END
        FROM    (
                VALUES (1), (2)
                ) vals(which)
        CROSS JOIN
                q
        WHERE   q.id IS NOT NULL
                AND $2 &gt; 0
        ORDER BY
                id, which;
$$ LANGUAGE sql;
</pre>
<p>The function accepts a node&#8217;s <code>id</code> and a level on input, and returns a set of arrays, each corresponding to one of the node&#8217;s children and its level. The level returned by the function decreases and in fact represents not the level as such, but the number of levels left to reach the filter-set bottom. But since the initial level is user-set, it is easy to cast it to the actual level.</p>
<p>Let&#8217;s run the function:</p>
<pre class="brush: sql">
SELECT  c[1], 3 - c[2]
FROM    fn_get_children(1, 2) c;
</pre>
<div class="terminal">
<table class="terminal">
<tr>
<th>c</th>
<th>?column?</th>
</tr>
<tr>
<td class="int4">6</td>
<td class="int4">1</td>
</tr>
<tr>
<td class="int4">34</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">35</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">33</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">31</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">32</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">7</td>
<td class="int4">1</td>
</tr>
<tr>
<td class="int4">38</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">40</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">39</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">36</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">37</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">8</td>
<td class="int4">1</td>
</tr>
<tr>
<td class="int4">41</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">42</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">43</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">44</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">45</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">9</td>
<td class="int4">1</td>
</tr>
<tr>
<td class="int4">47</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">50</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">46</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">48</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">49</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">10</td>
<td class="int4">1</td>
</tr>
<tr>
<td class="int4">53</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">55</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">51</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">52</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">54</td>
<td class="int4">2</td>
</tr>
<tr class="statusbar">
<td colspan="100">30 rows fetched in 0.0023s (0.0658s)</td>
</tr>
</table>
</div>
<pre>
Function Scan on fn_get_children c  (cost=0.00..262.50 rows=1000 width=32)
</pre>
<p>As we can see, the function returned all children and grandchildren of the node <strong>1</strong> along with their level, and did it in only <strong>65 ms</strong>.</p>
]]></content:encoded>
			<wfw:commentRss>http://explainextended.com/2010/03/02/postgresql-using-recursive-functions-in-nested-sets/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>PostgreSQL: nested sets and R-Tree</title>
		<link>http://explainextended.com/2010/03/01/postgresql-nested-sets-and-r-tree/</link>
		<comments>http://explainextended.com/2010/03/01/postgresql-nested-sets-and-r-tree/#comments</comments>
		<pubDate>Mon, 01 Mar 2010 20:00:04 +0000</pubDate>
		<dc:creator>Quassnoi</dc:creator>
				<category><![CDATA[PostgreSQL]]></category>

		<guid isPermaLink="false">http://explainextended.com/?p=4506</guid>
		<description><![CDATA[A feedback on one of my previous articles comparing adjacency list and nested sets models for PostgreSQL.
Jay writes:

In your series on adjacency lists vs nested sets, you discuss geometric types and R-Tree indexes in MySQL, but you don&#8217;t discuss them when discussing the same subject with PostgreSQL, which also has geometric types and R-Tree indexing [...]]]></description>
			<content:encoded><![CDATA[<p>A feedback on one of my previous articles comparing <a href="/2009/09/24/adjacency-list-vs-nested-sets-postgresql/">adjacency list and nested sets models for <strong>PostgreSQL</strong></a>.</p>
<p><strong>Jay</strong> writes:</p>
<blockquote>
<p>In your series on adjacency lists vs nested sets, you discuss <a href="/2009/09/29/adjacency-list-vs-nested-sets-mysql/">geometric types and <strong>R-Tree</strong> indexes in <strong>MySQL</strong></a>, but you don&#8217;t discuss them when discussing the same subject with <strong>PostgreSQL</strong>, which also has geometric types and <strong>R-Tree</strong> indexing (mostly available through <a href="http://www.postgresql.org/docs/8.4/static/gist-examples.html"><strong>GiST</strong> indexes</a>).</p>
<p>To make it simple I added the following line after the data insertion part of the script at Nested Sets &#8211; Postgresql:</p>
<pre class="brush: sql">
ALTER TABLE t_hierarchy ADD COLUMN sets POLYGON;
UPDATE t_hierarchy SET sets = POLYGON(BOX(POINT(-1,lft), POINT(1, rgt)));
</pre>
<p>It needed to be a <code>POLYGON</code> instead of a <code>BOX</code> since there is a <code>@>(POLYGON,POLYGON)</code> function but no <code>@>(BOX,BOX)</code> function, and the polygon was cast from the box to create the rectangle shape required.</p>
<p>It outperforms the adjacency list on <q>all descendants</q>; outperforms it on <q>all ancestors</q> (not by much); performs reasonably well on <q>all descendants up to a certain level</q> on items with few descendants (e. g. <strong>31415</strong>) and badly on items with many descendants (e. g. <strong>42</strong>).</p>
<p>It still completes in less than <strong>20</strong> seconds though, which is an improvement over <strong>1</strong> minute.</p>
</blockquote>
<p><strong>PostgreSQL</strong> does support <strong>R-Tree</strong> indexes indeed (through <strong>GiST</strong> interface), and they can be used to improve the efficiency of the nested sets model.</p>
<p>Let&#8217;s create a sample table and try some of the queries that <strong>Jay</strong> proposed:<br />
<span id="more-4506"></span><br />
<a href="#" onclick="xcollapse('X9066');return false;"><strong>Table creation details</strong></a><br />
</p>
<div id="X9066" style="display: none; ">
<pre class="brush: sql">
CREATE TABLE t_hierarchy (
        id INT NOT NULL,
        parent INT NOT NULL,
        lft INT NOT NULL,
        rgt INT NOT NULL,
        data VARCHAR(100) NOT NULL,
        stuffing VARCHAR(100) NOT NULL
);

INSERT
INTO    t_hierarchy
WITH RECURSIVE
        ini AS
        (
        SELECT  8 AS level, 5 AS children
        ),
        range AS
        (
        SELECT  level, children,
                (
                SELECT  SUM(POW(children, n)::INTEGER * ((n &lt; level)::INTEGER + 1))
                FROM    generate_series(level, 0, -1) n
                ) width
        FROM    ini
        ),
        q AS
        (
        SELECT  s AS id, 0 AS parent, level, children,
                1 + width * (s - 1) AS lft,
                1 + width * s - 1 AS rgt,
                width / children AS width
        FROM    (
                SELECT  r.*, generate_series(1, children) s
                FROM    range r
                ) q2
        UNION ALL
        SELECT  id * children + position, id, level - 1, children,
                1 + lft + width * (position - 1),
                1 + lft + width * position - 1,
                width / children
        FROM    (
                SELECT  generate_series(1, children) AS position, q.*
                FROM    q
                ) q2
        WHERE   level &gt; 0
        )
SELECT  id, parent, lft, rgt, &#039;Value &#039; || id, RPAD(&#039;&#039;, 100, &#039;*&#039;)
FROM    q;

ALTER TABLE t_hierarchy ADD CONSTRAINT pk_hierarchy_id PRIMARY KEY (id);
CREATE INDEX ix_hierarchy_lft ON t_hierarchy (lft);
CREATE INDEX ix_hierarchy_rgt ON t_hierarchy (rgt);
CREATE INDEX ix_hierarchy_parent ON t_hierarchy (parent);
CREATE INDEX ix_hierarchy_sets ON t_hierarchy USING GIST(POLYGON(BOX(POINT(-1, lft), POINT(1, rgt))));
</pre>
</div>
<p>To make the management of the table easier, I didn&#8217;t create an additional column with the geometric representation of the nested sets, but instead just defined an index on a derived expression, so that updating <code>lft</code> and <code>rgt</code> columns would be enough to update the set.</p>
<p>Now, let&#8217;s see how these queries perform.</p>
<h3>All descendants</h3>
<pre class="brush: sql">
SELECT  SUM(LENGTH(hc.stuffing)), COUNT(*)
FROM    t_hierarchy hp
JOIN    t_hierarchy hc
ON      POLYGON(BOX(POINT(-1, hc.lft), POINT(1, hc.rgt))) &lt;@ POLYGON(BOX(POINT(-1, hp.lft), POINT(1, hp.rgt)))
WHERE   hp.id = 42
</pre>
<p><a href="#" onclick="xcollapse('X3393');return false;"><strong>View query details</strong></a><br />
</p>
<div id="X3393" style="display: none; ">
<div class="terminal">
<table class="terminal">
<tr>
<th>sum</th>
<th>count</th>
</tr>
<tr>
<td class="int8">1953100</td>
<td class="int8">19531</td>
</tr>
<tr class="statusbar">
<td colspan="100">1 row fetched in 0.0003s (0.2139s)</td>
</tr>
</table>
</div>
<pre>
Aggregate  (cost=8253.58..8253.60 rows=1 width=101)
  -&gt;  Nested Loop  (cost=136.32..8241.37 rows=2441 width=101)
        -&gt;  Index Scan using pk_hierarchy_id on t_hierarchy hp  (cost=0.00..8.54 rows=1 width=8)
              Index Cond: (id = 42)
        -&gt;  Bitmap Heap Scan on t_hierarchy hc  (cost=136.32..8129.10 rows=2441 width=109)
              Recheck Cond: (polygon(box(point((-1)::double precision, (hc.lft)::double precision), point(1::double precision, (hc.rgt)::double precision))) &lt;@ polygon(box(point((-1)::double precision, (hp.lft)::double precision), point(1::double precision, (hp.rgt)::double precision))))
              -&gt;  Bitmap Index Scan on ix_hierarchy_sets  (cost=0.00..135.71 rows=2441 width=0)
                    Index Cond: (polygon(box(point((-1)::double precision, (hc.lft)::double precision), point(1::double precision, (hc.rgt)::double precision))) &lt;@ polygon(box(point((-1)::double precision, (hp.lft)::double precision), point(1::double precision, (hp.rgt)::double precision))))
</pre>
</div>
<p>Quite fast, <strong>213 ms</strong>.</p>
<h3>All ancestors</h3>
<pre class="brush: sql">
SELECT  hc.id, hc.lft, hc.rgt, hc.parent
FROM    t_hierarchy hp
JOIN    t_hierarchy hc
ON      POLYGON(BOX(POINT(-1, hc.lft), POINT(1, hc.rgt))) @&gt; POLYGON(BOX(POINT(-1, hp.lft), POINT(1, hp.rgt)))
WHERE   hp.id = 42
</pre>
<p><a href="#" onclick="xcollapse('X4471');return false;"><strong>View query details</strong></a><br />
</p>
<div id="X4471" style="display: none; ">
<div class="terminal">
<table class="terminal">
<tr>
<th>id</th>
<th>lft</th>
<th>rgt</th>
<th>parent</th>
</tr>
<tr>
<td class="int4">1</td>
<td class="int4">1</td>
<td class="int4">585937</td>
<td class="int4">0</td>
</tr>
<tr>
<td class="int4">8</td>
<td class="int4">234376</td>
<td class="int4">351562</td>
<td class="int4">1</td>
</tr>
<tr>
<td class="int4">42</td>
<td class="int4">257814</td>
<td class="int4">281250</td>
<td class="int4">8</td>
</tr>
<tr class="statusbar">
<td colspan="100">3 rows fetched in 0.0007s (0.0127s)</td>
</tr>
</table>
</div>
<pre>
Nested Loop  (cost=136.32..8241.37 rows=2441 width=16)
  -&gt;  Index Scan using pk_hierarchy_id on t_hierarchy hp  (cost=0.00..8.54 rows=1 width=8)
        Index Cond: (id = 42)
  -&gt;  Bitmap Heap Scan on t_hierarchy hc  (cost=136.32..8129.10 rows=2441 width=16)
        Recheck Cond: (polygon(box(point((-1)::double precision, (hc.lft)::double precision), point(1::double precision, (hc.rgt)::double precision))) @&gt; polygon(box(point((-1)::double precision, (hp.lft)::double precision), point(1::double precision, (hp.rgt)::double precision))))
        -&gt;  Bitmap Index Scan on ix_hierarchy_sets  (cost=0.00..135.71 rows=2441 width=0)
              Index Cond: (polygon(box(point((-1)::double precision, (hc.lft)::double precision), point(1::double precision, (hc.rgt)::double precision))) @&gt; polygon(box(point((-1)::double precision, (hp.lft)::double precision), point(1::double precision, (hp.rgt)::double precision))))
</pre>
</div>
<p>Extremely fast: only <strong>10 ms</strong>.</p>
<h3>All descendants up to a certain level</h3>
<pre class="brush: sql">
SELECT  hc.id, hc.lft, hc.rgt, hc.parent
FROM    t_hierarchy hp
JOIN    t_hierarchy hc
ON      POLYGON(BOX(POINT(-1, hc.lft), POINT(1, hc.rgt))) &lt;@ POLYGON(BOX(POINT(-1, hp.lft), POINT(1, hp.rgt)))
WHERE   hp.id = 42
        AND
        (
        SELECT  COUNT(*)
        FROM    t_hierarchy hcp
        WHERE   POLYGON(BOX(POINT(-1, hc.lft), POINT(1, hc.rgt))) &lt;@ POLYGON(BOX(POINT(-1, hcp.lft), POINT(1, hcp.rgt)))
        ) -
        (
        SELECT  COUNT(*)
        FROM    t_hierarchy hpp
        WHERE   POLYGON(BOX(POINT(-1, hp.lft), POINT(1, hp.rgt))) &lt;@ POLYGON(BOX(POINT(-1, hpp.lft), POINT(1, hpp.rgt)))
        ) &lt;= 2
</pre>
<p><a href="#" onclick="xcollapse('X2169');return false;"><strong>View query details</strong></a><br />
</p>
<div id="X2169" style="display: none; ">
<div class="terminal">
<table class="terminal">
<tr>
<th>id</th>
<th>lft</th>
<th>rgt</th>
<th>parent</th>
</tr>
<tr>
<td class="int4">212</td>
<td class="int4">262502</td>
<td class="int4">267188</td>
<td class="int4">42</td>
</tr>
<tr>
<td class="int4">1063</td>
<td class="int4">264377</td>
<td class="int4">265313</td>
<td class="int4">212</td>
</tr>
<tr>
<td class="int4">1059</td>
<td class="int4">260627</td>
<td class="int4">261563</td>
<td class="int4">211</td>
</tr>
<tr>
<td class="int4">211</td>
<td class="int4">257815</td>
<td class="int4">262501</td>
<td class="int4">42</td>
</tr>
<tr>
<td class="int4">1057</td>
<td class="int4">258753</td>
<td class="int4">259689</td>
<td class="int4">211</td>
</tr>
<tr>
<td class="int4">1066</td>
<td class="int4">267190</td>
<td class="int4">268126</td>
<td class="int4">213</td>
</tr>
<tr>
<td class="int4">1067</td>
<td class="int4">268127</td>
<td class="int4">269063</td>
<td class="int4">213</td>
</tr>
<tr>
<td class="int4">42</td>
<td class="int4">257814</td>
<td class="int4">281250</td>
<td class="int4">8</td>
</tr>
<tr>
<td class="int4">213</td>
<td class="int4">267189</td>
<td class="int4">271875</td>
<td class="int4">42</td>
</tr>
<tr>
<td class="int4">214</td>
<td class="int4">271876</td>
<td class="int4">276562</td>
<td class="int4">42</td>
</tr>
<tr>
<td class="int4">1068</td>
<td class="int4">269064</td>
<td class="int4">270000</td>
<td class="int4">213</td>
</tr>
<tr>
<td class="int4">1069</td>
<td class="int4">270001</td>
<td class="int4">270937</td>
<td class="int4">213</td>
</tr>
<tr>
<td class="int4">1070</td>
<td class="int4">270938</td>
<td class="int4">271874</td>
<td class="int4">213</td>
</tr>
<tr>
<td class="int4">1071</td>
<td class="int4">271877</td>
<td class="int4">272813</td>
<td class="int4">214</td>
</tr>
<tr>
<td class="int4">1072</td>
<td class="int4">272814</td>
<td class="int4">273750</td>
<td class="int4">214</td>
</tr>
<tr>
<td class="int4">1073</td>
<td class="int4">273751</td>
<td class="int4">274687</td>
<td class="int4">214</td>
</tr>
<tr>
<td class="int4">1065</td>
<td class="int4">266251</td>
<td class="int4">267187</td>
<td class="int4">212</td>
</tr>
<tr>
<td class="int4">1064</td>
<td class="int4">265314</td>
<td class="int4">266250</td>
<td class="int4">212</td>
</tr>
<tr>
<td class="int4">1062</td>
<td class="int4">263440</td>
<td class="int4">264376</td>
<td class="int4">212</td>
</tr>
<tr>
<td class="int4">1061</td>
<td class="int4">262503</td>
<td class="int4">263439</td>
<td class="int4">212</td>
</tr>
<tr>
<td class="int4">1060</td>
<td class="int4">261564</td>
<td class="int4">262500</td>
<td class="int4">211</td>
</tr>
<tr>
<td class="int4">1058</td>
<td class="int4">259690</td>
<td class="int4">260626</td>
<td class="int4">211</td>
</tr>
<tr>
<td class="int4">1056</td>
<td class="int4">257816</td>
<td class="int4">258752</td>
<td class="int4">211</td>
</tr>
<tr>
<td class="int4">215</td>
<td class="int4">276563</td>
<td class="int4">281249</td>
<td class="int4">42</td>
</tr>
<tr>
<td class="int4">1074</td>
<td class="int4">274688</td>
<td class="int4">275624</td>
<td class="int4">214</td>
</tr>
<tr>
<td class="int4">1075</td>
<td class="int4">275625</td>
<td class="int4">276561</td>
<td class="int4">214</td>
</tr>
<tr>
<td class="int4">1076</td>
<td class="int4">276564</td>
<td class="int4">277500</td>
<td class="int4">215</td>
</tr>
<tr>
<td class="int4">1077</td>
<td class="int4">277501</td>
<td class="int4">278437</td>
<td class="int4">215</td>
</tr>
<tr>
<td class="int4">1078</td>
<td class="int4">278438</td>
<td class="int4">279374</td>
<td class="int4">215</td>
</tr>
<tr>
<td class="int4">1079</td>
<td class="int4">279375</td>
<td class="int4">280311</td>
<td class="int4">215</td>
</tr>
<tr>
<td class="int4">1080</td>
<td class="int4">280312</td>
<td class="int4">281248</td>
<td class="int4">215</td>
</tr>
<tr class="statusbar">
<td colspan="100">31 rows fetched in 0.0039s (20.2523s)</td>
</tr>
</table>
</div>
<pre>
Nested Loop  (cost=0.03..40113216.41 rows=814 width=16)
  Join Filter: (((SubPlan 1) - (SubPlan 2)) &lt;= 2)
  -&gt;  Index Scan using pk_hierarchy_id on t_hierarchy hp  (cost=0.00..8.54 rows=1 width=8)
        Index Cond: (id = 42)
  -&gt;  Index Scan using ix_hierarchy_sets on t_hierarchy hc  (cost=0.03..9692.12 rows=2441 width=16)
        Index Cond: (polygon(box(point((-1)::double precision, (hc.lft)::double precision), point(1::double precision, (hc.rgt)::double precision))) &lt;@ polygon(box(point((-1)::double precision, (hp.lft)::double precision), point(1::double precision, (hp.rgt)::double precision))))
  SubPlan 1
    -&gt;  Aggregate  (cost=8214.53..8214.54 rows=1 width=0)
          -&gt;  Bitmap Heap Scan on t_hierarchy hcp  (cost=136.32..8208.43 rows=2441 width=0)
                Recheck Cond: (polygon(box(point((-1)::double precision, ($0)::double precision), point(1::double precision, ($1)::double precision))) &lt;@ polygon(box(point((-1)::double precision, (lft)::double precision), point(1::double precision, (rgt)::double precision))))
                -&gt;  Bitmap Index Scan on ix_hierarchy_sets  (cost=0.00..135.71 rows=2441 width=0)
                      Index Cond: (polygon(box(point((-1)::double precision, ($0)::double precision), point(1::double precision, ($1)::double precision))) &lt;@ polygon(box(point((-1)::double precision, (lft)::double precision), point(1::double precision, (rgt)::double precision))))
  SubPlan 2
    -&gt;  Aggregate  (cost=8214.53..8214.54 rows=1 width=0)
          -&gt;  Bitmap Heap Scan on t_hierarchy hpp  (cost=136.32..8208.43 rows=2441 width=0)
                Recheck Cond: (polygon(box(point((-1)::double precision, ($2)::double precision), point(1::double precision, ($3)::double precision))) &lt;@ polygon(box(point((-1)::double precision, (lft)::double precision), point(1::double precision, (rgt)::double precision))))
                -&gt;  Bitmap Index Scan on ix_hierarchy_sets  (cost=0.00..135.71 rows=2441 width=0)
                      Index Cond: (polygon(box(point((-1)::double precision, ($2)::double precision), point(1::double precision, ($3)::double precision))) &lt;@ polygon(box(point((-1)::double precision, (lft)::double precision), point(1::double precision, (rgt)::double precision))))
</pre>
</div>
<p>This, exactly as was mentioned by <strong>Jay</strong>, is much faster than using a <strong>B-Tree</strong> index but still too slow: <strong>20</strong> seconds.</p>
<h3>Analysis</h3>
<p>The nested sets model, improved by using the <strong>R-Tree</strong> indexes, provides a way to tell if two records are in the same ancestry chain.</p>
<p>However, even with the <strong>R-Tree</strong>, the model provides no simple means to tell how deep is a record nested.</p>
<p>To check it, an <strong>R-Tree</strong> index scan should be made which would return all of the record&#8217;s ancestors, the the number of the ancestors is to be compared with that of the parent node.</p>
<p>For a record with lots of ancestors (which was the case for the record <strong>42</strong> we used in the test queries), this means that thousands of records should be checked in a nested loop, out of which only a dozen will be returned.</p>
<p>Ironically, for the real-world models, this type of query is most often used, and used against the records with lots of descendants it is.</p>
<p>Usually, when hierarchical data are stored in a database, they are presented to a user in the form of a tree view. When the user opens the catalog, the first-level entries are show; when the user clicks on <q>expand</q> button of an entry, all immediate children of the entry should be shown.</p>
<p>Since users usually start browsing from the beginning, clicking the expand buttons on the first-level or second-level entries is what happens most often. And, unfortunately, it takes the most time to execute these queries.</p>
<p>Adjacency list model provides a constant time solution to this problem, since fetching all immediate children requires a single index scan. This is extremely fast on showing the immediate children.</p>
<p>A user can also click on <q>expand all</q> which should just return all children of the given entry.</p>
<p>However, clicking on <q>expand all</q> on a high-level entry will return too many records, so a time to download them or represent them in the GUI will be much more than that required to fetch them out of the table. A properly written GUI usually limits the level of the records returned so that GUI remains responsive, which, it its turn, implies the same problem of filtering on level.</p>
<p>The low-level entries (for which it makes sense to implement <q>expand all</q> without any limitations) can be queried for their descendants with the <strong>R-Tree</strong> query in the nested sets model or with a recursive query in the adjacency list model almost equally fast, since low-level entries contain few records.</p>
<p>The same applies to selecting all ancestors. Despite the fact that the nested sets model outperforms slightly the adjacency list model on this type of query, the absolute numbers are very small and the times that both queries take are almost imperceptible to the bare eye. A hierarchy is seldom more than a dozen levels deep, and fetching each ancestor even with a recursive query requires but one unique index scan per ancestor.</p>
<p>However, one may still be forced to use the nested tree model. This may be the way an ORM stores its data in the database; a heavily used legacy schema too old and scary to touch; or just some obscure model which mostly requires fetching all descendants fast with an occasional need to filter on the level.</p>
<p>Here are some methods to deal with it.</p>
<h3>Analytic functions</h3>
<p>Though there is no efficient way to filter all descendants on the level, there still is a way to fetch all <em>immediate</em> children of a record.</p>
<p>If we select all records within <code>lft</code> and <code>rgt</code> of a given entry and order them by <code>lft</code>, the first record will be the first immediate child of the entry.</p>
<p>All descendants of the first child will be returned before the second child and have <code>rgt</code> less than that of the first child.</p>
<p>This means that if we record the <code>MAX(rgt)</code> fetched so far, it will be that of the last immediate child of the entry fetched so far:</p>
<table class="terminal">
<tr>
<th>id</th>
<th>lft</th>
<th>rgt</th>
<th>MAX(rgt)</th>
</tr>
<tr>
<td>  2</td>
<td>2</td>
<td>11</td>
<td>11</td>
</tr>
<tr>
<td>    3</td>
<td>3</td>
<td>4</td>
<td>11</td>
</tr>
<tr>
<td>    4</td>
<td>5</td>
<td>8</td>
<td>11</td>
</tr>
<tr>
<td>      5</td>
<td>6</td>
<td>7</td>
<td>11</td>
</tr>
<tr>
<td>    6</td>
<td>9</td>
<td>10</td>
<td>11</td>
</tr>
<tr>
<td>  7</td>
<td>12</td>
<td>15</td>
<td>15</td>
</tr>
<tr>
<td>    8</td>
<td>13</td>
<td>14</td>
<td>15</td>
</tr>
</table>
<p>This means that each value of <code>MAX(rgt)</code> will correspond to exactly one immediate child; and the first entry in the recordset holding the value of <code>MAX(rgt)</code> will be that first child.</p>
<p>Several method exist to <a href="/2009/11/26/postgresql-selecting-records-holding-group-wise-maximum/">select records holding group-wise maximum in PostgreSQL</a>. In this case, is will be best to use <strong>PostgreSQL</strong>&#8217;s <code>DISTINCT ON</code>.</p>
<p>Here&#8217;s the query:</p>
<pre class="brush: sql">
SELECT  DISTINCT ON (MAX(hc.rgt) OVER (ORDER BY hc.lft)) hc.id, hc.lft, hc.rgt
FROM    t_hierarchy hp
JOIN    t_hierarchy hc
ON      hc.lft &gt; hp.lft
        AND hc.lft &lt; hp.rgt
WHERE   hp.id = 42
ORDER BY
        MAX(hc.rgt) OVER (ORDER BY hc.lft), hc.lft
</pre>
<p><a href="#" onclick="xcollapse('X5039');return false;"><strong>View query details</strong></a><br />
</p>
<div id="X5039" style="display: none; ">
<div class="terminal">
<table class="terminal">
<tr>
<th>id</th>
<th>lft</th>
<th>rgt</th>
</tr>
<tr>
<td class="int4">211</td>
<td class="int4">257815</td>
<td class="int4">262501</td>
</tr>
<tr>
<td class="int4">212</td>
<td class="int4">262502</td>
<td class="int4">267188</td>
</tr>
<tr>
<td class="int4">213</td>
<td class="int4">267189</td>
<td class="int4">271875</td>
</tr>
<tr>
<td class="int4">214</td>
<td class="int4">271876</td>
<td class="int4">276562</td>
</tr>
<tr>
<td class="int4">215</td>
<td class="int4">276563</td>
<td class="int4">281249</td>
</tr>
<tr class="statusbar">
<td colspan="100">5 rows fetched in 0.0008s (0.1642s)</td>
</tr>
</table>
</div>
<pre>
Unique  (cost=116073.33..117429.66 rows=271267 width=12)
  -&gt;  Sort  (cost=116073.33..116751.50 rows=271267 width=12)
        Sort Key: (max(hc.rgt) OVER (?)), hc.lft
        -&gt;  WindowAgg  (cost=86845.19..91592.36 rows=271267 width=12)
              -&gt;  Sort  (cost=86845.19..87523.35 rows=271267 width=12)
                    Sort Key: hc.lft
                    -&gt;  Nested Loop  (cost=5761.00..62364.22 rows=271267 width=12)
                          -&gt;  Index Scan using pk_hierarchy_id on t_hierarchy hp  (cost=0.00..8.54 rows=1 width=8)
                                Index Cond: (id = 42)
                          -&gt;  Bitmap Heap Scan on t_hierarchy hc  (cost=5761.00..58286.67 rows=271267 width=12)
                                Recheck Cond: ((hc.lft &gt; hp.lft) AND (hc.lft &lt; hp.rgt))
                                -&gt;  Bitmap Index Scan on ix_hierarchy_lft  (cost=0.00..5693.19 rows=271267 width=0)
                                      Index Cond: ((hc.lft &gt; hp.lft) AND (hc.lft &lt; hp.rgt))
</pre>
</div>
<p>, which is reasonably fast (only <strong>160 ms</strong>).</p>
<p>Using <strong>PostgreSQL 8.4</strong> recursive abilities, this approach can be extended to select the descendants up to any level (provided as a parameter to the query).</p>
<p>Here&#8217;s the query to select all children and grandchildren:</p>
<pre class="brush: sql">
WITH    RECURSIVE
        q AS
        (
        SELECT  id, lft, rgt, 1 AS lvl
        FROM    t_hierarchy
        WHERE   id = 42
        UNION ALL
        SELECT  *
        FROM    (
                SELECT  DISTINCT ON (MAX(hc.rgt) OVER (PARTITION BY q.id ORDER BY hc.lft)) hc.id, hc.lft, hc.rgt, lvl + 1
                FROM    q
                JOIN    t_hierarchy hc
                ON      hc.lft &gt; q.lft
                        AND hc.lft &lt; q.rgt
                WHERE   lvl &lt;= 2
                ORDER BY
                        MAX(hc.rgt) OVER (PARTITION BY q.id ORDER BY hc.lft), hc.lft
                ) q2
        )
SELECT  *
FROM    q
</pre>
<p><a href="#" onclick="xcollapse('X4763');return false;"><strong>View query details</strong></a><br />
</p>
<div id="X4763" style="display: none; ">
<div class="terminal">
<table class="terminal">
<tr>
<th>id</th>
<th>lft</th>
<th>rgt</th>
<th>lvl</th>
</tr>
<tr>
<td class="int4">42</td>
<td class="int4">257814</td>
<td class="int4">281250</td>
<td class="int4">1</td>
</tr>
<tr>
<td class="int4">211</td>
<td class="int4">257815</td>
<td class="int4">262501</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">212</td>
<td class="int4">262502</td>
<td class="int4">267188</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">213</td>
<td class="int4">267189</td>
<td class="int4">271875</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">214</td>
<td class="int4">271876</td>
<td class="int4">276562</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">215</td>
<td class="int4">276563</td>
<td class="int4">281249</td>
<td class="int4">2</td>
</tr>
<tr>
<td class="int4">1056</td>
<td class="int4">257816</td>
<td class="int4">258752</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">1057</td>
<td class="int4">258753</td>
<td class="int4">259689</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">1058</td>
<td class="int4">259690</td>
<td class="int4">260626</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">1059</td>
<td class="int4">260627</td>
<td class="int4">261563</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">1060</td>
<td class="int4">261564</td>
<td class="int4">262500</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">1061</td>
<td class="int4">262503</td>
<td class="int4">263439</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">1062</td>
<td class="int4">263440</td>
<td class="int4">264376</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">1063</td>
<td class="int4">264377</td>
<td class="int4">265313</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">1064</td>
<td class="int4">265314</td>
<td class="int4">266250</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">1065</td>
<td class="int4">266251</td>
<td class="int4">267187</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">1066</td>
<td class="int4">267190</td>
<td class="int4">268126</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">1067</td>
<td class="int4">268127</td>
<td class="int4">269063</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">1068</td>
<td class="int4">269064</td>
<td class="int4">270000</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">1069</td>
<td class="int4">270001</td>
<td class="int4">270937</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">1070</td>
<td class="int4">270938</td>
<td class="int4">271874</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">1071</td>
<td class="int4">271877</td>
<td class="int4">272813</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">1072</td>
<td class="int4">272814</td>
<td class="int4">273750</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">1073</td>
<td class="int4">273751</td>
<td class="int4">274687</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">1074</td>
<td class="int4">274688</td>
<td class="int4">275624</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">1075</td>
<td class="int4">275625</td>
<td class="int4">276561</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">1076</td>
<td class="int4">276564</td>
<td class="int4">277500</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">1077</td>
<td class="int4">277501</td>
<td class="int4">278437</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">1078</td>
<td class="int4">278438</td>
<td class="int4">279374</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">1079</td>
<td class="int4">279375</td>
<td class="int4">280311</td>
<td class="int4">3</td>
</tr>
<tr>
<td class="int4">1080</td>
<td class="int4">280312</td>
<td class="int4">281248</td>
<td class="int4">3</td>
</tr>
<tr class="statusbar">
<td colspan="100">31 rows fetched in 0.0042s (0.4342s)</td>
</tr>
</table>
</div>
<pre>
CTE Scan on q  (cost=3923702.09..4086462.51 rows=8138021 width=16)
  CTE q
    -&gt;  Recursive Union  (cost=0.00..3923702.09 rows=8138021 width=16)
          -&gt;  Index Scan using pk_hierarchy_id on t_hierarchy  (cost=0.00..8.54 rows=1 width=12)
                Index Cond: (id = 42)
          -&gt;  Subquery Scan q2  (cost=363886.28..376093.31 rows=813802 width=16)
                -&gt;  Unique  (cost=363886.28..367955.29 rows=813802 width=20)
                      -&gt;  Sort  (cost=363886.28..365920.79 rows=813802 width=20)
                            Sort Key: (max(hc.rgt) OVER (?)), hc.lft
                            -&gt;  WindowAgg  (cost=265683.50..283994.05 rows=813802 width=20)
                                  -&gt;  Sort  (cost=265683.50..267718.01 rows=813802 width=20)
                                        Sort Key: q.id, hc.lft
                                        -&gt;  Nested Loop  (cost=5335.67..185791.26 rows=813802 width=20)
                                              -&gt;  WorkTable Scan on q  (cost=0.00..0.22 rows=3 width=16)
                                                    Filter: (lvl &lt;= 2)
                                              -&gt;  Bitmap Heap Scan on t_hierarchy hc  (cost=5335.67..57861.34 rows=271267 width=12)
                                                    Recheck Cond: ((hc.lft &gt; q.lft) AND (hc.lft &lt; q.rgt))
                                                    -&gt;  Bitmap Index Scan on ix_hierarchy_lft  (cost=0.00..5267.85 rows=271267 width=0)
                                                          Index Cond: ((hc.lft &gt; q.lft) AND (hc.lft &lt; q.rgt))
</pre>
</div>
<p>This is also reasonably fast, only <strong>432 ms</strong>. It is slower than the same adjacency list query (which completes in several milliseconds), but still is much faster than <strong>R-Tree</strong> and of course the least efficient <strong>B-Tree</strong> solutions involving <code>COUNT(*)</code> and can ease your life if you have to deal with a nested sets model.</p>
<p><strong>To be continued.</strong></p>
]]></content:encoded>
			<wfw:commentRss>http://explainextended.com/2010/03/01/postgresql-nested-sets-and-r-tree/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Six degrees of separation</title>
		<link>http://explainextended.com/2010/02/27/six-degrees-of-separation/</link>
		<comments>http://explainextended.com/2010/02/27/six-degrees-of-separation/#comments</comments>
		<pubDate>Sat, 27 Feb 2010 20:00:45 +0000</pubDate>
		<dc:creator>Quassnoi</dc:creator>
				<category><![CDATA[PostgreSQL]]></category>

		<guid isPermaLink="false">http://explainextended.com/?p=4477</guid>
		<description><![CDATA[Answering questions asked on the site.
Kathy asks:

I am developing a social network site in PostgreSQL and want to find out if two people are no more than 6 friends apart.

If your site grows popular, most probably, they are not. But we better check.
On most social networks, friendship is a symmetric relationship (however, LiveJournal is a [...]]]></description>
			<content:encoded><![CDATA[<p>Answering questions asked on the site.</p>
<p><strong>Kathy</strong> asks:</p>
<blockquote>
<p>I am developing a social network site in <strong>PostgreSQL</strong> and want to find out if two people are no more than <strong>6</strong> friends apart.</p>
</blockquote>
<p>If your site grows popular, most probably, <a href="http://en.wikipedia.org/wiki/Six_degrees_of_separation">they are not</a>. But we better check.</p>
<p>On most social networks, friendship is a symmetric relationship (however, <a href="http://livejournal.com">LiveJournal</a> is a notable exception). This means that if Alice is a friend to Bob, then Bob is a friend to Alice as well.</p>
<p>The friendship relationship is best stored in a many-to-many link table with a <code>PRIMARY KEY</code> on both link fields and an additional check condition: the friend with the least id should be stored in the first column. This is to avoid storing a relationship twice: a <code>PRIMARY KEY</code> won&#8217;t be violated if the same record with the columns swapped will be inserted, but the check constraint will. The check constraint will also forbid storing a friend relationship to itself.</p>
<p>Let&#8217;s create a sample table:<br />
<span id="more-4477"></span><br />
<a href="#" onclick="xcollapse('X5926');return false;"><strong>Table creation details</strong></a><br />
</p>
<div id="X5926" style="display: none; ">
<pre class="brush: sql">
CREATE TABLE friends (
        orestes INT NOT NULL,
        pylades INT NOT NULL,
        CHECK (orestes &lt; pylades)
);

SELECT  SETSEED(0.20100227);

INSERT
INTO    friends
SELECT  o, p
FROM    (
        SELECT  o, SUM(FLOOR(RANDOM() * 100000) + 1) OVER (PARTITION BY o ORDER BY n) AS p
        FROM    (
                SELECT  o, generate_series(1, 20) n
                FROM    generate_series(1, 1000000) o
                ) q
        ) q2
WHERE   o &lt; p
        AND p &lt;= 1000000;

ALTER TABLE friends ADD CONSTRAINT pk_friends_op PRIMARY KEY (orestes, pylades);

CREATE UNIQUE INDEX ux_friends_po ON friends (pylades, orestes);
</pre>
</div>
<p>This table stores records for <strong>1,000,000</strong> people having <strong>20</strong> friends each in average. The first column is named <code>orestes</code> and the second one <code>pylades</code>.</p>
<p>With new <strong>PostgreSQL 8.4</strong> it is easy to write a recursive query that would traverse the relationship graph up to the given level and stop on the first match.</p>
<p>However, each recursion step requires a join, and as the number of records in the input recordset for the recusion grows with level, the joins become less and less efficient. The number of records grows exponentially, and on level <strong>6</strong> there will be about <strong>20 ^ 6 = 64,000,000</strong> records on input. This is just too much for a join with a <strong>20,000,000</strong> records table.</p>
<p>As the chain length increases, the tree diverges, the number of the records grows and it becomes more and more costly to join them with the table.</p>
<p>To work around this, we should use Bogdan the tunnel builder&#8217;s algorithm.</p>
<blockquote><p>British and French governments submit a tender to build a tunnel under the Channel. Many companies apply, all demanding years of time and billions pounds of money, so their offers are refused.</p>
<p>One day, Bogdan drops in and offers his services.</p>
<p><q>How much money would you demand for your work?</q>, the official asks. <q>Me and my brother Roman are good eaters, so we will need to buy a decent meal every day. 50 pounds a day will be OK.</q></p>
<p><q>That&#8217;s pretty cheap; and how much time will you need?</q> <q>Me and my brother Roman are fast diggers; one mile a day I think we will dig.</q></p>
<p><q>Oh, that&#8217;s pretty fast! But how will you be able to work on such a low budget in such a short time?</q>, the official asks out of curiosity.</p>
<p><q>That&#8217;s simple</q>, Bogdan answers, <q>I start digging from the British coast, my brother Roman starts digging from the French coast; the moment we meet, the work is over</q>.</p>
<p><q>OK</q>, says the official, <q>but what if you don&#8217;t meet?</q>.</p>
<p><q>No worries then: you get two tunnels for the price of one</q>
</p></blockquote>
<p>We should do a similar thing here. Instead of traversing the <strong>6</strong> levels from the beginning, we will traverse just <strong>3</strong> levels from each side, then join the resulting recordsets and hope some matching records will be found.</p>
<p>Traversing only <strong>3</strong> levels will be quite fast; and the resulting recordsets will be of moderate size so joining them will be easy.</p>
<p>As an extra, we will return the shortest path from one person to the other. To do this, we will need to record the friendship chain in an array. <strong>PostgreSQL</strong> does not offer an easy way to reverse an array, so in the first recordset, we will <em>append</em> the friends to the array, while in the second one we will <em>prepend</em> them. This way, we should just concatenate the resulting <del>tunnels</del>arrays.</p>
<p>Here&#8217;s the query:</p>
<pre class="brush: sql">
WITH    RECURSIVE
        q1 (person, chain, lvl) AS
        (
        SELECT  123456, ARRAY[123456], 1
        UNION ALL
        SELECT  friend, chain || friend, lvl + 1
        FROM    (
                SELECT  q1.*,
                        friend
                FROM    q1
                JOIN    (
                        SELECT  orestes AS me, pylades AS friend
                        FROM    friends
                        UNION ALL
                        SELECT  pylades AS me, orestes AS friend
                        FROM    friends
                        ) f
                ON      person = me
                WHERE   lvl &lt;= 3
                ) qo
        ),
        q2 (person, chain, lvl) AS
        (
        SELECT  654321, ARRAY[654321], 1
        UNION ALL
        SELECT  friend, friend || chain, lvl + 1
        FROM    (
                SELECT  q2.*,
                        friend
                FROM    q2
                JOIN    (
                        SELECT  orestes AS me, pylades AS friend
                        FROM    friends
                        UNION ALL
                        SELECT  pylades AS me, orestes AS friend
                        FROM    friends
                        ) f
                ON      person = me
                WHERE   lvl &lt;= 3
                ) qo
        )
SELECT  (q1.chain || q2.chain[2:q2.lvl])::TEXT AS chain
FROM    q1
JOIN    q2
ON      q2.person = q1.person
ORDER BY
        q1.lvl + q2.lvl
LIMIT 1
</pre>
<p><a href="#" onclick="xcollapse('X5520');return false;"><strong>View query details</strong></a><br />
</p>
<div id="X5520" style="display: none; ">
<div class="terminal">
<table class="terminal">
<tr>
<th>chain</th>
</tr>
<tr>
<td class="text">{123456,890237,278175,654321}</td>
</tr>
<tr class="statusbar">
<td colspan="100">1 row fetched in 0.0003s (0.5313s)</td>
</tr>
</table>
</div>
<pre>
Limit  (cost=1138629880.05..1138629880.05 rows=1 width=72)
  CTE q1
    -&gt;  Recursive Union  (cost=0.00..71529.77 rows=2753901 width=40)
          -&gt;  Result  (cost=0.00..0.01 rows=1 width=0)
          -&gt;  Nested Loop  (cost=0.00..1645.17 rows=275390 width=40)
                Join Filter: (q1.person = &quot;20100227_friends&quot;.friends.orestes)
                -&gt;  WorkTable Scan on q1  (cost=0.00..0.22 rows=3 width=40)
                      Filter: (lvl &lt;= 3)
                -&gt;  Append  (cost=0.00..88.96 rows=30 width=8)
                      -&gt;  Index Scan using pk_friends_op on friends  (cost=0.00..37.55 rows=17 width=8)
                            Index Cond: (&quot;20100227_friends&quot;.friends.orestes = q1.person)
                      -&gt;  Index Scan using ux_friends_po on friends  (cost=0.00..51.40 rows=13 width=8)
                            Index Cond: (&quot;20100227_friends&quot;.friends.pylades = q1.person)
  CTE q2
    -&gt;  Recursive Union  (cost=0.00..71529.77 rows=2753901 width=40)
          -&gt;  Result  (cost=0.00..0.01 rows=1 width=0)
          -&gt;  Nested Loop  (cost=0.00..1645.17 rows=275390 width=40)
                Join Filter: (q2.person = &quot;20100227_friends&quot;.friends.orestes)
                -&gt;  WorkTable Scan on q2  (cost=0.00..0.22 rows=3 width=40)
                      Filter: (lvl &lt;= 3)
                -&gt;  Append  (cost=0.00..88.96 rows=30 width=8)
                      -&gt;  Index Scan using pk_friends_op on friends  (cost=0.00..37.55 rows=17 width=8)
                            Index Cond: (&quot;20100227_friends&quot;.friends.orestes = q2.person)
                      -&gt;  Index Scan using ux_friends_po on friends  (cost=0.00..51.40 rows=13 width=8)
                            Index Cond: (&quot;20100227_friends&quot;.friends.pylades = q2.person)
  -&gt;  Sort  (cost=1138486820.51..1233286454.49 rows=37919853589 width=72)
        Sort Key: ((q1.lvl + q2.lvl))
        -&gt;  Merge Join  (cost=849904.33..948887552.57 rows=37919853589 width=72)
              Merge Cond: (q1.person = q2.person)
              -&gt;  Sort  (cost=424952.16..431836.92 rows=2753901 width=40)
                    Sort Key: q1.person
                    -&gt;  CTE Scan on q1  (cost=0.00..55078.02 rows=2753901 width=40)
              -&gt;  Materialize  (cost=424952.16..459375.93 rows=2753901 width=40)
                    -&gt;  Sort  (cost=424952.16..431836.92 rows=2753901 width=40)
                          Sort Key: q2.person
                          -&gt;  CTE Scan on q2  (cost=0.00..55078.02 rows=2753901 width=40)
</pre>
</div>
<p>Note that the anchor part can not be used more than once in a recursive expression. To work around that, we had to join it to a derived table (a <code>UNION ALL</code> of two copies of the table with the columns swapped). However, <strong>PostgreSQL</strong>&#8217;s optimizer was smart enough to push the join predicate into the derived table and distribute the queries so that each part uses a corresponding index efficiently. This helps to traverse the tree and build the recordsets from both ends.</p>
<p>Each of the recordsets has only about <strong>8,000</strong> records, so scanning and joining them is very fast.</p>
<p>The whole query takes just a little longer than <strong>0.5</strong> seconds.</p>
<p>Hope that helps.</p>
<hr/>
<p>I&#8217;m always glad to answer the questions regarding database queries.</p>
<p><a href="/ask-a-question"><strong>Ask me a question</strong></a></p>
]]></content:encoded>
			<wfw:commentRss>http://explainextended.com/2010/02/27/six-degrees-of-separation/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Efficient circle distance testing</title>
		<link>http://explainextended.com/2010/02/26/efficient-circle-distance-testing/</link>
		<comments>http://explainextended.com/2010/02/26/efficient-circle-distance-testing/#comments</comments>
		<pubDate>Fri, 26 Feb 2010 20:00:41 +0000</pubDate>
		<dc:creator>Quassnoi</dc:creator>
				<category><![CDATA[SQL Server]]></category>

		<guid isPermaLink="false">http://explainextended.com/?p=4449</guid>
		<description><![CDATA[Answering questions asked on the site.
eptil asks:
I am using SQL Server 2008, but not the spatial features.
I have a table with few entries, only 40,000. There is an id INT PRIMARY KEY column and two columns storing a 2d coordinate, both decimals.
I would like to find all the records that do not have other records [...]]]></description>
			<content:encoded><![CDATA[<p>Answering questions asked on the site.</p>
<p><strong>eptil</strong> asks:</p>
<blockquote><p>I am using <strong>SQL Server 2008</strong>, but not the spatial features.</p>
<p>I have a table with few entries, only <strong>40,000</strong>. There is an <code>id INT PRIMARY KEY</code> column and two columns storing a 2d coordinate, both decimals.</p>
<p>I would like to find all the records that do not have other records within a given radius. The query I am using at the moment is:</p>
<pre class="brush: sql">
SELECT  id, x, y
FROM    mytable t1
WHERE   (
        SELECT  COUNT(*)
        FROM    mytable t2
        WHERE   ABS(t1.x - t2.x) &lt; 25
                AND ABS(t1.y - t2.y) &lt; 25
        ) = 1
</pre>
<p><!-- --><br />
This is taking <strong>15</strong> minutes to run at times.</p>
<p>Is there a better way?
</p></blockquote>
<p>Of course using spatial abilities would be a better way, but it is possible to make do with plain <strong>SQL</strong>. This will also work in <strong>SQL Server 2005</strong>.</p>
<p>In most database engines, the spatial indexes are implemented as the <strong>R-Tree</strong> structures. <strong>SQL Server</strong>, however, uses another approach: surface tesselation.</p>
<p>Basically, it divides the surface into a finite number of tiles, each assigned with a unique number. The identifiers of tiles covered by the object are stored as keys of a plain <strong>B-Tree</strong> index.</p>
<p>When <strong>SQL Server</strong>&#8217;s optimizer sees a geometrical predicate against an indexed column, it calculates the numbers of tiles that <em>possibly</em> can satisfy this predicate. Say, if the tiles are defined as squares with side <strong>1</strong>, the predicate <code>column.STDistance(@mypoint) &lt; 2</code> can only be satisfied by the objects within <strong>2</strong> tiles away from <code>@mypoint</code>&#8217;s tile. This gives a square of <strong>25</strong> tiles with <code>@mypoint</code>&#8217;s tile in the center. The tile numbers can be found and searched for using the index. Exact filtering condition is then applied to each candidate value returned by the index.</p>
<p>Same solution can be used in our case even without the spatial functions. Comparing tile numbers is an equijoin and hash join method is eligible for this operation. We can even choose the tiling algorithm individually for each query, since we don&#8217;t have to store the tile identifiers in the table, and the hash table will be built dynamically anyway.</p>
<p>Let&#8217;s create a sample table and see how it works:<br />
<span id="more-4449"></span></p>
<pre class="brush: sql">
CREATE SCHEMA [20100226_circle]
CREATE TABLE t_circle (
        id INT NOT NULL PRIMARY KEY,
        x DECIMAL (15, 3) NOT NULL,
        y DECIMAL (15, 3) NOT NULL,
        )
GO
BEGIN TRANSACTION
SELECT  RAND(20100226)
DECLARE @cnt INT
SET @cnt = 1
WHILE @cnt &lt;= 50000
BEGIN
        INSERT
        INTO    [20100226_circle].t_circle (id, x, y)
        VALUES  (
                @cnt,
                RAND() * 3200,
                RAND() * 3200
                )
        SET @cnt = @cnt + 1
END
COMMIT
GO
</pre>
<p>The table contains <strong>50,000</strong> points on random places within a square of <strong>3,200 &times; 3,200</strong> units.</p>
<p>We can optimize the original query a little by using <code>NOT EXISTS</code> instead of <code>COUNT(*)</code>:</p>
<pre class="brush: sql">
SELECT  *
FROM    [20100226_circle].t_circle co
WHERE   NOT EXISTS
        (
        SELECT  NULL
        FROM    [20100226_circle].t_circle ci
        WHERE   SQRT(POWER(co.x - ci.x, 2) + POWER(co.y - ci.y, 2)) &lt; 25
                AND co.id &lt;&gt; ci.id
        )
ORDER BY
        id
</pre>
<p><a href="#" onclick="xcollapse('X6526');return false;"><strong>View query results</strong></a><br />
</p>
<div id="X6526" style="display: none; ">
<div class="terminal">
<table class="terminal">
<tr>
<th>id</th>
<th>x</th>
<th>y</th>
</tr>
<tr>
<td class="int">205</td>
<td class="decimal">2247.896</td>
<td class="decimal">3198.399</td>
</tr>
<tr>
<td class="int">2867</td>
<td class="decimal">2159.626</td>
<td class="decimal">1120.590</td>
</tr>
<tr>
<td class="int">13644</td>
<td class="decimal">4.951</td>
<td class="decimal">3165.734</td>
</tr>
<tr>
<td class="int">15917</td>
<td class="decimal">2747.826</td>
<td class="decimal">3041.280</td>
</tr>
<tr>
<td class="int">25183</td>
<td class="decimal">1858.866</td>
<td class="decimal">326.416</td>
</tr>
<tr>
<td class="int">43211</td>
<td class="decimal">1176.369</td>
<td class="decimal">98.079</td>
</tr>
<tr class="statusbar">
<td colspan="100">6 rows fetched in 0.0000s (354.2165s)</td>
</tr>
</table>
</div>
<pre>
Table 'Worktable'. Scan count 2, logical reads 1555993, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 't_circle'. Scan count 5, logical reads 1405, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 

SQL Server Execution Times:
   CPU time = 680766 ms,  elapsed time = 354208 ms.
</pre>
<pre>
  |--Parallelism(Gather Streams, ORDER BY:([co].[id] ASC))
       |--Nested Loops(Left Anti Semi Join, WHERE:(sqrt(CONVERT_IMPLICIT(float(53),power([test].[20100226_circle].[t_circle].[x] as [co].[x]-[test].[20100226_circle].[t_circle].[x] as [ci].[x],(2.000000000000000e+000))+power([test].[20100226_circle].[t_circle].[y] as [co].[y]-[test].[20100226_circle].[t_circle].[y] as [ci].[y],(2.000000000000000e+000)),0))&lt;(2.500000000000000e+001) AND [test].[20100226_circle].[t_circle].[id] as [co].[id]&lt;&gt;[test].[20100226_circle].[t_circle].[id] as [ci].[id]))
            |--Clustered Index Scan(OBJECT:([test].[20100226_circle].[t_circle].[PK__t_circle__62065FF3] AS [co]), ORDERED FORWARD)
            |--Table Spool
                 |--Clustered Index Scan(OBJECT:([test].[20100226_circle].[t_circle].[PK__t_circle__62065FF3] AS [ci]))
</pre>
</div>
<p>, but this would still be quite slow.</p>
<p>The problem is that the nested loops go nowhere: they are still inside the plan, but return earlier. As a result, the query takes <strong>5</strong> minutes instead of <strong>15</strong>, which is still too much.</p>
<p>To improve the query we need to make an efficient anti-join method to work, and the tesselation strategy is a way to go. Here&#8217;s what we need to do to implement this strategy:</p>
<ul>
<li>
<p>Tesselate the surface and assign a unique number to each tile. Since we need to search for the records within <strong>25</strong> units, it will be a reasonable idea to divide the surface into a number of squares <strong>25 &times; 25</strong> units in size, numbered column-wise. To find out the number of rows and columns, we should just find the <code>MIN</code> and <code>MAX</code> <code>x</code> and <code>y</code>.</p>
<p><img src="http://explainextended.com/wp-content/uploads/2010/02/tesselation.png" alt="" title="Tesselation" width="600" height="600" class="aligncenter size-full wp-image-4451 noborder" /></p>
<p>This is the sample tesselation, assuming that <code>MIN(x)</code> and <code>MIN(y)</code> are <strong>25 &times; 100 = 2500</strong> units apart (and same with <code>y</code>).</p>
</li>
<li>
<p>Find the tile each point belongs to.</p>
</li>
<li>
<p>Build a recordset that would correspond each point to each of the tiles the neighbors can <em>theoretically</em> reside in. Since a circle with radius of <strong>25</strong> units theoretically may cover up to <strong>9</strong> adjacent tiles, each tile should be corresponded to each of the <strong>9</strong> tiles forming a <strong>3 &times; 3</strong> square with a unit&#8217;s tile in the center.</p>
<p><img src="http://explainextended.com/wp-content/uploads/2010/02/coverage.png" alt="" title="Coverage" width="600" height="600" class="aligncenter size-full wp-image-4457 noborder" /></p>
</li>
<li>
<p>For each point, make sure that no other points exists within the neighboring tiles, additionally applying a fine-filtering condition.</p>
<p>This may sound redundant, since the point-neighbor combination is unique, as well as point-tile, so only one of the <strong>9</strong> candidates will satisfy the join, even if the points reside in the adjacent tiles. But finding the exact distance requires a complex expression while comparing the tile numbers is an equality correlation and as such is eligible by an efficient anti-join method like <code>HASH ANTI JOIN</code>. The coarse filtering on tiles will sieve out most of the far neighbors so that the only adjacent neighbors will require special attention.</p>
</li>
</ul>
<p>And here&#8217;s the query:</p>
<pre class="brush: sql">
WITH    extremes AS
        (
        SELECT  *,
                maxx - minx AS width,
                maxy - miny AS height
        FROM    (
                SELECT  FLOOR(MIN(x) / 25) AS minx,
                        CEILING(MAX(x) / 25) AS maxx,
                        FLOOR(MIN(y) / 25) AS miny,
                        CEILING(MAX(y) / 25) AS maxy
                FROM    [20100226_circle].t_circle
                ) q
        ),
        tileset (dim) AS
        (
        SELECT  -1
        UNION ALL
        SELECT  0
        UNION ALL
        SELECT  1
        ),
        tiles AS
        (
        SELECT  id, x, y, minx, miny, width,
                (FLOOR(x / 25) - minx) * width + FLOOR(y / 25) - miny AS tile
        FROM    extremes
        CROSS JOIN
                [20100226_circle].t_circle
        ),
        neighbors AS
        (
        SELECT  ti.*,
                (FLOOR(ti.x / 25) - ti.minx + nx.dim) * ti.width +
                FLOOR(ti.y / 25) - ti.miny + ny.dim AS mtile
        FROM    tiles ti
        CROSS JOIN
                tileset nx
        CROSS JOIN
                tileset ny
        )
SELECT  *
FROM    tiles tn
WHERE   NOT EXISTS
        (
        SELECT  NULL
        FROM    neighbors n
        WHERE   n.mtile = tn.tile
                AND n.id &lt;&gt; tn.id
                AND SQRT(SQUARE(n.x - tn.x) + SQUARE(n.y - tn.y)) &lt; 25
        )
ORDER BY
        id
</pre>
<div class="terminal">
<table class="terminal">
<tr>
<th>id</th>
<th>x</th>
<th>y</th>
<th>minx</th>
<th>miny</th>
<th>width</th>
<th>tile</th>
</tr>
<tr>
<td class="int">205</td>
<td class="decimal">2247.896</td>
<td class="decimal">3198.399</td>
<td class="decimal">0</td>
<td class="decimal">0</td>
<td class="decimal">128</td>
<td class="decimal">11519</td>
</tr>
<tr>
<td class="int">2867</td>
<td class="decimal">2159.626</td>
<td class="decimal">1120.590</td>
<td class="decimal">0</td>
<td class="decimal">0</td>
<td class="decimal">128</td>
<td class="decimal">11052</td>
</tr>
<tr>
<td class="int">13644</td>
<td class="decimal">4.951</td>
<td class="decimal">3165.734</td>
<td class="decimal">0</td>
<td class="decimal">0</td>
<td class="decimal">128</td>
<td class="decimal">126</td>
</tr>
<tr>
<td class="int">15917</td>
<td class="decimal">2747.826</td>
<td class="decimal">3041.280</td>
<td class="decimal">0</td>
<td class="decimal">0</td>
<td class="decimal">128</td>
<td class="decimal">14073</td>
</tr>
<tr>
<td class="int">25183</td>
<td class="decimal">1858.866</td>
<td class="decimal">326.416</td>
<td class="decimal">0</td>
<td class="decimal">0</td>
<td class="decimal">128</td>
<td class="decimal">9485</td>
</tr>
<tr>
<td class="int">43211</td>
<td class="decimal">1176.369</td>
<td class="decimal">98.079</td>
<td class="decimal">0</td>
<td class="decimal">0</td>
<td class="decimal">128</td>
<td class="decimal">6019</td>
</tr>
<tr class="statusbar">
<td colspan="100">6 rows fetched in 0.0017s (3.0781s)</td>
</tr>
</table>
</div>
<pre>
Table 't_circle'. Scan count 11, logical reads 3087, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 2, logical reads 207406, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Worktable'. Scan count 0, logical reads 0, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0. 

SQL Server Execution Times:
   CPU time = 4235 ms,  elapsed time = 3068 ms.
</pre>
<pre>
  |--Parallelism(Gather Streams, ORDER BY:([test].[20100226_circle].[t_circle].[id] ASC))
       |--Sort(ORDER BY:([test].[20100226_circle].[t_circle].[id] ASC))
            |--Compute Scalar(DEFINE:([Expr1007]=floor([Expr1003]/(25.)), [Expr1009]=floor([Expr1005]/(25.)), [Expr1011]=ceiling([Expr1004]/(25.))-floor([Expr1003]/(25.)), [Expr1016]=(([Expr1044]-floor([Expr1003]/(25.)))*(ceiling([Expr1004]/(25.))-floor([Expr1003]/(25.)))+[Expr1045])-floor([Expr1005]/(25.))))
                 |--Hash Match(Left Anti Semi Join, HASH:([Expr1055])=([Expr1054]), RESIDUAL:([Expr1054]=[Expr1055] AND [test].[20100226_circle].[t_circle].[id]&lt;&gt;[test].[20100226_circle].[t_circle].[id] AND sqrt(square(CONVERT_IMPLICIT(float(53),[test].[20100226_circle].[t_circle].[x]-[test].[20100226_circle].[t_circle].[x],0))+square(CONVERT_IMPLICIT(float(53),[test].[20100226_circle].[t_circle].[y]-[test].[20100226_circle].[t_circle].[y],0)))&lt;(2.500000000000000e+001)))
                      |--Bitmap(HASH:([Expr1055]), DEFINE:([Bitmap1056]))
                      |    |--Parallelism(Repartition Streams, Hash Partitioning, PARTITION COLUMNS:([Expr1055]))
                      |         |--Compute Scalar(DEFINE:([Expr1055]=(([Expr1044]-floor([Expr1003]/(25.)))*(ceiling([Expr1004]/(25.))-floor([Expr1003]/(25.)))+[Expr1045])-floor([Expr1005]/(25.))))
                      |              |--Nested Loops(Inner Join)
                      |                   |--Parallelism(Distribute Streams, Broadcast Partitioning)
                      |                   |    |--Stream Aggregate(DEFINE:([Expr1003]=MIN([partialagg1048]), [Expr1004]=MAX([partialagg1049]), [Expr1005]=MIN([partialagg1050])))
                      |                   |         |--Parallelism(Gather Streams)
                      |                   |              |--Stream Aggregate(DEFINE:([partialagg1048]=MIN([test].[20100226_circle].[t_circle].[x]), [partialagg1049]=MAX([test].[20100226_circle].[t_circle].[x]), [partialagg1050]=MIN([test].[20100226_circle].[t_circle].[y])))
                      |                   |                   |--Clustered Index Scan(OBJECT:([test].[20100226_circle].[t_circle].[PK__t_circle__62065FF3]))
                      |                   |--Compute Scalar(DEFINE:([Expr1044]=floor([test].[20100226_circle].[t_circle].[x]/(25.)), [Expr1045]=floor([test].[20100226_circle].[t_circle].[y]/(25.))))
                      |                        |--Clustered Index Scan(OBJECT:([test].[20100226_circle].[t_circle].[PK__t_circle__62065FF3]))
                      |--Parallelism(Repartition Streams, Hash Partitioning, PARTITION COLUMNS:([Expr1054]), WHERE:(PROBE([Bitmap1056])=TRUE))
                           |--Compute Scalar(DEFINE:([Expr1054]=(((([Expr1046]-floor([Expr1020]/(25.)))+CONVERT_IMPLICIT(decimal(10,0),[Union1037],0))*(ceiling([Expr1021]/(25.))-floor([Expr1020]/(25.)))+[Expr1047])-floor([Expr1022]/(25.)))+CONVERT_IMPLICIT(decimal(10,0),[Union1041],0)))
                                |--Nested Loops(Inner Join)
                                     |--Nested Loops(Inner Join)
                                     |    |--Parallelism(Distribute Streams, RoundRobin Partitioning)
                                     |    |    |--Nested Loops(Inner Join)
                                     |    |         |--Stream Aggregate(DEFINE:([Expr1020]=MIN([partialagg1051]), [Expr1021]=MAX([partialagg1052]), [Expr1022]=MIN([partialagg1053])))
                                     |    |         |    |--Parallelism(Gather Streams)
                                     |    |         |         |--Stream Aggregate(DEFINE:([partialagg1051]=MIN([test].[20100226_circle].[t_circle].[x]), [partialagg1052]=MAX([test].[20100226_circle].[t_circle].[x]), [partialagg1053]=MIN([test].[20100226_circle].[t_circle].[y])))
                                     |    |         |              |--Clustered Index Scan(OBJECT:([test].[20100226_circle].[t_circle].[PK__t_circle__62065FF3]))
                                     |    |         |--Constant Scan(VALUES:(((-1)),((0)),((1))))
                                     |    |--Constant Scan(VALUES:(((-1)),((0)),((1))))
                                     |--Table Spool
                                          |--Compute Scalar(DEFINE:([Expr1046]=floor([test].[20100226_circle].[t_circle].[x]/(25.)), [Expr1047]=floor([test].[20100226_circle].[t_circle].[y]/(25.))))
                                               |--Clustered Index Scan(OBJECT:([test].[20100226_circle].[t_circle].[PK__t_circle__62065FF3]))
</pre>
<p>This query only takes <strong>3 seconds</strong>.</p>
<p>Hope that helps.</p>
<hr/>
<p>I&#8217;m always glad to answer the questions regarding database queries.</p>
<p><a href="/ask-a-question"><strong>Ask me a question</strong></a></p>
]]></content:encoded>
			<wfw:commentRss>http://explainextended.com/2010/02/26/efficient-circle-distance-testing/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Matching sets: aggregates vs. first miss</title>
		<link>http://explainextended.com/2010/02/25/matching-sets-aggregates-vs-first-miss/</link>
		<comments>http://explainextended.com/2010/02/25/matching-sets-aggregates-vs-first-miss/#comments</comments>
		<pubDate>Thu, 25 Feb 2010 20:00:47 +0000</pubDate>
		<dc:creator>Quassnoi</dc:creator>
				<category><![CDATA[MySQL]]></category>

		<guid isPermaLink="false">http://explainextended.com/?p=4436</guid>
		<description><![CDATA[From Stack Overflow:

Here is my schema:

Suppliers

sid
sname
address 



Parts

pid
pname
color



Catalog

sid
pid
cost


I need to find the sids of suppliers who supply every red part or every green part.

This task requires matching the sets.
We need to compare two sets here: the first one is the set of the parts of given color; the second one is the set of parts provided [...]]]></description>
			<content:encoded><![CDATA[<p>From <a href="http://stackoverflow.com/questions/2328457/mysql-how-can-i-condense-this-verbose-query"><strong>Stack Overflow</strong></a>:</p>
<blockquote><p>
Here is my schema:</p>
<table class="excel">
<caption>Suppliers</caption>
<tr>
<th>sid</th>
<th>sname</th>
<th>address </th>
</tr>
</table>
<table class="excel">
<caption>Parts</caption>
<tr>
<th>pid</th>
<th>pname</th>
<th>color</th>
</tr>
</table>
<table class="excel">
<caption>Catalog</caption>
<tr>
<th>sid</th>
<th>pid</th>
<th>cost</th>
</tr>
</table>
<p>I need to find the sids of suppliers who supply every <strong>red</strong> part or every <strong>green</strong> part.
</p></blockquote>
<p>This task requires matching the sets.</p>
<p>We need to compare two sets here: the first one is the set of the parts of given color; the second one is the set of parts provided by a given supplier. The former should be the subset of the latter.</p>
<p>Unlike other engines, <strong>MySQL</strong> does not provide the set operators like <code>EXCEPT</code> or <code>MINUS</code> that allow to check the subset / superset relationship very easily. We have to use the record-based solutions.</p>
<p>There are two ways to check that:</p>
<ul>
<li><q>First miss</q> technique: test each record from the subset candidate against the superset candidate, returning <code>FALSE</code> if there is no match.</li>
<li><q>Aggregate</q> technique: compare the number of records in the subset candidate to the number of records in their intersection. If the numbers are equal, the sets match</li>
</ul>
<p>Let&#8217;s test which way is faster in which cases. To do this, we will need some sample tables:<br />
<span id="more-4436"></span><br />
<a href="#" onclick="xcollapse('X6352');return false;"><strong>Table creation details</strong></a><br />
</p>
<div id="X6352" style="display: none; ">
<pre class="brush: sql">
CREATE TABLE filler (
        id INT NOT NULL PRIMARY KEY AUTO_INCREMENT
) ENGINE=Memory;

CREATE TABLE suppliers (
        sid INT NOT NULL PRIMARY KEY,
        sname VARCHAR(100) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=UTF8;

CREATE TABLE parts (
        pid INT NOT NULL PRIMARY KEY,
        pname VARCHAR(100) NOT NULL,
        color VARCHAR(100) NOT NULL,
        KEY ix_parts_color (color)
) ENGINE=InnoDB DEFAULT CHARSET=UTF8;

CREATE TABLE catalog (
        sid INT NOT NULL,
        pid INT NOT NULL,
        cost REAL NOT NULL,
        PRIMARY KEY pk_catalog_sp(sid, pid),
        KEY ix_catalog_pid (pid)
) ENGINE=InnoDB DEFAULT CHARSET=UTF8;

DELIMITER $$

CREATE PROCEDURE prc_filler(cnt INT)
BEGIN
        DECLARE _cnt INT;
        SET _cnt = 1;
        WHILE _cnt &lt;= cnt DO
                INSERT
                INTO    filler
                SELECT  _cnt;
                SET _cnt = _cnt + 1;
        END WHILE;
END
$$

DELIMITER ;

START TRANSACTION;
CALL prc_filler(10000);
COMMIT;

INSERT
INTO    suppliers
SELECT  id, CONCAT(&#039;Supplier &#039;, id)
FROM    filler
ORDER BY
        id
LIMIT 10000;

INSERT
INTO    parts
SELECT  id, CONCAT(&#039;Part &#039;, id), CASE WHEN id &lt;= 20 THEN ELT(id % 2 + 1, &#039;red&#039;, &#039;green&#039;) ELSE &#039;blue&#039; END
FROM    filler
ORDER BY
        id
LIMIT 2000;

INSERT
INTO    catalog
SELECT  sid, pid, 100
FROM    (
        SELECT  sid, pid, RAND(20100225) AS rnd
        FROM    suppliers
        JOIN    parts
        ON      color IN (&#039;red&#039;, &#039;green&#039;)
        ) q
WHERE   rnd &lt; 0.4;

INSERT
INTO    catalog
SELECT  sid, pid, 100
FROM    (
        SELECT  sid, pid, RAND(20100225 &lt;&lt; 1) AS rnd
        FROM    (
                SELECT  sid
                FROM    suppliers
                ORDER BY
                        sid
                LIMIT 200
                ) q
        JOIN    parts
        ON      color = &#039;blue&#039;
        ) q2
WHERE   rnd &lt; 0.998;
</pre>
</div>
<p>There are <strong>10,000</strong> suppliers and <strong>2,000</strong> parts.</p>
<p>The parts can be red, green or blue. As for red and green, there are <strong>10</strong> parts of each color, and they are distributed evenly across the suppliers. With blue, the situation is different: there are <strong>1980</strong> blue parts and only <strong>200</strong> first suppliers provide it. However, for each of the blue part suppliers, the probability of each blue part to be available is very high.</p>
<h3>First miss</h3>
<p>The first miss is a combination of <code>NOT IN</code> / <code>NOT EXISTS</code> clauses that immediately return <code>FALSE</code> whenever a single non-matching record is found. Since <strong>MySQL</strong> can only do nested loops, the performance of these queries is heavily dependent on proper indexing.</p>
<p>Let&#8217;s run this query to search for red or green parts:</p>
<pre class="brush: sql">
SELECT  *
FROM    suppliers s
WHERE   EXISTS
        (
        SELECT  NULL
        FROM    (
                SELECT  &#039;red&#039; AS color
                UNION ALL
                SELECT  &#039;green&#039; AS color
                ) ci
        WHERE   color NOT IN
                (
                SELECT  color
                FROM    parts p
                WHERE   p.pid NOT IN
                        (
                        SELECT  pid
                        FROM    catalog c
                        WHERE   c.sid = s.sid
                        )
                )
        )
</pre>
<p><a href="#" onclick="xcollapse('X8563');return false;"><strong>View query details</strong></a><br />
</p>
<div id="X8563" style="display: none; ">
<div class="terminal">
<table class="terminal">
<tr>
<th>sid</th>
<th>sname</th>
</tr>
<tr>
<td class="integer">7442</td>
<td class="varchar">Supplier 7442</td>
</tr>
<tr class="statusbar">
<td colspan="100">1 row fetched in 0.0003s (2.1719s)</td>
</tr>
</table>
</div>
<div class="terminal">
<table class="terminal">
<tr>
<th>id</th>
<th>select_type</th>
<th>table</th>
<th>type</th>
<th>possible_keys</th>
<th>key</th>
<th>key_len</th>
<th>ref</th>
<th>rows</th>
<th>filtered</th>
<th>Extra</th>
</tr>
<tr>
<td class="bigint">1</td>
<td class="varchar">PRIMARY</td>
<td class="varchar">s</td>
<td class="varchar">ALL</td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="bigint">10342</td>
<td class="double">100.00</td>
<td class="varchar">Using where</td>
</tr>
<tr>
<td class="bigint">2</td>
<td class="varchar">DEPENDENT SUBQUERY</td>
<td class="varchar">&lt;derived3&gt;</td>
<td class="varchar">ALL</td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="bigint">2</td>
<td class="double">100.00</td>
<td class="varchar">Using where</td>
</tr>
<tr>
<td class="bigint">5</td>
<td class="varchar">DEPENDENT SUBQUERY</td>
<td class="varchar">p</td>
<td class="varchar">ref</td>
<td class="varchar">ix_parts_color</td>
<td class="varchar">ix_parts_color</td>
<td class="varchar">302</td>
<td class="varchar">func</td>
<td class="bigint">439</td>
<td class="double">100.00</td>
<td class="varchar">Using where; Using index</td>
</tr>
<tr>
<td class="bigint">6</td>
<td class="varchar">DEPENDENT SUBQUERY</td>
<td class="varchar">c</td>
<td class="varchar">eq_ref</td>
<td class="varchar">PRIMARY,ix_catalog_pid</td>
<td class="varchar">PRIMARY</td>
<td class="varchar">8</td>
<td class="varchar">20100225_sets.s.sid,func</td>
<td class="bigint">1</td>
<td class="double">100.00</td>
<td class="varchar">Using index</td>
</tr>
<tr>
<td class="bigint">3</td>
<td class="varchar">DERIVED</td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="bigint"></td>
<td class="double"></td>
<td class="varchar">No tables used</td>
</tr>
<tr>
<td class="bigint">4</td>
<td class="varchar">UNION</td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="bigint"></td>
<td class="double"></td>
<td class="varchar">No tables used</td>
</tr>
<tr>
<td class="bigint"></td>
<td class="varchar">UNION RESULT</td>
<td class="varchar">&lt;union3,4&gt;</td>
<td class="varchar">ALL</td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="bigint"></td>
<td class="double"></td>
<td class="varchar"></td>
</tr>
</table>
</div>
<pre>
Field or reference &#39;20100225_sets.s.sid&#39; of SELECT #6 was resolved in SELECT #1
select `20100225_sets`.`s`.`sid` AS `sid`,`20100225_sets`.`s`.`sname` AS `sname` from `20100225_sets`.`suppliers` `s` where exists(select NULL AS `NULL` from (select &#39;red&#39; AS `color` union all select &#39;green&#39; AS `color`) `ci` where (not(&lt;in_optimizer&gt;(`ci`.`color`,&lt;exists&gt;(select 1 AS `Not_used` from `20100225_sets`.`parts` `p` where ((not(&lt;in_optimizer&gt;(`20100225_sets`.`p`.`pid`,&lt;exists&gt;(select 1 AS `Not_used` from `20100225_sets`.`catalog` `c` where ((`20100225_sets`.`c`.`sid` = `20100225_sets`.`s`.`sid`) and (&lt;cache&gt;(`20100225_sets`.`p`.`pid`) = `20100225_sets`.`c`.`pid`)))))) and (convert(&lt;cache&gt;(`ci`.`color`) using utf8) = `20100225_sets`.`p`.`color`)))))))
</pre>
</div>
<p>This query runs for <strong>2.17 s</strong>.</p>
<p>The same query for the blue parts:</p>
<pre class="brush: sql">
SELECT  *
FROM    suppliers s
WHERE   EXISTS
        (
        SELECT  NULL
        FROM    (
                SELECT  &#039;blue&#039; AS color
                ) ci
        WHERE   color NOT IN
                (
                SELECT  color
                FROM    parts p
                WHERE   p.pid NOT IN
                        (
                        SELECT  pid
                        FROM    catalog c
                        WHERE   c.sid = s.sid
                        )
                )
        )
</pre>
<p><a href="#" onclick="xcollapse('X8564');return false;"><strong>View query details</strong></a><br />
</p>
<div id="X8564" style="display: none; ">
<div class="terminal">
<table class="terminal">
<tr>
<th>sid</th>
<th>sname</th>
</tr>
<tr>
<td class="integer">10</td>
<td class="varchar">Supplier 10</td>
</tr>
<tr>
<td class="integer">96</td>
<td class="varchar">Supplier 96</td>
</tr>
<tr>
<td class="integer">101</td>
<td class="varchar">Supplier 101</td>
</tr>
<tr class="statusbar">
<td colspan="100">3 rows fetched in 0.0005s (2.4375s)</td>
</tr>
</table>
</div>
<div class="terminal">
<table class="terminal">
<tr>
<th>id</th>
<th>select_type</th>
<th>table</th>
<th>type</th>
<th>possible_keys</th>
<th>key</th>
<th>key_len</th>
<th>ref</th>
<th>rows</th>
<th>filtered</th>
<th>Extra</th>
</tr>
<tr>
<td class="bigint">1</td>
<td class="varchar">PRIMARY</td>
<td class="varchar">s</td>
<td class="varchar">ALL</td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="bigint">10342</td>
<td class="double">100.00</td>
<td class="varchar">Using where</td>
</tr>
<tr>
<td class="bigint">2</td>
<td class="varchar">DEPENDENT SUBQUERY</td>
<td class="varchar">&lt;derived3&gt;</td>
<td class="varchar">system</td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="bigint">1</td>
<td class="double">100.00</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="bigint">4</td>
<td class="varchar">DEPENDENT SUBQUERY</td>
<td class="varchar">p</td>
<td class="varchar">ref</td>
<td class="varchar">ix_parts_color</td>
<td class="varchar">ix_parts_color</td>
<td class="varchar">302</td>
<td class="varchar">func</td>
<td class="bigint">439</td>
<td class="double">100.00</td>
<td class="varchar">Using where; Using index</td>
</tr>
<tr>
<td class="bigint">5</td>
<td class="varchar">DEPENDENT SUBQUERY</td>
<td class="varchar">c</td>
<td class="varchar">eq_ref</td>
<td class="varchar">PRIMARY,ix_catalog_pid</td>
<td class="varchar">PRIMARY</td>
<td class="varchar">8</td>
<td class="varchar">20100225_sets.s.sid,func</td>
<td class="bigint">1</td>
<td class="double">100.00</td>
<td class="varchar">Using index</td>
</tr>
<tr>
<td class="bigint">3</td>
<td class="varchar">DERIVED</td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="bigint"></td>
<td class="double"></td>
<td class="varchar">No tables used</td>
</tr>
</table>
</div>
<pre>
Field or reference &#39;20100225_sets.s.sid&#39; of SELECT #5 was resolved in SELECT #1
select `20100225_sets`.`s`.`sid` AS `sid`,`20100225_sets`.`s`.`sname` AS `sname` from `20100225_sets`.`suppliers` `s` where exists(select NULL AS `NULL` from (select &#39;blue&#39; AS `color`) `ci` where (not(&lt;in_optimizer&gt;(&#39;blue&#39;,&lt;exists&gt;(select 1 AS `Not_used` from `20100225_sets`.`parts` `p` where ((not(&lt;in_optimizer&gt;(`20100225_sets`.`p`.`pid`,&lt;exists&gt;(select 1 AS `Not_used` from `20100225_sets`.`catalog` `c` where ((`20100225_sets`.`c`.`sid` = `20100225_sets`.`s`.`sid`) and (&lt;cache&gt;(`20100225_sets`.`p`.`pid`) = `20100225_sets`.`c`.`pid`)))))) and (convert(&lt;cache&gt;(&#39;blue&#39;) using utf8) = `20100225_sets`.`p`.`color`)))))))
</pre>
</div>
<p>This time the query is a little bit slower: <strong>2.43 s</strong>.</p>
<h3>Aggregate</h3>
<p>In the aggregate solution, we first calculate the total number of parts for each color and the count the parts of this color supplied by each supplier. If the counts match, the sets match too.</p>
<p>Let&#8217;s try it on red and green parts first:</p>
<pre class="brush: sql">
SELECT  c.sid, p.color
FROM    (
        SELECT  color, COUNT(*) AS total
        FROM    parts
        WHERE   color IN (&#039;red&#039;, &#039;green&#039;)
        GROUP BY
                color
        ) t
JOIN    parts p
ON      p.color = t.color
JOIN    catalog c
ON      c.pid = p.pid
GROUP BY
        sid, color, total
HAVING  COUNT(*) = total
</pre>
<p><a href="#" onclick="xcollapse('X8565');return false;"><strong>View query details</strong></a><br />
</p>
<div id="X8565" style="display: none; ">
<div class="terminal">
<table class="terminal">
<tr>
<th>sid</th>
<th>color</th>
</tr>
<tr>
<td class="integer">7442</td>
<td class="varchar">green</td>
</tr>
<tr class="statusbar">
<td colspan="100">1 row fetched in 0.0003s (1.1406s)</td>
</tr>
</table>
</div>
<div class="terminal">
<table class="terminal">
<tr>
<th>id</th>
<th>select_type</th>
<th>table</th>
<th>type</th>
<th>possible_keys</th>
<th>key</th>
<th>key_len</th>
<th>ref</th>
<th>rows</th>
<th>filtered</th>
<th>Extra</th>
</tr>
<tr>
<td class="bigint">1</td>
<td class="varchar">PRIMARY</td>
<td class="varchar">&lt;derived2&gt;</td>
<td class="varchar">ALL</td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="bigint">2</td>
<td class="double">100.00</td>
<td class="varchar">Using temporary; Using filesort</td>
</tr>
<tr>
<td class="bigint">1</td>
<td class="varchar">PRIMARY</td>
<td class="varchar">p</td>
<td class="varchar">ref</td>
<td class="varchar">PRIMARY,ix_parts_color</td>
<td class="varchar">ix_parts_color</td>
<td class="varchar">302</td>
<td class="varchar">t.color</td>
<td class="bigint">439</td>
<td class="double">100.00</td>
<td class="varchar">Using index</td>
</tr>
<tr>
<td class="bigint">1</td>
<td class="varchar">PRIMARY</td>
<td class="varchar">c</td>
<td class="varchar">ref</td>
<td class="varchar">ix_catalog_pid</td>
<td class="varchar">ix_catalog_pid</td>
<td class="varchar">4</td>
<td class="varchar">20100225_sets.p.pid</td>
<td class="bigint">109</td>
<td class="double">100.00</td>
<td class="varchar">Using index</td>
</tr>
<tr>
<td class="bigint">2</td>
<td class="varchar">DERIVED</td>
<td class="varchar">parts</td>
<td class="varchar">range</td>
<td class="varchar">ix_parts_color</td>
<td class="varchar">ix_parts_color</td>
<td class="varchar">302</td>
<td class="varchar"></td>
<td class="bigint">19</td>
<td class="double">100.00</td>
<td class="varchar">Using where; Using index</td>
</tr>
</table>
</div>
<pre>
select `20100225_sets`.`c`.`sid` AS `sid`,`20100225_sets`.`p`.`color` AS `color` from (select `20100225_sets`.`parts`.`color` AS `color`,count(0) AS `total` from `20100225_sets`.`parts` where (`20100225_sets`.`parts`.`color` in (&#39;red&#39;,&#39;green&#39;)) group by `20100225_sets`.`parts`.`color`) `t` join `20100225_sets`.`parts` `p` join `20100225_sets`.`catalog` `c` where ((`20100225_sets`.`p`.`color` = `t`.`color`) and (`20100225_sets`.`c`.`pid` = `20100225_sets`.`p`.`pid`)) group by `20100225_sets`.`c`.`sid`,`20100225_sets`.`p`.`color`,`t`.`total` having (count(0) = `t`.`total`)
</pre>
</div>
<p>And the same query on the blue parts:</p>
<pre class="brush: sql">
SELECT  c.sid, p.color
FROM    (
        SELECT  color, COUNT(*) AS total
        FROM    parts
        WHERE   color IN (&#039;blue&#039;)
        GROUP BY
                color
        ) t
JOIN    parts p
ON      p.color = t.color
JOIN    catalog c
ON      c.pid = p.pid
GROUP BY
        sid, color, total
HAVING  COUNT(*) = total
</pre>
<p><a href="#" onclick="xcollapse('X8566');return false;"><strong>View query details</strong></a><br />
</p>
<div id="X8566" style="display: none; ">
<div class="terminal">
<table class="terminal">
<tr>
<th>sid</th>
<th>color</th>
</tr>
<tr>
<td class="integer">10</td>
<td class="varchar">blue</td>
</tr>
<tr>
<td class="integer">96</td>
<td class="varchar">blue</td>
</tr>
<tr>
<td class="integer">101</td>
<td class="varchar">blue</td>
</tr>
<tr class="statusbar">
<td colspan="100">3 rows fetched in 0.0004s (3.0906s)</td>
</tr>
</table>
</div>
<div class="terminal">
<table class="terminal">
<tr>
<th>id</th>
<th>select_type</th>
<th>table</th>
<th>type</th>
<th>possible_keys</th>
<th>key</th>
<th>key_len</th>
<th>ref</th>
<th>rows</th>
<th>filtered</th>
<th>Extra</th>
</tr>
<tr>
<td class="bigint">1</td>
<td class="varchar">PRIMARY</td>
<td class="varchar">&lt;derived2&gt;</td>
<td class="varchar">system</td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="bigint">1</td>
<td class="double">100.00</td>
<td class="varchar">Using temporary; Using filesort</td>
</tr>
<tr>
<td class="bigint">1</td>
<td class="varchar">PRIMARY</td>
<td class="varchar">p</td>
<td class="varchar">ref</td>
<td class="varchar">PRIMARY,ix_parts_color</td>
<td class="varchar">ix_parts_color</td>
<td class="varchar">302</td>
<td class="varchar">const</td>
<td class="bigint">1319</td>
<td class="double">100.00</td>
<td class="varchar">Using index</td>
</tr>
<tr>
<td class="bigint">1</td>
<td class="varchar">PRIMARY</td>
<td class="varchar">c</td>
<td class="varchar">ref</td>
<td class="varchar">ix_catalog_pid</td>
<td class="varchar">ix_catalog_pid</td>
<td class="varchar">4</td>
<td class="varchar">20100225_sets.p.pid</td>
<td class="bigint">109</td>
<td class="double">100.00</td>
<td class="varchar">Using index</td>
</tr>
<tr>
<td class="bigint">2</td>
<td class="varchar">DERIVED</td>
<td class="varchar">parts</td>
<td class="varchar">ref</td>
<td class="varchar">ix_parts_color</td>
<td class="varchar">ix_parts_color</td>
<td class="varchar">302</td>
<td class="varchar"></td>
<td class="bigint">1319</td>
<td class="double">100.00</td>
<td class="varchar">Using where; Using index</td>
</tr>
</table>
</div>
<pre>
select `20100225_sets`.`c`.`sid` AS `sid`,`20100225_sets`.`p`.`color` AS `color` from (select `20100225_sets`.`parts`.`color` AS `color`,count(0) AS `total` from `20100225_sets`.`parts` where (`20100225_sets`.`parts`.`color` = &#39;blue&#39;) group by `20100225_sets`.`parts`.`color`) `t` join `20100225_sets`.`parts` `p` join `20100225_sets`.`catalog` `c` where ((`20100225_sets`.`p`.`color` = &#39;blue&#39;) and (`20100225_sets`.`c`.`pid` = `20100225_sets`.`p`.`pid`)) group by `20100225_sets`.`c`.`sid` having (count(0) = &#39;1980&#39;)
</pre>
</div>
<p>We see that the queries complete in <strong>1.14 seconds</strong> and <strong>3.09 seconds</strong>, accordingly.</p>
<p>The <q>aggregate</q> method is more efficient for the red and green parts, while the <q>first miss</q> is more efficient for the blue parts.</p>
<h3>Analysis</h3>
<p>What is the reason of such a difference in performance?</p>
<p>The <q>first miss</q> method generally needs to parse less records (only those before the first miss), but each record is searched for in a nested loop, starting from the index root. The aggregate method needs to parse all records to calculate the <code>COUNT(*)</code>, but these records are fetched in a sequential index access.</p>
<p>The red and green parts have the large number of small sets, with at most <strong>10</strong> records in each. The probability of the miss is relatively small: a large number of records should be parsed before any record is missed in a set. The aggregates, on the other hand, can be calculated very fast, since there are relatively few records to aggregate.</p>
<p>With the blue parts, the situation is different. There are few large sets, and calculating the aggregates requires fetching, sorting and grouping of too many records. First misses, on the other hand, occurs almost instantly: the vast majority of the suppliers do not offer any blue parts at all.</p>
<h3>Summary</h3>
<p>As it often happens, the performance of the two methods to compare the sets depends on the data distribution.</p>
<p>The sets with less records and lower probability of the record miss will benefit from the aggregate method, since the performance increase caused by the sequential access to the records overweights the need to parse a larger number of the records.</p>
<p>The sets with more records and higher probability of a miss will return the misses very soon, so the first miss method is more beneficial for them.</p>
]]></content:encoded>
			<wfw:commentRss>http://explainextended.com/2010/02/25/matching-sets-aggregates-vs-first-miss/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Sargability of monotonic functions: example</title>
		<link>http://explainextended.com/2010/02/23/sargability-of-monotonic-functions-example/</link>
		<comments>http://explainextended.com/2010/02/23/sargability-of-monotonic-functions-example/#comments</comments>
		<pubDate>Tue, 23 Feb 2010 20:00:25 +0000</pubDate>
		<dc:creator>Quassnoi</dc:creator>
				<category><![CDATA[PostgreSQL]]></category>

		<guid isPermaLink="false">http://explainextended.com/?p=4407</guid>
		<description><![CDATA[In my previous article I presented a proposal to add sargability of monotonic functions into the SQL engines.
In a nutshell: a monotonic function is a function that preserves the order of the argument so that it gives the larger results for the larger values of the argument. It is easy to prove that a B-tree [...]]]></description>
			<content:encoded><![CDATA[<p>In my previous article I presented a proposal to add <a href="/2010/02/19/things-sql-needs-sargability-of-monotonic-functions/">sargability of monotonic functions</a> into the <strong>SQL</strong> engines.</p>
<p>In a nutshell: a monotonic function is a function that preserves the order of the argument so that it gives the larger results for the larger values of the argument. It is easy to prove that a <strong>B-tree</strong> with each key replaced by the result of the function will remain the valid <strong>B-Tree</strong> and hence can be used to search for ranges of function results just like it is used to search for ranges of values.</p>
<p>With a little effort, a <strong>B-Tree</strong> can also be used to search for the ranges of piecewise monotonic functions: those whose domain can be split into a number of continuous pieces with the function being monotonic within each piece (but it may be not monotonic and even not continuous across the pieces).</p>
<p>In this article, I&#8217;ll demonstrate the algorithm to do that (implemented in pure <strong>SQL</strong> on <strong>PostgreSQL</strong>).</p>
<p>I will show how the performance of simple query </p>
<pre class="brush: sql">
SELECT  *
FROM    t_sine
WHERE   SIN(value) BETWEEN 0.1234 AND 0.1235
</pre>
<p>could be improved if the sargability of monotonic functions had been implemented in the optimizer.<br />
<span id="more-4407"></span><br />
To do this, I will create a sample table:</p>
<p><a href="#" onclick="xcollapse('X1002');return false;"><strong>Table creation details</strong></a><br />
</p>
<div id="X1002" style="display: none; ">
<pre class="brush: sql">
CREATE TABLE t_sine (
        id INT NOT NULL PRIMARY KEY,
        value DOUBLE PRECISION NOT NULL
);

CREATE INDEX ix_sine_value ON t_sine (value);

SELECT  SETSEED(0.20100223);

INSERT
INTO    t_sine
SELECT  num, num / 10000.00 + RANDOM()
FROM    generate_series(1, 1000000) num;

ANALYZE t_sine;
</pre>
</div>
<p>This table contains <strong>1,000,000</strong> records with <code>value</code> randomly distributed from <strong>0</strong> to <strong>101</strong>.</p>
<p>To select the records we need, we can use a very simple and straightforward query:</p>
<pre class="brush: sql">
SELECT  *
FROM    t_sine
WHERE   SIN(value) BETWEEN 0.4452 AND 0.4453
</pre>
<p><a href="#" onclick="xcollapse('X1272');return false;"><strong>View query details</strong></a><br />
</p>
<div id="X1272" style="display: none; ">
<div class="terminal">
<table class="terminal">
<tr>
<th>id</th>
<th>value</th>
</tr>
<tr>
<td class="int4">3663</td>
<td class="float8">0.46150185738802</td>
</tr>
<tr>
<td class="int4">19263</td>
<td class="float8">2.68015610060766</td>
</tr>
<tr>
<td class="int4">23783</td>
<td class="float8">2.68013202354237</td>
</tr>
<tr>
<td class="int4">86110</td>
<td class="float8">8.963312032599</td>
</tr>
<tr>
<td class="int4">128053</td>
<td class="float8">13.0278523004308</td>
</tr>
<tr>
<td class="int4">150339</td>
<td class="float8">15.2465362691633</td>
</tr>
<tr>
<td class="int4">185849</td>
<td class="float8">19.310986539682</td>
</tr>
<tr>
<td class="int4">186788</td>
<td class="float8">19.3110526885197</td>
</tr>
<tr>
<td class="int4">191391</td>
<td class="float8">19.3110088731334</td>
</tr>
<tr>
<td class="int4">210841</td>
<td class="float8">21.5297331408583</td>
</tr>
<tr>
<td class="int4">212511</td>
<td class="float8">21.529697893659</td>
</tr>
<tr>
<td class="int4">247639</td>
<td class="float8">25.5941842560224</td>
</tr>
<tr>
<td class="int4">373019</td>
<td class="float8">38.1605504324339</td>
</tr>
<tr>
<td class="int4">373416</td>
<td class="float8">38.1606072025172</td>
</tr>
<tr>
<td class="int4">458141</td>
<td class="float8">46.6624391236514</td>
</tr>
<tr>
<td class="int4">462683</td>
<td class="float8">46.6623900452435</td>
</tr>
<tr>
<td class="int4">462704</td>
<td class="float8">46.662440645238</td>
</tr>
<tr>
<td class="int4">463233</td>
<td class="float8">46.6624209528782</td>
</tr>
<tr>
<td class="int4">520118</td>
<td class="float8">52.945639446865</td>
</tr>
<tr>
<td class="int4">522686</td>
<td class="float8">52.9456708737895</td>
</tr>
<tr>
<td class="int4">561721</td>
<td class="float8">57.0100855686799</td>
</tr>
<tr>
<td class="int4">686886</td>
<td class="float8">69.5764806582652</td>
</tr>
<tr>
<td class="int4">711952</td>
<td class="float8">71.7951245983548</td>
</tr>
<tr>
<td class="int4">716508</td>
<td class="float8">71.7952263388403</td>
</tr>
<tr>
<td class="int4">778116</td>
<td class="float8">78.0783171531357</td>
</tr>
<tr>
<td class="int4">877138</td>
<td class="float8">88.4260205388732</td>
</tr>
<tr>
<td class="int4">903050</td>
<td class="float8">90.6446926224232</td>
</tr>
<tr>
<td class="int4">942345</td>
<td class="float8">94.7092782433405</td>
</tr>
<tr>
<td class="int4">946181</td>
<td class="float8">94.7092303088069</td>
</tr>
<tr>
<td class="int4">966815</td>
<td class="float8">96.9279387165383</td>
</tr>
<tr>
<td class="int4">999931</td>
<td class="float8">100.992433886895</td>
</tr>
<tr class="statusbar">
<td colspan="100">31 rows fetched in 0.0022s (3.2500s)</td>
</tr>
</table>
</div>
<pre>
Seq Scan on t_sine  (cost=0.00..25406.00 rows=5000 width=12)
  Filter: ((sin(value) &gt;= 0.4452::double precision) AND (sin(value) &lt;= 0.4453::double precision))
</pre>
</div>
<p>which returns <strong>31</strong> records in <strong>3.25</strong> seconds.</p>
<p>The query uses a full table scan with the filter applied to each record.</p>
<p>Let&#8217;s try to improve it.</p>
<h3>Function description</h3>
<p>According to the notation I proposed in the previous article, the monotony of the function <code>SIN()</code> should be described as this:</p>
<p><code>SIN(arg FLOAT) MONOTONIC PIECEWISE<br />
DEFINED BY FLOOR(arg / PI() + 0.5)<br />
CASE PIECE % 2<br />
WHEN 0 THEN DECREASING INVERSE PIECE * PI() + ASIN(RESULT)<br />
ELSE INCREASING INVERSE PIECE * PI() - ASIN(RESULT)<br />
END</code></p>
<p>This means that:</p>
<ol>
<li>
<p>The function is piecewise monotonic,</p>
</li>
<li>
<p>The pieces are defined by the function <code>FLOOR(arg / PI() + 0.5)</code> (which essentially returns the number of the half-wave the argument belongs too),</p>
</li>
<li>
<p>The function monotony varies depending on the piece,</p>
</li>
<li>
<p>On odd pieces, the function increases, </p>
</li>
<li>
<p>On even pieces, the function decreases.</p>
</li>
<li>
<p>A single inverse expression is provided for each monotony</p>
</li>
</ol>
<p>Note that mathematically the function is strictly monotonic on each of its pieces. However, due to the rounding errors, different arguments can yield same function results, so the function value may map back to a range of the arguments rather than a single value.</p>
<p>In theory, it is possible to write a single expression which would map the function&#8217;s result to the pair of values defining the beginning and the end of such a range. However, the expression would be quite complex. So for illustration purposes I&#8217;ll make do with a single inverse function that yields an approximation of the back mapping. To find the exact range, some extra effort will be required.</p>
<h3>Building the pieces</h3>
<p>The function is piecewise monotonic and the pieces are defined by a function. For the pieces to be continuous, the function that defines them should be itself monotonic over all its domain.</p>
<p>The function that defines the pieces is <code>FLOOR(arg / PI() + 0.5)</code>.</p>
<p>It is a superposition of the three functions:</p>
<ul>
<li>
<p><code>OPERATOR_DIVISION(arg1 FLOAT, arg2 FLOAT)<br />
MONOTONIC OVER (arg1)<br />
CASE WHEN arg2 > 0 THEN INCREASING INVERSE RESULT * arg2<br />
WHEN arg2 = 0 THEN UNDEFINED<br />
WHEN arg2 < 0 THEN DECREASING INVERSE RESULT * arg2<br />
END</code></p>
</li>
<li>
<p><code>OPERATOR_PLUS(arg1 FLOAT, arg2 FLOAT) MONOTONIC<br />
OVER (arg1) STRICTLY INCREASING INVERSE RESULT - arg2,<br />
OVER (arg2) STRICTLY INCREASING INVERSE RESULT - arg1</code></p>
</li>
<li>
<p><code>FLOOR(arg FLOAT) MONOTONIC INCREASING<br />
INVERSE<br />
FROM RESULT EXACT<br />
TO RESULT + 1 EXACT EXCLUDE</code></p>
</li>
</ul>
<p>which are, given the values of the constants provided in the secondary arguments, are increasing over the argument. As we know from math, a superposition of monotonic functions is also monotonic.</p>
<p>Each function is defined with a single inverse condition which maps the result of the function back to a value <em>near</em> the range of the arguments yielding the result. The exact range has to be sought for using index seek over (hopefully) not too many records.</p>
<p>Sequentially applying the inverse expressions of each of the constituent functions to the result of the piece defining function, we get the following inverse expression for the latter:</p>
<ol>
<li><code>FLOOR(OPERATOR_PLUS(OPERATOR_DIVISION(arg, PI()), 0.5)) = PIECE</code></li>
<li><code>OPERATOR_PLUS(OPERATOR_DIVISION(arg, PI()), 0.5) ∈ [ PIECE, PIECE + 1 )</code></li>
<li><code>OPERATOR_DIVISION(arg, PI()) ∈ [ ≈(PIECE - 0.5), ≈((PIECE + 1) - 0.5) ]</code></li>
<li><code>arg ∈ [ ≈((PIECE - 0.5) * PI()), ≈(((PIECE + 1) - 0.5) * PI()) ]</code></li>
</ol>
<p>For each piece, we how have a pair of values <em>approximately</em> defining the range of values belonging to the piece.</p>
<p>To find out the exact bounds, we need to do the following:</p>
<ol>
<li>
<p>Calculate the piece for the minimal <code>value</code></p>
</li>
<li>
<p>Find the approximate upper bound for the piece.</p>
</li>
<li>
<p>Scanning the keys to the left, find the <strong>rightmost</strong> key to the <strong>left of the upper bound</strong> that belongs to the current (or previous) piece.</p>
</li>
<li>
<p>Scanning the keys to the right, find the <strong>first</strong> key of the <strong>next</strong> piece.</p>
</li>
<li>
<p>Scanning a single key to the left, find the <strong>last</strong> key of the <strong>current</strong> piece.</p>
</li>
<li>
<p>Recursively repeat steps <strong>1</strong> to <strong>5</strong>, taking the first value the next piece calculated on step <strong>4</strong> as a seed for the step <strong>1</strong>, until step <strong>4</strong> fails (which means that the pieces are over).</p>
</li>
</ol>
<p>This procedure guarantees that we always get the correct bounds even with the inexact inverse value, since it correctly handles both overflow and underflow of the inverse value, as show on the pictures below:</p>
<h4>Overflow</h4>
<p><img src="http://explainextended.com/wp-content/uploads/2010/02/overflow.png" alt="" title="Overflow" width="700" height="500" class="size-full wp-image-4420 noborder" /></p>
<h4>Underflow</h4>
<p><img src="http://explainextended.com/wp-content/uploads/2010/02/underflow.png" alt="" title="Underflow" width="700" height="500" class="aligncenter size-full wp-image-4419 noborder" /></p>
<p>Here's a query that selects the first and the last key of each piece:</p>
<p><a href="#" onclick="xcollapse('X822');return false;"><strong>View the query</strong></a><br />
</p>
<div id="X822" style="display: none; ">
<pre class="brush: sql">
WITH    RECURSIVE
        d AS (
        SELECT  piece,
                minv,
                COALESCE(
                (
                SELECT  value
                FROM    t_sine
                WHERE   value &lt; nv[1]
                ORDER BY
                        value DESC
                LIMIT 1
                ),
                (
                SELECT  MAX(value)
                FROM    t_sine
                )
                ) AS maxv,
                nv[1] AS nextv,
                nv[2] AS nextpiece
        FROM    (
                SELECT  minv, piece,
                        (
                        SELECT  ARRAY[value, FLOOR(value / PI() + 0.5)]
                        FROM    t_sine
                        WHERE   value &gt;
                                (
                                SELECT  value
                                FROM    t_sine
                                WHERE   value &lt;= ((piece + 1) - 0.5) * PI()
                                        AND FLOOR(value / PI() + 0.5) &lt;= piece
                                ORDER BY
                                        value DESC
                                LIMIT 1
                                )
                        ORDER BY
                                value
                        LIMIT 1
                        ) nv
                FROM    (
                        SELECT  minv, FLOOR(minv / PI() + 0.5) AS piece
                        FROM    (
                                SELECT  MIN(value) AS minv
                                FROM    t_sine
                                ) q
                        ) q2
                ) q3
        UNION ALL
        SELECT  piece,
                minv,
                COALESCE(
                (
                SELECT  value
                FROM    t_sine
                WHERE   value &lt; nv[1]
                ORDER BY
                        value DESC
                LIMIT 1
                ),
                (
                SELECT  MAX(value)
                FROM    t_sine
                )
                ) AS maxv,
                nv[1] AS nextv,
                nv[2] AS nextpiece
        FROM    (
                SELECT  minv, piece,
                        (
                        SELECT  ARRAY[value, FLOOR(value / PI() + 0.5)]
                        FROM    t_sine
                        WHERE   value &gt;
                                (
                                SELECT  value
                                FROM    t_sine
                                WHERE   value &lt;= ((piece + 1) - 0.5) * PI()
                                        AND FLOOR(value / PI() + 0.5) &lt;= piece
                                ORDER BY
                                        value DESC
                                LIMIT 1
                                )
                        ORDER BY
                                value
                        LIMIT 1
                        ) nv
                FROM    (
                        SELECT  nextv AS minv, nextpiece AS piece
                        FROM    d
                        WHERE   nextpiece IS NOT NULL
                        ) q2
                ) q3
        )
SELECT  *
FROM    d
</pre>
<div class="terminal">
<table class="terminal">
<tr>
<th>piece</th>
<th>minv</th>
<th>maxv</th>
<th>nextv</th>
<th>nextpiece</th>
</tr>
<tr>
<td class="float8">0</td>
<td class="float8">0.0172837972298265</td>
<td class="float8">1.57060804706216</td>
<td class="float8">1.57081433883309</td>
<td class="float8">1</td>
</tr>
<tr>
<td class="float8">1</td>
<td class="float8">1.57081433883309</td>
<td class="float8">4.7123523916252</td>
<td class="float8">4.71268981658742</td>
<td class="float8">2</td>
</tr>
<tr>
<td class="float8">2</td>
<td class="float8">4.71268981658742</td>
<td class="float8">7.85390641669333</td>
<td class="float8">7.85409013534784</td>
<td class="float8">3</td>
</tr>
<tr>
<td class="float8">3</td>
<td class="float8">7.85409013534784</td>
<td class="float8">10.9955484666698</td>
<td class="float8">10.9956333589859</td>
<td class="float8">4</td>
</tr>
<tr>
<td class="float8">4</td>
<td class="float8">10.9956333589859</td>
<td class="float8">14.1371444690436</td>
<td class="float8">14.1372372000463</td>
<td class="float8">5</td>
</tr>
<tr class="break">
<td colspan="100"/></tr>
<tr>
<td class="float8">31</td>
<td class="float8">95.8185816861346</td>
<td class="float8">98.9601405610755</td>
<td class="float8">98.9601765494391</td>
<td class="float8">32</td>
</tr>
<tr>
<td class="float8">32</td>
<td class="float8">98.9601765494391</td>
<td class="float8">100.992433886895</td>
<td class="float8"></td>
<td class="float8"></td>
</tr>
<tr class="statusbar">
<td colspan="100">33 rows fetched in 0.0057s (0.0484s)</td>
</tr>
</table>
</div>
<pre>
CTE Scan on d  (cost=65.97..67.99 rows=101 width=40)
  CTE d
    -&gt;  Recursive Union  (cost=0.08..65.97 rows=101 width=16)
          -&gt;  Subquery Scan q  (cost=0.08..0.80 rows=1 width=8)
                InitPlan 5 (returns $5)
                  -&gt;  Result  (cost=0.03..0.04 rows=1 width=0)
                        InitPlan 4 (returns $4)
                          -&gt;  Limit  (cost=0.00..0.03 rows=1 width=8)
                                -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.00..33799.01 rows=1000000 width=8)
                                      Filter: (value IS NOT NULL)
                -&gt;  Result  (cost=0.03..0.04 rows=1 width=0)
                      InitPlan 10 (returns $8)
                        -&gt;  Limit  (cost=0.00..0.03 rows=1 width=8)
                              -&gt;  Index Scan using ix_sine_value on t_sine  (cost=0.00..33799.01 rows=1000000 width=8)
                                    Filter: (value IS NOT NULL)
                SubPlan 3
                  -&gt;  Limit  (cost=0.22..0.26 rows=1 width=8)
                        InitPlan 2 (returns $3)
                          -&gt;  Limit  (cost=0.18..0.22 rows=1 width=8)
                                InitPlan 1 (returns $2)
                                  -&gt;  Limit  (cost=0.02..0.18 rows=1 width=8)
                                        -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.02..17938.04 rows=111111 width=8)
                                              Index Cond: (value &lt;= (((floor((($1 / 3.14159265358979::double precision) + 0.5::double precision)) + 1::double precision) - 0.5::double precision) * 3.14159265358979::double precision))
                                              Filter: (floor(((value / 3.14159265358979::double precision) + 0.5::double precision)) &lt;= floor((($1 / 3.14159265358979::double precision) + 0.5::double precision)))
                                -&gt;  Index Scan using ix_sine_value on t_sine  (cost=0.00..14604.70 rows=333333 width=8)
                                      Index Cond: (value &gt; $2)
                        -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.00..12104.70 rows=333333 width=8)
                              Index Cond: (value &lt; ($3)[1])
                SubPlan 7
                  -&gt;  Limit  (cost=0.18..0.22 rows=1 width=8)
                        InitPlan 6 (returns $6)
                          -&gt;  Limit  (cost=0.02..0.18 rows=1 width=8)
                                -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.02..17938.04 rows=111111 width=8)
                                      Index Cond: (value &lt;= (((floor((($1 / 3.14159265358979::double precision) + 0.5::double precision)) + 1::double precision) - 0.5::double precision) * 3.14159265358979::double precision))
                                      Filter: (floor(((value / 3.14159265358979::double precision) + 0.5::double precision)) &lt;= floor((($1 / 3.14159265358979::double precision) + 0.5::double precision)))
                        -&gt;  Index Scan using ix_sine_value on t_sine  (cost=0.00..14604.70 rows=333333 width=8)
                              Index Cond: (value &gt; $6)
                SubPlan 9
                  -&gt;  Limit  (cost=0.18..0.22 rows=1 width=8)
                        InitPlan 8 (returns $7)
                          -&gt;  Limit  (cost=0.02..0.18 rows=1 width=8)
                                -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.02..17938.04 rows=111111 width=8)
                                      Index Cond: (value &lt;= (((floor((($1 / 3.14159265358979::double precision) + 0.5::double precision)) + 1::double precision) - 0.5::double precision) * 3.14159265358979::double precision))
                                      Filter: (floor(((value / 3.14159265358979::double precision) + 0.5::double precision)) &lt;= floor((($1 / 3.14159265358979::double precision) + 0.5::double precision)))
                        -&gt;  Index Scan using ix_sine_value on t_sine  (cost=0.00..14604.70 rows=333333 width=8)
                              Index Cond: (value &gt; $7)
          -&gt;  WorkTable Scan on d  (cost=0.04..6.31 rows=10 width=16)
                Filter: (d.nextpiece IS NOT NULL)
                InitPlan 15 (returns $13)
                  -&gt;  Result  (cost=0.03..0.04 rows=1 width=0)
                        InitPlan 14 (returns $12)
                          -&gt;  Limit  (cost=0.00..0.03 rows=1 width=8)
                                -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.00..33799.01 rows=1000000 width=8)
                                      Filter: (value IS NOT NULL)
                SubPlan 13
                  -&gt;  Limit  (cost=0.19..0.23 rows=1 width=8)
                        InitPlan 12 (returns $11)
                          -&gt;  Limit  (cost=0.15..0.19 rows=1 width=8)
                                InitPlan 11 (returns $10)
                                  -&gt;  Limit  (cost=0.01..0.15 rows=1 width=8)
                                        -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.01..15438.04 rows=111111 width=8)
                                              Index Cond: (value &lt;= ((($9 + 1::double precision) - 0.5::double precision) * 3.14159265358979::double precision))
                                              Filter: (floor(((value / 3.14159265358979::double precision) + 0.5::double precision)) &lt;= $9)
                                -&gt;  Index Scan using ix_sine_value on t_sine  (cost=0.00..14604.70 rows=333333 width=8)
                                      Index Cond: (value &gt; $10)
                        -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.00..12104.70 rows=333333 width=8)
                              Index Cond: (value &lt; ($11)[1])
                SubPlan 17
                  -&gt;  Limit  (cost=0.15..0.19 rows=1 width=8)
                        InitPlan 16 (returns $14)
                          -&gt;  Limit  (cost=0.01..0.15 rows=1 width=8)
                                -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.01..15438.04 rows=111111 width=8)
                                      Index Cond: (value &lt;= ((($9 + 1::double precision) - 0.5::double precision) * 3.14159265358979::double precision))
                                      Filter: (floor(((value / 3.14159265358979::double precision) + 0.5::double precision)) &lt;= $9)
                        -&gt;  Index Scan using ix_sine_value on t_sine  (cost=0.00..14604.70 rows=333333 width=8)
                              Index Cond: (value &gt; $14)
                SubPlan 19
                  -&gt;  Limit  (cost=0.15..0.19 rows=1 width=8)
                        InitPlan 18 (returns $15)
                          -&gt;  Limit  (cost=0.01..0.15 rows=1 width=8)
                                -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.01..15438.04 rows=111111 width=8)
                                      Index Cond: (value &lt;= ((($9 + 1::double precision) - 0.5::double precision) * 3.14159265358979::double precision))
                                      Filter: (floor(((value / 3.14159265358979::double precision) + 0.5::double precision)) &lt;= $9)
                        -&gt;  Index Scan using ix_sine_value on t_sine  (cost=0.00..14604.70 rows=333333 width=8)
                              Index Cond: (value &gt; $15)
</pre>
</div>
<p>Despite being huge in size, the query is very efficient and completes in only <strong>48 ms</strong>.</p>
<h3>Locating values within the pieces</h3>
<p>Now, when we have the exact bounds of each piece, we need to locate the records within each piece.</p>
<p>Since we don't have exact inverse function here, the basic idea is the same as above: given the approximate inverse, locate the exact bound using the iterative approach:</p>
<ol>
<li>Locate the first key to the left of the inverse which yields the function result less than the one sought for, up to the first key of the piece. Should this search fail, the first key of the piece is the lower bound.</li>
<li>Locate the first key to the right of that found on the previous step that yields the function value equal to or greater than the one sought for, up to the last key of the piece. Return <code>NULL</code> should it fail</li>
</ol>
<p>Since we have an inclusive range here, we don't need the third step (final scan to the left to find the rightmost least value) that we used when searching for the pieces.</p>
<p>This algorithm searches for the lower bound; to search for the upper bound, we just need to inverse both directions and tests (<q>left</q> becomes <q>right</q>, <q>less</q> becomes <q>greater</q> etc).</p>
<p>Since the monotony of the function varies from piece to piece, we should take this into account. For the pieces where the function's monotony is <code>DECREASING</code> we should swap the order of the bounds: the upper bound of the expression becomes the lower bound or the range of values and vice versa. This can be handled merely by substituting the conditions into the very same <code>CASE</code> expression that defines the monotony.</p>
<p>When we locate the upper and the lower bounds for each piece, we should just join <code>t_sine</code> on the following condition:</p>
<pre class="brush: sql">
ON value BETWEEN llimit and ulimit
</pre>
<p>It can happen so that the lower bound found by the algorithm exceeds the upper bound. This is a perfectly normal situation meaning that no keys match the condition and the range diverged. <code>BETWEEN</code> predicate will handle this.</p>
<p>It also can happen that one of the bounds is a <code>NULL</code>. This is also a valid situation, meaning that no value within the piece exceeds the lower bound (or falls short of the upper one). <code>BETWEEN</code> will also take care of it.</p>
<h3>Final query</h3>
<p>And here's the final query. Be ready, you'll have to spin your mouse wheel a lot:</p>
<pre class="brush: sql">
WITH    RECURSIVE
        d AS (
        SELECT  piece,
                minv,
                COALESCE(
                (
                SELECT  value
                FROM    t_sine
                WHERE   value &lt; nv[1]
                ORDER BY
                        value DESC
                LIMIT 1
                ),
                (
                SELECT  MAX(value)
                FROM    t_sine
                )
                ) AS maxv,
                nv[1] AS nextv,
                nv[2] AS nextpiece
        FROM    (
                SELECT  minv, piece,
                        (
                        SELECT  ARRAY[value, FLOOR(value / PI() + 0.5)]
                        FROM    t_sine
                        WHERE   value &gt;
                                (
                                SELECT  value
                                FROM    t_sine
                                WHERE   value &lt;= ((piece + 1) - 0.5) * PI()
                                        AND FLOOR(value / PI() + 0.5) &lt;= piece
                                ORDER BY
                                        value DESC
                                LIMIT 1
                                )
                        ORDER BY
                                value
                        LIMIT 1
                        ) nv
                FROM    (
                        SELECT  minv, FLOOR(minv / PI() + 0.5) AS piece
                        FROM    (
                                SELECT  MIN(value) AS minv
                                FROM    t_sine
                                ) q
                        ) q2
                ) q3
        UNION ALL
        SELECT  piece,
                minv,
                COALESCE(
                (
                SELECT  value
                FROM    t_sine
                WHERE   value &lt; nv[1]
                ORDER BY
                        value DESC
                LIMIT 1
                ),
                (
                SELECT  MAX(value)
                FROM    t_sine
                )
                ) AS maxv,
                nv[1] AS nextv,
                nv[2] AS nextpiece
        FROM    (
                SELECT  minv, piece,
                        (
                        SELECT  ARRAY[value, FLOOR(value / PI() + 0.5)]
                        FROM    t_sine
                        WHERE   value &gt;
                                (
                                SELECT  value
                                FROM    t_sine
                                WHERE   value &lt;= ((piece + 1) - 0.5) * PI()
                                        AND FLOOR(value / PI() + 0.5) &lt;= piece
                                ORDER BY
                                        value DESC
                                LIMIT 1
                                )
                        ORDER BY
                                value
                        LIMIT 1
                        ) nv
                FROM    (
                        SELECT  nextv AS minv, nextpiece AS piece
                        FROM    d
                        WHERE   nextpiece IS NOT NULL
                        ) q2
                ) q3
        )
SELECT  l.*, s.*, SIN(value)
FROM    (
        SELECT  minv, maxv,
                CASE piece::INTEGER % 2
                WHEN 0 THEN
                        (
                        SELECT  value
                        FROM    t_sine
                        WHERE   value &gt;=
                                COALESCE(
                                (
                                SELECT  value
                                FROM    t_sine
                                WHERE   value &lt;= LEAST(piece * PI() + ASIN(0.4452), maxv)
                                        AND value &gt;= minv
                                        AND SIN(value) &lt; 0.4452
                                ORDER BY
                                        value DESC
                                LIMIT 1
                                ),
                                minv
                                )
                                AND value &lt;= maxv
                                AND SIN(value) &gt;= 0.4452
                        ORDER BY
                                value
                        LIMIT 1
                        )
                ELSE
                        (
                        SELECT  value
                        FROM    t_sine
                        WHERE   value &gt;=
                                COALESCE(
                                (
                                SELECT  value
                                FROM    t_sine
                                WHERE   value &lt;= LEAST(piece * PI() - ASIN(0.4453), maxv)
                                        AND value &gt;= minv
                                        AND SIN(value) &gt; 0.4453
                                ORDER BY
                                        value DESC
                                LIMIT 1
                                ),
                                minv
                                )
                                AND value &lt;= maxv
                                AND SIN(value) &lt;= 0.4453
                        ORDER BY
                                value
                        LIMIT 1
                        )
                END AS llimit,
                CASE piece::INTEGER % 2
                WHEN 0 THEN
                        (
                        SELECT  value
                        FROM    t_sine
                        WHERE   value &lt;=
                                COALESCE(
                                (
                                SELECT  value
                                FROM    t_sine
                                WHERE   value &gt;= GREATEST(piece * PI() + ASIN(0.4453), minv)
                                        AND value &lt;= maxv
                                        AND SIN(value) &gt; 0.4453
                                ORDER BY
                                        value
                                LIMIT 1
                                ),
                                maxv
                                )
                                AND value &gt;= minv
                                AND SIN(value) &lt;= 0.4453
                        ORDER BY
                                value DESC
                        LIMIT 1
                        )
                ELSE
                        (
                        SELECT  value
                        FROM    t_sine
                        WHERE   value &lt;=
                                COALESCE(
                                (
                                SELECT  value
                                FROM    t_sine
                                WHERE   value &gt;= GREATEST(piece * PI() - ASIN(0.4452), minv)
                                        AND value &lt;= maxv
                                        AND SIN(value) &lt; 0.4452
                                ORDER BY
                                        value
                                LIMIT 1
                                ),
                                maxv
                                )
                                AND value &gt;= minv
                                AND SIN(value) &gt;= 0.4452
                        ORDER BY
                                value DESC
                        LIMIT 1
                        )
                END AS ulimit
        FROM    d
        ) l
JOIN    t_sine s
ON      value BETWEEN llimit AND ulimit
</pre>
<p><a href="#" onclick="xcollapse('X1409');return false;"><strong>View query details</strong></a><br />
</p>
<div id="X1409" style="display: none; ">
<div class="terminal">
<table class="terminal">
<tr>
<th>minv</th>
<th>maxv</th>
<th>llimit</th>
<th>ulimit</th>
<th>id</th>
<th>value</th>
<th>sin</th>
</tr>
<tr>
<td class="float8">0.0172837972298265</td>
<td class="float8">1.57060804706216</td>
<td class="float8">0.46150185738802</td>
<td class="float8">0.46150185738802</td>
<td class="int4">3663</td>
<td class="float8">0.46150185738802</td>
<td class="float8">0.44529334884391</td>
</tr>
<tr>
<td class="float8">1.57081433883309</td>
<td class="float8">4.7123523916252</td>
<td class="float8">2.68013202354237</td>
<td class="float8">2.68015610060766</td>
<td class="int4">23783</td>
<td class="float8">2.68013202354237</td>
<td class="float8">0.445256434133825</td>
</tr>
<tr>
<td class="float8">1.57081433883309</td>
<td class="float8">4.7123523916252</td>
<td class="float8">2.68013202354237</td>
<td class="float8">2.68015610060766</td>
<td class="int4">19263</td>
<td class="float8">2.68015610060766</td>
<td class="float8">0.445234875325918</td>
</tr>
<tr>
<td class="float8">7.85409013534784</td>
<td class="float8">10.9955484666698</td>
<td class="float8">8.963312032599</td>
<td class="float8">8.963312032599</td>
<td class="int4">86110</td>
<td class="float8">8.963312032599</td>
<td class="float8">0.445261178083286</td>
</tr>
<tr>
<td class="float8">10.9956333589859</td>
<td class="float8">14.1371444690436</td>
<td class="float8">13.0278523004308</td>
<td class="float8">13.0278523004308</td>
<td class="int4">128053</td>
<td class="float8">13.0278523004308</td>
<td class="float8">0.445275287664458</td>
</tr>
<tr>
<td class="float8">14.1372372000463</td>
<td class="float8">17.2787474542275</td>
<td class="float8">15.2465362691633</td>
<td class="float8">15.2465362691633</td>
<td class="int4">150339</td>
<td class="float8">15.2465362691633</td>
<td class="float8">0.445226320346027</td>
</tr>
<tr>
<td class="float8">17.2790936637506</td>
<td class="float8">20.4199075459354</td>
<td class="float8">19.310986539682</td>
<td class="float8">19.3110526885197</td>
<td class="int4">185849</td>
<td class="float8">19.310986539682</td>
<td class="float8">0.445229561181329</td>
</tr>
<tr>
<td class="float8">17.2790936637506</td>
<td class="float8">20.4199075459354</td>
<td class="float8">19.310986539682</td>
<td class="float8">19.3110526885197</td>
<td class="int4">191391</td>
<td class="float8">19.3110088731334</td>
<td class="float8">0.445249558810258</td>
</tr>
<tr>
<td class="float8">17.2790936637506</td>
<td class="float8">20.4199075459354</td>
<td class="float8">19.310986539682</td>
<td class="float8">19.3110526885197</td>
<td class="int4">186788</td>
<td class="float8">19.3110526885197</td>
<td class="float8">0.44528879096528</td>
</tr>
<tr>
<td class="float8">20.4204805635758</td>
<td class="float8">23.561747650259</td>
<td class="float8">21.529697893659</td>
<td class="float8">21.5297331408583</td>
<td class="int4">212511</td>
<td class="float8">21.529697893659</td>
<td class="float8">0.445247526124325</td>
</tr>
<tr>
<td class="float8">20.4204805635758</td>
<td class="float8">23.561747650259</td>
<td class="float8">21.529697893659</td>
<td class="float8">21.5297331408583</td>
<td class="int4">210841</td>
<td class="float8">21.5297331408583</td>
<td class="float8">0.445215965240229</td>
</tr>
<tr>
<td class="float8">23.5619455649868</td>
<td class="float8">26.7032224601433</td>
<td class="float8">25.5941842560224</td>
<td class="float8">25.5941842560224</td>
<td class="int4">247639</td>
<td class="float8">25.5941842560224</td>
<td class="float8">0.445240672513922</td>
</tr>
<tr>
<td class="float8">36.1283162861556</td>
<td class="float8">39.2698307414278</td>
<td class="float8">38.1605504324339</td>
<td class="float8">38.1606072025172</td>
<td class="int4">373019</td>
<td class="float8">38.1605504324339</td>
<td class="float8">0.445236698722671</td>
</tr>
<tr>
<td class="float8">36.1283162861556</td>
<td class="float8">39.2698307414278</td>
<td class="float8">38.1605504324339</td>
<td class="float8">38.1606072025172</td>
<td class="int4">373416</td>
<td class="float8">38.1606072025172</td>
<td class="float8">0.445287530670759</td>
</tr>
<tr>
<td class="float8">45.5530965244733</td>
<td class="float8">48.694549004443</td>
<td class="float8">46.6623900452435</td>
<td class="float8">46.662440645238</td>
<td class="int4">462683</td>
<td class="float8">46.6623900452435</td>
<td class="float8">0.445291469623201</td>
</tr>
<tr>
<td class="float8">45.5530965244733</td>
<td class="float8">48.694549004443</td>
<td class="float8">46.6623900452435</td>
<td class="float8">46.662440645238</td>
<td class="int4">463233</td>
<td class="float8">46.6624209528782</td>
<td class="float8">0.445263795157205</td>
</tr>
<tr>
<td class="float8">45.5530965244733</td>
<td class="float8">48.694549004443</td>
<td class="float8">46.6623900452435</td>
<td class="float8">46.662440645238</td>
<td class="int4">458141</td>
<td class="float8">46.6624391236514</td>
<td class="float8">0.445247524983561</td>
</tr>
<tr>
<td class="float8">45.5530965244733</td>
<td class="float8">48.694549004443</td>
<td class="float8">46.6623900452435</td>
<td class="float8">46.662440645238</td>
<td class="int4">462704</td>
<td class="float8">46.662440645238</td>
<td class="float8">0.445246162542927</td>
</tr>
<tr>
<td class="float8">51.8363581438176</td>
<td class="float8">54.9774493159346</td>
<td class="float8">52.945639446865</td>
<td class="float8">52.9456708737895</td>
<td class="int4">520118</td>
<td class="float8">52.945639446865</td>
<td class="float8">0.445234079463435</td>
</tr>
<tr>
<td class="float8">51.8363581438176</td>
<td class="float8">54.9774493159346</td>
<td class="float8">52.945639446865</td>
<td class="float8">52.9456708737895</td>
<td class="int4">522686</td>
<td class="float8">52.9456708737895</td>
<td class="float8">0.445205939128705</td>
</tr>
<tr>
<td class="float8">54.9779645633645</td>
<td class="float8">58.1193389566675</td>
<td class="float8">57.0100855686799</td>
<td class="float8">57.0100855686799</td>
<td class="int4">561721</td>
<td class="float8">57.0100855686799</td>
<td class="float8">0.445218087206929</td>
</tr>
<tr>
<td class="float8">67.5442983367741</td>
<td class="float8">70.6858307610475</td>
<td class="float8">69.5764806582652</td>
<td class="float8">69.5764806582652</td>
<td class="int4">686886</td>
<td class="float8">69.5764806582652</td>
<td class="float8">0.445240002733585</td>
</tr>
<tr>
<td class="float8">70.6861408688866</td>
<td class="float8">73.8273966856673</td>
<td class="float8">71.7951245983548</td>
<td class="float8">71.7952263388403</td>
<td class="int4">711952</td>
<td class="float8">71.7951245983548</td>
<td class="float8">0.445297446856164</td>
</tr>
<tr>
<td class="float8">70.6861408688866</td>
<td class="float8">73.8273966856673</td>
<td class="float8">71.7951245983548</td>
<td class="float8">71.7952263388403</td>
<td class="int4">716508</td>
<td class="float8">71.7952263388403</td>
<td class="float8">0.445206347880861</td>
</tr>
<tr>
<td class="float8">76.9690608614191</td>
<td class="float8">80.1104563690938</td>
<td class="float8">78.0783171531357</td>
<td class="float8">78.0783171531357</td>
<td class="int4">778116</td>
<td class="float8">78.0783171531357</td>
<td class="float8">0.445290957467673</td>
</tr>
<tr>
<td class="float8">86.3938749629512</td>
<td class="float8">89.5353266705379</td>
<td class="float8">88.4260205388732</td>
<td class="float8">88.4260205388732</td>
<td class="int4">877138</td>
<td class="float8">88.4260205388732</td>
<td class="float8">0.445225639446173</td>
</tr>
<tr>
<td class="float8">89.5354929595716</td>
<td class="float8">92.6769711233795</td>
<td class="float8">90.6446926224232</td>
<td class="float8">90.6446926224232</td>
<td class="int4">903050</td>
<td class="float8">90.6446926224232</td>
<td class="float8">0.445286610427917</td>
</tr>
<tr>
<td class="float8">92.6770082124449</td>
<td class="float8">95.8184746547915</td>
<td class="float8">94.7092303088069</td>
<td class="float8">94.7092782433405</td>
<td class="int4">946181</td>
<td class="float8">94.7092303088069</td>
<td class="float8">0.445247543713327</td>
</tr>
<tr>
<td class="float8">92.6770082124449</td>
<td class="float8">95.8184746547915</td>
<td class="float8">94.7092303088069</td>
<td class="float8">94.7092782433405</td>
<td class="int4">942345</td>
<td class="float8">94.7092782433405</td>
<td class="float8">0.44529046414363</td>
</tr>
<tr>
<td class="float8">95.8185816861346</td>
<td class="float8">98.9601405610755</td>
<td class="float8">96.9279387165383</td>
<td class="float8">96.9279387165383</td>
<td class="int4">966815</td>
<td class="float8">96.9279387165383</td>
<td class="float8">0.445232181707114</td>
</tr>
<tr>
<td class="float8">98.9601765494391</td>
<td class="float8">100.992433886895</td>
<td class="float8">100.992433886895</td>
<td class="float8">100.992433886895</td>
<td class="int4">999931</td>
<td class="float8">100.992433886895</td>
<td class="float8">0.445263903548165</td>
</tr>
<tr class="statusbar">
<td colspan="100">31 rows fetched in 0.0071s (0.1153s)</td>
</tr>
</table>
</div>
<pre>
Nested Loop  (cost=67.09..25557916.04 rows=11222222 width=36)
  CTE d
    -&gt;  Recursive Union  (cost=0.08..65.97 rows=101 width=16)
          -&gt;  Subquery Scan q  (cost=0.08..0.80 rows=1 width=8)
                InitPlan 5 (returns $5)
                  -&gt;  Result  (cost=0.03..0.04 rows=1 width=0)
                        InitPlan 4 (returns $4)
                          -&gt;  Limit  (cost=0.00..0.03 rows=1 width=8)
                                -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.00..33799.01 rows=1000000 width=8)
                                      Filter: (value IS NOT NULL)
                -&gt;  Result  (cost=0.03..0.04 rows=1 width=0)
                      InitPlan 10 (returns $8)
                        -&gt;  Limit  (cost=0.00..0.03 rows=1 width=8)
                              -&gt;  Index Scan using ix_sine_value on t_sine  (cost=0.00..33799.01 rows=1000000 width=8)
                                    Filter: (value IS NOT NULL)
                SubPlan 3
                  -&gt;  Limit  (cost=0.22..0.26 rows=1 width=8)
                        InitPlan 2 (returns $3)
                          -&gt;  Limit  (cost=0.18..0.22 rows=1 width=8)
                                InitPlan 1 (returns $2)
                                  -&gt;  Limit  (cost=0.02..0.18 rows=1 width=8)
                                        -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.02..17938.04 rows=111111 width=8)
                                              Index Cond: (value &lt;= (((floor((($1 / 3.14159265358979::double precision) + 0.5::double precision)) + 1::double precision) - 0.5::double precision) * 3.14159265358979::double precision))
                                              Filter: (floor(((value / 3.14159265358979::double precision) + 0.5::double precision)) &lt;= floor((($1 / 3.14159265358979::double precision) + 0.5::double precision)))
                                -&gt;  Index Scan using ix_sine_value on t_sine  (cost=0.00..14604.70 rows=333333 width=8)
                                      Index Cond: (value &gt; $2)
                        -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.00..12104.70 rows=333333 width=8)
                              Index Cond: (value &lt; ($3)[1])
                SubPlan 7
                  -&gt;  Limit  (cost=0.18..0.22 rows=1 width=8)
                        InitPlan 6 (returns $6)
                          -&gt;  Limit  (cost=0.02..0.18 rows=1 width=8)
                                -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.02..17938.04 rows=111111 width=8)
                                      Index Cond: (value &lt;= (((floor((($1 / 3.14159265358979::double precision) + 0.5::double precision)) + 1::double precision) - 0.5::double precision) * 3.14159265358979::double precision))
                                      Filter: (floor(((value / 3.14159265358979::double precision) + 0.5::double precision)) &lt;= floor((($1 / 3.14159265358979::double precision) + 0.5::double precision)))
                        -&gt;  Index Scan using ix_sine_value on t_sine  (cost=0.00..14604.70 rows=333333 width=8)
                              Index Cond: (value &gt; $6)
                SubPlan 9
                  -&gt;  Limit  (cost=0.18..0.22 rows=1 width=8)
                        InitPlan 8 (returns $7)
                          -&gt;  Limit  (cost=0.02..0.18 rows=1 width=8)
                                -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.02..17938.04 rows=111111 width=8)
                                      Index Cond: (value &lt;= (((floor((($1 / 3.14159265358979::double precision) + 0.5::double precision)) + 1::double precision) - 0.5::double precision) * 3.14159265358979::double precision))
                                      Filter: (floor(((value / 3.14159265358979::double precision) + 0.5::double precision)) &lt;= floor((($1 / 3.14159265358979::double precision) + 0.5::double precision)))
                        -&gt;  Index Scan using ix_sine_value on t_sine  (cost=0.00..14604.70 rows=333333 width=8)
                              Index Cond: (value &gt; $7)
          -&gt;  WorkTable Scan on d  (cost=0.04..6.31 rows=10 width=16)
                Filter: (d.nextpiece IS NOT NULL)
                InitPlan 15 (returns $13)
                  -&gt;  Result  (cost=0.03..0.04 rows=1 width=0)
                        InitPlan 14 (returns $12)
                          -&gt;  Limit  (cost=0.00..0.03 rows=1 width=8)
                                -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.00..33799.01 rows=1000000 width=8)
                                      Filter: (value IS NOT NULL)
                SubPlan 13
                  -&gt;  Limit  (cost=0.19..0.23 rows=1 width=8)
                        InitPlan 12 (returns $11)
                          -&gt;  Limit  (cost=0.15..0.19 rows=1 width=8)
                                InitPlan 11 (returns $10)
                                  -&gt;  Limit  (cost=0.01..0.15 rows=1 width=8)
                                        -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.01..15438.04 rows=111111 width=8)
                                              Index Cond: (value &lt;= ((($9 + 1::double precision) - 0.5::double precision) * 3.14159265358979::double precision))
                                              Filter: (floor(((value / 3.14159265358979::double precision) + 0.5::double precision)) &lt;= $9)
                                -&gt;  Index Scan using ix_sine_value on t_sine  (cost=0.00..14604.70 rows=333333 width=8)
                                      Index Cond: (value &gt; $10)
                        -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.00..12104.70 rows=333333 width=8)
                              Index Cond: (value &lt; ($11)[1])
                SubPlan 17
                  -&gt;  Limit  (cost=0.15..0.19 rows=1 width=8)
                        InitPlan 16 (returns $14)
                          -&gt;  Limit  (cost=0.01..0.15 rows=1 width=8)
                                -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.01..15438.04 rows=111111 width=8)
                                      Index Cond: (value &lt;= ((($9 + 1::double precision) - 0.5::double precision) * 3.14159265358979::double precision))
                                      Filter: (floor(((value / 3.14159265358979::double precision) + 0.5::double precision)) &lt;= $9)
                        -&gt;  Index Scan using ix_sine_value on t_sine  (cost=0.00..14604.70 rows=333333 width=8)
                              Index Cond: (value &gt; $14)
                SubPlan 19
                  -&gt;  Limit  (cost=0.15..0.19 rows=1 width=8)
                        InitPlan 18 (returns $15)
                          -&gt;  Limit  (cost=0.01..0.15 rows=1 width=8)
                                -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.01..15438.04 rows=111111 width=8)
                                      Index Cond: (value &lt;= ((($9 + 1::double precision) - 0.5::double precision) * 3.14159265358979::double precision))
                                      Filter: (floor(((value / 3.14159265358979::double precision) + 0.5::double precision)) &lt;= $9)
                        -&gt;  Index Scan using ix_sine_value on t_sine  (cost=0.00..14604.70 rows=333333 width=8)
                              Index Cond: (value &gt; $15)
  -&gt;  CTE Scan on d  (cost=0.00..2.02 rows=101 width=24)
  -&gt;  Index Scan using ix_sine_value on t_sine s  (cost=1.12..2570.38 rows=111111 width=12)
        Index Cond: ((s.value &gt;= CASE ((d.piece)::integer % 2) WHEN 0 THEN (SubPlan 30) ELSE (SubPlan 32) END) AND (s.value &lt;= CASE ((d.piece)::integer % 2) WHEN 0 THEN (SubPlan 34) ELSE (SubPlan 36) END))
        SubPlan 30
          -&gt;  Limit  (cost=0.14..0.28 rows=1 width=8)
                InitPlan 29 (returns $24)
                  -&gt;  Limit  (cost=0.01..0.14 rows=1 width=8)
                        -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.01..225.76 rows=1667 width=8)
                              Index Cond: ((value &lt;= LEAST((($17 * 3.14159265358979::double precision) + 0.461397604523314::double precision), $18)) AND (value &gt;= $19))
                              Filter: (sin(value) &lt; 0.4452::double precision)
                -&gt;  Index Scan using ix_sine_value on t_sine  (cost=0.00..225.75 rows=1667 width=8)
                      Index Cond: ((value &gt;= COALESCE($24, $19)) AND (value &lt;= $18))
                      Filter: (sin(value) &gt;= 0.4452::double precision)
        SubPlan 32
          -&gt;  Limit  (cost=0.14..0.28 rows=1 width=8)
                InitPlan 31 (returns $25)
                  -&gt;  Limit  (cost=0.01..0.14 rows=1 width=8)
                        -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.01..225.76 rows=1667 width=8)
                              Index Cond: ((value &lt;= LEAST((($17 * 3.14159265358979::double precision) - 0.461509285667814::double precision), $18)) AND (value &gt;= $19))
                              Filter: (sin(value) &gt; 0.4453::double precision)
                -&gt;  Index Scan using ix_sine_value on t_sine  (cost=0.00..225.75 rows=1667 width=8)
                      Index Cond: ((value &gt;= COALESCE($25, $19)) AND (value &lt;= $18))
                      Filter: (sin(value) &lt;= 0.4453::double precision)
        SubPlan 34
          -&gt;  Limit  (cost=0.14..0.28 rows=1 width=8)
                InitPlan 33 (returns $26)
                  -&gt;  Limit  (cost=0.01..0.14 rows=1 width=8)
                        -&gt;  Index Scan using ix_sine_value on t_sine  (cost=0.01..225.76 rows=1667 width=8)
                              Index Cond: ((value &gt;= GREATEST((($17 * 3.14159265358979::double precision) + 0.461509285667814::double precision), $19)) AND (value &lt;= $18))
                              Filter: (sin(value) &gt; 0.4453::double precision)
                -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.00..225.75 rows=1667 width=8)
                      Index Cond: ((value &lt;= COALESCE($26, $18)) AND (value &gt;= $19))
                      Filter: (sin(value) &lt;= 0.4453::double precision)
        SubPlan 36
          -&gt;  Limit  (cost=0.14..0.28 rows=1 width=8)
                InitPlan 35 (returns $27)
                  -&gt;  Limit  (cost=0.01..0.14 rows=1 width=8)
                        -&gt;  Index Scan using ix_sine_value on t_sine  (cost=0.01..225.76 rows=1667 width=8)
                              Index Cond: ((value &gt;= GREATEST((($17 * 3.14159265358979::double precision) - 0.461397604523314::double precision), $19)) AND (value &lt;= $18))
                              Filter: (sin(value) &lt; 0.4452::double precision)
                -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.00..225.75 rows=1667 width=8)
                      Index Cond: ((value &lt;= COALESCE($27, $18)) AND (value &gt;= $19))
                      Filter: (sin(value) &gt;= 0.4452::double precision)
  SubPlan 22
    -&gt;  Limit  (cost=0.14..0.28 rows=1 width=8)
          InitPlan 21 (returns $20)
            -&gt;  Limit  (cost=0.01..0.14 rows=1 width=8)
                  -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.01..225.76 rows=1667 width=8)
                        Index Cond: ((value &lt;= LEAST((($17 * 3.14159265358979::double precision) + 0.461397604523314::double precision), $18)) AND (value &gt;= $19))
                        Filter: (sin(value) &lt; 0.4452::double precision)
          -&gt;  Index Scan using ix_sine_value on t_sine  (cost=0.00..225.75 rows=1667 width=8)
                Index Cond: ((value &gt;= COALESCE($20, $19)) AND (value &lt;= $18))
                Filter: (sin(value) &gt;= 0.4452::double precision)
  SubPlan 24
    -&gt;  Limit  (cost=0.14..0.28 rows=1 width=8)
          InitPlan 23 (returns $21)
            -&gt;  Limit  (cost=0.01..0.14 rows=1 width=8)
                  -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.01..225.76 rows=1667 width=8)
                        Index Cond: ((value &lt;= LEAST((($17 * 3.14159265358979::double precision) - 0.461509285667814::double precision), $18)) AND (value &gt;= $19))
                        Filter: (sin(value) &gt; 0.4453::double precision)
          -&gt;  Index Scan using ix_sine_value on t_sine  (cost=0.00..225.75 rows=1667 width=8)
                Index Cond: ((value &gt;= COALESCE($21, $19)) AND (value &lt;= $18))
                Filter: (sin(value) &lt;= 0.4453::double precision)
  SubPlan 26
    -&gt;  Limit  (cost=0.14..0.28 rows=1 width=8)
          InitPlan 25 (returns $22)
            -&gt;  Limit  (cost=0.01..0.14 rows=1 width=8)
                  -&gt;  Index Scan using ix_sine_value on t_sine  (cost=0.01..225.76 rows=1667 width=8)
                        Index Cond: ((value &gt;= GREATEST((($17 * 3.14159265358979::double precision) + 0.461509285667814::double precision), $19)) AND (value &lt;= $18))
                        Filter: (sin(value) &gt; 0.4453::double precision)
          -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.00..225.75 rows=1667 width=8)
                Index Cond: ((value &lt;= COALESCE($22, $18)) AND (value &gt;= $19))
                Filter: (sin(value) &lt;= 0.4453::double precision)
  SubPlan 28
    -&gt;  Limit  (cost=0.14..0.28 rows=1 width=8)
          InitPlan 27 (returns $23)
            -&gt;  Limit  (cost=0.01..0.14 rows=1 width=8)
                  -&gt;  Index Scan using ix_sine_value on t_sine  (cost=0.01..225.76 rows=1667 width=8)
                        Index Cond: ((value &gt;= GREATEST((($17 * 3.14159265358979::double precision) - 0.461397604523314::double precision), $19)) AND (value &lt;= $18))
                        Filter: (sin(value) &lt; 0.4452::double precision)
          -&gt;  Index Scan Backward using ix_sine_value on t_sine  (cost=0.00..225.75 rows=1667 width=8)
                Index Cond: ((value &lt;= COALESCE($23, $18)) AND (value &gt;= $19))
                Filter: (sin(value) &gt;= 0.4452::double precision)
</pre>
</div>
<p>This <strong>200</strong>-line monster completes in only <strong>110 ms</strong>, or <strong>30 times</strong> as fast as the original <strong>3</strong>-liner:</p>
<pre class="brush: sql">
SELECT  *
FROM    t_sine
WHERE   SIN(value) BETWEEN 0.1234 AND 0.1235
</pre>
<p>, yielding the same results.</p>
<h3>Summary</h3>
<p>This example was to demonstrate feasibility of the <strong>B-Tree</strong> indexes to be used in a search for the predicates involving monotonic functions and the performance gain achieved.</p>
<p>The performance gain is over <strong>30</strong> times for a table that fits completely into the cache, and will increase with the number of the cache misses increases.</p>
<p>The <strong>SQL</strong> implementation of the algorithm is in fact not optimal, since iterative searches for the value boundaries are implemented as the subqueries. Each subquery requires reentering the <strong>B-Tree</strong> and traversing it starting from the root. The native algorithm working within the optimizer could avoid this by caching the key position in the index and issuing <code>next_key</code> / <code>prev_key</code> commands, which would improve the algorithm yet more.</p>
<p>Sargability of the monotonic functions, as shown above, can help to make the queries like the one described in this article much more legible, maintainable and efficient.</p>
]]></content:encoded>
			<wfw:commentRss>http://explainextended.com/2010/02/23/sargability-of-monotonic-functions-example/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Things SQL needs: sargability of monotonic functions</title>
		<link>http://explainextended.com/2010/02/19/things-sql-needs-sargability-of-monotonic-functions/</link>
		<comments>http://explainextended.com/2010/02/19/things-sql-needs-sargability-of-monotonic-functions/#comments</comments>
		<pubDate>Fri, 19 Feb 2010 20:00:28 +0000</pubDate>
		<dc:creator>Quassnoi</dc:creator>
				<category><![CDATA[Miscellaneous]]></category>

		<guid isPermaLink="false">http://explainextended.com/?p=4149</guid>
		<description><![CDATA[I&#8217;m going to write a series of articles about the things SQL needs to work faster and more efficienly.
With these things implemented, most of the tricks I explain in my blog will become redundant and I&#8217;ll finally have some more time to spend with the family.
Ever wondered why a condition like this:
WHERE   TRUNC(mydate) [...]]]></description>
			<content:encoded><![CDATA[<div id="attachment_4352" class="wp-caption alignright" style="width: 410px"><img src="http://explainextended.com/wp-content/uploads/2010/02/graph.jpg" alt="" title="Graph" width="400" height="300" class="size-full wp-image-4352" /><p class="wp-caption-text"><small>Image by <a href='http://www.flickr.com/photos/ndevil/3491395689/'>ndevil</a></small></p></div>
<p>I&#8217;m going to write a series of articles about the things <strong>SQL</strong> needs to work faster and more efficienly.</p>
<p>With these things implemented, most of the tricks I explain in my blog will become redundant and I&#8217;ll finally have some more time to spend with the family.</p>
<p>Ever wondered why a condition like this:</p>
<p><code>WHERE   TRUNC(mydate) = TRUNC(SYSDATE)</code></p>
<p>, which searches for the current day&#8217;s records, is so elegant but so slow?</p>
<p>Of course this is because even if you create an index on <code>mydate</code>, this index cannot be used.</p>
<p>The expression in the left part of the equality is not a <code>mydate</code>. The database engine cannot find a way to use an index to search for it. It is said that this expression is not <a href="http://en.wikipedia.org/wiki/Sargable">sargable</a>.</p>
<p>Now, a little explanation about the indexes and sargability. If you are familiar with these, you can skip this chapter. But beware that this chapter is the only one illustrated, so skipping it will make the article too boring to read.</p>
<p>Ahem.</p>
<p>To locate a record in a <strong>B-Tree</strong> index, the keys of the index should be compared to the value being searched for.</p>
<p>Let&#8217;s consider this sample <strong>B-Tree</strong> index:</p>
<p><img src="http://explainextended.com/wp-content/uploads/2010/02/index.png" alt="" title="B-Tree" width="600" height="120" class="aligncenter size-full wp-image-4207 noborder" /><br />
<span id="more-4149"></span><br />
As you can see, this structure maintains the record order.</p>
<p>Within one page, records are just sorted; the links between the pages obey the sorting order too. A binary search can be used to locate the record on a page; if the search resulted in a pair of adjacent records and the search expression is within the range of these records, then we just follow the link between them. All records behind this link will belong to the range, no matter how deep is the tree.</p>
<p>This works well when we search for the exact field that was indexed.</p>
<p>But what happens when we search for a derived expression?</p>
<pre class="brush: sql">
SELECT  *
FROM    mytable
WHERE   value % 3 = 1
</pre>
<p>The <strong>B-Tree</strong> itself of course does not change. But, being casted to the expression we are searching for, the values stored will look like that:</p>
<p><img src="http://explainextended.com/wp-content/uploads/2010/02/mod3.png" alt="" title="MOD3" width="600" height="124" class="aligncenter size-full wp-image-4245 noborder" /></p>
<p>As you can see, there is no order anymore. The records are neither ordered within one page, nor the links follow the order. A <strong>1</strong> we are searching for can appear anywhere in the tree. There is no other way than to traverse the whole tree and compare each record.</p>
<p>It is said that this expression is <em>unsargable</em>: the index can not be used to search for this expression.</p>
<p>But let&#8217;s consider another example:</p>
<pre class="brush: sql">
SELECT  *
FROM    mytable
WHERE   value + 3 = 10
</pre>
<p>Here&#8217;s how the <strong>B-Tree</strong> looks now from the point of view of the expression in the <code>WHERE</code> clause</p>
<p><img src="http://explainextended.com/wp-content/uploads/2010/02/plus3.png" alt="" title="Plus3" width="600" height="124" class="aligncenter size-full wp-image-4248 noborder" /></p>
<p>Now, we have a perfectly valid <strong>B-Tree</strong>: everything stays in order and the search algorithms can still be used.</p>
<p>Another example (<code>DIV</code> is the integer division operator):</p>
<pre class="brush: sql">
SELECT  *
FROM    mytable
WHERE   value DIV 3 = 3
</pre>
<p>Here&#8217;s the <strong>B-Tree</strong> as seen from the expression&#8217;s side:</p>
<p><img src="http://explainextended.com/wp-content/uploads/2010/02/div3.png" alt="" title="DIV3" width="600" height="124" class="aligncenter size-full wp-image-4251 noborder" /></p>
<p>The order persists.</p>
<p>This <strong>B-Tree</strong> is a little bit harder to traverse, since the key values are not unique anymore. But the same values of the keys are still contiguous, and the order between the different keys is still maintained.</p>
<p>Is this case, the algorithm should be changed just a little. When a key is found in the database, we should just continue searching to the left until we find the very first occurrence of the key with lower value. Then we continue searching to the right and return all the records with the correct value of the key.</p>
<p>This problem is of course well known and has long since been solved by all database systems: you can create an index on a non-unique field.</p>
<p>Finally, let&#8217;s consider one more expression, a very simple one:</p>
<pre class="brush: sql">
SELECT  *
FROM    mytable
WHERE   20 - value = 10
</pre>
<p>and how it sees the <strong>B-Tree</strong>:</p>
<p><img src="http://explainextended.com/wp-content/uploads/2010/02/minus.png" alt="" title="Minus" width="600" height="124" class="aligncenter size-full wp-image-4255 noborder" /></p>
<p>In this case, the order is <em>reversed</em>. But there still is the order. The only difference is that the values are sorted right to left, not left to right, and all traversals should be made according to these new directions.</p>
<p>We see that some expressions break the <strong>B-Tree</strong> order, while the other ones maintain it.</p>
<p>What is the difference between the two?</p>
<p>For an expression to maintain the <strong>B-Tree</strong> order, it is necessary and sufficient for this expression to be a <a href="http://en.wikipedia.org/wiki/Monotonic_function">monotonic function</a> of the argument being indexed.</p>
<p>Here&#8217;s the definition of the monotonic function from Wikipedia:</p>
<blockquote><p>In calculus, a function <code>f</code> defined on a subset of the real numbers with real values is called monotonic (also monotonically increasing, increasing or non-decreasing), if for all <code>x</code> and <code>y</code> such that <code>x ≤ y</code> one has <code>f(x) ≤ f(y)</code>, so <code>f</code> <em>preserves</em> the order. Likewise, a function is called monotonically decreasing (also decreasing, or non-increasing) if, whenever <code>x ≤ y</code>, then <code>f(x) ≥ f(y)</code>, so it <em>reverses</em> the order.</p></blockquote>
<p>By definition, the monotonic function preserves (or reverses) the order, but does not break it.</p>
<p>This means that if an index is built over an expression, it also can be used to search over any monotonic function of this expression.</p>
<p>However, most query optimizers do not take this fact into account, while this behavior is relatively easy to implement.</p>
<h3>Sample implementation</h3>
<p>There will be many new keywords. I will introduce them all at the beginning and explain them later in the article. Also, there will be many fake functions, like <code>NEXT_COLLATION_CHARACTER</code> or <code>EXTRACT(calendar_date)</code>, which are absent in the actual engines but their name easily tells their purpose (and it&#8217;s quite simple to implement them anyway). These functions are used to demonstrate the concept.</p>
<p>A sargable function, as I shown above, needs to expose some monotony:</p>
<p><code><em>function</em>(<em>arg1</em>, …) <em><a href="#monotony_declaration">monotony_declaration</a></em></code></p>
<p>Function of two or more arguments can be sargable by some or all of it&#8217;s arguments:</p>
<p><code id="monotony_declaration"><em>monotony_declaration</em> := MONOTONIC ( <em><a href="#monotony_domain">monotony_domain</a></em> | OVER(<em>arg1</em>) <em><a href="#monotony_domain">monotony_domain</a></em> [, …])<br />
</code></p>
<p>When the function is a single-argument function, <code>OVER</code> clause should be omitted.</p>
<p><code id="monotony_domain"><em>monotony_domain</em> := (<em><a href="#monotony">monotony</a></em> | PIECEWISE <em><a href="#piecewise_monotony">piecewise_monotony</a></em>)<br />
</code></p>
<p>Monotony defines the direction and uniqueness of the function on the given range of the function&#8217;s domain:</p>
<p><code id="monotony"><em>monotony</em> := (STRICTLY INCREASING | INCREASING | STRICTLY DECREASING | DECREASING | UNDEFINED ) [ <em><a href="#inversion_clause">inversion_clause</a></em> ]</code></p>
<ul>
<li>
If monotony is <code>STRICTLY INCREASING</code>, it can be used for the <strong>B-Tree</strong> searches without changing the search order. A <code>UNIQUE</code> set of values is mapped to a <code>UNIQUE</code> set of the results of the function. The cardinality of the function over the indexed expression is the same as that of the expression.
</li>
<li>
If monotony is <code>INCREASING</code>, it can be used for the <strong>B-Tree</strong> searches without changing the search order. A <code>UNIQUE</code> set of values may be mapped to a non-unique set of the results, and no assumptions should be made about the uniqueness of the results. The cardinality of the function over the indexed expression may be not the same as that of the expression.
</li>
<li>
If monotony is <code>STRICTLY DECREASING</code>, it can be used for the <strong>B-Tree</strong> searches, changing the search order. A <code>UNIQUE</code> set of values is mapped to a <code>UNIQUE</code> set of the results of the function. The same algorithms that are used for scanning the index in the opposite order should be applied. The cardinality of the function over the indexed expression is the same as that of the expression.
</li>
<li>
If monotony is <code>STRICTLY DECREASING</code>, it can be used for the <strong>B-Tree</strong> searches, changing the search order. A <code>UNIQUE</code> set of values is mapped to a <code>UNIQUE</code> set of the results of the function. The same algorithms that are used for scanning the index in the opposite order should be applied. The cardinality of the function over the indexed expression may be not the same as that of the expression.
</li>
<li>
There is one more flavor, <code>UNDEFINED</code>, which states that the function is not monotonic or its monotony should not be relied upon. This has no sense as such, but can be used in the complex constructs we shall discuss below.
</li>
</ul>
<p>If the function is monotonic over the whole domain, then monotony is provided right after the keyword <code>MONOTONIC</code>.</p>
<p>However, a function can be piecewise monotonic. Its domain may consist of several (possibly infinite) number of pieces, with the function being monotonic over each piece but not across them.</p>
<h4>Piecewise monotony</h4>
<p><code id="piecewise_monotony"><em>piecewise_monotony</em> := PIECEWISE (<em><a href="#constant_piece_definition">constant_piece_definition</a></em> | <em><a href="#functional_piece_definition">functional_piece_definition</a></em>)</code></p>
<p>The pieces can be defined either by a set of constants or by another monotonic function, in which the pieces are defined by distinct values of the piece-defining function.</p>
<p><code id="constant_piece_definition"><em>constant_piece_definition</em> := WHEN VALUE [STRICTLY] LESS THAN <em>const</em> <em><a href="#monotony">monotony</a></em>, [ … ], WHEN VALUE LESS THAN MAXVALUE <em><a href="#monotony">monotony</a></em></code></p>
<p>Each <code>const</code> should be greater than the previous, and the last clause should be <code>WHEN VALUE THEN MAXVALUE</code>. The <code>const</code> comparison can be declared as strict: this may be important for handling discontinuities.</p>
<p><code id="functional_piece_definition"><em>functional_piece_definition</em> := DEFINED BY <em>piece_defining_function</em>(<em>arg</em>) CASE ( (WHEN <em>expression</em>(PIECE) THEN <em><a href="#monotony">monotony</a></em>) [ (…) | ELSE <em><a href="#monotony">monotony</a></em> ] ) END</code></p>
<p>Piece-defining function should itself be a monotonic function (but not a strict monotonic) over all domain of the original function.</p>
<p>The point of having this function is to split the domain into several pieces which themselves are sargable (hence the requirement for non-strict monotony), and then define the monotony within each piece.</p>
<p>The monotony for each piece is defined by <strong>SQL</strong>&#8217;s standard <code>CASE WHEN … THEN … ELSE … END</code> control flow construct (which of course returns monotonies instead of values).</p>
<h4>Two or more arguments</h4>
<p>A function of two or more arguments can be sargable too, as defined by <code><em><a href="#monotony_declaration">monotony_declaration</a></em></code>.</p>
<p>Each <code>OVER</code> clause defines monotony over one of the fuction&#8217;s arguments.</p>
<p>If an indexed field is used as a corresponding argument to this function and the other arguments are provided as constants or values from the leading table in the join, then the function is sargable.</p>
<h4>Superpositions</h4>
<p>When dealing with the superposition of functions, some assumptions can be made:</p>
<ul>
<li>A superposition of any number of <code>STRICTLY MONOTONIC</code> functions is <code>STRICTLY MONOTONIC</code>
</li>
<li>A superposition of any number of <code>STRICTLY MONOTONIC</code> functions and at least one <code>MONOTONIC</code> function is <code>MONOTONIC</code> (as opposed to <code>STRICTLY MONOTONIC</code>)
</li>
<li>A superposition of an <code>INCREASING</code> and a <code>DECREASING</code> function is <code>DECREASING</code>
</li>
<li>A superposition of an <code>INCREASING</code> and an <code>INCREASING</code> functions, or that of a <code>DECREASING</code> function and a <code>DECREASING</code> function is <code>INCREASING</code>
</li>
</ul>
<p>This only concerns functions monotonic over all domain (that is without <code>PIECEWISE</code> expressions).</p>
<h4>Inversions</h4>
<p><code id="inversion_clause">inversion_clause := INVERSE ( <em><a href="#inverse_expression">inverse_expression</a></em> | FROM <em><a href="#inverse_expression">inverse_expression</a></em> [ EXACT [ EXCLUDE ] ] TO <em><a href="#inverse_expression">inverse_expression</a></em> [ EXACT [ EXCLUDE ] ] )</code></p>
<p><code id="inverse_expression">inverse_expression := sql_expression ([ PIECE ], [ RESULT ], [ <em>non-over-arg1</em> [ ,… ] ])</code></p>
<p>Each monotony may be provided with an <em><a href="#inversion_clause">inversion_clause</a></em>, which maps the results back to the values of the arguments.</p>
<p>These expressions can accept the following arguments:</p>
<ul>
<li><code>RESULT</code>. This is the result of the function which needs to be mapped back to the value of the argument</li>
<li><code>PIECE</code>. This is the value defined by the piece-defining function. This only makes sense in a function-defined piecewise monotony</li>
<li><code>non-over-arg</code>. This is the argument to the function not used in the <code>OVER</code> clause. This only makes sense for multiple-argument functions.</li>
</ul>
<p>Inversion clause may come in two forms: scalar inversion and range inversion.</p>
<ul>
<li>
<p>A scalar inversion maps the function result to the single value of the argument.</p>
<p>If the monotony is strict, then the value of the expression is treated as a unique mapping. The decision on whether to return the value or not is made solely on the basis of the inverse result: function is not applied to the values taken from the index for more fine filtering.</p>
<p>If the monotony is not strict, them the value of the expression is treated as a reference point. Key reads back and forward to this reference point should be made with the function being applied to the result of each key read.</p>
</li>
<li>
<p>A range inversion maps the function result back to the range of values that are guaranteed to yield the <code>RESULT</code>, being provided as an argument to the function. The first expression provides the start value of the range, the second expression provides the end value.</p>
<p>Within the range, the results are returned from the index without any additional checking.</p>
<p>If any of the range expressions is marked as <code>EXACT</code>, then no values are returned outside the range in the corresponding direction. Otherwise, key reads are made in this direction and the function is applied to the results of the key reads to do the fine checking.</p>
<p>With <code>EXACT</code> clause, it is also possible to provide <code>EXCLUDE</code> clause. Depending on its presence, the corresponding range will be treated as including or not including the boundary.</p>
</li>
</ul>
<p>The point of allowing inexact inverse functions is to deal with the rounding errors introduced by floating point operations. Even if in theory the function can uniquely map back to a single value or a range of values, in practice, this may be not the case.</p>
<p>The <strong>B-Tree</strong> search is possible even without providing inverse functions at all. However, if inverse functions are provided, the number of the function evaluations is kept to the minimum: the function is only evaluated for several keys outside of the guaranteed range.</p>
<h3>Examples</h3>
<ul>
<li>
<p><code>EXP(arg FLOAT) MONOTONIC INCREASING INVERSE LN(RESULT)</code></p>
<p>Exponent function is increasing over the whole set of natural numbers. Its inverse function is natural logarithm.</p>
<p>Due to possible rounding errors, this function cannot be declared as strict.</p>
</li>
<li>
<p><code>YEAR(arg DATETIME) MONOTONIC INCREASING<br />
INVERSE<br />
FROM DATE_TRUNC('year', RESULT) EXACT<br />
TO DATE_TRUNC('year', RESULT) + INTERVAL 1 YEAR EXACT EXCLUDE</code></p>
<p><code>YEAR</code> is an increasing function but not a strictly increasing one: two distinct dates can share the same year. That&#8217;s why we provide two inverse functions: the first one provides the minimal possible date for a given year, the second one provides the maximal possible one. The latter function is marked as <code>EXCLUDE</code>, since the first date of the next year does not belong to the current year&#8217;s domain piece, but any value strictly less than it (and greater than the year&#8217;s beginning date) does.</p>
</li>
<li>
<p><code>ABS(arg INT) MONOTONIC PIECEWISE ON<br />
VALUES STRICTLY LESS THAN 0 STRICTLY DECREASING INVERSE -RESULT,<br />
VALUES LESS THAN MAXVALUE STRICTLY INCREASING INVERSE RESULT</code></p>
<p>The absolute value function is piecewise monotonic: it is strictly decreasing for the values below zero and strictly increasing for the values of zero and above.</p>
<p>The inverse functions for each piece are the negation of the result and result itself, accordingly.</p>
</li>
<li>
<p><code>FLOOR(arg FLOAT) MONOTONIC INCREASING<br />
INVERSE<br />
FROM RESULT EXACT<br />
TO RESULT + 1 EXACT EXCLUDE</code></p>
<p>The nearest lower integer function is monotonically increasing, almost the same as <code>YEAR</code> described above.</p>
</li>
<li>
<p><code>OPERATOR_DIVISION(arg1 FLOAT, arg2 FLOAT)<br />
MONOTONIC OVER (arg1)<br />
CASE WHEN arg2 > 0 THEN INCREASING INVERSE RESULT * arg2<br />
WHEN arg2 = 0 THEN UNDEFINED<br />
WHEN arg2 < 0 THEN DECREASING INVERSE RESULT * arg2<br />
END</code></p>
<p>This function defines division operator.</p>
<p>It is monotonic over the first argument. This means that the second argument should be provided as the run-time constant for it to be sargable: no field from the table being searched for can be used as a second argument to this function.</p>
<p>This function is monotonic over the whole domain of <code>arg1</code>, but the monotony depends on the value of <code>arg2</code>.</p>
<p>In theory, this function is strictly monotonic, but in practice, rounding errors can make the function yield same results for different values of <code>arg1</code>. That's why this function should be declared non-strict and a single inverse expression should be provided.</p>
</li>
<li>
<p><code>OPERATOR_PLUS(arg1 FLOAT, arg2 FLOAT) MONOTONIC<br />
OVER (arg1) STRICTLY INCREASING INVERSE RESULT - arg2,<br />
OVER (arg2) STRICTLY INCREASING INVERSE RESULT - arg1</code></p>
<p>This function defines the <code>+</code> operator.</p>
<p>This is quite simple: the function is strictly increasing over both arguments, and the inverse function is just a subtraction.</p>
</li>
<li>
<p><code>MONTH(arg DATETIME) MONOTONIC PIECEWISE<br />
DEFINED BY DATE_TRUNC('year', arg)<br />
INCREASING<br />
INVERSE<br />
FROM PIECE + INTERVAL RESULT - 1 MONTH EXACT<br />
TO PIECE + INTERVAL RESULT MONTH EXACT EXCLUDE</code></p>
<p><code>MONTH</code> returns the month number within a year.</p>
<p>This function is piecewise monotonic: within a year, higher dates always return same of higher months. Whenever the year changes, monotony breaks. <code>DATE_TRUNC</code> is monotonic and not strict, and, hence, can be used to define the pieces.</p>
<p>The reverse functions use both <code>PIECE</code> and <code>RESULT</code> arguments. The <code>PIECE</code> argument defines the current year, the <code>RESULT</code> is used to find the beginning of the current and the next months.</p>
</li>
<li>
<p><code>COS(arg FLOAT) MONOTONIC PIECEWISE<br />
DEFINED BY FLOOR(arg / PI())<br />
CASE PIECE % 2<br />
WHEN 0 THEN DECREASING INVERSE PIECE * PI() + ACOS(RESULT)<br />
ELSE INCREASING INVERSE (PIECE + 1) * PI() - ACOS(RESULT)<br />
END</code></p>
<p>Cosine is piecewise monotonic too. The pieces are defined by the cosine half-waves. Depending on the piece, it can be increasing or decreasing. The half-wave number is defined by <code>FLOOR(arg / PI())</code>. This piece-defining function, being a superposition of increasing functions, is increasing too.</p>
<p>On the even half-waves, the function is decreasing; on the odd half-waves, it is increasing. In principle, the function monotony is strict, but due to the possible rounding errors, it is marked as non-strict and only one inverse function provided.</p>
</li>
<li>
<p><code>SIN(arg FLOAT) MONOTONIC PIECEWISE<br />
DEFINED BY FLOOR(arg / PI() - 0.5)<br />
CASE PIECE % 2<br />
WHEN 0 THEN DECREASING INVERSE PIECE * PI() + ASIN(RESULT)<br />
ELSE INCREASING INVERSE PIECE * PI() - ASIN(RESULT)<br />
END</code></p>
<p>Same as cosine above, with a <code>&pi; / 2</code> shift.</p>
</li>
<li>
<p><code>LEFT(arg VARCHAR, length INT) MONOTONIC<br />
OVER(arg) INCREASING<br />
INVERSE<br />
FROM LEFT(arg, length) EXACT<br />
TO LEFT(arg, length - 1) || NEXT_COLLATION_CHARACTER(SUBSTRING(arg, length, 1)) EXACT EXCLUDE<br />
</code></p>
<p>Function <code>LEFT</code> takes the leading characters from the string.</p>
<p>It is sargable over the first argument: in the string is indexed, you can always search for a leading substring. I had to make up the function <code>NEXT_COLLATION_CHARACTER</code>, which would take next possible alphabet character according to the current collation.</p>
<p>Note that <strong>SQL Server</strong> and <strong>MySQL</strong> have traces of such optimization. In both systems, <code>LIKE</code> predicate is sargable if the search string does not contain the leading wildcards.</p>
<p>If you build a plan for a predicate like <code>column LIKE 'test%'</code>, you will see that is will use the index seek in this range:</p>
<p><code>column >= 'test' AND column < 'tesU'</code></p>
<p>This behavior is exactly like described above.</p>
</li>
</ul>
<h3>Search algorithm</h3>
<p>To select the values matching this condition:</p>
<p><code>function(col1 [ , arg2, … ]) = const</code></p>
<p>, the following procedures should be made:</p>
<ol>
<li>
<p>If the function is not sargable against the first argument, then just do the full table or index scan.</p>
</li>
<li>
<p>If the function is sargable against the first argument, check its monotony.</p>
</li>
<li>
<p>If the function is monotonic over all domain:</p>
<ol>
<li>
<p>If no inversion function is defined, then locate the values in the <strong>B-Tree</strong> using the <strong>B-Tree</strong> search algorithm and applying the function to each key before the comparison. The monotony direction (increasing or declreasing) should of course be taken into account.</p>
</li>
<li>
<p>If the function is strictly monotonic and a single inverse function is defined, apply the inverse function to the value of <code>const</code>, find the corresponding value of <code>col1</code> and locate it in the index using <strong>B-Tree</strong> search algorithm.</p>
</li>
<li>
<p>If the function is monotonic, but not strictly, check the inversion type:</p>
<ol>
<li>
<p>If a scalar inverse expression is defined, then:</p>
<ol>
<li>
<p>Apply the inverse function to <code>const</code> and find the key values in the <strong>B-Tree</strong> closest to the value of <code>inverse_expression(const)</code></p>
</li>
<li>
<p>Starting from <code>inverse_expression(const)</code>, iterate through the index back and forward, applying function to the each key found until the function exceeds (or falls short of) <code>const</code>, returning the records found.</p>
</li>
</ol>
</li>
<li>
<p>If a range inverse expression is defined, then:</p>
<ol>
<li>
<p>Find the beginning and the ending values of the <code>col1</code> range that yields <code>const</code><br />
If the beginning of range is not exact, iterate keys backwards, applying the function to each key found until the function equals to <code>const</code>.</p>
</li>
<li>
<p>Iterate the keys forward, returning the records found, until the end of the range is reached.</p>
</li>
<li>
<p>If the range end is not exact, then iterate keys beyond the range, applying the function to the keys found, until the function value exceeds or falls short of <code>const</code></p>
</li>
</ol>
</li>
</ol>
</li>
</ol>
</li>
<li>
<p>If the function is piecewise monotonic:</p>
<ol>
<li>
<p>If the pieces are defined by a set of constants, split the domain into the pieces, then apply the steps above to each piece separately with respect to the monotony of each piece.</p>
</li>
<li>
<p>If the pieces are defined by a function:</p>
<ol>
<li>
<p>Use loose index scan to build a distinct list of the function's values (each defining a piece). The loose index scan will jump over the <strong>B-Tree</strong>, starting from the minimal (or maximal) value of the function and recursively searching for the greater (or the lesser value). Each distinct value will reveal the least and the greatest value of the <code>col1</code> belonging to its piece.</p>
</li>
<li>
<p>Within the range defined by these values, apply the steps above, with respect to the monotony of each piece.</p>
</li>
</ol>
</li>
<li>
<p>If for any of the pieces the monotony is <code>UNDEFINED</code>, then just apply the function to every key in the piece's range, filtering the results.</p>
</li>
</ol>
</li>
</ol>
<h3>User-defined functions</h3>
<p>All examples above were applied to the built-in functions. In fact, they don't need declaration and all clauses above were added only to demonstrate the point.</p>
<p>However, with a little effort, the user-defined functions can be made sargable too.</p>
<p>Here are some suggestions on how to make it:</p>
<ol>
<li>To be sargable, a user-defined function should be strictly deterministic, that is its result should depend on and only on the values of the arguments (specifically, it should not depend on the database state)</li>
<li>If a function is a superposition of the monotonic functions (with the additional constraints stated above), it should be automatically treated as monotonic.</li>
<li>
<p>If a function is declared as monotonic by a user, special algorithms should be used to check its monotony (and hence sargability):</p>
<ol>
<li>
<p>Sargability of a monotonic user-defined function should be a property of index-function pair. Function's declared monotony is a necessary but not sufficient condition for sargability.</p>
<p>By default, sargability of index-function pair is <q>unknown</q>.</p>
</li>
<li>
<p>Whenever a query is issued which could be satisfied by using the index to search for the function values, the index-function pair is marked as <q>potentially sargable</q>. This property can be made dynamic: say, <strong>10</strong> or <strong>100</strong> queries should be issued before the sargability of the index-function pair can be checked.</p>
</li>
<li>When the query threshold is reached (this means that the certain number of queries which possibly could use the index had been run), the index checking process is initiated.</li>
<li>The database job which is responsible for collecting the index statistics, runs over the index and checks the declared monotony of the function over the given argument: within each piece, higher values of argument should map to higher (or lower, or not higher, or not lower) values of the function. If the function is piecewise monotonic with pieces defined by a user-defined function as well, the monotony of the piece-defined function should also be checked in the same routine.</li>
<li>If the check succeeds, the index-function is marked as <q>sargable</q> both for the function and its piece-defining function (if any).</li>
<li>At the same time, the statistics routine should collect the additional statistics for the function as if it was the index over the function's values.</li>
<li>
<p>If an index is marked as <q>usable</q> for any given function, all <code>DML</code> operations against this index should perform an additional validation for the function monotony:</p>
<ol>
<li>Whenever a new record is inserted into the index, the function should be applied to the values of the key inserted and both of its neighbors. The value of the function over the key inserted should be between (or, depending on the function's monotony, strictly between) the values of the function over the neighboring keys.</li>
<li>If the function is declared as piecewise monotonic with a piece defining function, the latter is checked first.</li>
<li>
<p>If any of the neighboring keys belongs to a piece different to that of the key being inserted, that neighboring key will not participate in the check.</p>
</li>
</ol>
</li>
<li>A failure to prove the function's monotony invalidates the function's usage in all indexes and also invalidates the sargability of all functions that use it as a piece-defining function. Additionally, the values of the arguments which are proven to violate the function's monotony are recorded with the function's metadata.
</li>
<li>Any change to the function's definition invalidates all sargability information linked with this function, as well as linked with the functions that uses it to define the pieces.</li>
<li>
<p>For any index-function pair, the validation and statistics collection process can be initiated manually. The database engine should provide means for that.</p>
</li>
</ol>
</li>
</ol>
<h3>Points of interest</h3>
<p>Function sargability may be used to improve many queries, specifically:</p>
<ul>
<li>
<p>Searching for dates within a given period (month, date, year) without a need to calculate the ranges manually:</p>
<pre class="brush: sql">
SELECT  *
FROM    mytable
WHERE   EXTRACT(YEAR_MONTH FROM col1) = &#039;201002&#039;
</pre>
</li>
<li>
<p>Searching for birthdates and calendar events:</p>
<pre class="brush: sql">
SELECT  *
FROM    mytable
WHERE   EXTRACT(calendar_date FROM mydate) = &#039;0310&#039;
</pre>
</li>
<li>
<p>Searching for the substrings:</p>
<pre class="brush: sql">
SELECT  *
FROM    mytable
WHERE   LEFT(value, 10) = &#039;abcdefghij&#039;
</pre>
</li>
<li>
<p>Searching for the <code>COALESCE</code>'d values:</p>
<pre class="brush: sql">
SELECT  *
FROM    mytable
WHERE   COALESCE(value, &#039;test&#039;) = &#039;test&#039;
</pre>
</li>
</ul>
<p>and many others.</p>
<p>I really hope that this will be added to the major database engines in the nearest future.</p>
]]></content:encoded>
			<wfw:commentRss>http://explainextended.com/2010/02/19/things-sql-needs-sargability-of-monotonic-functions/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Tags in nested sets: efficient indexing</title>
		<link>http://explainextended.com/2010/02/16/tags-in-nested-sets-efficient-indexing/</link>
		<comments>http://explainextended.com/2010/02/16/tags-in-nested-sets-efficient-indexing/#comments</comments>
		<pubDate>Tue, 16 Feb 2010 20:00:11 +0000</pubDate>
		<dc:creator>Quassnoi</dc:creator>
				<category><![CDATA[MySQL]]></category>

		<guid isPermaLink="false">http://explainextended.com/?p=4326</guid>
		<description><![CDATA[Answering questions asked on the site.
Travis asks:
I just read your excellent post: Hierarchical data in MySQL: parents and children in one query.
I am currently trying to do something very similar to this.  The main difference is, I am working with data that is in a nested set.
I would like to construct a query that [...]]]></description>
			<content:encoded><![CDATA[<p>Answering questions asked on the site.</p>
<p><strong>Travis</strong> asks:</p>
<blockquote><p>I just read your excellent post: <a href="/2009/07/20/hierarchical-data-in-mysql-parents-and-children-in-one-query/"><strong>Hierarchical data in MySQL: parents and children in one query</strong></a>.</p>
<p>I am currently trying to do something very similar to this.  The main difference is, I am working with data that is in a nested set.</p>
<p>I would like to construct a query that will return a resultset of nodes matched with <code>LIKE</code>, the ancestors of each of these nodes, and the immediate children of each of these nodes.
</p></blockquote>
<p>This is a very interesting question: it allows to demonstrate the usage of three types of indexes usable by <strong>MyISAM</strong> tables: <code>BTREE</code>, <code>SPATIAL</code> and <code>FULLTEXT</code>.</p>
<p><a href="http://en.wikipedia.org/wiki/Nested_set_model">Nested sets</a> is one of two models that are most often used to store hierarchical data in a relational table (the other one being <a href="http://en.wikipedia.org/wiki/Adjacency_list">adjacency list model</a>). Unlike adjacency list, nested sets does not require ability to write recursive queries, which <strong>SQL</strong> originally lacked and still lacks now in <strong>MySQL</strong> (albeit it can be emulated to some extent). It is widely used in <strong>MySQL</strong> world.</p>
<p>I described both methods and their comparison in the article I wrote some time ago:</p>
<ul>
<li><a href="/2009/09/29/adjacency-list-vs-nested-sets-mysql/"><strong>Adjacency list vs. nested sets: MySQL</strong></a></li>
</ul>
<p>The main problem with the nested sets model is that though it is extremely fast with selecting descendants of a given node, it is very slow in selecting ancestors.</p>
<p>This is because of the way the <strong>B-Tree</strong> indexes work. They can be used to query for values of a column within the range of two constants, but not for the values of two columns holding a single constant between them. One needs the first condition to select the children (found between the <code>lft</code> and <code>rgt</code> of the given node), and the second condition to select the ancestor (with <code>lft</code> and <code>rgt</code> containing the <code>lft</code> and <code>rgt</code> of the given node).</p>
<p>That&#8217;s why selecting the children is fast and selecting the ancestors is slow.</p>
<p>To work around this, the sets that form the hierarchy can be described as geometrical objects, with the larger sets containing the smaller sets. These sets can be indexed with a <code>SPATIAL</code> index which is designed specially for this purpose and both children and ancestors can be queried for very efficiently.</p>
<p>Unfortunately, finding the depth level is quite a hard task for the nested sets model even with the <code>SPATIAL</code> indexes.</p>
<p>It would be quite an easy task is <strong>MySQL</strong> supported recursion: we could just run a query to find the siblings of each record by skipping their whole domains recursively.</p>
<p>However, <strong>MySQL</strong>&#8217;s recursion support is very limited and it relies on the session variables, which are not recommended to use in the complex queries.</p>
<p>To cope with this, we need to mix the nested sets and the adjacency list models. Hierarchy will be stored in two seemingly redundant ways: the unique <code>parent</code> and the <code>LineString</code> representing the nested sets.</p>
<p>This will help us to use the <strong>R-Tree</strong> index to find all ancestors of a given node and also use <strong>B-Tree</strong> index to find its immediate children.</p>
<p>Finally, the question mentions using <code>LIKE</code> to find the initial nodes. <code>LIKE</code> predicate with the leading wildcards is not sargable in <strong>MySQL</strong>. However, it seems that the leading wildcards are only used to split the words. In this case, a <code>FULLTEXT</code> index and the <code>MATCH </code>query would be much more efficient, since <code>FULLTEXT</code> index allows indexing a single record with several keys (each one corresponding to a single word in the column&#8217;s text), so a search for the word in the space separated or a comma separated list uses the index and is much faster than scanning the whole table.</p>
<p>Hence, the query would use all three main types of indexes: <code>BTREE</code>, <code>SPATIAL</code> and <code>FULLTEXT</code>.</p>
<p>To illustrate everything said above, let&#8217;s create a sample table:<br />
<span id="more-4326"></span><br />
<a href="#" onclick="xcollapse('X9716');return false;"><strong>Table creation details</strong></a><br />
</p>
<div id="X9716" style="display: none; ">
<pre class="brush: sql">
CREATE TABLE filler (
        id INT NOT NULL PRIMARY KEY AUTO_INCREMENT
) ENGINE=Memory;

CREATE TABLE t_hierarchy (
        id INT NOT NULL PRIMARY KEY,
        parent INT NOT NULL,
        lft INT NOT NULL,
        rgt INT NOT NULL,
        sets LineString NOT NULL,
        data VARCHAR(100) NOT NULL,
        tags TEXT NOT NULL
) ENGINE=MyISAM;

DELIMITER $$

CREATE PROCEDURE prc_filler(cnt INT)
BEGIN
        DECLARE _cnt INT;
        SET _cnt = 1;
        WHILE _cnt &lt;= cnt DO
                INSERT
                INTO    filler
                SELECT  _cnt;
                SET _cnt = _cnt + 1;
        END WHILE;
END;

CREATE PROCEDURE prc_hierarchy(width INT)
main:BEGIN
        DECLARE last INT;
        DECLARE level INT;
        SET last = 0;
        SET level = 0;
        WHILE width &gt;= 1 DO
                INSERT
                INTO    t_hierarchy
                SELECT  COALESCE(h.id, 0) * 5 + f.id,
                        COALESCE(h.id, 0),
                        COALESCE(h.lft, 0) + 1 + (f.id - 1) * width,
                        COALESCE(h.lft, 0) + f.id * width,
                        LineString(
                        Point(-1, COALESCE(h.lft, 0) + 1 + (f.id - 1) * width),
                        Point(1, COALESCE(h.lft, 0) + f.id * width)
                        ),
                        CONCAT(&#039;Value &#039;, COALESCE(h.id, 0) * 5 + f.id),
                        (
                        SELECT  GROUP_CONCAT(CONCAT(&#039;tag&#039;, FLOOR(RAND(20100216 + width * 1000) * 300000)) SEPARATOR &#039; &#039;)
                        FROM    (
                                SELECT  1
                                UNION ALL
                                SELECT  1
                                UNION ALL
                                SELECT  1
                                ) q
                        )
                FROM    filler f
                LEFT JOIN
                        t_hierarchy h
                ON      h.id &gt;= last;
                SET width = width / 5;
                SET last = last + POWER(5, level);
                SET level = level + 1;
        END WHILE;
END
$$

DELIMITER ;

START TRANSACTION;
CALL prc_filler(5);
CALL prc_hierarchy(117187);
COMMIT;

CREATE INDEX ix_hierarchy_parent ON t_hierarchy (parent);
CREATE UNIQUE INDEX ix_hierarchy_lft ON t_hierarchy (lft);
CREATE UNIQUE INDEX ix_hierarchy_rgt ON t_hierarchy (rgt);
CREATE SPATIAL INDEX sx_hierarchy_sets ON t_hierarchy (sets);
CREATE FULLTEXT INDEX fx_hierarchy_tags ON t_hierarchy (tags);
</pre>
</div>
<p>This table represents a <strong>7</strong> level hierarchy, with <strong>5</strong> children to each non-leaf item and combines nested sets and adjacency list models.</p>
<p>The sets are stored in the plain <code>lft</code> and <code>rgt</code> columns as well as in the combined column of type <code>LineString</code> which represents a diagonal of a box horizontally spanning the interval from <code>lft</code> to <code>rgt</code>.</p>
<h3>Selecting nodes using FULLTEXT</h3>
<p>First, let&#8217;s select the nodes tagged with a given tag.</p>
<p><code>RLIKE</code> can be used for that but it is not very efficient:</p>
<pre class="brush: sql">
SELECT  id, tags
FROM    t_hierarchy
WHERE   tags RLIKE &#039;[[:&lt;:]]tag13480[[:&gt;:]]&#039;
</pre>
<p><a href="#" onclick="xcollapse('X10844');return false;"><strong>View query results</strong></a><br />
</p>
<div id="X10844" style="display: none; ">
<div class="terminal">
<table class="terminal">
<tr>
<th>id</th>
<th>tags</th>
</tr>
<tr>
<td class="integer">3</td>
<td class="blob">tag13480 tag33087 tag124996</td>
</tr>
<tr>
<td class="integer">440166</td>
<td class="blob">tag248489 tag271789 tag13480</td>
</tr>
<tr>
<td class="integer">212847</td>
<td class="blob">tag104605 tag13480 tag53585</td>
</tr>
<tr>
<td class="integer">161378</td>
<td class="blob">tag181040 tag231320 tag13480</td>
</tr>
<tr>
<td class="integer">324394</td>
<td class="blob">tag13480 tag181947 tag269297</td>
</tr>
<tr>
<td class="integer">400074</td>
<td class="blob">tag13480 tag176642 tag242772</td>
</tr>
<tr class="statusbar">
<td colspan="100">6 rows fetched in 0.0006s (1.2500s)</td>
</tr>
</table>
</div>
<div class="terminal">
<table class="terminal">
<tr>
<th>id</th>
<th>select_type</th>
<th>table</th>
<th>type</th>
<th>possible_keys</th>
<th>key</th>
<th>key_len</th>
<th>ref</th>
<th>rows</th>
<th>filtered</th>
<th>Extra</th>
</tr>
<tr>
<td class="bigint">1</td>
<td class="varchar">SIMPLE</td>
<td class="varchar">t_hierarchy</td>
<td class="varchar">ALL</td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="bigint">488280</td>
<td class="double">100.00</td>
<td class="varchar">Using where</td>
</tr>
</table>
</div>
<pre>
select `20100216_index`.`t_hierarchy`.`id` AS `id`,`20100216_index`.`t_hierarchy`.`tags` AS `tags` from `20100216_index`.`t_hierarchy` where (`20100216_index`.`t_hierarchy`.`tags` regexp &#39;[[:&lt;:]]tag13480[[:&gt;:]]&#39;)
</pre>
</div>
<p>This query uses full table scan and runs for <strong>1.25 second</strong>.</p>
<p>To improve the query, we should rewrite the <code>WHERE</code> condition using <code>MATCH</code> predicate (which in its turn allows a <code>FULLTEXT</code> index to be used for the search):</p>
<pre class="brush: sql">
SELECT  id, tags
FROM    t_hierarchy
WHERE   MATCH(tags) AGAINST(&#039;+tag13480&#039; IN BOOLEAN MODE)
</pre>
<div class="terminal">
<table class="terminal">
<tr>
<th>id</th>
<th>tags</th>
</tr>
<tr>
<td class="integer">3</td>
<td class="blob">tag13480 tag33087 tag124996</td>
</tr>
<tr>
<td class="integer">440166</td>
<td class="blob">tag248489 tag271789 tag13480</td>
</tr>
<tr>
<td class="integer">212847</td>
<td class="blob">tag104605 tag13480 tag53585</td>
</tr>
<tr>
<td class="integer">161378</td>
<td class="blob">tag181040 tag231320 tag13480</td>
</tr>
<tr>
<td class="integer">324394</td>
<td class="blob">tag13480 tag181947 tag269297</td>
</tr>
<tr>
<td class="integer">400074</td>
<td class="blob">tag13480 tag176642 tag242772</td>
</tr>
<tr class="statusbar">
<td colspan="100">6 rows fetched in 0.0005s (0.0043s)</td>
</tr>
</table>
</div>
<div class="terminal">
<table class="terminal">
<tr>
<th>id</th>
<th>select_type</th>
<th>table</th>
<th>type</th>
<th>possible_keys</th>
<th>key</th>
<th>key_len</th>
<th>ref</th>
<th>rows</th>
<th>filtered</th>
<th>Extra</th>
</tr>
<tr>
<td class="bigint">1</td>
<td class="varchar">SIMPLE</td>
<td class="varchar">t_hierarchy</td>
<td class="varchar">fulltext</td>
<td class="varchar">fx_hierarchy_tags</td>
<td class="varchar">fx_hierarchy_tags</td>
<td class="varchar">0</td>
<td class="varchar"></td>
<td class="bigint">1</td>
<td class="double">100.00</td>
<td class="varchar">Using where</td>
</tr>
</table>
</div>
<pre>
select `20100216_index`.`t_hierarchy`.`id` AS `id`,`20100216_index`.`t_hierarchy`.`tags` AS `tags` from `20100216_index`.`t_hierarchy` where (match `20100216_index`.`t_hierarchy`.`tags` against (&#39;+tag13480&#39; in boolean mode))
</pre>
<p>This query returns the same results but does it much faster, in only <strong>4 ms</strong>.</p>
<h3>Selecting ancestors using SPATIAL</h3>
<p>Now, when we have a list of nodes, we should build the list of ancestors for each node.</p>
<p>Since our model combines adjacency list and nested sets, it is possible to use either representation to build a query. However, the adjacency list model requires recursion, and, while it is possible to emulate it, it only works for a single-node query.</p>
<p>With nested sets, selecting the list of ancestors is much more simple: we should just selecting all records whose <code>sets</code> contains the <code>sets</code> of the node. This can be done using <code>MBRContains</code> (which is capable of using the <code>SPATIAL</code> index).</p>
<p>The query, however, will return us the ancestors in a plain list. To find out the level of each ancestor, we should put some more effort. Since the sets are nested and the <code>lft</code> and <code>rgt</code> fields naturally maintain the hierarchical order, it is enough just to enumerate the ancestors in that order. It would be very simple to do — if only <strong>MySQL</strong> supported <code>ROW_NUMBER()</code>. But it doesn&#8217;t, of course, so to enumerate the ancestors we should self-join them and just count the number of each ancestor&#8217;s ancestors:</p>
<pre class="brush: sql">
SELECT  hc.id, ha.id, COUNT(*) AS cnt
FROM    t_hierarchy hc
STRAIGHT_JOIN
        t_hierarchy ha
ON      MBRContains(ha.sets, hc.sets)
STRAIGHT_JOIN
        t_hierarchy hcnt
ON      MBRContains(hcnt.sets, ha.sets)
WHERE   MATCH(hc.tags) AGAINST(&#039;+tag13480&#039; IN BOOLEAN MODE)
GROUP BY
        hc.id, ha.id
ORDER BY
        hc.id, cnt
</pre>
<p><a href="#" onclick="xcollapse('X8308');return false;"><strong>View query results</strong></a><br />
</p>
<div id="X8308" style="display: none; ">
<div class="terminal">
<table class="terminal">
<tr>
<th>id</th>
<th>id</th>
<th>cnt</th>
</tr>
<tr>
<td class="integer">3</td>
<td class="integer">3</td>
<td class="bigint">1</td>
</tr>
<tr>
<td class="integer">161378</td>
<td class="integer">1</td>
<td class="bigint">1</td>
</tr>
<tr>
<td class="integer">161378</td>
<td class="integer">10</td>
<td class="bigint">2</td>
</tr>
<tr>
<td class="integer">161378</td>
<td class="integer">51</td>
<td class="bigint">3</td>
</tr>
<tr>
<td class="integer">161378</td>
<td class="integer">257</td>
<td class="bigint">4</td>
</tr>
<tr>
<td class="integer">161378</td>
<td class="integer">1290</td>
<td class="bigint">5</td>
</tr>
<tr>
<td class="integer">161378</td>
<td class="integer">6454</td>
<td class="bigint">6</td>
</tr>
<tr>
<td class="integer">161378</td>
<td class="integer">32275</td>
<td class="bigint">7</td>
</tr>
<tr>
<td class="integer">161378</td>
<td class="integer">161378</td>
<td class="bigint">8</td>
</tr>
<tr>
<td class="integer">212847</td>
<td class="integer">2</td>
<td class="bigint">1</td>
</tr>
<tr>
<td class="integer">212847</td>
<td class="integer">13</td>
<td class="bigint">2</td>
</tr>
<tr>
<td class="integer">212847</td>
<td class="integer">67</td>
<td class="bigint">3</td>
</tr>
<tr>
<td class="integer">212847</td>
<td class="integer">340</td>
<td class="bigint">4</td>
</tr>
<tr>
<td class="integer">212847</td>
<td class="integer">1702</td>
<td class="bigint">5</td>
</tr>
<tr>
<td class="integer">212847</td>
<td class="integer">8513</td>
<td class="bigint">6</td>
</tr>
<tr>
<td class="integer">212847</td>
<td class="integer">42569</td>
<td class="bigint">7</td>
</tr>
<tr>
<td class="integer">212847</td>
<td class="integer">212847</td>
<td class="bigint">8</td>
</tr>
<tr>
<td class="integer">324394</td>
<td class="integer">3</td>
<td class="bigint">1</td>
</tr>
<tr>
<td class="integer">324394</td>
<td class="integer">20</td>
<td class="bigint">2</td>
</tr>
<tr>
<td class="integer">324394</td>
<td class="integer">103</td>
<td class="bigint">3</td>
</tr>
<tr>
<td class="integer">324394</td>
<td class="integer">518</td>
<td class="bigint">4</td>
</tr>
<tr>
<td class="integer">324394</td>
<td class="integer">2594</td>
<td class="bigint">5</td>
</tr>
<tr>
<td class="integer">324394</td>
<td class="integer">12975</td>
<td class="bigint">6</td>
</tr>
<tr>
<td class="integer">324394</td>
<td class="integer">64878</td>
<td class="bigint">7</td>
</tr>
<tr>
<td class="integer">324394</td>
<td class="integer">324394</td>
<td class="bigint">8</td>
</tr>
<tr>
<td class="integer">400074</td>
<td class="integer">4</td>
<td class="bigint">1</td>
</tr>
<tr>
<td class="integer">400074</td>
<td class="integer">25</td>
<td class="bigint">2</td>
</tr>
<tr>
<td class="integer">400074</td>
<td class="integer">127</td>
<td class="bigint">3</td>
</tr>
<tr>
<td class="integer">400074</td>
<td class="integer">639</td>
<td class="bigint">4</td>
</tr>
<tr>
<td class="integer">400074</td>
<td class="integer">3200</td>
<td class="bigint">5</td>
</tr>
<tr>
<td class="integer">400074</td>
<td class="integer">16002</td>
<td class="bigint">6</td>
</tr>
<tr>
<td class="integer">400074</td>
<td class="integer">80014</td>
<td class="bigint">7</td>
</tr>
<tr>
<td class="integer">400074</td>
<td class="integer">400074</td>
<td class="bigint">8</td>
</tr>
<tr>
<td class="integer">440166</td>
<td class="integer">5</td>
<td class="bigint">1</td>
</tr>
<tr>
<td class="integer">440166</td>
<td class="integer">27</td>
<td class="bigint">2</td>
</tr>
<tr>
<td class="integer">440166</td>
<td class="integer">140</td>
<td class="bigint">3</td>
</tr>
<tr>
<td class="integer">440166</td>
<td class="integer">704</td>
<td class="bigint">4</td>
</tr>
<tr>
<td class="integer">440166</td>
<td class="integer">3521</td>
<td class="bigint">5</td>
</tr>
<tr>
<td class="integer">440166</td>
<td class="integer">17606</td>
<td class="bigint">6</td>
</tr>
<tr>
<td class="integer">440166</td>
<td class="integer">88033</td>
<td class="bigint">7</td>
</tr>
<tr>
<td class="integer">440166</td>
<td class="integer">440166</td>
<td class="bigint">8</td>
</tr>
<tr class="statusbar">
<td colspan="100">41 rows fetched in 0.0036s (0.0258s)</td>
</tr>
</table>
</div>
<div class="terminal">
<table class="terminal">
<tr>
<th>id</th>
<th>select_type</th>
<th>table</th>
<th>type</th>
<th>possible_keys</th>
<th>key</th>
<th>key_len</th>
<th>ref</th>
<th>rows</th>
<th>filtered</th>
<th>Extra</th>
</tr>
<tr>
<td class="bigint">1</td>
<td class="varchar">SIMPLE</td>
<td class="varchar">hc</td>
<td class="varchar">fulltext</td>
<td class="varchar">sx_hierarchy_sets,fx_hierarchy_tags</td>
<td class="varchar">fx_hierarchy_tags</td>
<td class="varchar">0</td>
<td class="varchar"></td>
<td class="bigint">1</td>
<td class="double">100.00</td>
<td class="varchar">Using where; Using temporary; Using filesort</td>
</tr>
<tr>
<td class="bigint">1</td>
<td class="varchar">SIMPLE</td>
<td class="varchar">ha</td>
<td class="varchar">ALL</td>
<td class="varchar">sx_hierarchy_sets</td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="bigint">488280</td>
<td class="double">100.00</td>
<td class="varchar">Range checked for each record (index map: 0&#215;10)</td>
</tr>
<tr>
<td class="bigint">1</td>
<td class="varchar">SIMPLE</td>
<td class="varchar">hcnt</td>
<td class="varchar">ALL</td>
<td class="varchar">sx_hierarchy_sets</td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="bigint">488280</td>
<td class="double">100.00</td>
<td class="varchar">Range checked for each record (index map: 0&#215;10)</td>
</tr>
</table>
</div>
<pre>
select `20100216_index`.`hc`.`id` AS `id`,`20100216_index`.`ha`.`id` AS `id`,count(0) AS `cnt` from `20100216_index`.`t_hierarchy` `hc` straight_join `20100216_index`.`t_hierarchy` `ha` straight_join `20100216_index`.`t_hierarchy` `hcnt` where ((match `20100216_index`.`hc`.`tags` against (&#39;+tag13480&#39; in boolean mode)) and contains(`20100216_index`.`hcnt`.`sets`,`20100216_index`.`ha`.`sets`) and contains(`20100216_index`.`ha`.`sets`,`20100216_index`.`hc`.`sets`)) group by `20100216_index`.`hc`.`id`,`20100216_index`.`ha`.`id` order by `20100216_index`.`hc`.`id`,count(0)
</pre>
</div>
<p>The algorithm that calculates the level of each ancestor is not very efficient, however, it does its job quite well, and the query completes in only <strong>20 ms</strong>.</p>
<h3>Selecting immediate children using BTREE</h3>
<p>Selecting the immediate children is the easiest task: we just need an equijoin on <code>parent</code>. The level of each of the node&#8217;s children will be that of the node plus <strong>1</strong>, so calculating it is quite simple too:</p>
<pre class="brush: sql">
SELECT  h.id, hc.id, cnt + 1
FROM    (
        SELECT  hn.id, COUNT(*) AS cnt
        FROM    t_hierarchy hn
        STRAIGHT_JOIN
                t_hierarchy hcnt
        ON      MBRContains(hcnt.sets, hn.sets)
        WHERE   MATCH(hn.tags) AGAINST(&#039;+tag13480&#039; IN BOOLEAN MODE)
        GROUP BY
                hn.id
        ) h
JOIN    t_hierarchy hc
ON      hc.parent = h.id
</pre>
<p><a href="#" onclick="xcollapse('X9305');return false;"><strong>View query results</strong></a><br />
</p>
<div id="X9305" style="display: none; ">
<div class="terminal">
<table class="terminal">
<tr>
<th>id</th>
<th>id</th>
<th>cnt + 1</th>
</tr>
<tr>
<td class="integer">3</td>
<td class="integer">16</td>
<td class="bigint">2</td>
</tr>
<tr>
<td class="integer">3</td>
<td class="integer">17</td>
<td class="bigint">2</td>
</tr>
<tr>
<td class="integer">3</td>
<td class="integer">18</td>
<td class="bigint">2</td>
</tr>
<tr>
<td class="integer">3</td>
<td class="integer">19</td>
<td class="bigint">2</td>
</tr>
<tr>
<td class="integer">3</td>
<td class="integer">20</td>
<td class="bigint">2</td>
</tr>
<tr class="statusbar">
<td colspan="100">5 rows fetched in 0.0006s (0.0093s)</td>
</tr>
</table>
</div>
<div class="terminal">
<table class="terminal">
<tr>
<th>id</th>
<th>select_type</th>
<th>table</th>
<th>type</th>
<th>possible_keys</th>
<th>key</th>
<th>key_len</th>
<th>ref</th>
<th>rows</th>
<th>filtered</th>
<th>Extra</th>
</tr>
<tr>
<td class="bigint">1</td>
<td class="varchar">PRIMARY</td>
<td class="varchar">&lt;derived2&gt;</td>
<td class="varchar">ALL</td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="bigint">6</td>
<td class="double">100.00</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="bigint">1</td>
<td class="varchar">PRIMARY</td>
<td class="varchar">hc</td>
<td class="varchar">ref</td>
<td class="varchar">ix_hierarchy_parent</td>
<td class="varchar">ix_hierarchy_parent</td>
<td class="varchar">4</td>
<td class="varchar">h.id</td>
<td class="bigint">9207</td>
<td class="double">100.01</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="bigint">2</td>
<td class="varchar">DERIVED</td>
<td class="varchar">hn</td>
<td class="varchar">fulltext</td>
<td class="varchar">sx_hierarchy_sets,fx_hierarchy_tags</td>
<td class="varchar">fx_hierarchy_tags</td>
<td class="varchar">0</td>
<td class="varchar"></td>
<td class="bigint">1</td>
<td class="double">100.00</td>
<td class="varchar">Using where; Using temporary; Using filesort</td>
</tr>
<tr>
<td class="bigint">2</td>
<td class="varchar">DERIVED</td>
<td class="varchar">hcnt</td>
<td class="varchar">range</td>
<td class="varchar">sx_hierarchy_sets</td>
<td class="varchar">sx_hierarchy_sets</td>
<td class="varchar">34</td>
<td class="varchar"></td>
<td class="bigint">1</td>
<td class="double">48828000.00</td>
<td class="varchar">Range checked for each record (index map: 0&#215;10)</td>
</tr>
</table>
</div>
<pre>
select `h`.`id` AS `id`,`20100216_index`.`hc`.`id` AS `id`,(`h`.`cnt` + 1) AS `cnt + 1` from (select `20100216_index`.`hn`.`id` AS `id`,count(0) AS `cnt` from `20100216_index`.`t_hierarchy` `hn` straight_join `20100216_index`.`t_hierarchy` `hcnt` where ((match `20100216_index`.`hn`.`tags` against (&#39;+tag13480&#39; in boolean mode)) and contains(`20100216_index`.`hcnt`.`sets`,`20100216_index`.`hn`.`sets`)) group by `20100216_index`.`hn`.`id`) `h` join `20100216_index`.`t_hierarchy` `hc` where (`20100216_index`.`hc`.`parent` = `h`.`id`)
</pre>
</div>
<h3>Putting it together</h3>
<p>Now we should just combine the two queries and apply nice formatting to them:</p>
<pre class="brush: sql">
SELECT  h.id,
        CONCAT(LPAD(&#039;&#039;, (level - 1) * 2, &#039; &#039;), h.data) AS name,
        CASE WHEN hq.id = hq.node THEN &#039;*&#039; ELSE &#039;&#039; END AS hit
FROM    (
        SELECT  hc.id AS node, ha.id AS id, COUNT(*) AS level
        FROM    t_hierarchy hc
        STRAIGHT_JOIN
                t_hierarchy ha
        ON      MBRContains(ha.sets, hc.sets)
        STRAIGHT_JOIN
                t_hierarchy hcnt
        ON      MBRContains(hcnt.sets, ha.sets)
        WHERE   MATCH(hc.tags) AGAINST(&#039;+tag13480&#039; IN BOOLEAN MODE)
        GROUP BY
                hc.id, ha.id
        UNION ALL
        SELECT  h.id, hc.id, cnt + 1 AS level
        FROM    (
                SELECT  hn.id, COUNT(*) AS cnt
                FROM    t_hierarchy hn
                STRAIGHT_JOIN
                        t_hierarchy hcnt
                ON      MBRContains(hcnt.sets, hn.sets)
                WHERE   MATCH(hn.tags) AGAINST(&#039;+tag13480&#039; IN BOOLEAN MODE)
                GROUP BY
                        hn.id
                ) h
        JOIN    t_hierarchy hc
        ON      hc.parent = h.id
        ) hq
JOIN    t_hierarchy h
ON      h.id = hq.id
ORDER BY
        node, level, lft
</pre>
<div class="terminal">
<table class="terminal">
<tr>
<th>id</th>
<th>name</th>
<th>hit</th>
</tr>
<tr>
<td class="integer">3</td>
<td class="blob">Value 3</td>
<td class="varchar">*</td>
</tr>
<tr>
<td class="integer">16</td>
<td class="blob">  Value 16</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">17</td>
<td class="blob">  Value 17</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">18</td>
<td class="blob">  Value 18</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">19</td>
<td class="blob">  Value 19</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">20</td>
<td class="blob">  Value 20</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">1</td>
<td class="blob">Value 1</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">10</td>
<td class="blob">  Value 10</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">51</td>
<td class="blob">    Value 51</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">257</td>
<td class="blob">      Value 257</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">1290</td>
<td class="blob">        Value 1290</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">6454</td>
<td class="blob">          Value 6454</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">32275</td>
<td class="blob">            Value 32275</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">161378</td>
<td class="blob">              Value 161378</td>
<td class="varchar">*</td>
</tr>
<tr>
<td class="integer">2</td>
<td class="blob">Value 2</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">13</td>
<td class="blob">  Value 13</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">67</td>
<td class="blob">    Value 67</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">340</td>
<td class="blob">      Value 340</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">1702</td>
<td class="blob">        Value 1702</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">8513</td>
<td class="blob">          Value 8513</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">42569</td>
<td class="blob">            Value 42569</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">212847</td>
<td class="blob">              Value 212847</td>
<td class="varchar">*</td>
</tr>
<tr>
<td class="integer">3</td>
<td class="blob">Value 3</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">20</td>
<td class="blob">  Value 20</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">103</td>
<td class="blob">    Value 103</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">518</td>
<td class="blob">      Value 518</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">2594</td>
<td class="blob">        Value 2594</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">12975</td>
<td class="blob">          Value 12975</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">64878</td>
<td class="blob">            Value 64878</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">324394</td>
<td class="blob">              Value 324394</td>
<td class="varchar">*</td>
</tr>
<tr>
<td class="integer">4</td>
<td class="blob">Value 4</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">25</td>
<td class="blob">  Value 25</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">127</td>
<td class="blob">    Value 127</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">639</td>
<td class="blob">      Value 639</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">3200</td>
<td class="blob">        Value 3200</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">16002</td>
<td class="blob">          Value 16002</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">80014</td>
<td class="blob">            Value 80014</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">400074</td>
<td class="blob">              Value 400074</td>
<td class="varchar">*</td>
</tr>
<tr>
<td class="integer">5</td>
<td class="blob">Value 5</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">27</td>
<td class="blob">  Value 27</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">140</td>
<td class="blob">    Value 140</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">704</td>
<td class="blob">      Value 704</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">3521</td>
<td class="blob">        Value 3521</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">17606</td>
<td class="blob">          Value 17606</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">88033</td>
<td class="blob">            Value 88033</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="integer">440166</td>
<td class="blob">              Value 440166</td>
<td class="varchar">*</td>
</tr>
<tr class="statusbar">
<td colspan="100">46 rows fetched in 0.0039s (0.0466s)</td>
</tr>
</table>
</div>
<div class="terminal">
<table class="terminal">
<tr>
<th>id</th>
<th>select_type</th>
<th>table</th>
<th>type</th>
<th>possible_keys</th>
<th>key</th>
<th>key_len</th>
<th>ref</th>
<th>rows</th>
<th>filtered</th>
<th>Extra</th>
</tr>
<tr>
<td class="bigint">1</td>
<td class="varchar">PRIMARY</td>
<td class="varchar">&lt;derived2&gt;</td>
<td class="varchar">ALL</td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="bigint">46</td>
<td class="double">100.00</td>
<td class="varchar">Using temporary; Using filesort</td>
</tr>
<tr>
<td class="bigint">1</td>
<td class="varchar">PRIMARY</td>
<td class="varchar">h</td>
<td class="varchar">eq_ref</td>
<td class="varchar">PRIMARY</td>
<td class="varchar">PRIMARY</td>
<td class="varchar">4</td>
<td class="varchar">hq.id</td>
<td class="bigint">1</td>
<td class="double">100.00</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="bigint">2</td>
<td class="varchar">DERIVED</td>
<td class="varchar">hc</td>
<td class="varchar">fulltext</td>
<td class="varchar">sx_hierarchy_sets,fx_hierarchy_tags</td>
<td class="varchar">fx_hierarchy_tags</td>
<td class="varchar">0</td>
<td class="varchar"></td>
<td class="bigint">1</td>
<td class="double">100.00</td>
<td class="varchar">Using where; Using temporary; Using filesort</td>
</tr>
<tr>
<td class="bigint">2</td>
<td class="varchar">DERIVED</td>
<td class="varchar">ha</td>
<td class="varchar">range</td>
<td class="varchar">sx_hierarchy_sets</td>
<td class="varchar">sx_hierarchy_sets</td>
<td class="varchar">34</td>
<td class="varchar"></td>
<td class="bigint">1</td>
<td class="double">48828000.00</td>
<td class="varchar">Range checked for each record (index map: 0&#215;10)</td>
</tr>
<tr>
<td class="bigint">2</td>
<td class="varchar">DERIVED</td>
<td class="varchar">hcnt</td>
<td class="varchar">range</td>
<td class="varchar">sx_hierarchy_sets</td>
<td class="varchar">sx_hierarchy_sets</td>
<td class="varchar">34</td>
<td class="varchar"></td>
<td class="bigint">1</td>
<td class="double">48828000.00</td>
<td class="varchar">Range checked for each record (index map: 0&#215;10)</td>
</tr>
<tr>
<td class="bigint">3</td>
<td class="varchar">UNION</td>
<td class="varchar">&lt;derived4&gt;</td>
<td class="varchar">ALL</td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="bigint">6</td>
<td class="double">100.00</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="bigint">3</td>
<td class="varchar">UNION</td>
<td class="varchar">hc</td>
<td class="varchar">ref</td>
<td class="varchar">ix_hierarchy_parent</td>
<td class="varchar">ix_hierarchy_parent</td>
<td class="varchar">4</td>
<td class="varchar">h.id</td>
<td class="bigint">9207</td>
<td class="double">100.01</td>
<td class="varchar"></td>
</tr>
<tr>
<td class="bigint">4</td>
<td class="varchar">DERIVED</td>
<td class="varchar">hn</td>
<td class="varchar">fulltext</td>
<td class="varchar">sx_hierarchy_sets,fx_hierarchy_tags</td>
<td class="varchar">fx_hierarchy_tags</td>
<td class="varchar">0</td>
<td class="varchar"></td>
<td class="bigint">1</td>
<td class="double">100.00</td>
<td class="varchar">Using where; Using temporary; Using filesort</td>
</tr>
<tr>
<td class="bigint">4</td>
<td class="varchar">DERIVED</td>
<td class="varchar">hcnt</td>
<td class="varchar">range</td>
<td class="varchar">sx_hierarchy_sets</td>
<td class="varchar">sx_hierarchy_sets</td>
<td class="varchar">34</td>
<td class="varchar"></td>
<td class="bigint">1</td>
<td class="double">48828000.00</td>
<td class="varchar">Range checked for each record (index map: 0&#215;10)</td>
</tr>
<tr>
<td class="bigint"></td>
<td class="varchar">UNION RESULT</td>
<td class="varchar">&lt;union2,3&gt;</td>
<td class="varchar">ALL</td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="varchar"></td>
<td class="bigint"></td>
<td class="double"></td>
<td class="varchar"></td>
</tr>
</table>
</div>
<pre>
select `20100216_index`.`h`.`id` AS `id`,concat(convert(lpad(&#39;&#39;,((`hq`.`level` - 1) * 2),&#39; &#39;) using utf8),`20100216_index`.`h`.`data`) AS `name`,(case when (`hq`.`id` = `hq`.`node`) then &#39;*&#39; else &#39;&#39; end) AS `hit` from (select `20100216_index`.`hc`.`id` AS `node`,`20100216_index`.`ha`.`id` AS `id`,count(0) AS `level` from `20100216_index`.`t_hierarchy` `hc` straight_join `20100216_index`.`t_hierarchy` `ha` straight_join `20100216_index`.`t_hierarchy` `hcnt` where (match `20100216_index`.`hc`.`tags` against (&#39;+tag13480&#39; in boolean mode)) group by `20100216_index`.`hc`.`id`,`20100216_index`.`ha`.`id` union all select `h`.`id` AS `id`,`20100216_index`.`hc`.`id` AS `id`,(`h`.`cnt` + 1) AS `level` from (select `20100216_index`.`hn`.`id` AS `id`,count(0) AS `cnt` from `20100216_index`.`t_hierarchy` `hn` straight_join `20100216_index`.`t_hierarchy` `hcnt` where ((match `20100216_index`.`hn`.`tags` against (&#39;+tag13480&#39; in boolean mode)) and contains(`20100216_index`.`hcnt`.`sets`,`20100216_index`.`hn`.`sets`)) group by `20100216_index`.`hn`.`id`) `h` join `20100216_index`.`t_hierarchy` `hc`) `hq` join `20100216_index`.`t_hierarchy` `h` where (`20100216_index`.`h`.`id` = `hq`.`id`) order by `hq`.`node`,`hq`.`level`,`20100216_index`.`h`.`lft`
</pre>
<p>For each matching node, the query returns the node itself, its ancestors and its immediate children. The matching nodes are marked with the asterisk in the field <code>hit</code>.</p>
<p>The query efficiently combines the <code>FULLTEXT</code>, <code>SPATIAL</code> and <code>BTREE</code> indexes and completes in only <strong>40 ms</strong>.</p>
<p>Hope that helps.</p>
<hr/>
<p>I&#8217;m always glad to answer the questions regarding database queries.</p>
<p><a href="/ask-a-question"><strong>Ask me a question</strong></a></p>
]]></content:encoded>
			<wfw:commentRss>http://explainextended.com/2010/02/16/tags-in-nested-sets-efficient-indexing/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
