Комментарий от 17.10.2003. Вопрос остался актуальным, несмотря на появление в
Borland InterBase 7.x типа BOOLEAN. Этот тип данных имеет длину 4 байта, и все равно в большинстве случаев требует специальной обработки TField.GetText/SetText, если визуализация значения столбца отличается от checkbox или True/False.
Очень часто возникает необходимость хранения однобайтовых значений – 0/1, Y/N, М/Ж и т. п. Многие задаются вопросом – какой тип поля использовать в данном случае? Char(1) или smallint? А может, integer? На первый взгляд, кажется что char(1) лучше. Но если
вспомнить, как IB хранит символьные данные, то окажется, что char(1) на самом деле занимает 3 байта (2 байта длины и 1 байт на символ). По этой же причине varchar(1) вообще не рассматривается, поскольку занимает вообще 5 байт (2 байта длины, 1 байт на символ, 2 байта на количество концевых пробелов). Integer занимает 4 байта, smallint соответственно 2.
Если исходить из размера типа данных, то кажется, что лучше выбрать smallint. Однако, вы увидите из теста, что это не совсем верное предположение.
Аппаратура и программное обеспечение
- MB ASUS P5NP6, Pentium Pro 150MHz 128K cache, 128MB RAM, HDD Quantum Fireball ST 6.4Gb (IDE), BusMaster drivers.
- Windows NT Workstation 4.0, SP4, RollUp post sp4 fix.
- IB Database 5.5.0.742
Параметры IBCONFIG:
DATABASE_CACHE_PAGES 7500
SERVER_CLIENT_MAPPING 8192
SERVER_WORKING_SIZE_MIN 10000
SERVER_WORKING_SIZE_MAX 70000
LOCK_HASH_SLOTS 511
База данных TEST.GDB создана с размером страницы 8192 байта. Все тестовые запросы выполнялись через локальный интерфейс ("e:\test.gdb").
Тестовые таблицы
Основная таблица для тестов по скорости выборки и поиска:
CREATE TABLE TT (
ID INTEGER NOT NULL PRIMARY KEY,
C1 CHAR(1),
I INTEGER,
S SMALLINT)
Таблицы для определения размера хранимых данных:
CREATE TABLE C1 (C1 CHAR(1));
CREATE TABLE I (I INTEGER);
CREATE TABLE S (S SMALLINT);
Процедура заливки
Предварительно создан генератор idgen и объявлена функция GetRandom и SetRandSeed из randomudf. GetRandom(10) выдает случайное число от 0 до 9.
create procedure filltt (rnum integer)
as
declare variable i integer;
declare variable r integer;
begin
i=1;
while (:i < :rnum) do
begin
r=getrandom(10);
insert into tt values(gen_id(idgen, 1), cast(:r as char(1)), :r, :r);
i=:i+1;
end
end
Перед вызовом процедуры инициализирован генератор случайных чисел – SELECT SETRANDSEED(0) FROM RDB$DATABASE – чтобы Вы могли повторить этот тест (получить идентичное распределение random).
Примечание. Если вы решили повторить этот тест, не забудьте перед наполнением таблицы и созданием индексов выключить Forced Writes – это сэкономит обращение к диску при записи данных.
Вызовом EXECUTE PROCEDURE FILLTT(1000000) создан 1 миллион записей (можно и порциями, например, по 100 тысяч – не имеет значения). Созданы 3 индекса по всем полям (отдельно) для каждого поля:
CREATE INDEX BYC1 ON TT (C1);
CREATE INDEX BYI ON TT (I);
CREATE INDEX BYS ON TT (S);
Статистика
Для чистоты эксперимента база данных установлена в режим "no reserve" (не резервировать пространство на страницах для версий записей) при помощи
IB_WISQL, и сделан backup/restore. На самом деле, изначально я проводил тест на БД, у которой заполнение страниц таблиц было ~52%, т. е. при отключенном параметре "no reserve". Оказалось, что на скорость выполнения запросов это не влияет никоим образом, причем время выполнения запросов идентично вплоть до сотых секунды в обоих случаях. Параметр "no reserve" был установлен только для того, чтобы максимально точно определить разницу в объеме хранимых данных для каждого типа индексов (и таблиц C1, I, S статистика приведена в другом разделе этого документа. См. дальше). По крайней мере предполагалось, что это поможет. На самом деле получается, что "no reserve" ни на что, кроме объема базы данных, не влияет.
Database "e:\test.gdb"
Database header page information:
Flags 0
Checksum 12345
Generation 13
Page size 8192
ODS version 9.1
Oldest transaction 1
Oldest active 2
Oldest snapshot 1
Next transaction 6
Bumped transaction 1
Sequence number 0
Next attachment ID 0
Implementation ID 16
Shadow count 0
Page buffers 0
Next header page 0
Creation date Jul 22, 1999 17:12:53
Attributes no reserve
Variable header data:
Sweep interval: 0
*END*
Database file sequence:
File e:\test.gdb is the only file
Database log page information:
Creation date
Log flags: 2
No write ahead log
Next log page: 0
Variable log data:
Control Point 1:
File name:
Partition offset: 0 Seqno: 0 Offset: 0
Control Point 2:
File name:
Partition offset: 0 Seqno: 0 Offset: 0
Current File:
File name:
Partition offset: 0 Seqno: 0 Offset: 0
*END*
Analyzing database pages ...
TT (128)
Primary pointer page: 133, Index root page: 134
Data pages: 4362, data page slots: 4362, average fill: 91%
Fill distribution:
0 - 19% = 0
20 - 39% = 0
40 - 59% = 0
60 - 79% = 0
80 - 99% = 4362
Index BYC1 (1)
Depth: 2, leaf buckets: 737, nodes: 1000000
Average data length: 0.00, total dup: 999990, max dup: 32707
Fill distribution:
0 - 19% = 0
20 - 39% = 0
40 - 59% = 0
60 - 79% = 0
80 - 99% = 737
Index BYI (2)
Depth: 2, leaf buckets: 737, nodes: 1000000
Average data length: 0.00, total dup: 999990, max dup: 32707
Fill distribution:
0 - 19% = 0
20 - 39% = 0
40 - 59% = 0
60 - 79% = 0
80 - 99% = 737
Index BYS (3)
Depth: 2, leaf buckets: 737, nodes: 1000000
Average data length: 0.00, total dup: 999990, max dup: 32707
Fill distribution:
0 - 19% = 0
20 - 39% = 0
40 - 59% = 0
60 - 79% = 0
80 - 99% = 737
Index RDB$PRIMARY1 (0)
Depth: 2, leaf buckets: 860, nodes: 1000000
Average data length: 1.00, total dup: 0, max dup: 0
Fill distribution:
0 - 19% = 0
20 - 39% = 0
40 - 59% = 0
60 - 79% = 0
80 - 99% = 860
Как ясно видно из статистики, индексы по полям имеют абсолютно одинаковое количество страниц. С одной стороны это означает, что с точки зрения индекса нет разницы между char(1), integer и smallint, а с другой – что индексы не будут вносить искажения в результаты запросов.
Запросы
Параметр IBCONFIG – DATABASE_CACHE_PAGES был выбран таким (7500), чтобы постараться вместить в кэш как все страницы таблицы, так и страницы используемого индекса. Например, количество страниц таблицы – 4362, одного индекса – 737. Итого 4362+737 = 5099. Это минимум для database_cache_pages.
Поиск количества по одному значению – выборка диапазона из индекса, и просчет по нему count. (Несущественные параметры статистики убраны. Одинаковые значения статистики для разных запросов оставлены только у первого запроса)
SELECT COUNT(C1) FROM TT
WHERE C1 = 0
PLAN (TT INDEX (BYC1))
COUNT
===========
99878
Elapsed time= 5.52 sec
Buffers = 7500
Reads = 0
Fetches = 199843
SELECT COUNT(C1) FROM TT
WHERE C1 = '0'
PLAN (TT INDEX (BYC1))
COUNT
===========
99878
Elapsed time= 4.36 sec
SELECT COUNT(I) FROM TT
WHERE I = 0
PLAN (TT INDEX (BYI))
COUNT
===========
99878
Elapsed time= 3.24 sec
SELECT COUNT(S) FROM TT
WHERE S = 0
PLAN (TT INDEX (BYS))
COUNT
===========
99878
Elapsed time= 3.53 sec
Группировка всех значений в памяти. Параметр Reads показывает, что кэша в памяти не хватает для всех страниц таблицы и индекса, и происходит считывание с диска. Однако для всех запросов это значение одинаково, поэтому факт чтения страниц с диска можно игнорировать.
SELECT C1, COUNT(C1) FROM TT
GROUP BY C1
ORDER BY C1
PLAN (TT ORDER BYC1)
C1 COUNT
====== ===========
0 99878
1 99468
2 100207
3 99988
4 99988
5 100127
6 100265
7 100196
8 100156
9 99727
Elapsed time= 71.42 sec
Buffers = 7500
Reads = 743
Fetches = 3000750
SELECT I, COUNT(I) FROM TT
GROUP BY I
ORDER BY I
PLAN (TT ORDER BYI)
I COUNT
=========== ===========
0 99878
1 99468
2 100207
3 99988
4 99988
5 100127
6 100265
7 100196
8 100156
9 99727
Elapsed time= 59.80 sec
SELECT S, COUNT(S) FROM TT
GROUP BY S
ORDER BY S
PLAN (TT ORDER BYS)
S COUNT
====== ===========
0 99878
1 99468
2 100207
3 99988
4 99988
5 100127
6 100265
7 100196
8 100156
9 99727
Elapsed time= 59.99 sec
Битовое слияние индексов при поиске (выборке диапазона значений):
SELECT COUNT(C1) FROM TT
WHERE C1 = 0 OR C1 = 1
PLAN (TT INDEX (BYC1,BYC1))
COUNT
===========
199346
Elapsed time= 13.67 sec
SELECT COUNT(C1) FROM TT
WHERE C1 = '0' OR C1 = '1'
Elapsed time= 10.22 sec
SELECT COUNT(I) FROM TT
WHERE I = 0 OR I = 1
PLAN (TT INDEX (BYI,BYI))
COUNT
===========
199346
Elapsed time= 7.25 sec
SELECT COUNT(S) FROM TT
WHERE S = 0 OR S = 1
PLAN (TT INDEX (BYS,BYS))
COUNT
===========
199346
Elapsed time= 8.04 sec
Fetches = 398865
И последний тест – на чистый объем занимаемых данных. Посчитать в таблице TT это невозможно, поэтому я создал три отдельные таблицы, состоящие только из соответствующего поля, и перенес данные из tt в эти таблицы (insert into x select x from tt).
Примечание. Максимальный размер БД после всех тестов достигает 230 мегабайт (с параметром no_reserve – 144 мегабайта), а backup – до 90 мегабайт. Следовательно, для повторения теста потребуется минимум 320 мегабайт дискового пространства.
C1 (129)
Primary pointer page: 135, Index root page: 136
Data pages: 3437, data page slots: 3437, average fill: 93%
Fill distribution:
0 - 19% = 0
20 - 39% = 1
40 - 59% = 0
60 - 79% = 0
80 - 99% = 3436
I (130)
Primary pointer page: 137, Index root page: 138
Data pages: 3437, data page slots: 3437, average fill: 93%
Fill distribution:
0 - 19% = 0
20 - 39% = 1
40 - 59% = 0
60 - 79% = 0
80 - 99% = 3436
S (131)
Primary pointer page: 139, Index root page: 140
Data pages: 3437, data page slots: 3437, average fill: 93%
Fill distribution:
0 - 19% = 0
20 - 39% = 1
40 - 59% = 0
60 - 79% = 0
80 - 99% = 3436
Из статистики следует, что разница в объеме данных в миллион записей между char(1), integer и smallint отсутствует. Таким образом, даже предположение, что smallint будет занимать на диске места меньше чем integer, неверно (не говоря о char(1)).
Выводы
- Если вы храните числа в строках, никогда не производите сравнение значения поля с числом. Обрамляйте число кавычками, т. е. производите сравнение значения поля со строкой.
- Разницы в объеме занимаемых данных между char(1), integer и smallint нет как для таблиц, так и для индексов.
- Скорость обработки integer и smallint выше чем char(1), и integer обрабатывается быстрее smallint.
- Для хранения булевских значений integer подходит намного лучше char(1). Можно воспользоваться и smallint.
C1 (128)
Primary pointer page: 144, Index root page: 145
Data pages: 6897, data page slots: 6897, average fill: 93%
Fill distribution:
0 - 19% = 0
20 - 39% = 0
40 - 59% = 1
60 - 79% = 0
80 - 99% = 6896
I (129)
Primary pointer page: 146, Index root page: 147
Data pages: 6897, data page slots: 6897, average fill: 93%
Fill distribution:
0 - 19% = 0
20 - 39% = 0
40 - 59% = 1
60 - 79% = 0
80 - 99% = 6896
S (130)
Primary pointer page: 149, Index root page: 150
Data pages: 6897, data page slots: 6897, average fill: 93%
Fill distribution:
0 - 19% = 0
20 - 39% = 0
40 - 59% = 1
60 - 79% = 0
80 - 99% = 6896