Speichern von NULL versus Speichern von '' in einer Varchar-Spalte

7

Mir ist klar, dass dies möglicherweise als Duplikat markiert ist, aber ich frage speziell in Bezug auf SQL Server 2005

Ich habe widersprüchliche Ratschläge im Internet gelesen und frage hier. Insbesondere in SQL Server 2005 nimmt ein NULL-Wert in einer varchar-Spalte denselben Platz ein wie eine leere Zeichenfolge?

Ich habe eine 'Holding'-Tabelle auf einem anderen Laufwerk erstellt und sie mit den Daten aus der nullif([field],'')Quelltabelle gefüllt. Wo immer die Felder leer waren, habe ich anstelle der Leerzeichen Nullen eingefügt.

Dann habe ich eine neue Tabelle mit genau der gleichen Struktur wie die Haltetabelle erstellt, aber anstatt Leerzeichen durch Null zu ersetzen, habe ich nur die Leerzeichen eingefügt, und bis jetzt scheint es mehr Platz zu beanspruchen (ich bin noch nicht damit fertig, sie zu füllen und Ich kann nicht sicher sein, ob es noch mehr Daten aufnimmt.)

Bevor ich es weiter fülle und eine Tabelle erhalte, die größer ist als ich dachte, ist es besser, Nullen oder Leerzeichen einzufügen?

Bearbeiten:

Nach der Migration der Daten von der Haltetabelle in die neue Tabelle ist die neue Tabelle ca. 4 GB größer.

Unterschiede in der Tischgröße

Es gibt nur zwei kleine Unterschiede im Tabellendesign: Das Feld 'serial_number' ist char (15) in der Haltetabelle, varchar (15) in der Zieltabelle. (Die maximale Länge einer Seriennummer beträgt 14 und es gibt viele leere Werte - ich denke, wenn ich mich recht erinnere, ungefähr 30 Millionen), und der Clustered-Index für die Haltetabelle hat eine zusätzliche Spalte - program_name ..

Tisch halten

USE [Temp_holding_EWS]
GO
/****** Object:  Table [dbo].[AmtoteAccountActivity_holding]    
 Script Date: 02/17/2017 20:41:32 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_PADDING ON
GO
CREATE TABLE [dbo].[AmtoteAccountActivity_holding](
    [_Date] [char](8) NULL,[Community] [varchar](10) NULL,
    [AccountNumber] [varchar](50) NULL,
    [Branch] [varchar](10) NULL,
    [Window] [varchar](3) NULL,
    [Time] [char](8) NULL,[Balance_Forward] [varchar](10) NULL,
    [Transaction_Type] [varchar](10) NULL,
    [Program_Name] [varchar](10) NULL,
    [Race] [varchar](10) NULL,[Pool_Type] [varchar](10) NULL,
    [Amount] [money] NULL,[Runners] [varchar](60) NULL,
    [Total_Bet_Amount] [varchar](10) NULL,
    [Debit_Amount] [varchar](10) NULL,
    [Credit_Amount] [varchar](10) NULL,
    [Tx_Date] [char](8) NULL,
    [Check_Clear_Date] [varchar](10) NULL,
    [Refund_Amt] [varchar](10) NULL,
    [Bet_Pool_Modifier] [varchar](5) NULL,
    [RecordID] [int] IDENTITY(1,1) NOT NULL,
    [serial_number] [char](15) NULL,
    [handle]  AS 
       (CONVERT([money],[total_bet_amount],(0))-CONVERT([money],[refund_amt],(0))),
    [txdatetime]  AS (CONVERT([datetime],([tx_date]+' ')+[time],(11))),
    [dbdate]  AS (CONVERT([datetime],[_date],(11))),
    [Audit_Trail] [varchar](20) NULL,
 CONSTRAINT [PK_AmtoteAccountActivity_holding] PRIMARY KEY NONCLUSTERED 
(
    [RecordID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, 
ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

GO
SET ANSI_PADDING OFF

(Clustered Index)

USE [Temp_holding_EWS]
GO
/****** Object:  Index [IX_AmtoteAccountActivity_holding] 
    Script Date: 02/17/2017 21:08:44 ******/
