{"id":108481,"date":"2026-03-27T13:21:12","date_gmt":"2026-03-27T13:21:12","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=108481"},"modified":"2026-04-17T09:46:45","modified_gmt":"2026-04-17T09:46:45","slug":"learning-postgresql-with-grant-user-defined-types","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/databases\/postgresql\/learning-postgresql-with-grant-user-defined-types\/","title":{"rendered":"How User-Defined Types work in PostgreSQL: a complete guide"},"content":{"rendered":"\n<blockquote class=\"wp-block-quote is-layout-flow wp-block-quote-is-layout-flow\">\n<p><strong>Learn how to work with user-defined types in PostgreSQL, including how to define, use, and manage them effectively. This guide explores how custom types can improve schema design, enforce data integrity, and support cleaner, more maintainable database structures in real-world applications.<\/strong><\/p>\n\n\n\n<p>I\u2019m sure I\u2019m not alone when I say, sometimes I get sidetracked. In this particular instance, I hadn\u2019t intended to start learning about <a href=\"https:\/\/www.postgresql.org\/docs\/current\/xtypes.html\" target=\"_blank\" rel=\"noreferrer noopener\">User-Defined Types (UDT)<\/a> in <a href=\"https:\/\/www.postgresql.org\/\" target=\"_blank\" rel=\"noreferrer noopener\">PostgreSQL<\/a> &#8211; I just wanted to test a behavior that involved creating a UDT. But, once I started reading, I was hooked. I mean, four distinct UDTs with different behaviors? That\u2019s pretty cool. Let\u2019s get into it.<\/p>\n<\/blockquote>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-what-is-a-user-defined-type-udt-and-why-do-we-have-them\">What is a User-Defined Type (UDT), and why do we have them?<\/h2>\n\n\n\n<p>First, it\u2019s PostgreSQL, so I have to address the true User-Defined Type, not simply the derived types that we\u2019re going to talk about in the rest of the article. See, in PostgreSQL, you can quite literally build your own <a href=\"https:\/\/www.red-gate.com\/simple-talk\/blogs\/guidelines-for-choosing-data-types\/\" target=\"_blank\" rel=\"noreferrer noopener\">data type<\/a>. You\u2019ve got to break out the C compiler and write a bunch of code to input and output constructors and more. That is far beyond my capacity to describe. If you\u2019re interested in this, I\u2019d suggest starting with <a href=\"https:\/\/www.postgresql.org\/docs\/17\/xtypes.html\" target=\"_blank\" rel=\"noreferrer noopener\">their excellent documentation<\/a>.<\/p>\n\n\n\n<p>Outside Postgres, a standard UDT is usually just an existing data type, or data types, that can be referenced as if it was a standard data type when you add a column to a table. Further, we can add rules around the data type so that we can readily enforce exactly what gets stored. It\u2019s used for constructs that have specific use cases and rules that we want to define once, and then reuse without worrying ever again.<\/p>\n\n\n\n<p>An example of such a set of rules would be for amateur radio call signs in the United States. We have a relatively precise system. It\u2019s a string, up to six characters long that consists of one to two letters, a number, and then one to three letters. For example, my call sign is KC1KCE, all six characters. But my local club\u2019s call sign is W5IAS, only five. Another example is W1AW, four characters, and the call sign for the ARRL headquarters. You get the idea. Other countries have other rules.<\/p>\n\n\n\n<p>Now, let\u2019s say I want to use this call sign value in multiple places in my database. I can easily create a column with a variable length limit of six characters. I can create a <a href=\"https:\/\/regex101.com\/\" target=\"_blank\" rel=\"noreferrer noopener\">regular expression<\/a> in a constraint to enforce the naming standard too: [A-Z]{1,2}[0-9][A-Z]{1,3}. If I decide to use call signs in multiple places in my database, I\u2019ll be creating that column and that constraint over and over. Or, I can define it as a User-Defined Type and only define the code once. That\u2019s the beauty of the UDT.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-what-user-defined-types-are-available-in-postgresql\">What User-Defined Types are available in PostgreSQL?<\/h2>\n\n\n\n<p>PostgreSQL provides four different types of UDT:<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><code>DOMAIN<\/code>: a single value within the column based on an existing PostgreSQL data type that may or may not include additional constraints.<br><br><\/li>\n\n\n\n<li><code>COMPOSITE<\/code>: more than one value within the column, again based on existing PostgreSQL data types that may or may not include additional constraints.<br><br><\/li>\n\n\n\n<li><code>ENUM<\/code>: a data type that is pulled from a defined list, similar to a lookup table, but instead is defined within the column of a table<br><br><\/li>\n\n\n\n<li><code>RANGE<\/code>: an interval data type to keep numbers or dates within a certain range of values in a column.<\/li>\n<\/ul>\n<\/div>\n\n\n<p>Let\u2019s see how each of these can be used within your database.<\/p>\n\n\n\n<section id=\"my-first-block-block_1a700e7512b0d0d310a036ab54386922\" class=\"my-first-block alignwide\">\n    <div class=\"bg-brand-600 text-base-white py-5xl px-4xl rounded-sm bg-gradient-to-r from-brand-600 to-brand-500 red\">\n        <div class=\"gap-4xl items-start md:items-center flex flex-col md:flex-row justify-between\">\n            <div class=\"flex-1 col-span-10 lg:col-span-7\">\n                <h3 class=\"mt-0 font-display mb-2 text-display-sm\">Get started with PostgreSQL &#8211; free book download<\/h3>\n                <div class=\"child:last-of-type:mb-0\">\n                                            &#8216;Introduction to PostgreSQL for the data professional&#8217;, written by Grant Fritchey and Ryan Booz, covers all the basics of how to get started with PostgreSQL.                                    <\/div>\n            <\/div>\n                                            <a href=\"https:\/\/www.red-gate.com\/hub\/books\/introduction-to-postgresql-for-the-data-professional\/\" class=\"btn btn--secondary btn--lg\" aria-label=\"Download your free copy: Get started with PostgreSQL - free book download\">Download your free copy<\/a>\n                    <\/div>\n    <\/div>\n<\/section>\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-domain-user-defined-type-in-postgresql\">The DOMAIN User-Defined Type in PostgreSQL<\/h2>\n\n\n\n<p>I\u2019m just going to take the example I outlined above to show how you could create a \u2018call sign\u2019 data type using <code>DOMAIN<\/code> in PostgreSQL:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">CREATE DOMAIN callsign AS VARCHAR(6)\n   CONSTRAINT valid_callsign\n   CHECK (\n      VALUE ~ '^[A-Z]{1,2}[0-9][A-Z]{1,3}$'\n   );\n<\/pre><\/div>\n\n\n\n<p>The explanation of the code is simple. The core behavior we\u2019re after is to use <code>DOMAIN <\/code>as our own controlled type based on an existing data type. So we have to supply a name, callsign, and then a data type: <code>VARCHAR(6)<\/code>. I put the limit in rather than setting it as text because we intend to control how many characters are used based on the definitions of US call signs. If we were going to store both US and UK call signs, for example, the data type would have to be limited to seven characters, not six, since the UK has different call sign rules.<\/p>\n\n\n\n<p>With the data type in place, we then define the <code>CONSTRAINT<\/code> that is fundamental to the UDT ensuring we only get the data we want, correctly formed. You can see that using the regex we defined earlier. To put this to work is fairly straightforward &#8211; here\u2019s an example when creating a table:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">CREATE TABLE radio.us_operators\n(us_operators_id int CONSTRAINT pk_us_operators PRIMARY KEY GENERATED ALWAYS AS IDENTITY,\ncall_sign callsign NOT NULL\n);\n<\/pre><\/div>\n\n\n\n<p>In order to test our new table, let\u2019s try adding an incorrect call sign first:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">INSERT INTO radio.us_operators \n(call_sign)\nVALUES\n('2CEKCE');<\/pre><\/div>\n\n\n\n<p>Sure enough, this generates an error:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SQL Error [23514]: ERROR: value for domain callsign violates \ncheck constraint \"valid_callsign\"<\/pre><\/div>\n\n\n\n<p>Whereas if we input a valid call sign, everything works fine:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">INSERT INTO radio.us_operators \n(call_sign)\nVALUES\n('KC1KCE');<\/pre><\/div>\n\n\n\n<p>Once a&nbsp;<code>DOMAIN<\/code> UDT is defined, you can\u2019t really edit it. Instead, people will go through the process of renaming it, creating a new UDT with the newer structure and the old name, and then replacing that before dropping the old UDT. For more on the <code>DOMAIN<\/code> UDT, consult the <a href=\"https:\/\/www.postgresql.org\/docs\/17\/sql-createdomain.html\" target=\"_blank\" rel=\"noreferrer noopener\">PostgreSQL documentation<\/a>.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-composite-user-defined-type-in-postgresql\">The COMPOSITE User-Defined Type in PostgreSQL<\/h2>\n\n\n\n<p>The <code>COMPOSITE<\/code> data type again takes advantage of existing data types to build something new. However, in this case, it\u2019s more than one value in the type. For example, in Amateur Radio, we have different bands. These are just frequency ranges with a minimum and a maximum. I&#8217;d argue that this would be best served in most relational databases by using a table, but we could create a data type as follows:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">CREATE TYPE band AS (\nband_name TEXT,\nlower NUMERIC,\nupper NUMERIC);\n<\/pre><\/div>\n\n\n\n<p>In this case, we define the name of the band it\u2019s lower and upper limits. We could alter the radios table to add the data type like this:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">ALTER TABLE radio.radios ADD COLUMN bands band NULL;<\/pre><\/div>\n\n\n\n<p>With that in place, we could add a radio and include a band for it to operate on:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">INSERT INTO radio.radios\n(radio_id,\nradio_name,\nbands)\nVALUES\n(2,\n'FT-3D', \n('2 Meter',144,148));<\/pre><\/div>\n\n\n\n<p>In this case, I simply used the parentheses to contain the definition of the <code>COMPOSITE<\/code> value. However, we could rewrite the <code>INSERT<\/code> statement and use the <code>ROW<\/code> construct like this:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">INSERT INTO radio.radios\n(radio_id,\nradio_name,\nbands)\nVALUES\n(3,\n'THD-75', \nROW('2 Meter',144,148));<\/pre><\/div>\n\n\n\n<p>We can query directly into the <code>COMPOSITE<\/code> type like this:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SELECT \n    radio_name,\n    (bands).lower AS lower_frequency,\n    (bands).upper AS upper_frequency\nFROM \n    radio.radios\nWHERE \n    (bands).band_name = '2 Meter';<\/pre><\/div>\n\n\n\n<p>The parentheses around the <code>COMPOSITE<\/code> data type makes it clear to PostgreSQL that we are querying a composite. Otherwise, it\u2019s likely to interpret the \u2018bands\u2019 column as another table and generate an error.<\/p>\n\n\n\n<p>You can even nest <code>COMPOSITE<\/code> types, creating a <code>COMPOSITE<\/code> type that has other <code>COMPOSITE<\/code> types within it. Clearly, this will start to get quite messy, so should probably be avoided.<\/p>\n\n\n\n<p>You can make changes to a <code>COMPOSITE<\/code> type. You simply use the <code>ALTER TYPE<\/code> command to make those changes. So, if we wanted to rename the attributes lower and upper to something more descriptive, we could use the following code:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">ALTER TYPE band RENAME ATTRIBUTE lower TO band_start;\nALTER TYPE band RENAME ATTRIBUTE upper TO band_stop;<\/pre><\/div>\n\n\n\n<p>I could also change data types, add new attributes or drop existing ones. Now, if there are tables out there using the type already, you\u2019ll have to be sure they\u2019re updated using <code>CASCADE<\/code> in the command above, or going to the individual tables and updating their definition. In the case of <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/mastering-sql-views\/\" target=\"_blank\" rel=\"noreferrer noopener\">views<\/a> or functions, if you change a type that they use, you\u2019ll have to drop and recreate those objects.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-enum-user-defined-type-in-postgresql\">The ENUM User-Defined Type in PostgreSQL<\/h2>\n\n\n\n<p>The <a href=\"https:\/\/www.typescriptlang.org\/docs\/handbook\/enums.html\" target=\"_blank\" rel=\"noreferrer noopener\">enumerated type (<code>ENUM<\/code>)<\/a> is a list of possible values that can be in the column in question. We&#8217;d create a simple <code>ENUM<\/code> for types of radios, something like this:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">CREATE TYPE public.transmissiontype AS ENUM (\n\t'Analog',\n\t'Digital');\n<\/pre><\/div>\n\n\n\n<p>We can modify the radios table to add this data type pretty simply:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">ALTER TABLE radio.radios \nADD COLUMN radio_type transmissiontype NULL;\n<\/pre><\/div>\n\n\n\n<p>From there, updating an existing radio with a specified transmission type is also quite straightforward:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">UPDATE radio.radios\nSET radio_type = 'Analog'\nWHERE radio_name = 'UV-5R';\n<\/pre><\/div>\n\n\n\n<p>However, the <code>ENUM<\/code> quite strictly limits what can be added to the column. For example, this would fail:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">UPDATE radio.radios\nSET radio_type = 'M-17'\nWHERE radio_id = 3;\n<\/pre><\/div>\n\n\n\n<p>You would receive this error:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SQL Error [22P02]: ERROR: invalid input value for enum \ntransmissiontype: \"M-17\"\n  Position: 39<\/pre><\/div>\n\n\n\n<p>That\u2019s hardly a surprise but, since the <code>ENUM<\/code> data type is case sensitive, this would <em>also<\/em> fail:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">UPDATE radio.radios\nSET radio_type = 'ANALOG'\nWHERE radio_name = 'UV-5R';\n<\/pre><\/div>\n\n\n\n<p>The list of values in an <code>ENUM<\/code> can be modified through various means. For example, you can query the list of <code>ENUM<\/code> values, providing a start and stop point. In this example, I don\u2019t have a start, so it\u2019s all the values:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SELECT enum_range(NULL::transmissiontype);<\/pre><\/div>\n\n\n\n<p>The values in the <code>ENUM<\/code> type are considered to be ordered by the definition order meaning that, by giving the above code a start point, it&#8217;ll find that value in the <code>ENUM<\/code> and then list values from there. You can also then <code>ORDER BY<\/code> an <code>ENUM<\/code> knowing that the order the values were defined in will be the sort order.<\/p>\n\n\n\n<p>We can modify <code>ENUM<\/code> values as well. For example, let\u2019s add one more, <code>HYBRID<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">ALTER TYPE transmissiontype\nADD VALUE IF NOT EXISTS 'HYBRID';\n<\/pre><\/div>\n\n\n\n<p>If needed, I can control where this value gets inserted by using \u201cBEFORE\/AFTER \u2018another value\u2019\u201d to designate the new value\u2019s location.<\/p>\n\n\n\n<p>If I then query the <code>ENUM<\/code>, I see the following:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"538\" height=\"344\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/02\/image-52.png\" alt=\"What the ENUM shows when queried.\" class=\"wp-image-108482\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/02\/image-52.png 538w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/02\/image-52-300x192.png 300w\" sizes=\"auto, (max-width: 538px) 100vw, 538px\" \/><\/figure>\n\n\n\n<p>Since this isn\u2019t following my naming standard already established, let\u2019s change the value in the <code>ENUM<\/code>:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">ALTER TYPE transmissiontype\nRENAME VALUE 'HYBRID' TO 'Hybrid';\n<\/pre><\/div>\n\n\n\n<p>There\u2019s no need to do anything else since the <code>ENUM<\/code> type acts a bit like a little table &#8211; changes to it are reflected wherever it&#8217;s used. However, you can\u2019t eliminate values once they\u2019re in the <code>ENUM<\/code> definition, and you can\u2019t change the order of the <code>ENUM<\/code> either; these actions would require you to drop and recreate the <code>ENUM<\/code>.<\/p>\n\n\n\n<section id=\"my-first-block-block_40b295a21dd96f2b5d17ae8a7051c511\" class=\"my-first-block alignwide\">\n    <div class=\"bg-brand-600 text-base-white py-5xl px-4xl rounded-sm bg-gradient-to-r from-brand-600 to-brand-500 red\">\n        <div class=\"gap-4xl items-start md:items-center flex flex-col md:flex-row justify-between\">\n            <div class=\"flex-1 col-span-10 lg:col-span-7\">\n                <h3 class=\"mt-0 font-display mb-2 text-display-sm\">Enjoying this article? Subscribe to the Simple Talk newsletter<\/h3>\n                <div class=\"child:last-of-type:mb-0\">\n                                            Get selected articles, event information, podcasts and other industry content delivered straight to your inbox.                                    <\/div>\n            <\/div>\n                                            <a href=\"https:\/\/www.red-gate.com\/simple-talk\/subscribe\/\" class=\"btn btn--secondary btn--lg\" aria-label=\"Subscribe now: Enjoying this article? Subscribe to the Simple Talk newsletter\">Subscribe now<\/a>\n                    <\/div>\n    <\/div>\n<\/section>\n\n\n<h2 class=\"wp-block-heading\" id=\"h-the-range-user-defined-type-in-postgresql\">The RANGE User-Defined Type in PostgreSQL<\/h2>\n\n\n\n<p>Earlier, I used the <code>COMPOSITE<\/code> type to define ham radio bands. However, that\u2019s probably not an ideal use of <code>COMPOSITE<\/code>. For example, I know that I need the lower limit on the band to be less than the upper limit, yet there\u2019s nothing in the type definition to control that. This is where the <code>RANGE<\/code> type would likely be a better choice.<\/p>\n\n\n\n<p>The <code>RANGE<\/code> type is defined by its name: it represents a range of values with a defined start and stop, with the start being less than the stop. PostgreSQL already defines ranges for:<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/mysql\/mysql-vs-postgresql-date-and-time-data-types\/\" target=\"_blank\" rel=\"noreferrer noopener\">Timestamp<\/a><\/li>\n\n\n\n<li>Integer<\/li>\n\n\n\n<li><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/mysql-vs-postgresql-numeric-datatype-comparison\/\" target=\"_blank\" rel=\"noreferrer noopener\">Numeric<\/a><\/li>\n\n\n\n<li>Date<\/li>\n<\/ul>\n<\/div>\n\n\n<p>You can use these predefined range types as data types. However, if you want to use something not covered, such as <code>float8<\/code>, we can create our own UDT. When defining the <code>RANGE<\/code>, you provide the subtype (the data type that\u2019s going to store the range):<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">CREATE TYPE frequency_range AS RANGE (\n    subtype = float8,\n    subtype_diff = float8mi\n);\n<\/pre><\/div>\n\n\n\n<p>In addition to defining the subtype, I also have to explain which data type to use in it and provide a comparison function, <code>subtype_diff<\/code>. In this case, I can use the built-in <code>float8mi<\/code> function, but I might have to build my own &#8211; depending on what&#8217;s defined as the subtype and what rules I want to put in place in my <code>RANGE<\/code>.<\/p>\n\n\n\n<p>Since many radios support more than one band, I\u2019m going to slightly change the architecture of my database to take advantage of my new <code>RANGE<\/code> UDT:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">ALTER TABLE radio.radios DROP COLUMN bands;\nDROP TYPE band;\n\nALTER TABLE radio.bands DROP COLUMN frequency_start_khz;\nALTER TABLE radio.bands DROP COLUMN frquency_end_khz;\nALTER TABLE radio.bands ADD COLUMN frequencies frequency_range NOT NULL;\n<\/pre><\/div>\n\n\n\n<p>I actually already had a bands table as well as a many-to-many table, <code>radio_bands<\/code>, to join between radios and bands. With this, my bands table now needs some up-to-date data:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">INSERT INTO radio.bands\n(band_name,frequencies,country_id)\nVALUES\n('2 Meters',frequency_range(144000.0,148000.0),1),\n('70 cm',frequency_range(420000.0,450000.0),1),\n('20 Meters',frequency_range(14000.0,14350.0),1),\n('144MHz Band',frequency_range(144000.0,146000.0),2),\n('432MHz Band',frequency_range(430000.0,440000.0),2);\n<\/pre><\/div>\n\n\n\n<p>Now, I can try to add this data&#8230;<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">INSERT INTO radio.bands \n(band_name,frequencies,country_id )\nVALUES\n('6 Meters',frequency_range(54000.0,50000.0),2);<\/pre><\/div>\n\n\n\n<p>&#8230;but I\u2019m presented with an error:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SQL Error [22000]: ERROR: range lower bound must be less than or \nequal to range upper bound<\/pre><\/div>\n\n\n\n<p>This is great. I don\u2019t have to define behaviors &#8211; I just inherently receive them because I\u2019m using the <code>RANGE<\/code> type. There\u2019s a number of other inherent functions as well. Let\u2019s say I have a frequency (in khz), 145000. I want to know which band, or bands, has this value. I can query the data like this:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SELECT b.band_name FROM radio.bands b \nWHERE b.frequencies @&gt; 145000::float8;\n<\/pre><\/div>\n\n\n\n<p>And I get this:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"520\" height=\"209\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/02\/image-53.png\" alt=\"An image showing what's returned from the query.\n\" class=\"wp-image-108483\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/02\/image-53.png 520w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/02\/image-53-300x121.png 300w\" sizes=\"auto, (max-width: 520px) 100vw, 520px\" \/><\/figure>\n\n\n\n<p>The \u2018@&gt;\u2019 acts as a query specifying that the value is contained. We could look for overlapping bands as well:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">SELECT\n\tb1.band_name,\n\tb2.band_name\nFROM\n\tradio.bands b1\nJOIN radio.bands b2\nON\n\tb1.frequencies &amp;&amp; b2.frequencies\nWHERE\n\tb1.band_name &lt;&gt; b2.band_name;\n<\/pre><\/div>\n\n\n\n<p>In this case, the operator \u2018&amp;&amp;\u2019 identifies where parts of the bands overlap so we get the following:<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><img loading=\"lazy\" decoding=\"async\" width=\"913\" height=\"467\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/02\/image-54.png\" alt=\"An image showing where the bands overlap.\" class=\"wp-image-108484\" srcset=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/02\/image-54.png 913w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/02\/image-54-300x153.png 300w, https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2026\/02\/image-54-768x393.png 768w\" sizes=\"auto, (max-width: 913px) 100vw, 913px\" \/><\/figure>\n\n\n\n<p>I could filter this by country to only show the overlap for one instead of all of them, for example, but there are various other functions for working with <code>RANGE<\/code> data types as well.<\/p>\n\n\n\n<p>I did, however, make a mistake here: I added the 2 Meters frequencies in the use with an upper bound of 146000khz. It\u2019s actually 148000khz, so I need to fix it:<\/p>\n\n\n\n<div class=\"wp-block-urvanov-syntax-highlighter-code-block\"><pre class=\"lang:tsql decode:true \">DO $$\nDECLARE newfreq float8 := 148000.0;\n\nBEGIN\nUPDATE\n\tradio.bands\nSET\n\tfrequencies = frequency_range(lower(frequencies), newfreq)\nWHERE\n\tband_name = '2 Meters';\nEND $$;\n<\/pre><\/div>\n\n\n\n<p>I used a variable just because I wanted to, for practice. However, you can see that I kept the existing value but referencing lower (frequencies), and then modified it by changing the upper to a new value.<\/p>\n\n\n\n<p>There are a number of other functions associated with the <code>RANGE<\/code> type including defining the inclusivity of the upper or lower values, additional comparison functions, and more. You can\u2019t, however, change the definition of a range once it\u2019s in place &#8211; only drop and recreate it after removing it from all the places it\u2019s in use.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-final-thoughts-amp-next-steps\">Final thoughts &amp; next steps<\/h2>\n\n\n\n<p>As you can see, there\u2019s a lot of functionality in and around UDTs, and I haven&#8217;t even mentioned indexing them (yes, you can apply indexes to UDTs!) In my examples, the <code>COMPOSITE<\/code> type I created probably wasn\u2019t the best choice. However, the <code>ENUM<\/code> and <code>RANGE<\/code> data types were the correct choices for the data in question. It\u2019s all about figuring out what\u2019s going to work best in a given situation.<\/p>\n\n\n\n<p>Want to learn more PostgreSQL with Grant? Click <a href=\"https:\/\/www.red-gate.com\/simple-talk\/collections\/series-learning-postgresql-with-grant\/\" target=\"_blank\" rel=\"noreferrer noopener\">here<\/a> for the full collection.<\/p>\n\n\n\n<section id=\"faq\" class=\"faq-block my-5xl\">\n    <h2>FAQs: User-Defined Types in PostgreSQL<\/h2>\n\n                        <h3 class=\"mt-4xl\">1. What is a User-Defined Type (UDT) in PostgreSQL?<\/h3>\n            <div class=\"faq-answer\">\n                <p>A User-Defined Type (UDT) in PostgreSQL is a custom data type you create to enforce structure and rules in your database. PostgreSQL supports <code>DOMAIN<\/code>, <code>COMPOSITE<\/code>, <code>ENUM<\/code>and <code>RANGE<\/code> types.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">2. What are the four types of User-Defined Types in PostgreSQL?<\/h3>\n            <div class=\"faq-answer\">\n                <p data-start=\"374\" data-end=\"546\">PostgreSQL provides four UDTs: <code>DOMAIN<\/code> (constrained single value), <code>COMPOSITE<\/code> (multiple related fields), <code>ENUM<\/code> (predefined value list), and <code>RANGE<\/code> (bounded interval of values).<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">3. When should you use a DOMAIN User-Defined Type in PostgreSQL?<\/h3>\n            <div class=\"faq-answer\">\n                <p data-start=\"601\" data-end=\"752\">Use a <code>DOMAIN<\/code> type when you need to apply consistent constraints &#8211; such as regex validation or length limits &#8211; to a data type reused across multiple tables.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">4. What is a COMPOSITE User-Defined Type in PostgreSQL?<\/h3>\n            <div class=\"faq-answer\">\n                <p data-start=\"798\" data-end=\"925\">A <code>COMPOSITE<\/code> type stores multiple related fields as a single column value, similar to a structured record with named attributes.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">5. How does an ENUM User-Defined Type work in PostgreSQL?<\/h3>\n            <div class=\"faq-answer\">\n                <p data-start=\"973\" data-end=\"1102\">An <code>ENUM<\/code> type restricts a column to a predefined, ordered list of values. It enforces strict data integrity and is case sensitive.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">6. What is a RANGE User-Defined Type in PostgreSQL?<\/h3>\n            <div class=\"faq-answer\">\n                <p data-start=\"1144\" data-end=\"1343\">A <code>RANGE<\/code> type stores a lower and upper bound as a single value and ensures the lower bound is less than or equal to the upper bound. It supports powerful operators like containment and overlap checks.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">7. Can you modify PostgreSQL User-Defined Types after creation?<\/h3>\n            <div class=\"faq-answer\">\n                <p data-start=\"1410\" data-end=\"1614\">Some UDTs can be altered (such as adding <code>ENUM<\/code> values or modifying <code>COMPOSITE<\/code> attributes), but others &#8211; like <code>DOMAIN<\/code> and <code>RANGE<\/code> definitions &#8211; often require dropping and recreating the type for structural changes.<\/p>\n            <\/div>\n            <\/section>\n\n\n\n<p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Learn PostgreSQL User-Defined Types (UDTs) with clear examples of DOMAIN, COMPOSITE, ENUM, and RANGE types, plus practical SQL code for real-world use.&hellip;<\/p>\n","protected":false},"author":221792,"featured_media":105583,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[143523,53,143534],"tags":[158977,158978,4150],"coauthors":[6785],"class_list":["post-108481","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-databases","category-featured","category-postgresql","tag-learningpostgresqlwithgrant","tag-postgresql","tag-sql"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/108481","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/221792"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=108481"}],"version-history":[{"count":6,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/108481\/revisions"}],"predecessor-version":[{"id":109993,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/108481\/revisions\/109993"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media\/105583"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=108481"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=108481"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=108481"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=108481"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}