<?xml version="1.0" encoding="utf-8"?>
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
  version="2.0">
  <channel>
    <title>martinc</title>
    <atom:link
      href="https://docs.microsoft.com/archive/blogs/martinc/feed.xml"
      rel="self"
      type="application/rss+xml" />
    <link>https://docs.microsoft.com/archive/blogs/martinc/feed.xml</link>
    <description>Mostly about ESENT, the database engine.</description>
    <lastBuildDate>Wed, 17 Apr 2019 04:00:52 GMT</lastBuildDate>
    <language>en-US</language>
    <sy:updatePeriod>hourly</sy:updatePeriod>
    <sy:updateFrequency>1</sy:updateFrequency>
    <item>
      <title>Code Pages versus Locales/Locale Names</title>
      <link>https://docs.microsoft.com/archive/blogs/martinc/code-pages-versus-localeslocale-names</link>
      <pubDate>Thu, 16 Apr 2015 14:58:40 GMT</pubDate>
      <dc:creator><![CDATA[Martin Chisholm]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/martinc/2015/04/16/code-pages-versus-localeslocale-names/</guid>
      <description><![CDATA[The question recently came up on why someone's index range on a text column wasn't working properly....]]></description>
      <content:encoded><![CDATA[<p><span style="font-family: verdana, geneva;">The question recently came up on why someone's index range on a text column wasn't working properly. It turns out they forgot to set the Code Page on the column, but they were still storing Unicode data in the column.</span></p>
<p><span style="font-family: verdana, geneva;">So the question came up about just what the heck is a Code Page. I think it comes out of a long history of Windows support for globalization. It goes back to probably the 1980s, since I remember pages in the appendix of the MS-DOS 5 manual on different code pages.</span></p>
<p><span style="font-family: verdana, geneva;">ASCII is technically 7-bits only. The values 0-127.</span></p>
<p><span style="font-family: verdana, geneva;">ANSI characters are 8-bits, but the meaning of characters 128-255 change depending on which 'code page' you choose. ESE mostly cares about UTF-16 (1200) and Latin (1252). See <a title="Wikipaedia" href="http://en.wikipedia.org/wiki/Code_page#Windows_.28ANSI.29_code_pages">Wikipaedia</a> for more details.</span></p>
<p><span style="font-family: verdana, geneva;">Code pages are defined on the column itself.</span></p>
<pre class="scroll"><code class="cplusplus">typedef struct<br /> {<br /> // ...<br /> unsigned short cp;<br /> unsigned short wCollate; /* Must be 0 */<br /> // ...<br /> } JET_COLUMNDEF;<br /> </code></pre>
<p><span style="font-family: verdana, geneva;">But then you could say 'Code Page 1200', which meant Unicode (I think UCS-2 was the only form of Unicode initially, and UTF-7, UTF-8, UTF-16, and UTF-32 came later? But that's a different topic...).</span></p>
<p><span style="font-family: verdana, geneva;">And it was realized that you need to know the locale in order to sort Unicode text. First we used LCIDs (locale identifiers), and used them for many years. Then BCP-47 and Locale Names came along. Now, LCIDs are deprecated (starting in Windows Vista), and locale names are preferred. Example of an LCID: 1033 (0x409). The equivalent locale name is "en-US".</span><br /><br /><span style="font-family: verdana, geneva;">So we need to know the *locale* in order to know how to sort a Unicode column. Different locales sort characters in different ways. The LCID in in the JET_UNICODEINDEX structure, which is part of JET_INDEXCREATE/JET_INDEXCREATE2and the Locale Name is in JET_UNICODEINDEX2, which is referenced by JET_INDEXCREATE3. You need Windows 8 in order to use Locale Names. If you need to support Windows 7, you're stuck with LCIDs.</span></p>
<pre class="scroll"><code class="cplusplus">typedef struct tagJET_UNICODEINDEX<br /> {<br /> unsigned long lcid;<br /> unsigned long dwMapFlags;<br /> } JET_UNICODEINDEX;<br /> <br /> #if ( JET_VERSION &gt;= 0x0602 )<br /> typedef struct tagJET_UNICODEINDEX2<br /> {<br /> WCHAR *szLocaleName;<br /> unsigned long dwMapFlags;<br /> } JET_UNICODEINDEX2;<br /> #endif //JET_VERSION &gt;= 0x0602<br /> </code></pre>
<p><span>The 'dwMapFlags' member is passed as 'dwMapFlags' to&nbsp;<a title="LCMapStringEx" href="https://msdn.microsoft.com/en-us/library/windows/desktop/dd318702(v=vs.85).aspx">LCMapStringEx</a>.</span></p>
<div><span style="font-family: verdana, geneva;">Given what we know today, I'm not sure where we'd stick it, I doubt we'd stick with the current scheme of having two different places. I suspect we'd choose it at index creation time.</span></div>
<p><span style="font-family: verdana, geneva;">Note that the code below thinks that it's using Unicode data, because it stores and retrieves it successfully. But the index is completely confused.</span></p>
<pre class="scroll"><code class="csharp"> /// &lt;summary&gt;<br /> /// Repro of Fleischman index range<br /> /// &lt;/summary&gt;<br /> [TestMethod]<br /> [Priority(2)]<br /> [Description("Repro of Fleischman index range.")]<br /> public void TestFleischmanIndexRange()<br /> {<br /> JET_COLUMNID columnidName;<br /> var columndefName = new JET_COLUMNDEF()<br /> {<br /> //// Uncomment this line to make it work properly.<br /> //// cp = JET_CP.Unicode,<br /> coltyp = JET_coltyp.LongText,<br /> grbit = ColumndefGrbit.ColumnNotNULL<br /> };<br /> Api.JetAddColumn(this.sesid, this.tableid, "Name", columndefName, null, 0, out columnidName);<br /> <br /> JET_COLUMNID columnidStatus;<br /> var columndefStatus = new JET_COLUMNDEF() { coltyp = JET_coltyp.UnsignedByte, };<br /> Api.JetAddColumn(this.sesid, this.tableid, "Status", columndefStatus, null, 0, out columnidStatus);<br /> <br /> JET_COLUMNID columnidDifferentiator;<br /> var columndefDifferentiator = new JET_COLUMNDEF()<br /> {<br /> cp = JET_CP.Unicode,<br /> coltyp = JET_coltyp.LongText,<br /> grbit = ColumndefGrbit.ColumnNotNULL<br /> };<br /> Api.JetAddColumn(this.sesid, this.tableid, "Differentiator", columndefDifferentiator, null, 0, out columnidDifferentiator);<br /> <br /> string indexDef = "+Name\0+Status\0\0";<br /> var indexcreate = new JET_INDEXCREATE[]<br /> {<br /> new JET_INDEXCREATE()<br /> {<br /> szIndexName = "fleischman",<br /> szKey = indexDef,<br /> cbKey = indexDef.Length,<br /> grbit = CreateIndexGrbit.None,<br /> ulDensity = 100,<br /> cbKeyMost = 1000,<br /> cbVarSegMac = 1000,<br /> pidxUnicode =<br /> new JET_UNICODEINDEX() { szLocaleName = "en-us", }<br /> },<br /> };<br /> <br /> Windows8Api.JetCreateIndex4(this.sesid, this.tableid, indexcreate, indexcreate.Length);<br /> <br /> var records = new[]<br /> {<br /> new { name = "a1", status = 0x2, differentiator = "a1-2" },<br /> new { name = "a2", status = 0x2, differentiator = "a2-2" },<br /> new { name = "n1", status = 0x2, differentiator = "n1-2" },<br /> new { name = "n1", status = 0x2, differentiator = "n1-2_dupe!" },<br /> new { name = "n2", status = 0x2, differentiator = "n2-2" },<br /> new { name = "q1", status = 0x2, differentiator = "q1-2" },<br /> new { name = "q2", status = 0x2, differentiator = "q2-2" },<br /> };<br /> <br /> using (Transaction trx = new Transaction(this.sesid))<br /> {<br /> foreach (var record in records)<br /> {<br /> using (var update = new Update(this.sesid, this.tableid, JET_prep.Insert))<br /> {<br /> Console.WriteLine("Inserting name = '{0}', status = '{1}'", record.name, record.status, record.differentiator);<br /> Api.SetColumn(this.sesid, this.tableid, columnidName, record.name, Encoding.Unicode);<br /> Api.SetColumn(this.sesid, this.tableid, columnidStatus, (byte)record.status);<br /> Api.SetColumn(this.sesid, this.tableid, columnidDifferentiator, record.differentiator, Encoding.Unicode);<br /> update.Save();<br /> }<br /> }<br /> trx.Commit(CommitTransactionGrbit.LazyFlush);<br /> }<br /> <br /> if (this.NamedKeyTrySeek(this.sesid, this.tableid, "fleischman", "n1", 2))<br /> {<br /> do<br /> {<br /> string retrievedName = Api.RetrieveColumnAsString(<br /> this.sesid,<br /> this.tableid,<br /> columnidName,<br /> Encoding.Unicode);<br /> byte retrievedStatus= Api.RetrieveColumnAsByte(<br /> this.sesid,<br /> this.tableid,<br /> columnidStatus).GetValueOrDefault();<br /> string retrievedDifferentiator= Api.RetrieveColumnAsString(<br /> this.sesid,<br /> this.tableid,<br /> columnidName,<br /> Encoding.Unicode);<br /> <br /> Console.WriteLine(<br /> "Retrieved name = '{0}', status = '{1}', differentiator = '{2}'",<br /> retrievedName,<br /> retrievedStatus,<br /> retrievedDifferentiator);<br /> }<br /> while (Api.TryMoveNext(this.sesid, this.tableid));<br /> }<br /> }<br /> <br /> /// &lt;summary&gt;<br /> /// Courtesy of Fleischman.<br /> /// &lt;/summary&gt;<br /> /// &lt;param name="sesid"&gt;&lt;/param&gt;<br /> /// &lt;param name="tableid"&gt;&lt;/param&gt;<br /> /// &lt;param name="indexName"&gt;&lt;/param&gt;<br /> /// &lt;param name="name"&gt;The first column to seek for.&lt;/param&gt;<br /> /// &lt;param name="status"&gt;The second column to seek for.&lt;/param&gt;<br /> /// &lt;returns&gt;&lt;/returns&gt;<br /> public bool NamedKeyTrySeek(<br /> JET_SESID sesid,<br /> JET_TABLEID tableid,<br /> string indexName,<br /> string name,<br /> byte status)<br /> {<br /> Api.JetSetCurrentIndex(sesid, tableid, indexName);<br /> Api.MakeKey(sesid, tableid, name, Encoding.Unicode, MakeKeyGrbit.NewKey);<br /> Api.MakeKey(sesid, tableid, status, MakeKeyGrbit.None);<br /> bool ret = Api.TrySeek(sesid, tableid, SeekGrbit.SeekEQ);<br /> if (ret)<br /> {<br /> Api.MakeKey(sesid, tableid, name, Encoding.Unicode, MakeKeyGrbit.NewKey);<br /> Api.MakeKey(sesid, tableid, status, MakeKeyGrbit.None);<br /> ret = Api.TrySetIndexRange(sesid, tableid, SetIndexRangeGrbit.RangeInclusive | SetIndexRangeGrbit.RangeUpperLimit);<br /> }<br /> <br /> return ret;<br /> }<br /> </code></pre>]]></content:encoded>
    </item>
    <item>
      <title>Using `esentutl.exe /vss` to examine an in-use database</title>
      <link>https://docs.microsoft.com/archive/blogs/martinc/using-esentutl-exe-vss-to-examine-an-in-use-database</link>
      <pubDate>Wed, 11 Feb 2015 17:36:21 GMT</pubDate>
      <dc:creator><![CDATA[Martin Chisholm]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/martinc/2015/02/11/using-esentutl-exe-vss-to-examine-an-in-use-database/</guid>
      <description><![CDATA[This feature has been present in Exchange with eseutil.exe, but now it will be part of Windows 10...]]></description>
      <content:encoded><![CDATA[<p>This feature has been present in Exchange with eseutil.exe, but now it will be part of Windows 10 esentutl.exe.</p>
<p>Normally, when we try to access ESE database files, we get ACCESS_DENIED, because the ESE engine opens files exclusively.</p>
<p>VSS (Volume Shadow Copy Services) allows us to grab a Snapshot of the volume, and access the file in a Read-Only mode.</p>
<p>But with a VSS writer, there is a callback that allows us to modify these files in a small window (OnPostSnapshot). We do this in 'esevss.dll', and it allows us to replay the log files on the in-use database, get to a clean state, and then have the Read-Only copy of the database in a clean state. Some examples are below.</p>
<p>This is rather useful for accessing database files without stopping the process. But beware that any transactions that haven't been committed at the time of the snapshot will be rolled back, and will be missing from the snapshotted database. Therefore it's more useful as a diagnostic tool, and not very useful for backups (well, some people may still find it useful to get most of the data...).</p>
<p>Performance implications: Yes, there are implications. VSS snapshots end up increasing the I/O load on the volume. The first write operation for a file turns in to THREE I/Os: (Read the old value; Write the old value in a special Diff area; Write the new value). I won't go in to the gory details here, because it's been around for years.</p>
<p>&nbsp;</p>
<p>Normally, we can't access the database:</p>
<pre><span style="font-family: 'courier new', courier; color: #ff0000;">c:\Users\martinc\AppData\Local\Microsoft\Internet Explorer\Indexed DB $ esentutl -mh AppQuota.edb</span><br /><br /><span style="font-family: 'courier new', courier; color: #ff0000;">Extensible Storage Engine Utilities for Microsoft(R) Windows(R)</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;">Version 10.0</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;">Copyright (C) Microsoft Corporation. All Rights Reserved.</span><br /><br /><span style="font-family: 'courier new', courier; color: #ff0000;">Initiating FILE DUMP mode...</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;">Error: Access to source database 'AppQuota.edb' failed with Jet error -1032.</span><br /><br /><span style="font-family: 'courier new', courier; color: #ff0000;">Operation terminated with error -1032 (<span style="background-color: #ffff00;">JET_errFileAccessDenied, Cannot access file, the file is locked or in use</span>) after 0.188 seconds.</span><br /><br /><br /><br />We can dump an in-use database. Note that it's in a Dirty state. Check out the funky file name -- HarddiskVolumeShadowCopy1.<br /><span style="font-family: 'courier new', courier; color: #ff0000;">c:\Users\martinc\AppData\Local\Microsoft\Internet Explorer\Indexed DB $ esentutl -mh AppQuota.edb -vss</span><br /><br /><span style="font-family: 'courier new', courier; color: #ff0000;">Extensible Storage Engine Utilities for Microsoft(R) Windows(R)</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;">Version 10.0</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;">Copyright (C) Microsoft Corporation. All Rights Reserved.</span><br /><br /><span style="font-family: 'courier new', courier; color: #ff0000;">Initializing VSS subsystem...</span><br /><br /><span style="font-family: 'courier new', courier; color: #ff0000;">Initiating FILE DUMP mode...</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;"> Database: \\?\GLOBALROOT\Device\<span style="background-color: #ffff00;">HarddiskVolumeShadowCopy1</span>\Users\martinc\AppData\Local\Microsoft\Internet Explorer\Indexed DB\AppQuota.edb</span><br /><br /><br /><span style="font-family: 'courier new', courier; color: #ff0000;">DATABASE HEADER:</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;">Checksum Information:</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;">Expected Checksum: 0x7ea2e77c</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;"> Actual Checksum: 0x7ea2e77c</span><br /><br /><span style="font-family: 'courier new', courier; color: #ff0000;">Fields:</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;"> File Type: Database</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;"> Checksum: 0x7ea2e77c</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;"> Format ulMagic: 0x89abcdef</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;"> Engine ulMagic: 0x89abcdef</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;"> Format ulVersion: 0x620,20</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;"> Engine ulVersion: 0x620,20</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;">Created ulVersion: 0x620,20</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;"> DB Signature: Create time:11/19/2014 18:38:18.049 Rand:408092127 Computer:</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;"> cbDbPage: 32768</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;"> dbtime: 65844 (0x10134)</span><br /><span style="font-family: 'courier new', courier; color: #ff0000; background-color: #ffff00;"> State: Dirty Shutdown</span><br /><span style="color: #ff0000; font-family: 'courier new', courier;">...</span><br /><br /><span style="font-family: 'courier new', courier; color: #ff0000;">Operation completed successfully in 6.282 seconds.</span><br /><br />Here's an example using vssrec to get it ta clean state first:<br /><span style="font-family: 'courier new', courier; color: #ff0000;">c:\Users\martinc\AppData\Local\Microsoft\Internet Explorer\Indexed DB $ esentutl -mh AppQuota.edb -vssrec edb .</span><br /><br /><span style="font-family: 'courier new', courier; color: #ff0000;">Extensible Storage Engine Utilities for Microsoft(R) Windows(R)</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;">Version 10.0</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;">Copyright (C) Microsoft Corporation. All Rights Reserved.</span><br /><br /><span style="font-family: 'courier new', courier; color: #ff0000;">Initializing VSS subsystem...</span><br /><br /><span style="font-family: 'courier new', courier; color: #ff0000;">Initiating FILE DUMP mode...</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;"> Database: \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy2\Users\martinc\AppData\Local\Microsoft\Internet Explorer\Indexed DB\AppQuota.edb</span><br /><br /><br /><span style="font-family: 'courier new', courier; color: #ff0000;">DATABASE HEADER:</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;">Checksum Information:</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;">Expected Checksum: 0x7fadd805</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;"> Actual Checksum: 0x7fadd805</span><br /><br /><span style="font-family: 'courier new', courier; color: #ff0000;">Fields:</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;"> File Type: Database</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;"> Checksum: 0x7fadd805</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;"> Format ulMagic: 0x89abcdef</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;"> Engine ulMagic: 0x89abcdef</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;">...</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;"> DB Signature: Create time:11/19/2014 18:38:18.049 Rand:408092127 Computer:</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;"> cbDbPage: 32768</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;"> dbtime: 65854 (0x1013e)</span><br /><span style="font-family: 'courier new', courier; color: #ff0000; background-color: #ffff00;"> State: Clean Shutdown</span><br /><span style="font-family: 'courier new', courier; color: #ff0000;">...</span><br /><br /><br /><span style="font-family: 'courier new', courier; color: #ff0000;">Operation completed successfully in 6.422 seconds.</span></pre>
<pre>&nbsp;</pre>
<p>Also very useful is the ability to copy the database, in case you want to do multiple operations (e.g. esentutl.exe -ms to dump the space used). This copies the file to c:\tmp\clean.edb.</p>
<pre><span style="color: #ff0000; font-family: 'courier new', courier;">c:\Users\martinc\AppData\Local\Microsoft\Internet Explorer\Indexed DB [14:24:49.24] +$ esentutl -y AppQuota.edb -d \tmp\clean.edb -vssrec edb .</span><br /><br /><span style="color: #ff0000; font-family: 'courier new', courier;">Extensible Storage Engine Utilities for Microsoft(R) Windows(R)</span><br /><span style="color: #ff0000; font-family: 'courier new', courier;">Version 10.0</span><br /><span style="color: #ff0000; font-family: 'courier new', courier;">Copyright (C) Microsoft Corporation. All Rights Reserved.</span><br /><br /><span style="color: #ff0000; font-family: 'courier new', courier;">Initializing VSS subsystem...</span><br /><br /><span style="color: #ff0000; font-family: 'courier new', courier;">Initiating COPY FILE mode...</span><br /><span style="color: #ff0000; font-family: 'courier new', courier;"> Source File: \\?\GLOBALROOT\Device\HarddiskVolumeShadowCopy3\Users\martinc\Ap</span><br /><span style="color: #ff0000; font-family: 'courier new', courier;">Destination File: \tmp\clean.edb</span><br /><br /><span style="color: #ff0000; font-family: 'courier new', courier;"> Copy Progress (% complete)</span><br /><br /><span style="color: #ff0000; font-family: 'courier new', courier;"> 0 10 20 30 40 50 60 70 80 90 100</span><br /><span style="color: #ff0000; font-family: 'courier new', courier;"> |----|----|----|----|----|----|----|----|----|----|</span><br /><span style="color: #ff0000; font-family: 'courier new', courier;"> ...................................................</span><br /><br /><span style="color: #ff0000; font-family: 'courier new', courier;"> Total bytes read = 0x1c80000 (29884416) (28 MB)</span><br /><span style="color: #ff0000; font-family: 'courier new', courier;"> Total bytes written = 0x1c80000 (29884416) (28 MB)</span><br /><br /><br /><span style="color: #ff0000; font-family: 'courier new', courier;">Operation completed successfully in 8.359 seconds.</span></pre>
<p>Theoretically, `esentutl.exe -y -vss` could work for any file that's locked exclusively.</p>
<p>We hope you find it useful!</p>
<p>-martin</p>]]></content:encoded>
    </item>
    <item>
      <title>Sparse Files: ESE can now give space back to the file system</title>
      <link>https://docs.microsoft.com/archive/blogs/martinc/sparse-files-ese-can-now-give-space-back-to-the-file-system</link>
      <pubDate>Wed, 10 Sep 2014 16:56:00 GMT</pubDate>
      <dc:creator><![CDATA[Martin Chisholm]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/martinc/2014/09/10/sparse-files-ese-can-now-give-space-back-to-the-file-system/</guid>
      <description><![CDATA[A long-standing limitation of ESE is that database files never shrink, even after deleting...]]></description>
      <content:encoded><![CDATA[<p>A long-standing limitation of ESE is that database files never shrink, even after deleting substantial amounts of the data. While the space is freed and is re-usable within the database, the on-disk size does not change. The main limitation is that the first page of each table can never be moved, so even if the file is shrunk from the end, a single unmovable page can prevent shrinking the file. This is similar to an unmovable file preventing the shrinking of a disk partition in Volume Manager. In Windows 8.1, ESE started using Sparse Files to release space back to the file system, even in the middle of database files.</p>
<h1>What is a Sparse File?</h1>
<p>A sparse file has &lsquo;holes&rsquo;.</p>
<p>The File System has a special FSCTL called FSCTL_SET_ZERO_DATA. Normally it is used to write a bunch of zeroes to part of a file, but if the file is marked as sparse, and this space gets released to the file system.</p>
<p>But what does the File System do with sparse regions?</p>
<p>Any reads from the file don&rsquo;t go to disk; instead the FS knows to just return zeroes.</p>
<p>Any writes will need to first allocate a cluster from the file system, and then write to this new block. The new block could be in a completely different part of the disk though -- and the file is now fragmented.</p>
<h1>Downsides of Sparse Files</h1>
<p>Sparse Files are only supported on NTFS and ReFS; not FAT (and all of the FAT variants).</p>
<p>I do not know of any copy utility (cmd.exe copy, xcopy, robocopy) that preserves the &lsquo;sparseness&rsquo; of a file. They all expand the copied file to its full length.</p>
<p>Windows 8.1 and earlier did not do a great job of allocating contiguous space when the sparse regions were re-written. For example, if there was a 10 MB hole in the middle of a file, and the application writes a single 1 MB chunk, the file system often split up that 1 MB in to many 64 kb segments (depending on the fragmented free space of the volume).</p>
<p>Fragmented files hurt sequential access performance, which is very important for fast data access on spinning media (disks). SSDs can also be affected by block size, since it is difficult to know their underlying &lsquo;native&rsquo; block size. SSDs may need to do a &lsquo;read-modify-write&rsquo; if a program writes to less than the native block size.</p>
<p>Prior to Windows 8.1, memory-mapped files could not be made sparse.</p>
<p>We may get out-of-disk errors in places where we never got them before.</p>
<h1>How does ESE use Sparse Files?</h1>
<p>ESE manages space hierarchically. We will allocate a large multi-page block of space to be used by a table. We call these blocks of space &lsquo;extents&rsquo;. The default size of an extent is 16 pages. A table has a main btree, but will also have a btree for each of its secondary indices, and also one for its Long Values (LVs).</p>
<p>An extent starts out being &lsquo;Owned&rsquo; by the table, and initially all of the extent is &lsquo;Available&rsquo;. As the btrees grow and require more space, the pages are allocated from the &lsquo;Available&rsquo; part of the extent. Only when there are no more pages in the Available Extent will we ask for another Extent.</p>
<p>When enough records on a page are deleted, the page can be freed, and returned back to the Available Extent.</p>
<p>When an entire extent is available, it can then be freed back to the file system.</p>
<h1>Manually Examining Sparse Files</h1>
<p>Many utilities don&rsquo;t know whether a file is Sparse or not. Just doing a &lsquo;dir&rsquo; won&rsquo;t help. But compact.exe can be used. Normally it&rsquo;s used for examining NTFS-compressed files, but it turns out that the Win32 API GetCompressedFileSize works for sparse files as well.</p>
<p>Let's use an example. Indexed DB is an HTML 5 feature, and Internet Explorer uses ESE for its storage. Here we see that the AppQuota.edb file is 8,454,144 bytes large, and takes up 8,454,144 bytes on disk. It has no space savings whatsoever.</p>
<p><span style="color: #ff0000; font-family: terminal,monaco; font-size: x-small;">c:\Users\martinc\AppData\Local\Microsoft\Internet Explorer\Indexed DB [19:42:30.44] ++$ compact.exe *edb<br /><br /></span><span style="color: #ff0000; font-family: terminal,monaco; font-size: x-small;">&nbsp;Listing c:\Users\martinc\AppData\Local\Microsoft\Internet Explorer\Indexed DB\<br /></span><span style="color: #ff0000; font-family: terminal,monaco; font-size: x-small;">&nbsp;New files added to this directory will not be&nbsp;compressed.<br /><br /></span><span style="color: #ff0000; font-family: terminal,monaco; font-size: x-small;">&nbsp; 8454144 :&nbsp;&nbsp;</span><span style="color: #ff0000; font-family: terminal,monaco; font-size: x-small;">8454144 = 1.0 to 1&nbsp;&nbsp; AppQuota.edb<br /></span><span style="color: #ff0000; font-family: terminal,monaco; font-size: x-small;">151060480 : 139788288 </span><span style="color: #ff0000; font-family: terminal,monaco; font-size: x-small;">= 1.1 to 1 d Internet.edb<br /></span><span style="color: #ff0000; font-family: terminal,monaco; font-size: x-small;"><br />Of 2 files within 1 </span><span style="color: #ff0000; font-family: terminal,monaco; font-size: x-small;">directories<br /></span><span style="color: #ff0000; font-family: terminal,monaco; font-size: x-small;">1 are compressed and 1 </span><span style="color: #ff0000; font-family: terminal,monaco; font-size: x-small;">are not compressed.<br /></span><span style="color: #ff0000; font-family: terminal,monaco; font-size: x-small;">159,514,624 total </span><span style="color: #ff0000; font-family: terminal,monaco; font-size: x-small;">bytes of data are stored in 148,242,432 bytes.<br /></span><span style="color: #ff0000; font-family: terminal,monaco; font-size: x-small;">The compression ratio </span><span style="color: #ff0000; font-family: terminal,monaco; font-size: x-small;">is 1.1 to 1.</span></p>
<p>Now we can run esentutl.exe to manually walk the space trees and free up all of the space that it can. Use the -z option (Zero, originally meant to write zeroes to the deleted parts of a database) along with -t (Trim).</p>
<p><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">c:\Users\martinc\AppData\Local\Microsoft\Internet Explorer\Indexed DB [19:43:10.26] $ esentutl -z AppQuota.edb -t</span><br /><br /><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">Extensible Storage Engine Utilities for Microsoft(R) Windows(R) Version 6.3</span><br /><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">Copyright (C) Microsoft Corporation. All Rights Reserved.</span><br /><br /><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">Initiating SECURE mode...<br /></span><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Database: AppQuota.edb<br /></span><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; :&nbsp; AE[&nbsp;&nbsp;&nbsp; 3]:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 47-&nbsp;&nbsp; 256 (210);&nbsp;&nbsp;&nbsp; A&nbsp;&nbsp;&nbsp; 47-&nbsp;&nbsp; 256 (210)&nbsp;</span><br /><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">cpgSparseBefore:&nbsp;&nbsp; 0 cpgSparseAfter: 210 cpgSparseNew: 210</span></p>
<p><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0 trimmed (before), 210 trimmed (after), 210 pages newly trimmed.</span></p>
<p><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">Warning:</span><br /><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">&nbsp; You MUST delete the logfiles for this database</span><br /><br /><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">Note:</span></p>
<p><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">&nbsp; It is recommended that you immediately perform a full backup</span><br /><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">&nbsp; of this database. If you restore a backup made before the</span><br /><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">&nbsp; repair, the database will be rolled back to the state</span><br /><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">&nbsp; it was in at the time of that backup.</span><br /><br /><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">Operation completed successfully in 0.313 seconds.</span></p>
<p>(Ignore the bits about deleting the logfiles &ndash; that&rsquo;s meant for securely zeroing out data, and it&rsquo;s a reminder that the potentially-sensitive deleted data may still exist in the transaction logfiles.)</p>
<p>Hey look, it even mentions AE -- that&rsquo;s Available Extents, mentioned above. We just trimmed (freed) 210 pages back to the file system. Compact.exe now says it only takes up 1,572,864 bytes:</p>
<p><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">c:\Users\martinc\AppData\Local\Microsoft\Internet Explorer\Indexed DB [19:43:30.18] $ compact.exe *edb</span><br /><br /><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">&nbsp;Listing c:\Users\martinc\AppData\Local\Microsoft\Internet Explorer\Indexed DB\</span><br /><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">&nbsp;New files added to this directory will not be compressed.</span><br /><br /><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">&nbsp; 8454144 :&nbsp;&nbsp; 1572864 = 5.4 to 1 d AppQuota.edb</span><br /><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">151060480 : 139788288 = 1.1 to 1 d Internet.edb</span><br /><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">Of 2 files within 1 directories</span><br /><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">2 are compressed and 0 are not compressed.<br /></span><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">159,514,624 total bytes of data are stored in 141,361,152 bytes.<br /></span><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">The compression ratio </span><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">is 1.1 to 1.</span></p>
<p>&nbsp;</p>
<p>Fsutil.exe is supposed to allow us to dump the allocated bits of a file, but for some reason it doesn&rsquo;t dump everything. So we&rsquo;ll try a tool that a friend of mine wrote to dump file fragmentation:</p>
<p><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">c:\Users\martinc\AppData\Local\Microsoft\Internet Explorer\Indexed DB [19:43:49.38] $ fsutil sparse queryrange AppQuota.edb<br /></span><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">Allocated range[1]:</span><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">Offset: 0x0&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Length: 0x180000<br /></span><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;"><br />c:\Users\martinc\AppData\Local\Microsoft\Internet </span><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">Explorer\Indexed DB [19:47:35.09] $ diskbash -f AppQuota.edb<br /></span><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">VCN:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 0, LCN:&nbsp;</span><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">4270808, LEN:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 16 (&nbsp;&nbsp; 65536 bytes)<br /></span><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">VCN:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 16, LCN: 19183938, LEN:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 368 ( 1507328 bytes)<br /></span><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">VCN:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; 384, LCN:&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; -1, LEN:&nbsp;&nbsp;&nbsp;&nbsp; 1680 ( 6881280 bytes)<br /></span><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">============================================================<br /></span><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; File: AppQuota.edb<br /></span><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; Fragments: 3, Bytes Allocated: </span><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">8454144<br /></span><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">Fragment Sizes </span><span style="color: #ff0000; font-family: courier new,courier; font-size: x-small;">(bytes): Min= 65536, Max= 6881280, Avg= 2818048</span></p>
<p>The VCN is &lsquo;Virtual Cluster Number&rsquo;, and it is relative to the beginning of the file.</p>
<p>The LCN is &lsquo;Logical Cluster Number&rsquo;, and is relative to the beginning of the disk.</p>
<p>So the first block of the file is VCN #0, and there are 16 of them sequentially on the disk, starting at LCN #4270808. It&rsquo;s a total of 65536 bytes, meaning that this disk has 65535/16=4096<br />clusters.</p>
<p>The next block of the file starts at VCN #16, for 368 clusters (about 1.5 MB), at LCN #19183938.</p>
<p>The last block of the file starts at VCN #384 (16+368), for 1680 clusters, and at LCN #-1. This means it isn&rsquo;t actually on the disk!</p>
<p>Also note that this run of 1680 clusters matches the output above of freeing up 210 pages. Each 32k database page takes up eight 4k clusters, and 210*8=1680.</p>
<h1>Programmatically Telling ESE to use Sparse Files</h1>
<p>Using the regular C API, JET_paramEnableShrinkDatabase must be set to one of the following:</p>
<p>&nbsp;</p>
<pre class="scroll"><code class="cplusplus">#define JET_bitShrinkDatabaseOff 0x0<br /> <br />#define JET_bitShrinkDatabaseOn 0x1 // Uses the file system's Sparse Files feature to release space in the middle of a file.<br /> <br />#define JET_bitShrinkDatabaseRealtime 0x2 // Attempts to reclaim space back to the file system after freeing significant amounts of data (when space is marked as Available to the Root space tree).<br /> </code></pre>
<p>&nbsp;</p>
<p>Normally JetResizeDatabase (<a href="http://msdn.microsoft.com/en-us/library/jj835047(v=exchg.10).aspx">http://msdn.microsoft.com/en-us/library/jj835047(v=exchg.10).aspx</a>) will act the same as JetGrowDatabase (<a href="http://msdn.microsoft.com/en-us/library/gg294109(v=EXCHG.10).aspx">http://msdn.microsoft.com/en-us/library/gg294109(v=EXCHG.10).aspx</a>) -- that is, the database will either stay the same size, or grow. In order to get JetResizeDatabase() to use sparse files, use JET_bitShrinkDatabaseOn. Then pass in 0 for the cpgDesired.</p>
<p>JET_bitShrinkDatabaseRealtime affects the runtime behavior of what happens when enough data is deleted in tables to free an extent. ESE will make that extent sparse, but unfortunately there are some other background tasks which may then cause some data to be written in this sparse area. It's not ideal, but it's reality.</p>
<p>With our C# layer (<a href="http://codeplex.com/managedesent">http://codeplex.com/managedesent</a>), the logic is the same, but the names are slightly different. The setup:</p>
<pre class="scroll"><code class="csharp">InstanceParameters instanceParameters = new InstanceParameters(instance);<br /> instanceParameters.EnableShrinkDatabase = ShrinkDatabaseGrbit.On;<br /> </code></pre>
<p>&nbsp;</p>
<p>And the actual code:<br /><code class="csharp"></code></p>
<pre class="scroll"><code class="cplusplus"> int actualPagesAfterTrim = 0;<br /> Windows8Api.JetResizeDatabase(sesid, dbid, 0, out actualPagesAfterTrim, ResizeDatabaseGrbit.None);<br /> </code></pre>
<p><code class="csharp">&nbsp;</code></p>]]></content:encoded>
    </item>
    <item>
      <title>cbKeyMost when indexing needs additional overhead</title>
      <link>https://docs.microsoft.com/archive/blogs/martinc/cbkeymost-when-indexing-needs-additional-overhead</link>
      <pubDate>Tue, 08 Oct 2013 16:41:40 GMT</pubDate>
      <dc:creator><![CDATA[Martin Chisholm]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/martinc/2013/10/08/cbkeymost-when-indexing-needs-additional-overhead/</guid>
      <description><![CDATA[I recently helped someone track down why they were getting JET_errKeyTruncated. They declared an...]]></description>
      <content:encoded><![CDATA[<p>I recently helped someone track down why they were getting JET_errKeyTruncated. They declared an index over a JET_coltypLongBinary column with cbMax = 0x20c, and when they created the index, they specified cbKeyMost = 0x20c, and also passed in VistaGrbits.IndexDisallowTruncation (they were using ManagedEsent).</p>
<p>But then they were getting JET_errKeyTruncated when setting a value with length 0x204, which is obviously less than 0x20c. But why?</p>
<p>If the column was JET_coltypLongText and sorted as Unicode, the answer would be obvious. I'll back up a bit though:</p>
<p>When ESE creates indices, it normalizes the keys first so that 'memcmp' is able to work on the raw memory. That way, the B-Tree code doesn't have to know what type a particular key is; it just knows raw bytes and lengths:</p>
<ul>
<li>For unsigned integers, this is easy. (Although depending on the endian-ness of the platform, the bytes may need to be shuffled around first.)</li>
<li>For signed integers, they need to be biased first, and then can be sorted as signed numbers. (e.g. for bytes, add 128 to them first, so that instead of [-128 .. 127] we end up storing [0 .. 255], which sort naturally.) When retrieving from the key, this can be trivially reversed. (Useful with RetrieveColumnGrbit.RetrieveFromIndex)</li>
<li>Date/times are normalized to a floating point number.</li>
<li>ASCII/ANSI text is all converted to upper case. This is the first of the 'lossy' normalizations, since we don't know what the initial case was.</li>
<li>Unicode is more complicated. We use the OS's <a title="LCMapStringEx" href="http://msdn.microsoft.com/en-us/library/windows/desktop/dd318702(v=vs.85).aspx">LCMapStringEx</a>&nbsp;function to normalize the text. Because the world is a big place, it has a very hard problem to solve. 'U' and 'u' and 'u-with-umlaut' should all be sorted together (depending on the flags that are passed in). This ends up expanding a string to many times its original length. A 20-character string takes up 40 bytes, and the normalized binary form could be twice or even up to five times that size (200 bytes -- although that's pretty rare).</li>
</ul>
<p>Now for LongBinary. Internally we break up the LongBinary values in to much smaller chunks, and add a bit of overhead. Every 8 bytes we store another header byte.</p>
<p>For that 0x204-length LongBinary column, we do:</p>
<p>(0x204 + (8-1) )&nbsp;/ 8 = 65. This is the number of 'chunks'. Each of which takes up 9 bytes. That's 585 bytes. And then add another header byte, and that's 586 bytes (0x24a). That's 70 extra bytes on a 516-byte column, or about 14% more.</p>
<p><strong>Summary</strong>: You may need a fudge factor when calculating cbKeyMost. You can't just assume it's the same as the column's cbMax.</p>]]></content:encoded>
    </item>
    <item>
      <title>ManagedEsent is in Windows 8.1 and on MSDN.</title>
      <link>https://docs.microsoft.com/archive/blogs/martinc/managedesent-is-in-windows-8-1-and-on-msdn</link>
      <pubDate>Thu, 05 Sep 2013 10:05:45 GMT</pubDate>
      <dc:creator><![CDATA[Martin Chisholm]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/martinc/2013/09/05/managedesent-is-in-windows-8-1-and-on-msdn/</guid>
      <description><![CDATA[I'm super excited to share this:
ManagedEsent is&nbsp;documented on...]]></description>
      <content:encoded><![CDATA[<p>I'm super excited to share this:</p>
<p>ManagedEsent is&nbsp;documented on MSDN:</p>
<p><a href="http://msdn.microsoft.com/en-us/library/windows/desktop/dn375980.aspx">http://msdn.microsoft.com/en-us/library/windows/desktop/dn375980.aspx</a></p>
<p>And it's part of Windows 8.1. No need to download from CodePlex! Here's what's on my 8.1 machine:</p>
<p><span style="font-family: arial,helvetica,sans-serif;">C:\Windows\assembly\NativeImages_v4.0.30319_32\Microsoft.Ic1a2041b#\3862e50725b3d2f5ef48c19f24b17ef3\Microsoft.Isam.Esent.Interop.ni.dll</span><br /><span style="font-family: arial,helvetica,sans-serif;">C:\Windows\assembly\NativeImages_v4.0.30319_32\Microsoft.Ic1a2041b#\3862e50725b3d2f5ef48c19f24b17ef3\Microsoft.Isam.Esent.Interop.ni.dll.aux</span><br /><span style="font-family: arial,helvetica,sans-serif;">C:\Windows\assembly\NativeImages_v4.0.30319_64\Microsoft.Ic1a2041b#\85216777bd911aef15b30bda8b0046b8\Microsoft.Isam.Esent.Interop.ni.dll</span><br /><span style="font-family: arial,helvetica,sans-serif;">C:\Windows\assembly\NativeImages_v4.0.30319_64\Microsoft.Ic1a2041b#\85216777bd911aef15b30bda8b0046b8\Microsoft.Isam.Esent.Interop.ni.dll.aux</span><br /><span style="font-family: arial,helvetica,sans-serif;">C:\Windows\Microsoft.NET\assembly\GAC_MSIL\microsoft.isam.esent.interop\v4.0_6.3.0.0__31bf3856ad364e35\Microsoft.Isam.Esent.Interop.dll</span><br /><span style="font-family: arial,helvetica,sans-serif;">C:\Windows\WinSxS\msil_microsoft.isam.esent.interop_31bf3856ad364e35_6.3.9600.16384_none_b4cbc5488cc56490\Microsoft.Isam.Esent.Interop.dll</span></p>
<p>&nbsp;</p>
<p>Enjoy!</p>
<p>-martin</p>]]></content:encoded>
    </item>
    <item>
      <title>Cost of JetIntersectIndexes</title>
      <link>https://docs.microsoft.com/archive/blogs/martinc/cost-of-jetintersectindexes</link>
      <pubDate>Fri, 19 Apr 2013 13:50:48 GMT</pubDate>
      <dc:creator><![CDATA[Martin Chisholm]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/martinc/2013/04/19/cost-of-jetintersectindexes/</guid>
      <description><![CDATA[We recently had a question about JetIntersectIndexes()'s performance. Here's what Brett wrote...]]></description>
      <content:encoded><![CDATA[<p>We recently had a question about JetIntersectIndexes()'s performance. Here's what Brett wrote up:</p>
<p>&nbsp;</p>
<p dir="LTR" align="LEFT"><span style="color: #0000ff; font-family: arial black,avant garde;">Intersect is to save disk IO at the cost of CPU time.</span></p>
<p dir="LTR" align="LEFT"><span style="color: #0000ff; font-family: arial black,avant garde;">The cost of intersect indices, is the cost of iterating over ALL the index ranges (choice #3 below).　 That may not sound like a savings, HOWEVER index entries are stored together, and we try to maintain sequentially &hellip; and so iterating over index entries generally speaking is very fast off disk.　 If you walk an index, and retrieve columns you have to get from the real record &hellip; you have to jump back and forth between the index and the table, you will end up seeking into the table, and seeking is slow if the cache is not full of that data.　 Likely your cache is full of data &hellip; but let me continue on the cold case points though &hellip;</span></p>
<p dir="LTR" align="LEFT"><span style="color: #0000ff; font-family: arial black,avant garde;">Some examples help imagining cold IO cases:</span></p>
<ul>
<li><span style="color: #0000ff; font-family: arial black,avant garde;">Base Index Range Scan &ndash; Scanning or counting a range of secondary index entries (where you do not have to retrieve the main data) you can walk 100s of thousands or even single digit millions of entries in a second, because the index entry data is often sequentially laid out and a good index range tells us exactly what you&rsquo;ll want to read (so we preread aggressively often in a few IOs [1] &hellip; let&rsquo;s say 3 IOs for argument&hellip; though a range of 241k entries would probably be more than 3 IOs), and the indices often have very dense / smaller entries &hellip; so we sometimes even end up being CPU bound here even if the index is cold.</span></li>
<li><span style="color: #0000ff; font-family: arial black,avant garde;">Base Index Range + Primary Data Scan &ndash; Generally on a _<em>cold</em>_ DB, you can walk like 100 entries / sec via a secondary index &hellip; b/c while you can retrieve a range of 100s or thousands of index entries in a second (per #1), each seek into the main table is cold, requires a disk operation and random disk operations are typically ~100 IO per sec (IOPS)[2] on HDs (not SSD).　 Such a case is thoroughly random-IO disk bound.</span></li>
<li><span style="color: #0000ff; font-family: arial black,avant garde;">a. Best Case 3-way Index Range &ndash; So if you have 3 index ranges that selects (as you have) 3k records, 240k records, and 71k records &hellip; and it turns out only 1 record satisfies all 3 index ranges, then it will only cost say 3 IOs * 3 index ranges + 1 seek IO for the final data of the primary record = 9 random IOs total.　 But how much did the 240k index range save you &hellip;</span></li>
<li><span style="color: #0000ff; font-family: arial black,avant garde;">b.&nbsp;Even the most costly 240k index range probably processes in ~1 second or less (maybe a bit more, not truly sure of the speed of our insert into the sort) &hellip; so if you dropped that, and it ended up giving you an extra 150 records to process (over the 1 that you&rsquo;ll eventually select), you just barely lose out w/ the extra 150 random disk IOPS (1.5 seconds) from dropping that index range.</span></li>
<li><span style="color: #0000ff; font-family: arial black,avant garde;">c. If dropping the 240k index range gave you an extra 2999 records to process (b/c not sure if this is obvious, but implicitly we would know in this case the 71k index range didn&rsquo;t add much additional selectivity over the 3k range), then that index range will have been EXTREMELY beneficial as that&rsquo;d be 2999 extra seeks (an extra 30 seconds of disk IO savings) to determine the 1 true good record.　 Vs. ~1 second to process the super large index range, that&rsquo;s extremely fast (30x improvement).</span></li>
<li><span style="color: #0000ff; font-family: arial black,avant garde;">If dropping the 240k index range gave you NO extra records (b/c there is already only 1 record in common between the 3k and the 71k index ranges &hellip; sort of the opposite of 3.b.), then the 241k is pure cost &hellip; this is of course difficult to predict.</span></li>
</ul>
<p dir="LTR" align="LEFT">&nbsp;</p>
<p dir="LTR" align="LEFT"><span style="color: #0000ff; font-family: arial black,avant garde;">Every time another index range is added it generally takes a few IOs[1] / 30 ms and let&rsquo;s say 1 sec CPU / 250k entries of range.　 For a small index range of say 100 entries (where IO cost is dominant), if the selectivity of the eventual set of records you have to look up improves by only 3 records per 3 IOs, then you save those costly #2 type seeks that can only be performed at a measly 100 IOPS pace.　 For a large index range, if the selectivity of the eventual set of records improves by 100 records per 250k entries processed (CPU wise) then you save enough costly #2 type disk seeks that you win over the CPU cost.　 Keep in mind also you have 4 cores to process things, but generally speaking only 1 disk &hellip; so again there CPU over disk is nicer to the system as a whole.</span></p>
<p dir="LTR" align="LEFT"><span style="color: #0000ff; font-family: arial black,avant garde;">However, if everything is warm, generally it becomes an "even numbers" game &hellip; where more entries is just more processing &hellip; it probably would&rsquo;ve been better to just select the most selective (smallest) index range and process them straight off by applying the left over terms (residual predicate in query parlance) through the primary record &hellip; as that&rsquo;s 2000 index entries + 2000 seeks (but no IO, just CPU)&hellip; every index range you add is a cost / penalty of CPU processing you add to your operational time at likely little improvement.　 Which is likely what you&rsquo;re seeing here &hellip; but this is again another thing that is difficult to know a-priori.</span></p>
<p dir="LTR" align="LEFT"><span style="color: #0000ff; font-family: arial black,avant garde;">[1] I am generally assuming we have generally good indices, like those over dates, auto-inc-like IDs, common terms or common names, etc.　 Keep in mind some really randomized indices, like GUID indices can have much poorer contiguity and thus can cost a lot more IO to traverse a range of them &hellip; and that can change the results dramatically.</span></p>
<p dir="LTR" align="LEFT"><span style="color: #0000ff; font-family: arial black,avant garde;">[2] This is numbers for HDs &hellip; SSDs change things significantly, as they can often do 2000+ random IOPS, and don&rsquo;t experience as much speed differential from sequential IOs.</span></p>
<p dir="LTR" align="LEFT">&nbsp;</p>]]></content:encoded>
    </item>
    <item>
      <title>JetGetObjectInfo() does not get record count by default</title>
      <link>https://docs.microsoft.com/archive/blogs/martinc/jetgetobjectinfo-does-not-get-record-count-by-default</link>
      <pubDate>Fri, 07 Dec 2012 15:29:00 GMT</pubDate>
      <dc:creator><![CDATA[Martin Chisholm]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/martinc/2012/12/07/jetgetobjectinfo-does-not-get-record-count-by-default/</guid>
      <description><![CDATA[We recently had the following question:

&nbsp;I am trying to get the space occupied by each table...]]></description>
      <content:encoded><![CDATA[<p>We recently had the following question:</p>
<hr style="width: 100%;" width="100%" />
<p><em>&nbsp;I am trying to get the space occupied by each table that we have by using JetGetObjectInfo. The api succeeds but it returns 0 for both number of cRecords and cPages.</em></p>
<p dir="LTR" align="LEFT">　</p>
<pre class="scroll"><em><code class="cplusplus">JET_OBJECTINFO tableInfo = {0};<br /> <br /> hr = ::JetGetObjectInfoA(m_spSession-&gt;GetSessionId(), m_dbId, JET_objtypTable, nullptr, strTableName, &amp;tableInfo, sizeof(tableInfo), JET_ObjInfo);<br /> <br /> if (FAILED(hr)) return hr;<br /> </code></em></pre>
<p dir="LTR" align="LEFT"><em>JetGetTableInfo didnt help as well.</em></p>
<p dir="LTR" align="LEFT">　</p>
<p dir="LTR" align="LEFT"><em>However if i open the cursor and do a&nbsp;<span style="font-family: Consolas,Lucida Console; font-size: x-small;"><span style="font-family: Consolas,Lucida Console; font-size: x-small;">JetIndexRecordCount </span></span><span style="font-family: Tahoma,Times New Roman; font-size: x-small;"><span style="font-family: Tahoma,Times New Roman; font-size: x-small;">I see lots of records present in this table/index. </span></span></em></p>
<p dir="LTR" align="LEFT"><em>If i use JetGetIndexInfo on the clustered index I get the space occupied (using JET_IdxInfoSpaceAlloc) i get 80 as result (I beleive this is in pages).</em></p>
<p dir="LTR" align="LEFT">　</p>
<p dir="LTR" align="LEFT"><em>Please let me know if i am missing some options that ought to be passed (grbits in the tableInfo ?).</em></p>
<p dir="LTR" align="LEFT">　</p>
<p dir="LTR" align="LEFT"><em>Thanks,</em></p>
<p>&nbsp;</p>
<hr style="width: 100%;" width="100%" />
<p>It's not obvious, but you need to call JetComputeStats() first.</p>
<p>Calculating these numbers can be expensive to calculate on a large table, which is why the default behaviour is to leave them at zero.</p>
<p>Documentation on&nbsp;JetComputeStats() can be found at <a href="http://msdn.microsoft.com/en-us/library/windows/desktop/gg269192(v=exchg.10).aspx"><span style="font-family: Tahoma,Times New Roman; font-size: x-small;"><span style="font-family: Tahoma,Times New Roman; font-size: x-small;"><span lang="EN">http://msdn.microsoft.com/en-us/library/windows/desktop/gg269192(v=exchg.10).aspx</span></span></span></a></p>
<p>.</p>]]></content:encoded>
    </item>
    <item>
      <title>Introduction to Indices</title>
      <link>https://docs.microsoft.com/archive/blogs/martinc/introduction-to-indices</link>
      <pubDate>Mon, 10 Sep 2012 00:33:45 GMT</pubDate>
      <dc:creator><![CDATA[Martin Chisholm]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/martinc/2012/09/10/introduction-to-indices/</guid>
      <description><![CDATA[ESE is an ISAM engine -- Indexed Sequential Access Method. Wikipedia has an article on ISAMs:...]]></description>
      <content:encoded><![CDATA[<p>ESE is an ISAM engine -- Indexed Sequential Access Method. Wikipedia has an article on ISAMs: <a href="http://en.wikipedia.org/wiki/ISAM">http://en.wikipedia.org/wiki/ISAM</a>.</p>
<p>ESE&nbsp;allows you to insert data in a structured manner, with data types such as integer or strings. Then you need to be able to find the data, which is where indexing comes in. As one co-worker put it '80% of ESE's coolness is in its indexing'.</p>
<p>ESE stores records in a B+tree (<a href="http://en.wikipedia.org/wiki/B%2B_tree">http://en.wikipedia.org/wiki/B%2B_tree</a>). This tree is ordered by the Primary Key. You can either use the default order (an internally-incrementing number, so any new records inserted just end up at the end), or you can explicitly define a primary index (sometimes called 'Clustered Index') by specifying JET_bitIndexPrimary to the index definition. This is very beneficial when you have a unique property that you know will be accessed sequentially. (For example sorting by Employee ID or by Social Security Number [US] or Social Insurance Number [Canada] could be useful. But a primary index on employee name wouldn't be good because two people may have the same name.)</p>
<p>It's usually more interesting to be able to sort on properties other than the Primary Index. For example, sorting by employee names can be useful. This can't be a primary index, since full names aren't unique. You may want to create an index over "+LastName\0+FirstName\0\0". And maybe an index over "+Salary\0\0". These would be secondary indices.</p>
<p>But how do secondary indices work? They are also B+trees. Take the following data:</p>
<table border="1" align="left">
<tbody>
<tr>
<td><strong>EmployeeId</strong></td>
<td><strong>LastName</strong></td>
<td><strong>FirstName</strong></td>
<td><strong>Salary</strong></td>
</tr>
<tr>
<td>3</td>
<td>Pertwee</td>
<td>Jon</td>
<td>1000</td>
</tr>
<tr>
<td>5</td>
<td>Agyeman</td>
<td>Freema</td>
<td>2200</td>
</tr>
<tr>
<td>101</td>
<td>Gillan</td>
<td>Karen</td>
<td>2200</td>
</tr>
<tr>
<td>202</td>
<td>Smith</td>
<td>Matt</td>
<td>2000</td>
</tr>
</tbody>
</table>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<p>(Obviously 'Salary' is not unique, but 'EmployeeId' is.)</p>
<p>The Primary Index over 'EmployeeId' will have the primary keys of {3, 5, 101, 202}. The contents in that tree will be the records themselves.</p>
<p>The LastName/FirstName index would be: { [ "AGYEMAN\0FREEMA\0", 5 ], [ "GILLAN\0KAREN\0", 101 ], [ "PERTWEE\0JON\0", 3 ], [ "SMITH\0MATT\0", 202 ] }.</p>
<p>The Salary index would be { [ 1000, 3],&nbsp;[2000, 202], [2200, 5], [2200, 101] }.</p>
<p>Some&nbsp;things to note are:<br />-The contents of the secondary indices are the primary keys.<br />-By default, ESE converts ANSI text to uppercase for sorting. (Unicode text is treated differently.)<br />-The Salary index has a couple of duplicate entries. it's not a Unique index, so this is OK. The tie-breaker isthe value of the Primary Key.</p>
<p>If one was to insert a new record (e.g.&nbsp;{ 55, "Baker", "Tom", 3000 } ), then a few things happen:<br />-By inserting in the middle, if there is no space left on the database page, then it may be necessary to split the page in two. Doing this page split may result in some discontiguous data. So specifying an index density of less than 100 when creating the index can avoid splits. (Data contiguity in general should be another blog topic...)<br />-It also requires updating the two secondary indices (including uniqueness checks). The more secondary indices you have, the more expensive this will be.</p>
<p>The uniqueness check is done at JetUpdate() time, not JetSetColumn/JetSetColumns.</p>
<p>&nbsp;</p>
<p><strong>Conclusion:</strong></p>
<p>That just scratches the surface of indexing. Secondary indices are useful, but can also expensive. Use them wisely!</p>]]></content:encoded>
    </item>
    <item>
      <title>Sharp Corner: Close a table after creating it</title>
      <link>https://docs.microsoft.com/archive/blogs/martinc/sharp-corner-close-a-table-after-creating-it</link>
      <pubDate>Sat, 23 Jun 2012 18:02:00 GMT</pubDate>
      <dc:creator><![CDATA[Martin Chisholm]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/martinc/2012/06/23/sharp-corner-close-a-table-after-creating-it/</guid>
      <description><![CDATA[The ESENT API has existed for a while, so there are quite a few oddities to it.
After creating a...]]></description>
      <content:encoded><![CDATA[<p>The ESENT API has existed for a while, so there are quite a few oddities to it.</p>
<p>After creating a database with ESENT, you need to close the returned JET_TABLEID.</p>
<p>The original way to create a usable table was to create the table with <a href="http://blogs.msdn.com/controlpanel/blogs/posteditor.aspx/JetCreateTable" title="http://msdn.microsoft.com/en-us/library/windows/desktop/gg269210(v=exchg.10).aspx">JetCreateTable</a>, add some columns with repeated calls to <a href="http://blogs.msdn.com/controlpanel/blogs/posteditor.aspx/JetAddColumn" title="http://msdn.microsoft.com/en-us/library/windows/desktop/gg294122(v=exchg.10).aspx">JetAddColumn</a>, then index the columns with <a href="http://blogs.msdn.com/controlpanel/blogs/posteditor.aspx/JetCreateIndex" title="http://msdn.microsoft.com/en-us/library/windows/desktop/gg294099(v=exchg.10).aspx">JetCreateIndex</a>. But in a multi-threaded concurrent program, you wouldn't want two threads to try and do the same thing, so the returned JET_TABLEID from JetCreateTable() is opened with exclusive access. No other JET_SESID can open that table.</p>
<p>Fast-forward to the introduction of <a href="http://blogs.msdn.com/controlpanel/blogs/posteditor.aspx/JetCreateTableColumnIndex" title="http://msdn.microsoft.com/en-us/library/windows/desktop/gg269343(v=exchg.10).aspx">JetCreateTableColumnIndex</a> (and later <a href="http://blogs.msdn.com/controlpanel/blogs/posteditor.aspx/JetCreateTableColumnIndex3" title="http://msdn.microsoft.com/en-us/library/windows/desktop/gg294079(v=exchg.10).aspx">JetCreateTableColumnIndex3</a>, and in Windows 8 JetCreateTableColumnIndex4 [not yet on MSDN]). This API takes a large <a href="http://blogs.msdn.com/controlpanel/blogs/posteditor.aspx/JET_TABLECREATE" title="http://msdn.microsoft.com/en-us/library/windows/desktop/gg294146(v=exchg.10).aspx">JET_TABLECREATE</a> structure that holds the information needed for the table, column, and indices. Everything can be done with a single API call, without a need to do a bunch of loops.</p>
<p>But take a closer look at the JET_TABLECREATE structure:</p>
<pre class="scroll"><code class="cplusplus">typedef struct tagJET_TABLECREATE {<br /> unsigned long cbStruct;<br /> tchar* szTableName;<br /> tchar* szTemplateTableName;<br /> unsigned long ulPages;<br /> unsigned long ulDensity;<br /> JET_COLUMNCREATE* rgcolumncreate;<br /> unsigned long cColumns;<br /> JET_INDEXCREATE* rgindexcreate;<br /> unsigned long cIndexes;<br /> JET_GRBIT grbit;<br /> JET_TABLEID tableid;<br /> unsigned long cCreated;<br /> } JET_TABLECREATE;</code></pre>
<p>There are two output variables there: tableid and cCreated. cCreated returns the number of objects returned (e.g. when create a table with 5 columns and 3 indices, cCreated will be 1+5+3). This helps narrow down which table/column/index had a problem if the API failed. tableid is the JET_TABLEID of the newly created table.</p>
<p>We don't like to have multiple implementations of the code lying around, so internally JetCreateTable() will call the same function as JetCreateTableColumnIndex.&nbsp;The tableid was opened exclusively, to be just like the JetCreateTable/JetAddColumn/JetCreateIndex combination, it was also opened exclusively. So now it should be closed with JetCloseTable().</p>]]></content:encoded>
    </item>
    <item>
      <title>ManagedEsent is Metro Compliant</title>
      <link>https://docs.microsoft.com/archive/blogs/martinc/managedesent-is-metro-compliant</link>
      <pubDate>Mon, 11 Jun 2012 17:00:05 GMT</pubDate>
      <dc:creator><![CDATA[Martin Chisholm]]></dc:creator>
      <guid
        isPermaLink="false">https://blogs.msdn.microsoft.com/martinc/2012/06/11/managedesent-is-metro-compliant/</guid>
      <description><![CDATA[I'm inaugurating this blog with an announcement: ManagedEsent is Metro Compliant.
What does this...]]></description>
      <content:encoded><![CDATA[<p>I'm inaugurating this blog with an announcement: ManagedEsent is Metro Compliant.</p>
<p>What does this mean to you? If you write a Metro program in C#, you can grab ManagedEsent from CodePlex&nbsp;(<a href="http://managedesent.codeplex.com/SourceControl/list/changesets">http://managedesent.codeplex.com</a>)&nbsp;and you can use your favourite ESENT APIs from from C#, and still pass certification.</p>
<p>The update is currently available only in source form (<a href="http://managedesent.codeplex.com/SourceControl/list/changesets">http://managedesent.codeplex.com/SourceControl/list/changesets</a>). Binaries will be coming.</p>
<p>Don't hesitate to start new discussions or bug reports&nbsp;in the Discussions area (<a href="http://managedesent.codeplex.com/discussions">http://managedesent.codeplex.com/discussions</a>).</p>
<p>&nbsp;</p>
<p>-martin</p>]]></content:encoded>
    </item>
  </channel>
</rss>