CREATE CLUSTERED INDEX [IX_AmtoteAccountActivity_holding] ON 
    [dbo].[AmtoteAccountActivity_holding] 
(
    [AccountNumber] ASC,
    [_Date] ASC,
    [Program_Name] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, 
    SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF,
ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

Zieltabelle

USE [EWS]
GO
/****** Object:  Table [dbo].[AmtoteAccountActivity]    
Script Date: 02/17/2017 20:48:16 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
SET ANSI_PADDING ON
GO
CREATE TABLE [dbo].[AmtoteAccountActivity](
    [_Date] [char](8) NULL,     [Community] [varchar](10) NULL,
    [AccountNumber] [varchar](50) NULL,
    [Branch] [varchar](10) NULL,[Window] [varchar](3) NULL,
    [Time] [char](8) NULL,  [Balance_Forward] [varchar](10) NULL,
    [Transaction_Type] [varchar](10) NULL,
    [Program_Name] [varchar](10) NULL,
    [Race] [varchar](10) NULL,
    [Pool_Type] [varchar](10) NULL,
    [Amount] [money] NULL,[Runners] [varchar](60) NULL,
    [Total_Bet_Amount] [varchar](10) NULL,
    [Debit_Amount] [varchar](10) NULL,
    [Credit_Amount] [varchar](10) NULL,
    [Tx_Date] [char](8) NULL,
    [Check_Clear_Date] [varchar](10) NULL,
    [Refund_Amt] [varchar](10) NULL,
    [Bet_Pool_Modifier] [varchar](5) NULL,
    [RecordID] [int] IDENTITY(1,1) NOT NULL,
    [serial_number] [varchar](15) NULL,
    [handle]  AS 
       (CONVERT([money],[total_bet_amount],(0))-CONVERT([money],[refund_amt],(0))),
    [txdatetime]  AS (CONVERT([datetime],([tx_date]+' ')+[time],(11))),
    [dbdate]  AS (CONVERT([datetime],[_date],(11))),
    [Audit_Trail] [varchar](20) NULL,
 CONSTRAINT [PK_AmtoteAccountActivity2] PRIMARY KEY NONCLUSTERED 
(
    [RecordID] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, 
ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

GO
SET ANSI_PADDING OFF

(Clustered Index)

USE [EWS]
GO
/****** Object:  Index [IX_AmtoteAccountActivity2]  Script Date: 02/17/2017 21:06:29 ******/
CREATE CLUSTERED INDEX [IX_AmtoteAccountActivity2] ON [dbo].[AmtoteAccountActivity] 
(
    [AccountNumber] ASC,
    [_Date] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, 
SORT_IN_TEMPDB = OFF, IGNORE_DUP_KEY = OFF, DROP_EXISTING = OFF, 
ONLINE = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]

( Hinweis: Für alle, die sich fragen, warum anscheinend finanzielle und numerische Werte in Zeichenfeldern gespeichert sind: Das war das ursprüngliche Tabellendesign vor 17 Jahren (nicht von mir), und es gibt jetzt Hunderte von SQL-Abfragen, die in dieser Datenbank ausgeführt werden arbeiten Sie daran, sie als varchar zu behalten, und die Abfragen behalten ihr Casting bei, als sie in Geld, int oder dezimal umzuwandeln und Hunderte von Abfragen zu ändern.

MrVimes
quelle
3
Der Platzunterschied ist hier gegebenenfalls vernachlässigbar (Share DDL). Der wichtigere Faktor ist die Semantik. Bedeutet "Leerzeichen" Wertlosigkeit? Was ist, wenn eine leere Zeichenfolge später etwas anderes bedeutet? NULL ist eine nützliche Sache; Fürchte es nicht .
Aaron Bertrand
Mein Problem ist, dass es in einer Tabelle mit 120 Millionen Zeilen auf einem alten Server mit begrenztem Speicherplatz nicht zu vernachlässigen ist. Vernachlässigbar deutet darauf hin, dass es einen Unterschied gibt. Einige Artikel sagen, dass null 0 Bytes + 2 Bytes Overhead beansprucht, andere sagen, dass null value keinen Speicherplatz beansprucht.
MrVimes
1
@ MrVimes - Wenn Sie sagen "Ich habe eine neue Tabelle mit genau derselben Struktur erstellt", ist die betreffende Spalte als NULLoder definiert NOT NULL?
Max Vernon
1
Um eine gute Antwort zu erhalten, müssen Sie die DDL für beide Tabellen hinzufügen.
Max Vernon
Es scheint, dass Sie in der Lage sind, einen Test durchzuführen, um festzustellen, welcher dieser Artikel zu glauben ist. Wenn Sie sich also nur um den Weltraum kümmern, sind Sie sich nicht sicher, warum Sie fragen.
Aaron Bertrand

Antworten:

9

Erstellen wir drei Tabellen mit einer varchar-Spalte, von denen zwei NULL zulassen, eine nicht.

CREATE TABLE dbo.x1(id int IDENTITY(1,1) PRIMARY KEY, field varchar(5) null);
CREATE TABLE dbo.x2(id int IDENTITY(1,1) PRIMARY KEY, field varchar(5) null);
CREATE TABLE dbo.x3(id int IDENTITY(1,1) PRIMARY KEY, field varchar(5) not null);

Füllen Sie sie mit 1.000.000 Zeilen:

;WITH x(x) AS (SELECT 0 UNION ALL SELECT x+1 FROM x WHERE x < 1000000)
INSERT dbo.x1(field) SELECT NULL FROM x OPTION (MAXRECURSION 0);
INSERT dbo.x2(field) SELECT '' FROM dbo.x1;
INSERT dbo.x3(field) SELECT '' FROM dbo.x1;

Lassen Sie uns die Größe überprüfen:

SELECT COUNT(*)*8192/1024. FROM sys.dm_db_database_page_allocations(DB_ID(), 
  OBJECT_ID(N'dbo.x1'), 1, NULL, 'DETAILED');
SELECT COUNT(*)*8192/1024. FROM sys.dm_db_database_page_allocations(DB_ID(), 
  OBJECT_ID(N'dbo.x2'), 1, NULL, 'DETAILED');
SELECT COUNT(*)*8192/1024. FROM sys.dm_db_database_page_allocations(DB_ID(), 
  OBJECT_ID(N'dbo.x3'), 1, NULL, 'DETAILED');

Ergebnisse:

12,928 KB
12,936 KB
12,936 KB

So ist es wie für 1.000.000 Zeilen sieht, die Wahl NULLüber ''spart ein sattes 8 KB (und dies spiegelt sich nicht einmal in sp_spaceused, denn das ist eine Seite , die Sie noch reserviert gespeichert wird, nur nicht zugeordnet).

Wiederholt für einen Heap (müssen erneut mehrere Tests durchführen, da wir über Ihre tatsächliche Tabellenstruktur raten):

12,872 KB
12,872 KB
12,928 KB

Wie ich bereits angedeutet habe und sogar 120.000.000 Zeilen extrapoliert, wäre der größtmögliche Unterschied (je nach Schema noch einmal) 960 KB in einer richtigen Tabelle und 6,7 MB in einem Heap. Wenn auf Ihrem Server so wenig Speicherplatz zur Verfügung steht, dass 6,7 MB Entscheidungen treffen, können Sie überlegen, wie viel eine zusätzliche Festplatte im Vergleich zu der Zeit, die Sie für die Untersuchung dieses Speicherplatzes benötigen, kosten würde.

Meiner Meinung nach gibt es weitaus wichtigere Gründe zwischen der Entscheidung, NULL-Werte zu verwenden oder nicht "keine Daten" darzustellen. Eine gute Frage mit vielen Meinungen und Kommentaren ist hier:

Aaron Bertrand
quelle
Es könnte erwähnenswert sein, wie die Komprimierung einen Unterschied macht oder nicht, insbesondere wenn ihm auf seinem Server der Speicherplatz ausgeht.
Joe Obbish
1
@ Joe Nun, sie sind derzeit auf 2005, die keine Komprimierung haben. Aber sicher, sehr wenig Einfluss hier. Die Daten in meinen Testtabellen hier erhalten nicht viel von der Komprimierung (ungefähr 14-16%). Wenn wir eine vollständige Schema- und Datenverteilung erhalten können, ist dies möglicherweise realistischer. Natürlich kann das OP dies selbst testen und sollte dies tun, sobald es sich um eine nicht prähistorische Version handelt, da diese stark von Schema + Daten abhängt und die Kosten / der Wert der Komprimierung stark von der Arbeitslast abhängen (Wir wissen, dass es nicht viel Headroom für die Festplatte gibt, aber möglicherweise auch nicht viel Headroom für die CPU).
Aaron Bertrand
Mein Migrationsvorgang wurde abgeschlossen, und es scheint, dass die Tabelle jetzt mit leeren Zeichenfolgen anstelle von Nullen deutlich mehr Speicherplatz beansprucht.
MrVimes
@MrVimas Dies könnte daran liegen, wie die Daten während der Migration auf Seiten gelegt wurden, was sich erheblich davon unterscheiden kann, wie die Daten auf natürliche Weise in die Quelle gelangt sind, insbesondere wenn sie parallel sind. Könnten Sie es nach einem Umbau überprüfen?
Aaron Bertrand
Ich habe Angst vor einem Umbau. Auf dem Datenlaufwerk befinden sich 30 GB Speicherplatz. In der Vergangenheit habe ich beobachtet, wie der verfügbare Speicherplatz beim Wiederherstellen des Clustered-Index für eine große Tabelle allmählich abfällt. Ich bin ziemlich froh, dass der Tisch kleiner ist als er war, auch wenn er nicht so klein ist wie der "Haltetisch".
MrVimes
3

In diesem Artikel wird erläutert, wie SQL NULL-Werte speichert .

Grundsätzlich speichert eine Spalte mit variabler Breite (varchar) eine Bitmap, die null oder nicht null angibt. Wenn es null ist, werden dem varchar-Feld null Bytes zugewiesen und das Bit wird umgedreht.

Bei Spalten mit fester Breite (char) wird weiterhin das gesamte Feld zugewiesen, ohne dass Daten darin gespeichert sind. Ein 10-Yte-Zeichenfeld weist also 10 Bytes zu, NULL oder nicht.

Dieser Artikel führt eine Einfügung mit Daten, mit NULL und mit leerer Zeichenfolge durch. Anschließend wird die Seitengröße abgefragt, um festzustellen, was intern vor sich geht.

Sowohl für Null- als auch für leere Zeichenfolgen werden 0 Bytes für Varchar-Felder zugewiesen.

Nocken
quelle
2

Zumindest für das Standarddatensatzformat ( FixedVar ) spielt es keine Rolle , wie viel Speicherplatz die Tabelle belegt (dies kann einen geringfügigen Unterschied zu den Indizes bewirken, wie später erläutert wird).

Sowohl eine Null- varcharals auch eine leere Zeichenfolge werden genauso gespeichert. Sie können nur unterschieden werden, ob eine 1oder eine 0Null-Bitmap vorhanden war. Beide nehmen im Abschnitt mit den Spaltendaten variabler Länge die Länge Null an, und beide können auch vermeiden, zwei Bytes im Array mit variablem Spaltenversatz zu belegen, wenn ihnen keine Spalten folgen, die Daten enthalten.

Einer der Kommentare sagt

Für die Tabelle, in der NULL-Werte nicht zulässig sind, möchten Sie die Spalte aus einer Reihe von Gründen als NICHT NULL definieren. Dies führt nicht zuletzt dazu, dass keine Null-Bitmap für diese Spalte erforderlich ist

Dies gilt nicht für Datenseiten. Siehe Mythos Nr. 6b: Die Null-Bitmap enthält nur Bits für nullfähige Spalten .

Bei Indizes besteht ein geringfügiger Unterschied darin, dass die Null-Bitmap weggelassen wird , wenn nicht alle an einem Index beteiligten Spalten nullwertfähig sind.

Der Unterschied ist jedoch vernachlässigbar und Sie sollten die Option auswählen, die Ihnen die gewünschte Semantik bietet.

Martin Smith
quelle